diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index a4f667ac46..68bd149419 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -11,3 +11,8 @@ # refactor: Run lintfix (no-changelog) (#7537) 62c096710fab2f7e886518abdbded34b55e93f62 + +# refactor: Move test files alongside tested files (#11504) + +7e58fc4fec468aca0b45d5bfe6150e1af632acbc +f32b13c6ed078be042a735bc8621f27e00dc3116 diff --git a/.github/docker-compose.yml b/.github/docker-compose.yml index 84a1b9c961..d4c8dc2ba8 100644 --- a/.github/docker-compose.yml +++ b/.github/docker-compose.yml @@ -1,18 +1,14 @@ -version: '3.9' - services: - mysql: - image: mysql:5.7 + mariadb: + image: mariadb:10.9 environment: - - MYSQL_DATABASE=n8n - - MYSQL_ROOT_PASSWORD=password + - MARIADB_DATABASE=n8n + - MARIADB_ROOT_PASSWORD=password + - MARIADB_MYSQL_LOCALHOST_USER=true ports: - 3306:3306 - ulimits: - nproc: 65535 - nofile: - soft: 26677 - hard: 46677 + tmpfs: + - /var/lib/mysql postgres: image: postgres:16 @@ -23,3 +19,5 @@ services: - POSTGRES_PASSWORD=password ports: - 5432:5432 + tmpfs: + - /var/lib/postgresql/data diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 0df76d2700..0788c7c480 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -11,6 +11,8 @@ Photos and videos are recommended. 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 diff --git a/.github/scripts/check-tests.mjs b/.github/scripts/check-tests.mjs deleted file mode 100644 index 1e2e38e05b..0000000000 --- a/.github/scripts/check-tests.mjs +++ /dev/null @@ -1,103 +0,0 @@ -import { readFile } from 'fs/promises'; -import path from 'path'; -import util from 'util'; -import { exec } from 'child_process'; -import { glob } from 'glob'; -import ts from 'typescript'; - -const execAsync = util.promisify(exec); - -const filterAsync = async (asyncPredicate, arr) => { - const filterResults = await Promise.all( - arr.map(async (item) => ({ - item, - shouldKeep: await asyncPredicate(item), - })), - ); - - return filterResults.filter(({ shouldKeep }) => shouldKeep).map(({ item }) => item); -}; - -const isAbstractClass = (node) => { - if (ts.isClassDeclaration(node)) { - return ( - node.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.AbstractKeyword) || false - ); - } - return false; -}; - -const isAbstractMethod = (node) => { - return ( - ts.isMethodDeclaration(node) && - Boolean(node.modifiers?.find((modifier) => modifier.kind === ts.SyntaxKind.AbstractKeyword)) - ); -}; - -// Function to check if a file has a function declaration, function expression, object method or class -const hasFunctionOrClass = async (filePath) => { - const fileContent = await readFile(filePath, 'utf-8'); - const sourceFile = ts.createSourceFile(filePath, fileContent, ts.ScriptTarget.Latest, true); - - let hasFunctionOrClass = false; - const visit = (node) => { - if ( - ts.isFunctionDeclaration(node) || - ts.isFunctionExpression(node) || - ts.isArrowFunction(node) || - (ts.isMethodDeclaration(node) && !isAbstractMethod(node)) || - (ts.isClassDeclaration(node) && !isAbstractClass(node)) - ) { - hasFunctionOrClass = true; - } - node.forEachChild(visit); - }; - - visit(sourceFile); - - return hasFunctionOrClass; -}; - -const main = async () => { - // Run a git command to get a list of all changed files in the branch (branch has to be up to date with master) - const changedFiles = await execAsync( - 'git diff --name-only --diff-filter=d origin/master..HEAD', - ).then(({ stdout }) => stdout.trim().split('\n').filter(Boolean)); - - // Get all .spec.ts and .test.ts files from the packages - const specAndTestTsFiles = await glob('packages/*/**/{test,__tests__}/**/*.{spec,test}.ts'); - const specAndTestTsFilesNames = specAndTestTsFiles.map((file) => - path.parse(file).name.replace(/\.(test|spec)/, ''), - ); - - // Filter out the .ts and .vue files from the changed files - const changedVueFiles = changedFiles.filter((file) => file.endsWith('.vue')); - // .ts files with any kind of function declaration or class and not in any of the test folders - const changedTsFilesWithFunction = await filterAsync( - async (filePath) => - filePath.endsWith('.ts') && - !(await glob('packages/*/**/{test,__tests__}/*.ts')).includes(filePath) && - (await hasFunctionOrClass(filePath)), - changedFiles, - ); - - // For each .ts or .vue file, check if there's a corresponding .test.ts or .spec.ts file in the repository - const missingTests = changedVueFiles - .concat(changedTsFilesWithFunction) - .reduce((filesList, nextFile) => { - const fileName = path.parse(nextFile).name; - - if (!specAndTestTsFilesNames.includes(fileName)) { - filesList.push(nextFile); - } - - return filesList; - }, []); - - if (missingTests.length) { - console.error(`Missing tests for:\n${missingTests.join('\n')}`); - process.exit(1); - } -}; - -main(); diff --git a/.github/scripts/package.json b/.github/scripts/package.json index 80bf0baf11..7640790f2d 100644 --- a/.github/scripts/package.json +++ b/.github/scripts/package.json @@ -7,7 +7,6 @@ "p-limit": "3.1.0", "picocolors": "1.0.1", "semver": "7.5.4", - "tempfile": "5.0.0", - "typescript": "*" + "tempfile": "5.0.0" } } diff --git a/.github/workflows/check-tests.yml b/.github/workflows/check-tests.yml deleted file mode 100644 index 97f380974a..0000000000 --- a/.github/workflows/check-tests.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: Check Test Files - -on: - pull_request: - branches: - - '**' - - '!release/*' - pull_request_target: - branches: - - master - -jobs: - check-tests: - runs-on: ubuntu-latest - continue-on-error: true - steps: - - name: Checkout code - uses: actions/checkout@v4.1.1 - with: - fetch-depth: 0 - - - name: Use Node.js - uses: actions/setup-node@v4.0.2 - with: - node-version: 20.x - - - run: npm install --prefix=.github/scripts --no-package-lock - - - name: Check for test files - run: node .github/scripts/check-tests.mjs diff --git a/.github/workflows/chromatic.yml b/.github/workflows/chromatic.yml index 7c5682076a..4e77fbd580 100644 --- a/.github/workflows/chromatic.yml +++ b/.github/workflows/chromatic.yml @@ -1,6 +1,8 @@ name: Chromatic on: + schedule: + - cron: '0 0 * * *' workflow_dispatch: pull_request_review: types: [submitted] @@ -65,11 +67,12 @@ jobs: continue-on-error: true with: workingDir: packages/design-system + onlyChanged: true projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} exitZeroOnChanges: false - name: Success comment - if: steps.chromatic_tests.outcome == 'success' + if: steps.chromatic_tests.outcome == 'success' && github.ref != 'refs/heads/master' uses: peter-evans/create-or-update-comment@v4.0.0 with: issue-number: ${{ github.event.pull_request.number }} @@ -79,7 +82,7 @@ jobs: :white_check_mark: No visual regressions found. - name: Fail comment - if: steps.chromatic_tests.outcome != 'success' + if: steps.chromatic_tests.outcome != 'success' && github.ref != 'refs/heads/master' uses: peter-evans/create-or-update-comment@v4.0.0 with: issue-number: ${{ github.event.pull_request.number }} diff --git a/.github/workflows/ci-postgres-mysql.yml b/.github/workflows/ci-postgres-mysql.yml index 23088633b5..f6e5e773cb 100644 --- a/.github/workflows/ci-postgres-mysql.yml +++ b/.github/workflows/ci-postgres-mysql.yml @@ -8,6 +8,7 @@ on: paths: - packages/cli/src/databases/** - .github/workflows/ci-postgres-mysql.yml + - .github/docker-compose.yml pull_request_review: types: [submitted] @@ -71,8 +72,8 @@ jobs: working-directory: packages/cli run: pnpm jest - mysql: - name: MySQL + mariadb: + name: MariaDB runs-on: ubuntu-latest needs: build timeout-minutes: 20 @@ -96,16 +97,16 @@ jobs: path: ./packages/**/dist key: ${{ github.sha }}:db-tests - - name: Start MySQL + - name: Start MariaDB uses: isbang/compose-action@v2.0.0 with: compose-file: ./.github/docker-compose.yml services: | - mysql + mariadb - - name: Test MySQL + - name: Test MariaDB working-directory: packages/cli - run: pnpm test:mysql --testTimeout 20000 + run: pnpm test:mariadb --testTimeout 30000 postgres: name: Postgres @@ -147,7 +148,7 @@ jobs: notify-on-failure: name: Notify Slack on failure runs-on: ubuntu-latest - needs: [mysql, postgres] + needs: [mariadb, postgres] steps: - name: Notify Slack on failure uses: act10ns/slack@v2.0.0 @@ -156,4 +157,4 @@ jobs: status: ${{ job.status }} channel: '#alerts-build' 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 MariaDB tests failed (${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) diff --git a/.github/workflows/ci-pull-requests.yml b/.github/workflows/ci-pull-requests.yml index 48a34c5eab..1ce7427098 100644 --- a/.github/workflows/ci-pull-requests.yml +++ b/.github/workflows/ci-pull-requests.yml @@ -49,6 +49,9 @@ jobs: with: ref: refs/pull/${{ github.event.pull_request.number }}/merge cacheKey: ${{ github.sha }}-base:build + collectCoverage: true + secrets: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} lint: name: Lint diff --git a/.github/workflows/e2e-reusable.yml b/.github/workflows/e2e-reusable.yml index ab88930a3a..b55d6728d2 100644 --- a/.github/workflows/e2e-reusable.yml +++ b/.github/workflows/e2e-reusable.yml @@ -41,6 +41,11 @@ on: description: 'PR number to run tests for.' required: false type: number + node_view_version: + description: 'Node View version to run tests with.' + required: false + default: '1' + type: string secrets: CYPRESS_RECORD_KEY: description: 'Cypress record key.' @@ -160,6 +165,7 @@ jobs: spec: '${{ inputs.spec }}' env: NODE_OPTIONS: --dns-result-order=ipv4first + CYPRESS_NODE_VIEW_VERSION: ${{ inputs.node_view_version }} CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} E2E_TESTS: true diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index e7400adecb..2f63f61bc6 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -27,6 +27,11 @@ on: description: 'URL to call after workflow is done.' required: false default: '' + node_view_version: + description: 'Node View version to run tests with.' + required: false + default: '1' + type: string jobs: calls-start-url: @@ -46,6 +51,7 @@ jobs: branch: ${{ github.event.inputs.branch || 'master' }} user: ${{ github.event.inputs.user || 'PR User' }} spec: ${{ github.event.inputs.spec || 'e2e/*' }} + node_view_version: ${{ github.event.inputs.node_view_version || '1' }} secrets: CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml index c3548a0cda..9196a7fccb 100644 --- a/.github/workflows/release-publish.yml +++ b/.github/workflows/release-publish.yml @@ -38,6 +38,12 @@ jobs: - name: Build run: pnpm build + - name: Cache build artifacts + uses: actions/cache/save@v4.0.0 + with: + path: ./packages/**/dist + key: ${{ github.sha }}-release:build + - name: Dry-run publishing run: pnpm publish -r --no-git-checks --dry-run @@ -119,6 +125,40 @@ jobs: makeLatest: false body: ${{github.event.pull_request.body}} + create-sentry-release: + name: Create a Sentry Release + needs: [publish-to-npm, publish-to-docker-hub] + runs-on: ubuntu-latest + if: github.event.pull_request.merged == true + timeout-minutes: 5 + env: + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + SENTRY_ORG: ${{ secrets.SENTRY_ORG }} + + steps: + - uses: actions/checkout@v4.1.1 + - name: Restore cached build artifacts + uses: actions/cache/restore@v4.0.0 + with: + path: ./packages/**/dist + key: ${{ github.sha }}-release:build + + - name: Create a frontend release + uses: getsentry/action-release@v1.7.0 + continue-on-error: true + with: + projects: ${{ secrets.SENTRY_FRONTEND_PROJECT }} + version: ${{ needs.publish-to-npm.outputs.release }} + sourcemaps: packages/editor-ui/dist + + - name: Create a backend release + uses: getsentry/action-release@v1.7.0 + continue-on-error: true + with: + projects: ${{ secrets.SENTRY_BACKEND_PROJECT }} + version: ${{ needs.publish-to-npm.outputs.release }} + sourcemaps: packages/cli/dist packages/core/dist packages/nodes-base/dist packages/@n8n/n8n-nodes-langchain/dist + trigger-release-note: name: Trigger a release note needs: [publish-to-npm, create-github-release] diff --git a/.github/workflows/test-workflows.yml b/.github/workflows/test-workflows.yml index 2bb91dd065..90a69438ab 100644 --- a/.github/workflows/test-workflows.yml +++ b/.github/workflows/test-workflows.yml @@ -4,18 +4,70 @@ on: schedule: - cron: '0 2 * * *' workflow_dispatch: + pull_request: + paths: + - packages/core/package.json + - packages/nodes-base/package.json + - packages/@n8n/nodes-langchain/package.json + - .github/workflows/test-workflows.yml + pull_request_review: + types: [submitted] jobs: - run-test-workflows: + build: + name: Install & Build runs-on: ubuntu-latest - - timeout-minutes: 30 - + if: github.event_name != 'pull_request_review' || startsWith(github.event.pull_request.base.ref, 'release/') steps: - - name: Checkout - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@v4.1.1 + - run: corepack enable + - uses: actions/setup-node@v4.0.2 with: - path: n8n + node-version: 20.x + cache: 'pnpm' + - run: pnpm install --frozen-lockfile + + - name: Setup build cache + uses: rharkor/caching-for-turbo@v1.5 + + - name: Build Backend + run: pnpm build:backend + + - name: Cache build artifacts + uses: actions/cache/save@v4.0.0 + with: + path: ./packages/**/dist + key: ${{ github.sha }}:workflow-tests + + run-test-workflows: + name: Workflow Tests + runs-on: ubuntu-latest + needs: build + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4.1.1 + - run: corepack enable + - uses: actions/setup-node@v4.0.2 + with: + node-version: 20.x + cache: 'pnpm' + - run: pnpm install --frozen-lockfile + + - name: Setup build cache + uses: rharkor/caching-for-turbo@v1.5 + + - name: Restore cached build artifacts + uses: actions/cache/restore@v4.0.0 + with: + path: ./packages/**/dist + key: ${{ github.sha }}:workflow-tests + + - name: Install OS dependencies + run: | + sudo apt update -y + echo 'tzdata tzdata/Areas select Europe' | sudo debconf-set-selections + echo 'tzdata tzdata/Zones/Europe select Paris' | sudo debconf-set-selections + DEBIAN_FRONTEND="noninteractive" sudo apt-get install -y graphicsmagick - name: Checkout workflows repo uses: actions/checkout@v4.1.1 @@ -23,77 +75,34 @@ jobs: repository: n8n-io/test-workflows path: test-workflows - - run: corepack enable - working-directory: n8n - - - uses: actions/setup-node@v4.0.2 - with: - node-version: 20.x - cache: 'pnpm' - cache-dependency-path: 'n8n/pnpm-lock.yaml' - - - name: Install dependencies - run: | - sudo apt update -y - echo 'tzdata tzdata/Areas select Europe' | sudo debconf-set-selections - echo 'tzdata tzdata/Zones/Europe select Paris' | sudo debconf-set-selections - DEBIAN_FRONTEND="noninteractive" sudo apt-get install -y graphicsmagick - shell: bash - - - name: pnpm install and build - working-directory: n8n - run: | - pnpm install --frozen-lockfile - pnpm build:backend - shell: bash - - name: Import credentials - run: n8n/packages/cli/bin/n8n import:credentials --input=test-workflows/credentials.json - shell: bash + run: packages/cli/bin/n8n import:credentials --input=test-workflows/credentials.json env: N8N_ENCRYPTION_KEY: ${{secrets.ENCRYPTION_KEY}} - name: Import workflows - run: n8n/packages/cli/bin/n8n import:workflow --separate --input=test-workflows/workflows - shell: bash + run: packages/cli/bin/n8n import:workflow --separate --input=test-workflows/workflows env: N8N_ENCRYPTION_KEY: ${{secrets.ENCRYPTION_KEY}} - name: Copy static assets run: | - cp n8n/assets/n8n-logo.png /tmp/n8n-logo.png - cp n8n/assets/n8n-screenshot.png /tmp/n8n-screenshot.png + cp assets/n8n-logo.png /tmp/n8n-logo.png + cp assets/n8n-screenshot.png /tmp/n8n-screenshot.png cp test-workflows/testData/pdfs/*.pdf /tmp/ - shell: bash - name: Run tests - run: n8n/packages/cli/bin/n8n executeBatch --shallow --skipList=test-workflows/skipList.txt --githubWorkflow --shortOutput --concurrency=16 --compare=test-workflows/snapshots - shell: bash + run: packages/cli/bin/n8n executeBatch --shallow --skipList=test-workflows/skipList.txt --githubWorkflow --shortOutput --concurrency=16 --compare=test-workflows/snapshots id: tests env: N8N_ENCRYPTION_KEY: ${{secrets.ENCRYPTION_KEY}} SKIP_STATISTICS_EVENTS: true DB_SQLITE_POOL_SIZE: 4 - # - - # name: Export credentials - # if: always() - # run: n8n/packages/cli/bin/n8n export:credentials --output=test-workflows/credentials.json --all --pretty - # shell: bash - # env: - # N8N_ENCRYPTION_KEY: ${{secrets.ENCRYPTION_KEY}} - # - - # name: Commit and push credential changes - # if: always() - # run: | - # cd test-workflows - # git config --global user.name 'n8n test bot' - # git config --global user.email 'n8n-test-bot@users.noreply.github.com' - # git commit -am "Automated credential update" - # git push --force --quiet "https://janober:${{ secrets.TOKEN }}@github.com/n8n-io/test-workflows.git" main:main + N8N_SENTRY_DSN: ${{secrets.CI_SENTRY_DSN}} - name: Notify Slack on failure uses: act10ns/slack@v2.0.0 - if: failure() + if: failure() && github.ref == 'refs/heads/master' with: status: ${{ job.status }} channel: '#alerts-build' diff --git a/.github/workflows/units-tests-reusable.yml b/.github/workflows/units-tests-reusable.yml index 60bf593e82..6475dcccc5 100644 --- a/.github/workflows/units-tests-reusable.yml +++ b/.github/workflows/units-tests-reusable.yml @@ -49,7 +49,6 @@ jobs: run: pnpm install --frozen-lockfile - name: Setup build cache - if: inputs.collectCoverage != true uses: rharkor/caching-for-turbo@v1.5 - name: Build diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 681de6c024..0c5abcba47 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -7,6 +7,7 @@ "EditorConfig.EditorConfig", "esbenp.prettier-vscode", "mjmlio.vscode-mjml", - "Vue.volar" + "Vue.volar", + "vitest.explorer" ] } diff --git a/.vscode/launch.json b/.vscode/launch.json index 448d745236..5501fe4439 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -47,9 +47,7 @@ "request": "launch", "skipFiles": ["/**"], "type": "node", - "env": { - // "N8N_PORT": "5679", - }, + "envFile": "${workspaceFolder}/.env", "outputCapture": "std", "killBehavior": "polite" }, diff --git a/CHANGELOG.md b/CHANGELOG.md index b1558420d0..9e5ffd4e1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,423 @@ +# [1.73.0](https://github.com/n8n-io/n8n/compare/n8n@1.72.0...n8n@1.73.0) (2024-12-19) + + +### Bug Fixes + +* **core:** Ensure runners do not throw on unsupported console methods ([#12167](https://github.com/n8n-io/n8n/issues/12167)) ([57c6a61](https://github.com/n8n-io/n8n/commit/57c6a6167dd2b30f0082a416daefce994ecad33a)) +* **core:** Fix `$getWorkflowStaticData` on task runners ([#12153](https://github.com/n8n-io/n8n/issues/12153)) ([b479f14](https://github.com/n8n-io/n8n/commit/b479f14ef5012551b823bea5d2ffbddedfd50a77)) +* **core:** Fix binary data helpers (like `prepareBinaryData`) with task runner ([#12259](https://github.com/n8n-io/n8n/issues/12259)) ([0f1461f](https://github.com/n8n-io/n8n/commit/0f1461f2d5d7ec34236ed7fcec3e2f9ee7eb73c4)) +* **core:** Fix race condition in AI tool invocation with multiple items from the parent ([#12169](https://github.com/n8n-io/n8n/issues/12169)) ([dce0c58](https://github.com/n8n-io/n8n/commit/dce0c58f8605c33fc50ec8aa422f0fb5eee07637)) +* **core:** Fix serialization of circular json with task runner ([#12288](https://github.com/n8n-io/n8n/issues/12288)) ([a99d726](https://github.com/n8n-io/n8n/commit/a99d726f42d027b64f94eda0d385b597c5d5be2e)) +* **core:** Upgrade nanoid to address CVE-2024-55565 ([#12171](https://github.com/n8n-io/n8n/issues/12171)) ([8c0bd02](https://github.com/n8n-io/n8n/commit/8c0bd0200c386b122f495c453ccc97a001e4729c)) +* **editor:** Add new create first project CTA ([#12189](https://github.com/n8n-io/n8n/issues/12189)) ([878b419](https://github.com/n8n-io/n8n/commit/878b41904d76eda3ee230f850127b4d56993de24)) +* **editor:** Fix canvas ready opacity transition on new canvas ([#12264](https://github.com/n8n-io/n8n/issues/12264)) ([5d33a6b](https://github.com/n8n-io/n8n/commit/5d33a6ba8a2bccea097402fd04c0e2b00e423e76)) +* **editor:** Fix rendering of code-blocks in sticky notes ([#12227](https://github.com/n8n-io/n8n/issues/12227)) ([9b59035](https://github.com/n8n-io/n8n/commit/9b5903524b95bd21d5915908780942790cf88d27)) +* **editor:** Fix sticky color picker getting covered by nodes on new canvas ([#12263](https://github.com/n8n-io/n8n/issues/12263)) ([27bd3c8](https://github.com/n8n-io/n8n/commit/27bd3c85b3a4ddcf763a543b232069bb108130cf)) +* **editor:** Improve commit modal user facing messaging ([#12161](https://github.com/n8n-io/n8n/issues/12161)) ([ad39243](https://github.com/n8n-io/n8n/commit/ad392439826b17bd0b84f981e0958d88f09e7fe9)) +* **editor:** Prevent connection line from showing when clicking the plus button of a node ([#12265](https://github.com/n8n-io/n8n/issues/12265)) ([9180b46](https://github.com/n8n-io/n8n/commit/9180b46b52302b203eecf3bb81c3f2132527a1e6)) +* **editor:** Prevent stickies from being edited in preview mode in the new canvas ([#12222](https://github.com/n8n-io/n8n/issues/12222)) ([6706dcd](https://github.com/n8n-io/n8n/commit/6706dcdf72d54f33c1cf4956602c3a64a1578826)) +* **editor:** Reduce cases for Auto-Add of ChatTrigger for AI Agents ([#12154](https://github.com/n8n-io/n8n/issues/12154)) ([365e82d](https://github.com/n8n-io/n8n/commit/365e82d2008dff2f9c91664ee04d7a78363a8b30)) +* **editor:** Remove invalid connections after node handles change ([#12247](https://github.com/n8n-io/n8n/issues/12247)) ([6330bec](https://github.com/n8n-io/n8n/commit/6330bec4db0175b558f2747837323fdbb25b634a)) +* **editor:** Set dangerouslyUseHTMLString in composable ([#12280](https://github.com/n8n-io/n8n/issues/12280)) ([6ba91b5](https://github.com/n8n-io/n8n/commit/6ba91b5e1ed197c67146347a6f6e663ecdf3de48)) +* **editor:** Set RunData outputIndex based on incoming data ([#12182](https://github.com/n8n-io/n8n/issues/12182)) ([dc4261a](https://github.com/n8n-io/n8n/commit/dc4261ae7eca6cf277404cd514c90fad42f14ae0)) +* **editor:** Update the universal create button interaction ([#12105](https://github.com/n8n-io/n8n/issues/12105)) ([5300e0a](https://github.com/n8n-io/n8n/commit/5300e0ac45bf832b3d2957198a49a1c687f3fe1f)) +* **Elasticsearch Node:** Fix issue stopping search queries being sent ([#11464](https://github.com/n8n-io/n8n/issues/11464)) ([388a83d](https://github.com/n8n-io/n8n/commit/388a83dfbdc6ac301e4df704666df9f09fb7d0b3)) +* **Extract from File Node:** Detect file encoding ([#12081](https://github.com/n8n-io/n8n/issues/12081)) ([92af245](https://github.com/n8n-io/n8n/commit/92af245d1aab5bfad8618fda69b2405f5206875d)) +* **Github Node:** Fix fetch of file names with ? character ([#12206](https://github.com/n8n-io/n8n/issues/12206)) ([39462ab](https://github.com/n8n-io/n8n/commit/39462abe1fde7e82b5e5b8f3ceebfcadbfd7c925)) +* **Invoice Ninja Node:** Fix actions for bank transactions ([#11511](https://github.com/n8n-io/n8n/issues/11511)) ([80eea49](https://github.com/n8n-io/n8n/commit/80eea49cf0bf9db438eb85af7cd22aeb11fbfed2)) +* **Linear Node:** Fix issue with error handling ([#12191](https://github.com/n8n-io/n8n/issues/12191)) ([b8eae5f](https://github.com/n8n-io/n8n/commit/b8eae5f28a7d523195f4715cd8da77b3a884ae4c)) +* **MongoDB Node:** Fix checks on projection feature call ([#10563](https://github.com/n8n-io/n8n/issues/10563)) ([58bab46](https://github.com/n8n-io/n8n/commit/58bab461c4c5026b2ca5ea143cbcf98bf3a4ced8)) +* **Postgres Node:** Allow users to wrap strings with $$ ([#12034](https://github.com/n8n-io/n8n/issues/12034)) ([0c15e30](https://github.com/n8n-io/n8n/commit/0c15e30778cc5cb10ed368df144d6fbb2504ec70)) +* **Redis Node:** Add support for username auth ([#12274](https://github.com/n8n-io/n8n/issues/12274)) ([64c0414](https://github.com/n8n-io/n8n/commit/64c0414ef28acf0f7ec42b4b0bb21cbf2921ebe7)) + + +### Features + +* Add solarwinds ipam credentials ([#12005](https://github.com/n8n-io/n8n/issues/12005)) ([882484e](https://github.com/n8n-io/n8n/commit/882484e8ee7d1841d5d600414ca48e9915abcfa8)) +* Add SolarWinds Observability node credentials ([#11805](https://github.com/n8n-io/n8n/issues/11805)) ([e8a5db5](https://github.com/n8n-io/n8n/commit/e8a5db5beb572edbb61dd9100b70827ccc4cca58)) +* **AI Agent Node:** Update descriptions and titles for Chat Trigger options in AI Agents and Memory ([#12155](https://github.com/n8n-io/n8n/issues/12155)) ([07a6ae1](https://github.com/n8n-io/n8n/commit/07a6ae11b3291c1805553d55ba089fe8dd919fd8)) +* **API:** Exclude pinned data from workflows ([#12261](https://github.com/n8n-io/n8n/issues/12261)) ([e0dc385](https://github.com/n8n-io/n8n/commit/e0dc385f8bc8ee13fbc5bbf35e07654e52b193e9)) +* **editor:** Params pane collection improvements ([#11607](https://github.com/n8n-io/n8n/issues/11607)) ([6e44c71](https://github.com/n8n-io/n8n/commit/6e44c71c9ca82cce20eb55bb9003930bbf66a16c)) +* **editor:** Support adding nodes via drag and drop from node creator on new canvas ([#12197](https://github.com/n8n-io/n8n/issues/12197)) ([1bfd9c0](https://github.com/n8n-io/n8n/commit/1bfd9c0e913f3eefc4593f6c344db1ae1f6e4df4)) +* **Facebook Graph API Node:** Update node to support API v21.0 ([#12116](https://github.com/n8n-io/n8n/issues/12116)) ([14c33f6](https://github.com/n8n-io/n8n/commit/14c33f666fe92f7173e4f471fb478e629e775c62)) +* **Linear Trigger Node:** Add support for admin scope ([#12211](https://github.com/n8n-io/n8n/issues/12211)) ([410ea9a](https://github.com/n8n-io/n8n/commit/410ea9a2ef2e14b5e8e4493e5db66cfc2290d8f6)) +* **MailerLite Node:** Update node to support new api ([#11933](https://github.com/n8n-io/n8n/issues/11933)) ([d6b8e65](https://github.com/n8n-io/n8n/commit/d6b8e65abeb411f86538c1630dcce832ee0846a9)) +* Send and wait operation - freeText and customForm response types ([#12106](https://github.com/n8n-io/n8n/issues/12106)) ([e98c7f1](https://github.com/n8n-io/n8n/commit/e98c7f160b018243dc88490d46fb1047a4d7fcdc)) + + +### Performance Improvements + +* **editor:** SchemaView performance improvement by ≈90% 🚀 ([#12180](https://github.com/n8n-io/n8n/issues/12180)) ([6a58309](https://github.com/n8n-io/n8n/commit/6a5830959f5fb493a4119869b8298d8ed702c84a)) + + + +# [1.72.0](https://github.com/n8n-io/n8n/compare/n8n@1.71.0...n8n@1.72.0) (2024-12-11) + + +### Bug Fixes + +* Allow disabling MFA with recovery codes ([#12014](https://github.com/n8n-io/n8n/issues/12014)) ([95d56fe](https://github.com/n8n-io/n8n/commit/95d56fee8d0168b75fca6dcf41702d2f10c930a8)) +* Chat triggers don't work with the new partial execution flow ([#11952](https://github.com/n8n-io/n8n/issues/11952)) ([2b6a72f](https://github.com/n8n-io/n8n/commit/2b6a72f1289c01145edf2b88e5027d2b9b2ed624)) +* **core:** Execute nodes after loops correctly with the new partial execution flow ([#11978](https://github.com/n8n-io/n8n/issues/11978)) ([891dd7f](https://github.com/n8n-io/n8n/commit/891dd7f995c78a2355a049b7ced981a5f6b1c40c)) +* **core:** Fix support for multiple invocation of AI tools ([#12141](https://github.com/n8n-io/n8n/issues/12141)) ([c572c06](https://github.com/n8n-io/n8n/commit/c572c0648ca5b644b222157b3cabac9c05704a84)) +* **core:** Make sure task runner exits ([#12123](https://github.com/n8n-io/n8n/issues/12123)) ([c5effca](https://github.com/n8n-io/n8n/commit/c5effca7d47a713f157eea21d7892002e9ab7283)) +* **core:** Remove run data of nodes unrelated to the current partial execution ([#12099](https://github.com/n8n-io/n8n/issues/12099)) ([c4e4d37](https://github.com/n8n-io/n8n/commit/c4e4d37a8785d1a4bcd376cb1c49b82a80aa4391)) +* **core:** Return homeProject when filtering workflows by project id ([#12077](https://github.com/n8n-io/n8n/issues/12077)) ([efafeed](https://github.com/n8n-io/n8n/commit/efafeed33482100a23fa0163a53b9ce93cd6b2c3)) +* **editor:** Don't reset all Parameter Inputs when switched to read-only ([#12063](https://github.com/n8n-io/n8n/issues/12063)) ([706702d](https://github.com/n8n-io/n8n/commit/706702dff8da3c2e949e2c98dd5b34b299a1f17c)) +* **editor:** Fix canvas panning using `Control` + `Left Mouse Button` on Windows ([#12104](https://github.com/n8n-io/n8n/issues/12104)) ([43009b6](https://github.com/n8n-io/n8n/commit/43009b6aa820f24b9e6f519e7a45592aa21db03e)) +* **editor:** Fix Nodeview.v2 reinitialise based on route changes ([#12062](https://github.com/n8n-io/n8n/issues/12062)) ([b1f8663](https://github.com/n8n-io/n8n/commit/b1f866326574974eb2936e6b02771346e83e7137)) +* **editor:** Fix svg background pattern rendering on safari ([#12079](https://github.com/n8n-io/n8n/issues/12079)) ([596f221](https://github.com/n8n-io/n8n/commit/596f22103c01e14063ebb2388c4dabf4714d37c6)) +* **editor:** Fix switching from v2 to v1 ([#12050](https://github.com/n8n-io/n8n/issues/12050)) ([5c76de3](https://github.com/n8n-io/n8n/commit/5c76de324c2e25b0d8b74cdab79f04aa616d8c4f)) +* **editor:** Improvements to the commit modal ([#12031](https://github.com/n8n-io/n8n/issues/12031)) ([4fe1952](https://github.com/n8n-io/n8n/commit/4fe1952e2fb3379d95da42a7bb531851af6d0094)) +* **editor:** Load node types in demo and preview modes ([#12048](https://github.com/n8n-io/n8n/issues/12048)) ([4ac5f95](https://github.com/n8n-io/n8n/commit/4ac5f9527bbec382a65ed3f1d9c41d6948c154e3)) +* **editor:** Polyfill crypto.randomUUID ([#12052](https://github.com/n8n-io/n8n/issues/12052)) ([0537524](https://github.com/n8n-io/n8n/commit/0537524c3e45d7633415c7a9175a3857ad52cd58)) +* **editor:** Redirect Settings to the proper sub page depending on the instance type (cloud or not) ([#12053](https://github.com/n8n-io/n8n/issues/12053)) ([a16d006](https://github.com/n8n-io/n8n/commit/a16d006f893cac927d674fa447b08c1205b67c54)) +* **editor:** Render sanitized HTML content in toast messages ([#12139](https://github.com/n8n-io/n8n/issues/12139)) ([0468945](https://github.com/n8n-io/n8n/commit/0468945c99f083577c4cc71f671b4b950f6aeb86)) +* **editor:** Universal button snags ([#11974](https://github.com/n8n-io/n8n/issues/11974)) ([956b11a](https://github.com/n8n-io/n8n/commit/956b11a560528336a74be40f722fa05bf3cca94d)) +* **editor:** Update concurrency UI considering different types of instances ([#12068](https://github.com/n8n-io/n8n/issues/12068)) ([fa572bb](https://github.com/n8n-io/n8n/commit/fa572bbca4397b1cc42668530497444630ed17eb)) +* **FTP Node:** Fix issue with creating folders on rename ([#9340](https://github.com/n8n-io/n8n/issues/9340)) ([eb7d593](https://github.com/n8n-io/n8n/commit/eb7d5934ef8bc6e999d6de4c0b8025ce175df5dd)) +* **n8n Form Node:** Completion page display if EXECUTIONS_DATA_SAVE_ON_SUCCESS=none ([#11869](https://github.com/n8n-io/n8n/issues/11869)) ([f4c2523](https://github.com/n8n-io/n8n/commit/f4c252341985fe03927a2fd5d60ba846ec3dfc77)) +* **OpenAI Node:** Allow updating assistant files ([#12042](https://github.com/n8n-io/n8n/issues/12042)) ([7b20f8a](https://github.com/n8n-io/n8n/commit/7b20f8aaa8befd19dbad0af3bf1b881342c1fca5)) + + +### Features + +* **AI Transform Node:** Reduce payload size ([#11965](https://github.com/n8n-io/n8n/issues/11965)) ([d8ca8de](https://github.com/n8n-io/n8n/commit/d8ca8de13a4cbb856696873bdb56c66b12a5b027)) +* **core:** Add option to filter for empty variables ([#12112](https://github.com/n8n-io/n8n/issues/12112)) ([a63f0e8](https://github.com/n8n-io/n8n/commit/a63f0e878e21da9924451e2679939209b34b6583)) +* **core:** Cancel runner task on timeout in external mode ([#12101](https://github.com/n8n-io/n8n/issues/12101)) ([addb4fa](https://github.com/n8n-io/n8n/commit/addb4fa352c88d856e463bb2b7001173c4fd6a7d)) +* **core:** Parent workflows should wait for sub-workflows to finish ([#11985](https://github.com/n8n-io/n8n/issues/11985)) ([60b3dcc](https://github.com/n8n-io/n8n/commit/60b3dccf9317da6f3013be35a78ce21d0416ad80)) +* **editor:** Implementing the `Easy AI Workflow` experiment ([#12043](https://github.com/n8n-io/n8n/issues/12043)) ([67ed1d2](https://github.com/n8n-io/n8n/commit/67ed1d2c3c2e69d5a96daf7de2795c02f5d8f15b)) +* **Redis Node:** Add support for continue on fail / error output branch ([#11714](https://github.com/n8n-io/n8n/issues/11714)) ([ed35958](https://github.com/n8n-io/n8n/commit/ed359586c88a7662f4d94d58c5a87cf91d027ab9)) + + + +# [1.71.0](https://github.com/n8n-io/n8n/compare/n8n@1.70.0...n8n@1.71.0) (2024-12-04) + + +### Bug Fixes + +* **core:** Fix push for waiting executions ([#11984](https://github.com/n8n-io/n8n/issues/11984)) ([8d71307](https://github.com/n8n-io/n8n/commit/8d71307da0398e7e39bf53e8e1cfa21ac1ceaf69)) +* **core:** Improve header parameter parsing on http client responses ([#11953](https://github.com/n8n-io/n8n/issues/11953)) ([41e9e39](https://github.com/n8n-io/n8n/commit/41e9e39b5b53ecd9d8d1b385df65a26ecb9bccd8)) +* **core:** Opt-out from optimizations if `$item` is used ([#12036](https://github.com/n8n-io/n8n/issues/12036)) ([872535a](https://github.com/n8n-io/n8n/commit/872535a40c85dcfad3a4b27c57c026ae003f562f)) +* **core:** Use the configured timezone in task runner ([#12032](https://github.com/n8n-io/n8n/issues/12032)) ([2e6845a](https://github.com/n8n-io/n8n/commit/2e6845afcbc30dff73c3f3f15f21278cab397387)) +* **core:** Validate node name when creating `NodeOperationErrror` ([#11999](https://github.com/n8n-io/n8n/issues/11999)) ([e68c9da](https://github.com/n8n-io/n8n/commit/e68c9da30c31cd5f994cb01ce759192562bfbd40)) +* **editor:** Add execution concurrency info and paywall ([#11847](https://github.com/n8n-io/n8n/issues/11847)) ([57d3269](https://github.com/n8n-io/n8n/commit/57d3269e400ee4e7e3636614870ebdfdb0aa8c1d)) +* **editor:** Fix bug causing connection lines to disappear when hovering stickies ([#11950](https://github.com/n8n-io/n8n/issues/11950)) ([439a1cc](https://github.com/n8n-io/n8n/commit/439a1cc4f39243e91715b21a84b8e7266ce872cd)) +* **editor:** Fix canvas keybindings using splitter keys such as zooming using `+` key ([#12022](https://github.com/n8n-io/n8n/issues/12022)) ([6af9c82](https://github.com/n8n-io/n8n/commit/6af9c82af6020e99d61e442ee9c2d40761baf027)) +* **editor:** Fix community check ([#11979](https://github.com/n8n-io/n8n/issues/11979)) ([af0398a](https://github.com/n8n-io/n8n/commit/af0398a5e3a8987c01c7112e6f689b35e1ef92fe)) +* **editor:** Fix copy/paste keyboard events in canvas chat ([#12004](https://github.com/n8n-io/n8n/issues/12004)) ([967340a](https://github.com/n8n-io/n8n/commit/967340a2938a79c89319121bf57a8d654f88e06c)) +* **editor:** Fix node showing as successful if errors exists on subsequent runs ([#12019](https://github.com/n8n-io/n8n/issues/12019)) ([8616b17](https://github.com/n8n-io/n8n/commit/8616b17cc6c305da69bbb54fd56ab7cb34213f7c)) +* **editor:** Fix pin data showing up in production executions on new canvas ([#11951](https://github.com/n8n-io/n8n/issues/11951)) ([5f6f8a1](https://github.com/n8n-io/n8n/commit/5f6f8a1bddfd76b586c08da821e8b59070f449fc)) +* **editor:** Handle source control initialization to prevent UI form crashing ([#11776](https://github.com/n8n-io/n8n/issues/11776)) ([6be8e86](https://github.com/n8n-io/n8n/commit/6be8e86c45bd64d000bc95d2ef2d68220e930c02)) +* **editor:** Implement dirty nodes for partial executions ([#11739](https://github.com/n8n-io/n8n/issues/11739)) ([b8da4ff](https://github.com/n8n-io/n8n/commit/b8da4ff9edb0fbb0093c4c41fe11f8e67b696ca3)) +* **editor:** Resolve going back from Settings ([#11958](https://github.com/n8n-io/n8n/issues/11958)) ([d74423c](https://github.com/n8n-io/n8n/commit/d74423c75198d38d0d99a1879051b5e964ecae74)) +* **editor:** Unify executions card label color ([#11949](https://github.com/n8n-io/n8n/issues/11949)) ([fc79718](https://github.com/n8n-io/n8n/commit/fc797188d63e87df34b3a153eb4a0d0b7361b3f5)) +* **editor:** Use optional chaining for all members in execution data when using the debug feature ([#12024](https://github.com/n8n-io/n8n/issues/12024)) ([67aa0c9](https://github.com/n8n-io/n8n/commit/67aa0c9107bda16b1cb6d273e17c3cde77035f51)) +* **GraphQL Node:** Throw error if GraphQL variables are not objects or strings ([#11904](https://github.com/n8n-io/n8n/issues/11904)) ([85f30b2](https://github.com/n8n-io/n8n/commit/85f30b27ae282da58a25186d13ff17196dcd7d9c)) +* **HTTP Request Node:** Use iconv-lite to decode http responses, to support more encoding types ([#11930](https://github.com/n8n-io/n8n/issues/11930)) ([461b39c](https://github.com/n8n-io/n8n/commit/461b39c5df5dd446cb8ceef469b204c7c5111229)) +* Load workflows with unconnected Switch outputs ([#12020](https://github.com/n8n-io/n8n/issues/12020)) ([abc851c](https://github.com/n8n-io/n8n/commit/abc851c0cff298607a0dc2f2882aa17136898f45)) +* **n8n Form Node:** Use https to load google fonts ([#11948](https://github.com/n8n-io/n8n/issues/11948)) ([eccd924](https://github.com/n8n-io/n8n/commit/eccd924f5e8dbe59e37099d1a6fbe8866fef55bf)) +* **Telegram Trigger Node:** Fix header secret check ([#12018](https://github.com/n8n-io/n8n/issues/12018)) ([f16de4d](https://github.com/n8n-io/n8n/commit/f16de4db01c0496205635a3203a44098e7908453)) +* **Webflow Node:** Fix issue with pagination in v2 node ([#11934](https://github.com/n8n-io/n8n/issues/11934)) ([1eb94bc](https://github.com/n8n-io/n8n/commit/1eb94bcaf54d9e581856ce0b87253e1c28fa68e2)) +* **Webflow Node:** Fix issue with publishing items ([#11982](https://github.com/n8n-io/n8n/issues/11982)) ([0a8a57e](https://github.com/n8n-io/n8n/commit/0a8a57e4ec8081ab1a53f36d686b3d5dcaae2476)) + + +### Features + +* **AI Transform Node:** Node Prompt improvements ([#11611](https://github.com/n8n-io/n8n/issues/11611)) ([40a7445](https://github.com/n8n-io/n8n/commit/40a7445f0873af2cdbd10b12bd691c07a43e27cc)) +* **Code Node:** Warning if pairedItem absent or could not be auto mapped ([#11737](https://github.com/n8n-io/n8n/issues/11737)) ([3a5bd12](https://github.com/n8n-io/n8n/commit/3a5bd129459272cbac960ae2754db3028943f87e)) +* **editor:** Canvas chat UI & UX improvements ([#11924](https://github.com/n8n-io/n8n/issues/11924)) ([1e25774](https://github.com/n8n-io/n8n/commit/1e25774541461c86da5c4af8efec792e2814eeb1)) +* **editor:** Persist user's preferred display modes on localStorage ([#11929](https://github.com/n8n-io/n8n/issues/11929)) ([bd69316](https://github.com/n8n-io/n8n/commit/bd693162b86a21c90880bab2c2e67aab733095ff)) + + +### Performance Improvements + +* **editor:** Virtualize SchemaView ([#11694](https://github.com/n8n-io/n8n/issues/11694)) ([9c6def9](https://github.com/n8n-io/n8n/commit/9c6def91975764522fa52cdf21e9cb5bdb4d721d)) + + + +# [1.70.0](https://github.com/n8n-io/n8n/compare/n8n@1.69.0...n8n@1.70.0) (2024-11-27) + + +### Bug Fixes + +* **AI Agent Node:** Add binary message before scratchpad to prevent tool calling loops ([#11845](https://github.com/n8n-io/n8n/issues/11845)) ([5c80cb5](https://github.com/n8n-io/n8n/commit/5c80cb57cf709a1097a38e0394aad6fce5330eba)) +* CodeNodeEditor walk cannot read properties of null ([#11129](https://github.com/n8n-io/n8n/issues/11129)) ([d99e0a7](https://github.com/n8n-io/n8n/commit/d99e0a7c979a1ee96b2eea1b9011d5bce375289a)) +* **core:** Bring back execution data on the `executionFinished` push message ([#11821](https://github.com/n8n-io/n8n/issues/11821)) ([0313570](https://github.com/n8n-io/n8n/commit/03135702f18e750ba44840dccfec042270629a2b)) +* **core:** Correct invalid WS status code on removing connection ([#11901](https://github.com/n8n-io/n8n/issues/11901)) ([1d80225](https://github.com/n8n-io/n8n/commit/1d80225d26ba01f78934a455acdcca7b83be7205)) +* **core:** Don't use unbound context methods in code sandboxes ([#11914](https://github.com/n8n-io/n8n/issues/11914)) ([f6c0d04](https://github.com/n8n-io/n8n/commit/f6c0d045e9683cd04ee849f37b96697097c5b41d)) +* **core:** Fix broken execution query when using projectId ([#11852](https://github.com/n8n-io/n8n/issues/11852)) ([a061dbc](https://github.com/n8n-io/n8n/commit/a061dbca07ad686c563e85c56081bc1a7830259b)) +* **core:** Fix validation of items returned in the task runner ([#11897](https://github.com/n8n-io/n8n/issues/11897)) ([a535e88](https://github.com/n8n-io/n8n/commit/a535e88f1aec8fbbf2eb9397d38748f49773de2d)) +* **editor:** Add missing trigger waiting tooltip on new canvas ([#11918](https://github.com/n8n-io/n8n/issues/11918)) ([a8df221](https://github.com/n8n-io/n8n/commit/a8df221bfbb5428d93d03f539bcfdaf29ee20c21)) +* **editor:** Don't re-render input panel after node finishes executing ([#11813](https://github.com/n8n-io/n8n/issues/11813)) ([b3a99a2](https://github.com/n8n-io/n8n/commit/b3a99a2351079c37ed6d83f43920ba80f3832234)) +* **editor:** Fix AI assistant loading message layout ([#11819](https://github.com/n8n-io/n8n/issues/11819)) ([89b4807](https://github.com/n8n-io/n8n/commit/89b48072432753137b498c338af7777036fdde7a)) +* **editor:** Fix new canvas discovery tooltip position after adding github stars button ([#11898](https://github.com/n8n-io/n8n/issues/11898)) ([f4ab5c7](https://github.com/n8n-io/n8n/commit/f4ab5c7b9244b8fdde427c12c1a152fbaaba0c34)) +* **editor:** Fix node position not getting set when dragging selection on new canvas ([#11871](https://github.com/n8n-io/n8n/issues/11871)) ([595de81](https://github.com/n8n-io/n8n/commit/595de81c03b3e488ab41fb8d1d316c3db6a8372a)) +* **editor:** Restore workers view ([#11876](https://github.com/n8n-io/n8n/issues/11876)) ([3aa72f6](https://github.com/n8n-io/n8n/commit/3aa72f613f64c16d7dff67ffe66037894e45aa7c)) +* **editor:** Turn NPS survey into a modal and make sure it shows above the Ask AI button ([#11814](https://github.com/n8n-io/n8n/issues/11814)) ([ca169f3](https://github.com/n8n-io/n8n/commit/ca169f3f3455fa39ce9120b30d7b409bade6561e)) +* **editor:** Use `crypto.randomUUID()` to initialize node id if missing on new canvas ([#11873](https://github.com/n8n-io/n8n/issues/11873)) ([bc4857a](https://github.com/n8n-io/n8n/commit/bc4857a1b3d6ea389f11fb8246a1cee33b8a008e)) +* **n8n Form Node:** Duplicate popup in manual mode ([#11925](https://github.com/n8n-io/n8n/issues/11925)) ([2c34bf4](https://github.com/n8n-io/n8n/commit/2c34bf4ea6137fb0fb321969684ffa621da20fa3)) +* **n8n Form Node:** Redirect if completion page to trigger ([#11822](https://github.com/n8n-io/n8n/issues/11822)) ([1a8fb7b](https://github.com/n8n-io/n8n/commit/1a8fb7bdc428c6a23c8708e2dcf924f1f10b47a9)) +* **OpenAI Node:** Remove preview chatInput parameter for `Assistant:Messsage` operation ([#11825](https://github.com/n8n-io/n8n/issues/11825)) ([4dde287](https://github.com/n8n-io/n8n/commit/4dde287cde3af7c9c0e57248e96b8f1270da9332)) +* Retain execution data between partial executions (new flow) ([#11828](https://github.com/n8n-io/n8n/issues/11828)) ([3320436](https://github.com/n8n-io/n8n/commit/3320436a6fdf8472b3843b9fe8d4de7af7f5ef5c)) + + +### Features + +* Add SharePoint credentials ([#11570](https://github.com/n8n-io/n8n/issues/11570)) ([05c6109](https://github.com/n8n-io/n8n/commit/05c61091db9bdd62fdcca910ead50d0bd512966a)) +* Add Zabbix credential only node ([#11489](https://github.com/n8n-io/n8n/issues/11489)) ([fbd1ecf](https://github.com/n8n-io/n8n/commit/fbd1ecfb29461fee393914bc200ec72c654d8944)) +* **AI Transform Node:** Support for drag and drop ([#11276](https://github.com/n8n-io/n8n/issues/11276)) ([2c252b0](https://github.com/n8n-io/n8n/commit/2c252b0b2d5282f4a87bce76f93c4c02dd8ff5e3)) +* **editor:** Drop `response` wrapper requirement from Subworkflow Tool output ([#11785](https://github.com/n8n-io/n8n/issues/11785)) ([cd3598a](https://github.com/n8n-io/n8n/commit/cd3598aaab6cefe58a4cb9df7d93fb501415e9d3)) +* **editor:** Improve node and edge bring-to-front mechanism on new canvas ([#11793](https://github.com/n8n-io/n8n/issues/11793)) ([b89ca9d](https://github.com/n8n-io/n8n/commit/b89ca9d482faa5cb542898f3973fb6e7c9a8437a)) +* **editor:** Make new canvas connections go underneath node when looping backwards ([#11833](https://github.com/n8n-io/n8n/issues/11833)) ([91d1bd8](https://github.com/n8n-io/n8n/commit/91d1bd8d333454f3971605df73c3703102d2a9e9)) +* **editor:** Make the left sidebar in Expressions editor draggable ([#11838](https://github.com/n8n-io/n8n/issues/11838)) ([a713b3e](https://github.com/n8n-io/n8n/commit/a713b3ed25feb1790412fc320cf41a0967635263)) +* **editor:** Migrate existing users to new canvas and set new canvas as default ([#11896](https://github.com/n8n-io/n8n/issues/11896)) ([caa7447](https://github.com/n8n-io/n8n/commit/caa744785a2cc5063a5fb9d269c0ea53ea432298)) +* **Slack Node:** Update wait for approval to use markdown ([#11754](https://github.com/n8n-io/n8n/issues/11754)) ([40dd02f](https://github.com/n8n-io/n8n/commit/40dd02f360d0d8752fe89c4304c18cac9858c530)) + + + +# [1.69.0](https://github.com/n8n-io/n8n/compare/n8n@1.68.0...n8n@1.69.0) (2024-11-20) + + +### Bug Fixes + +* Add supported versions warning to Zep memory node ([#11803](https://github.com/n8n-io/n8n/issues/11803)) ([9cc5bc1](https://github.com/n8n-io/n8n/commit/9cc5bc1aef974fe6c2511c1597b90c8b54ba6b9c)) +* **AI Agent Node:** Escape curly brackets in tools description for non Tool agents ([#11772](https://github.com/n8n-io/n8n/issues/11772)) ([83abdfa](https://github.com/n8n-io/n8n/commit/83abdfaf027a0533824a3ac3e4bab3cad971821a)) +* **Anthropic Chat Model Node:** Update credentials test endpoint ([#11756](https://github.com/n8n-io/n8n/issues/11756)) ([6cf0aba](https://github.com/n8n-io/n8n/commit/6cf0abab5bcddb407571271b9f174e66bb209790)) +* **core:** Add missing env vars to task runner config ([#11810](https://github.com/n8n-io/n8n/issues/11810)) ([870c576](https://github.com/n8n-io/n8n/commit/870c576ed9d7ce4ef005db9c8bedd78e91084c9c)) +* **core:** Allow Azure's SAML metadata XML containing WS-Federation nodes to pass validation ([#11724](https://github.com/n8n-io/n8n/issues/11724)) ([3b62bd5](https://github.com/n8n-io/n8n/commit/3b62bd58c264be0225a74ae0eb35c4761c419b79)) +* **core:** Delete binary data parent folder when pruning executions ([#11790](https://github.com/n8n-io/n8n/issues/11790)) ([17ef2c6](https://github.com/n8n-io/n8n/commit/17ef2c63f69b811bdd28006df3b6edd446837971)) +* **core:** Fix `diagnostics.enabled` default value ([#11809](https://github.com/n8n-io/n8n/issues/11809)) ([5fa72b0](https://github.com/n8n-io/n8n/commit/5fa72b0512b00bdc6a1065b7b604c9640f469454)) +* **core:** Improve the security on OAuth callback endpoints ([#11593](https://github.com/n8n-io/n8n/issues/11593)) ([274fcf4](https://github.com/n8n-io/n8n/commit/274fcf45d393d8db1d2fb5ae1e774a4c9198a178)) +* **core:** Restore old names for pruning config keys ([#11782](https://github.com/n8n-io/n8n/issues/11782)) ([d15b8d0](https://github.com/n8n-io/n8n/commit/d15b8d05092d2ed9dd45fcfa34b4177f60469ebd)) +* **core:** Unload any existing version of a community nodes package before upgrading it ([#11727](https://github.com/n8n-io/n8n/issues/11727)) ([1d8fd13](https://github.com/n8n-io/n8n/commit/1d8fd13d841b73466ba5f8044d17d7199da7e856)) +* **editor:** Add documentation link to insufficient quota message ([#11777](https://github.com/n8n-io/n8n/issues/11777)) ([1987363](https://github.com/n8n-io/n8n/commit/1987363f7941285c51fda849a4ac92832368b25a)) +* **editor:** Add project header subtitle ([#11797](https://github.com/n8n-io/n8n/issues/11797)) ([ff4261c](https://github.com/n8n-io/n8n/commit/ff4261c16845c7de1790fdf0eaa9f57b37822289)) +* **editor:** Change Home label to Overview ([#11736](https://github.com/n8n-io/n8n/issues/11736)) ([1a78360](https://github.com/n8n-io/n8n/commit/1a783606b4ef22d85e173a2a780d5c49ff208932)) +* **editor:** Fix executions sorting ([#11808](https://github.com/n8n-io/n8n/issues/11808)) ([cd5ad65](https://github.com/n8n-io/n8n/commit/cd5ad65e90a3be4d67b51521772e0fceb7f4abc7)) +* **editor:** Fix partial executions not working due to broken push message queue and race conditions ([#11798](https://github.com/n8n-io/n8n/issues/11798)) ([b05d435](https://github.com/n8n-io/n8n/commit/b05d43519994abdd34a65462d14184c779d0b667)) +* **editor:** Fix reordered switch connections when copying nodes on new canvas ([#11788](https://github.com/n8n-io/n8n/issues/11788)) ([6c2dad7](https://github.com/n8n-io/n8n/commit/6c2dad79143f5b0c255ab8c97c3255314834c458)) +* **editor:** Fix the issue with RMC Values to Send collection disappears ([#11710](https://github.com/n8n-io/n8n/issues/11710)) ([7bb9002](https://github.com/n8n-io/n8n/commit/7bb9002cbc10cf58550f53a30c6fd7151f8e7355)) +* **editor:** Improve formatting of expired trial error message ([#11708](https://github.com/n8n-io/n8n/issues/11708)) ([8a0ad0f](https://github.com/n8n-io/n8n/commit/8a0ad0f910feeada6d0c63e81c3e97a1a6e44de7)) +* **editor:** Optimize application layout ([#11769](https://github.com/n8n-io/n8n/issues/11769)) ([91f9390](https://github.com/n8n-io/n8n/commit/91f9390b90a68d064ea00d10505bf3767ddec1d4)) +* **Google Sheets Trigger Node:** Fix issue with regex showing correct sheet as invalid ([#11770](https://github.com/n8n-io/n8n/issues/11770)) ([d5ba1a0](https://github.com/n8n-io/n8n/commit/d5ba1a059b7a67154f17f8ad3fcfe66c5c031059)) +* **HTTP Request Node:** Continue using error ([#11733](https://github.com/n8n-io/n8n/issues/11733)) ([d1bae1a](https://github.com/n8n-io/n8n/commit/d1bae1ace062dd5b64087e0313e78599b5994355)) +* **n8n Form Node:** Support expressions in completion page ([#11781](https://github.com/n8n-io/n8n/issues/11781)) ([1099167](https://github.com/n8n-io/n8n/commit/10991675fe2e6913e8f03d565b670257941f18e5)) +* Prevent workflow to run if active and single webhook service ([#11752](https://github.com/n8n-io/n8n/issues/11752)) ([bcb9a20](https://github.com/n8n-io/n8n/commit/bcb9a2078186ff80e03ca3b8532d3585c307d86b)) +* **Read/Write Files from Disk Node:** Escape parenthesis when reading file ([#11753](https://github.com/n8n-io/n8n/issues/11753)) ([285534e](https://github.com/n8n-io/n8n/commit/285534e6d0ceb60290bd0a928054e494252148fe)) +* **YouTube Node:** Issue in published before and after dates filters ([#11741](https://github.com/n8n-io/n8n/issues/11741)) ([7381c28](https://github.com/n8n-io/n8n/commit/7381c28af00148b329690021b921267a48a6eaa3)) + + +### Features + +* **core:** Improve debugging of sub-workflows ([#11602](https://github.com/n8n-io/n8n/issues/11602)) ([fd3254d](https://github.com/n8n-io/n8n/commit/fd3254d5874a03b57421246b77a519787536a93e)) +* **core:** Improve handling of manual executions with wait nodes ([#11750](https://github.com/n8n-io/n8n/issues/11750)) ([61696c3](https://github.com/n8n-io/n8n/commit/61696c3db313cdc97925af728ff5c68415f9b6b2)) +* **editor:** Add Info Note to NDV Output Panel if no existing Tools were used during Execution ([#11672](https://github.com/n8n-io/n8n/issues/11672)) ([de0e861](https://github.com/n8n-io/n8n/commit/de0e86150f4d0615481e5ec3869465cfd1ce822f)) +* **editor:** Add option to create sub workflow from workflows list in `Execute Workflow` node ([#11706](https://github.com/n8n-io/n8n/issues/11706)) ([c265d44](https://github.com/n8n-io/n8n/commit/c265d44841eb147115563ce24c56666b1e321433)) +* **editor:** Add selection navigation using the keyboard on new canvas ([#11679](https://github.com/n8n-io/n8n/issues/11679)) ([6cd9b99](https://github.com/n8n-io/n8n/commit/6cd9b996af0406caf65941503276524de9e2add4)) +* **editor:** Add universal Create Resource Menu ([#11564](https://github.com/n8n-io/n8n/issues/11564)) ([b38ce14](https://github.com/n8n-io/n8n/commit/b38ce14ec94d74aa1c9780a0572804ff6266588d)) +* **Embeddings Azure OpenAI Node, Azure OpenAI Chat Model Node:** Add support for basePath url in Azure Open AI nodes ([#11784](https://github.com/n8n-io/n8n/issues/11784)) ([e298ebe](https://github.com/n8n-io/n8n/commit/e298ebe90d69f466ee897855472eaa7be1d96aba)) +* **Embeddings OpenAI Node, Embeddings Azure OpenAI Node:** Add dimensions option ([#11773](https://github.com/n8n-io/n8n/issues/11773)) ([de01a8a](https://github.com/n8n-io/n8n/commit/de01a8a01d37f33ab8bcbc65588cafebda969922)) +* GitHub stars dismiss button ([#11794](https://github.com/n8n-io/n8n/issues/11794)) ([8fbad74](https://github.com/n8n-io/n8n/commit/8fbad74ab685c2ba0395c30cee0ddf9498fb8984)) + + + +# [1.68.0](https://github.com/n8n-io/n8n/compare/n8n@1.67.0...n8n@1.68.0) (2024-11-13) + + +### Bug Fixes + +* **AI Agent Node:** Throw better errors for non-tool agents when using structured tools ([#11582](https://github.com/n8n-io/n8n/issues/11582)) ([9b6123d](https://github.com/n8n-io/n8n/commit/9b6123dfb2648f880c7829211fa07666611ad0ea)) +* **Auto-fixing Output Parser Node:** Only run retry chain on parsing errors ([#11569](https://github.com/n8n-io/n8n/issues/11569)) ([21b31e4](https://github.com/n8n-io/n8n/commit/21b31e488ff6ab0bcf3c79edcd17b9e37d4c64a4)) +* **core:** Continue with error output reverse items in success branch ([#11684](https://github.com/n8n-io/n8n/issues/11684)) ([6d5ee83](https://github.com/n8n-io/n8n/commit/6d5ee832966fab96043b0d65697c059ced61d334)) +* **core:** Ensure task runner server closes websocket connection correctly ([#11633](https://github.com/n8n-io/n8n/issues/11633)) ([b496bf3](https://github.com/n8n-io/n8n/commit/b496bf3147d2cd873d24371be02cb7ea5dbd8621)) +* **core:** Handle websocket connection error more gracefully in task runners ([#11635](https://github.com/n8n-io/n8n/issues/11635)) ([af7d6e6](https://github.com/n8n-io/n8n/commit/af7d6e68d0436ff8a3d4e8410dc8ee4f3a035c44)) +* **core:** Improve model sub-nodes error handling ([#11418](https://github.com/n8n-io/n8n/issues/11418)) ([57467d0](https://github.com/n8n-io/n8n/commit/57467d0285d67509322630c4c01130022f274a41)) +* **core:** Make push work for waiting webhooks ([#11678](https://github.com/n8n-io/n8n/issues/11678)) ([600479b](https://github.com/n8n-io/n8n/commit/600479bf36ba8870d4aecacad19a2dc5f2d97959)) +* **core:** Revert all the context helpers changes ([#11616](https://github.com/n8n-io/n8n/issues/11616)) ([20fd38f](https://github.com/n8n-io/n8n/commit/20fd38f3517f7ef35604ba16abb4d951270b4d50)) +* **core:** Set the authentication methad to `email` during startup if the SAML configuration in the database has been corrupted ([#11600](https://github.com/n8n-io/n8n/issues/11600)) ([6439291](https://github.com/n8n-io/n8n/commit/6439291738dec16261979d6d835acbc63743d51a)) +* **core:** Use cached value in retrieval of personal project owner ([#11533](https://github.com/n8n-io/n8n/issues/11533)) ([04029d8](https://github.com/n8n-io/n8n/commit/04029d82a11b52990890380ba7094055b18e7c1f)) +* Credentials save button is hidden unless you make changes to the ([#11492](https://github.com/n8n-io/n8n/issues/11492)) ([835fbfe](https://github.com/n8n-io/n8n/commit/835fbfe337dd8dc0d0b0318c7227e174484e1328)) +* **editor:** Add stickies to node insert position conflict check allowlist ([#11624](https://github.com/n8n-io/n8n/issues/11624)) ([fc39e3c](https://github.com/n8n-io/n8n/commit/fc39e3ca16231c176957e2504d55df6b416874fe)) +* **editor:** Adjust Scrollbar Width of RunData Header Row ([#11561](https://github.com/n8n-io/n8n/issues/11561)) ([d17d76a](https://github.com/n8n-io/n8n/commit/d17d76a85d5425bc091d29fc84605ffbccbca984)) +* **editor:** Cap NDV Output View Tab Index to prevent rare edge case ([#11614](https://github.com/n8n-io/n8n/issues/11614)) ([a6c8ee4](https://github.com/n8n-io/n8n/commit/a6c8ee4a82e6055766dc1307f79c774c17bb5f4d)) +* **editor:** Do not show hover tooltip when autocomplete is active ([#11653](https://github.com/n8n-io/n8n/issues/11653)) ([23caf43](https://github.com/n8n-io/n8n/commit/23caf43e30342a21d45c825f438aa1e6193601d1)) +* **editor:** Enable pinning main output with error and always allow unpinning ([#11452](https://github.com/n8n-io/n8n/issues/11452)) ([40c8882](https://github.com/n8n-io/n8n/commit/40c88822acdcda6401bd92b9cf89d013c44b8453)) +* **editor:** Fix collapsing nested items in expression modal schema view ([#11645](https://github.com/n8n-io/n8n/issues/11645)) ([41dea52](https://github.com/n8n-io/n8n/commit/41dea522fbfb1c9acee51f47f384973914454b5f)) +* **editor:** Fix default workflow settings ([#11632](https://github.com/n8n-io/n8n/issues/11632)) ([658568e](https://github.com/n8n-io/n8n/commit/658568e2700bfd5b61da53f3052403d0098c2d90)) +* **editor:** Fix duplicate chat trigger ([#11693](https://github.com/n8n-io/n8n/issues/11693)) ([a025848](https://github.com/n8n-io/n8n/commit/a025848ec4be96f74d4de2ab104256b6d89bb837)) +* **editor:** Fix hiding SQL query output when trying to select ([#11649](https://github.com/n8n-io/n8n/issues/11649)) ([4dbf2f4](https://github.com/n8n-io/n8n/commit/4dbf2f4256111985b367030020f1494b8a8b95af)) +* **editor:** Fix scrolling in code edit modal ([#11647](https://github.com/n8n-io/n8n/issues/11647)) ([8f695f3](https://github.com/n8n-io/n8n/commit/8f695f3417820e7b9bb04b78972f6abbd61abbe8)) +* **editor:** Prevent error being thrown in RLC while loading ([#11676](https://github.com/n8n-io/n8n/issues/11676)) ([ca8cb45](https://github.com/n8n-io/n8n/commit/ca8cb455ba59831295c238afb11aeab6ad18428e)) +* **editor:** Prevent NodeCreator from swallowing AskAssistant enter event ([#11532](https://github.com/n8n-io/n8n/issues/11532)) ([db94f16](https://github.com/n8n-io/n8n/commit/db94f169fcd03983fc78a3b4c5e11543610825bf)) +* **editor:** Show node executing status shortly before switching to success on new canvas ([#11675](https://github.com/n8n-io/n8n/issues/11675)) ([b0ba24c](https://github.com/n8n-io/n8n/commit/b0ba24cbbc55cebc26e9f62ead7396c4c8fbd062)) +* **editor:** Show only error title and 'Open errored node' button; hide 'Ask Assistant' in root for sub-node errors ([#11573](https://github.com/n8n-io/n8n/issues/11573)) ([8cba100](https://github.com/n8n-io/n8n/commit/8cba1004888f60ca653ee069501c13b3cadcc561)) +* **Facebook Lead Ads Trigger Node:** Fix issue with optional fields ([#11692](https://github.com/n8n-io/n8n/issues/11692)) ([70d315b](https://github.com/n8n-io/n8n/commit/70d315b3d5b8f5f3e0f39527bba11e254a52028e)) +* **Google BigQuery Node:** Add item index to insert error ([#11702](https://github.com/n8n-io/n8n/issues/11702)) ([145d092](https://github.com/n8n-io/n8n/commit/145d0921b217bbd4b625beaacfa14429560bf51b)) +* **Google Drive Node:** Fix file upload for streams ([#11698](https://github.com/n8n-io/n8n/issues/11698)) ([770230f](https://github.com/n8n-io/n8n/commit/770230fbfe0b9e86527254e201c4602fbced94ff)) +* **In-Memory Vector Store Node:** Fix displaying execution data of connected embedding nodes ([#11701](https://github.com/n8n-io/n8n/issues/11701)) ([40ade15](https://github.com/n8n-io/n8n/commit/40ade151724f4af28a6ed959fd9363450ea711fd)) +* **Item List Output Parser Node:** Fix number of items parameter issue ([#11696](https://github.com/n8n-io/n8n/issues/11696)) ([01ebe9d](https://github.com/n8n-io/n8n/commit/01ebe9dd38629afbab954fb489f3ef2bb7ab5b34)) +* **n8n Form Node:** Find completion page ([#11674](https://github.com/n8n-io/n8n/issues/11674)) ([ed3ad6d](https://github.com/n8n-io/n8n/commit/ed3ad6d684597f7c4b7419dfa81d476e66f10eba)) +* **n8n Form Node:** Open form page if form trigger has pin data ([#11673](https://github.com/n8n-io/n8n/issues/11673)) ([f0492bd](https://github.com/n8n-io/n8n/commit/f0492bd3bb0d94802a2707fb1cf861313b6ea808)) +* **n8n Form Node:** Trigger page stack in waiting if error in workflow ([#11671](https://github.com/n8n-io/n8n/issues/11671)) ([94b5873](https://github.com/n8n-io/n8n/commit/94b5873248212a5500f02cf3c0d74df6f9d8fb26)) +* **n8n Form Trigger Node:** Checkboxes different sizes ([#11677](https://github.com/n8n-io/n8n/issues/11677)) ([c08d23c](https://github.com/n8n-io/n8n/commit/c08d23c00f01bb6fcb3b75f02e0338af375f9b32)) +* NDV search bugs ([#11613](https://github.com/n8n-io/n8n/issues/11613)) ([c32cf64](https://github.com/n8n-io/n8n/commit/c32cf644a6b8c21558e802449329877845de70b1)) +* **Notion Node:** Extract page url ([#11643](https://github.com/n8n-io/n8n/issues/11643)) ([cbdd535](https://github.com/n8n-io/n8n/commit/cbdd535fe0cb4e032ea82f008dcf35cc5f2264c2)) +* **Redis Chat Memory Node:** Respect the SSL flag from the credential ([#11689](https://github.com/n8n-io/n8n/issues/11689)) ([b5cbf75](https://github.com/n8n-io/n8n/commit/b5cbf7566d351d8a8e9972f13ff5867ff1c8d7d0)) +* **Supabase Node:** Reset query parameters in get many operation ([#11630](https://github.com/n8n-io/n8n/issues/11630)) ([7458229](https://github.com/n8n-io/n8n/commit/74582290c04d2dd32300b1a6c7715862ae837d34)) +* **Switch Node:** Maintain output connections ([#11162](https://github.com/n8n-io/n8n/issues/11162)) ([9bd79fc](https://github.com/n8n-io/n8n/commit/9bd79fceebc4453d0fe40ae5f628d5e31ff2b326)) + + +### Features + +* **AI Transform Node:** Show warning for binary data ([#11560](https://github.com/n8n-io/n8n/issues/11560)) ([ddbb263](https://github.com/n8n-io/n8n/commit/ddbb263dce0fc458abc95d850217251bb49d2b83)) +* **core:** Make all http requests made with `httpRequestWithAuthentication` abortable ([#11704](https://github.com/n8n-io/n8n/issues/11704)) ([0d8aada](https://github.com/n8n-io/n8n/commit/0d8aada49005d6a6078e8460003a0de61a8f423c)) +* **editor:** Improve how we show default Agent prompt and Memory session parameters ([#11491](https://github.com/n8n-io/n8n/issues/11491)) ([565f8cd](https://github.com/n8n-io/n8n/commit/565f8cd8c78b534a50e272997d659d162fa86625)) +* **editor:** Improve workflow loading performance on new canvas ([#11629](https://github.com/n8n-io/n8n/issues/11629)) ([f1e2df7](https://github.com/n8n-io/n8n/commit/f1e2df7d0753aa0f33cf299100a063bf89cc8b35)) +* **editor:** Redesign Canvas Chat ([#11634](https://github.com/n8n-io/n8n/issues/11634)) ([a412ab7](https://github.com/n8n-io/n8n/commit/a412ab7ebfcd6aa9051a8ca36e34f1067102c998)) +* **editor:** Restrict when a ChatTrigger Node is added automatically ([#11523](https://github.com/n8n-io/n8n/issues/11523)) ([93a6f85](https://github.com/n8n-io/n8n/commit/93a6f858fa3eb53f8b48b2a3d6b7377279dd6ed1)) +* Github star button in-app ([#11695](https://github.com/n8n-io/n8n/issues/11695)) ([0fd684d](https://github.com/n8n-io/n8n/commit/0fd684d90c830f8b0aab12b7f78a1fa5619c62c9)) +* **Oura Node:** Update node for v2 api ([#11604](https://github.com/n8n-io/n8n/issues/11604)) ([3348fbb](https://github.com/n8n-io/n8n/commit/3348fbb1547c430ff8707b298640e3461d3f6536)) + + +### Performance Improvements + +* **editor:** Add lint rules for optimization-friendly syntax ([#11681](https://github.com/n8n-io/n8n/issues/11681)) ([88295c7](https://github.com/n8n-io/n8n/commit/88295c70495ae3d017674d5745972a346fcbaf12)) + + + +# [1.67.0](https://github.com/n8n-io/n8n/compare/n8n@1.66.0...n8n@1.67.0) (2024-11-06) + + +### Bug Fixes + +* Bring back nodes panel telemetry events ([#11456](https://github.com/n8n-io/n8n/issues/11456)) ([130c942](https://github.com/n8n-io/n8n/commit/130c942f633788d1b2f937d6fea342d4450c6e3d)) +* **core:** Account for double quotes in instance base URL ([#11495](https://github.com/n8n-io/n8n/issues/11495)) ([c5191e6](https://github.com/n8n-io/n8n/commit/c5191e697a9a9ebfa2b67587cd01b5835ebf6ea8)) +* **core:** Do not delete waiting executions when saving of successful executions is disabled ([#11458](https://github.com/n8n-io/n8n/issues/11458)) ([e8757e5](https://github.com/n8n-io/n8n/commit/e8757e58f69e091ac3d2a2f8e8c8e33ac57c1e47)) +* **core:** Don't send a `executionFinished` event to the browser with no run data if the execution has already been cleaned up ([#11502](https://github.com/n8n-io/n8n/issues/11502)) ([d1153f5](https://github.com/n8n-io/n8n/commit/d1153f51e80911cbc8f34ba5f038f349b75295c3)) +* **core:** Include `projectId` in range query middleware ([#11590](https://github.com/n8n-io/n8n/issues/11590)) ([a6070af](https://github.com/n8n-io/n8n/commit/a6070afdda29631fd36e5213f52bf815268bcda4)) +* **core:** Save exeution progress for waiting executions, even when progress saving is disabled ([#11535](https://github.com/n8n-io/n8n/issues/11535)) ([6b9353c](https://github.com/n8n-io/n8n/commit/6b9353c80f61ab36945fff434d98242dc1cab7b3)) +* **core:** Use the correct docs URL for regular nodes when used as tools ([#11529](https://github.com/n8n-io/n8n/issues/11529)) ([a092b8e](https://github.com/n8n-io/n8n/commit/a092b8e972e1253d92df416f19096a045858e7c1)) +* **Edit Image Node:** Fix Text operation by setting Arial as default font ([#11125](https://github.com/n8n-io/n8n/issues/11125)) ([60c1ace](https://github.com/n8n-io/n8n/commit/60c1ace64be29d651ce7b777fbb576598e38b9d7)) +* **editor:** Auto focus first fields on SignIn, SignUp and ForgotMyPassword views ([#11445](https://github.com/n8n-io/n8n/issues/11445)) ([5b5bd72](https://github.com/n8n-io/n8n/commit/5b5bd7291dde17880b7699f7e6832938599ffd8f)) +* **editor:** Do not overwrite the webhookId in the new canvas ([#11562](https://github.com/n8n-io/n8n/issues/11562)) ([dfd785b](https://github.com/n8n-io/n8n/commit/dfd785bc0894257eb6e62b0dd8f71248c27aae53)) +* **editor:** Ensure Enter key on Cancel button correctly cancels node rename ([#11563](https://github.com/n8n-io/n8n/issues/11563)) ([be05ae3](https://github.com/n8n-io/n8n/commit/be05ae36e7790156cb48b317fc254ae46a3b2d8c)) +* **editor:** Fix emitting `n8nReady` notification via `postmessage` on new canvas ([#11558](https://github.com/n8n-io/n8n/issues/11558)) ([463d101](https://github.com/n8n-io/n8n/commit/463d101f3592e6df4afd66c4d0fde0cb4aec34cc)) +* **editor:** Fix run index input for RunData view in sub-nodes ([#11538](https://github.com/n8n-io/n8n/issues/11538)) ([434d31c](https://github.com/n8n-io/n8n/commit/434d31ce928342d52b6ab8b78639afd7829216d4)) +* **editor:** Fix selected credential being overwritten in NDV ([#11496](https://github.com/n8n-io/n8n/issues/11496)) ([a26c0e2](https://github.com/n8n-io/n8n/commit/a26c0e2c3c7da87bfaba9737a967aa0070810d85)) +* **editor:** Keep workflow pristine after load on new canvas ([#11579](https://github.com/n8n-io/n8n/issues/11579)) ([7254359](https://github.com/n8n-io/n8n/commit/7254359855b89769613cd5cc24dbb4f45a7cc76f)) +* Show Pinned data in demo mode ([#11490](https://github.com/n8n-io/n8n/issues/11490)) ([ca2a583](https://github.com/n8n-io/n8n/commit/ca2a583b5cbb0cba3ecb694261806de16547aa91)) +* Toast not aligned to the bottom when AI assistant disable ([#11549](https://github.com/n8n-io/n8n/issues/11549)) ([e80f7e0](https://github.com/n8n-io/n8n/commit/e80f7e0a02a972379f73af6a44de11768081086e)) + + +### Features + +* Add Rapid7 InsightVm credentials ([#11462](https://github.com/n8n-io/n8n/issues/11462)) ([46eceab](https://github.com/n8n-io/n8n/commit/46eceabc27ac219b11b85c16c533a2cff848c5dd)) +* **AI Transform Node:** UX improvements ([#11280](https://github.com/n8n-io/n8n/issues/11280)) ([8a48407](https://github.com/n8n-io/n8n/commit/8a484077af3d3e1fe2d1b90b1ea9edf4ba41fcb6)) +* **Anthropic Chat Model Node:** Add support for Haiku 3.5 ([#11551](https://github.com/n8n-io/n8n/issues/11551)) ([8b39825](https://github.com/n8n-io/n8n/commit/8b398256a81594a52f20f8eb8adf8ff205209bc1)) +* **Convert to File Node:** Add delimiter convert to csv ([#11556](https://github.com/n8n-io/n8n/issues/11556)) ([63d454b](https://github.com/n8n-io/n8n/commit/63d454b776c092ff8c6c521a7e083774adb8f649)) +* **editor:** Update panning and selection keybindings on new canvas ([#11534](https://github.com/n8n-io/n8n/issues/11534)) ([5e2e205](https://github.com/n8n-io/n8n/commit/5e2e205394adf76faf02aee2d4f21df71848e1d4)) +* **Gmail Trigger Node:** Add filter option to include drafts ([#11441](https://github.com/n8n-io/n8n/issues/11441)) ([7a2be77](https://github.com/n8n-io/n8n/commit/7a2be77f384a32ede3acad8fe24fb89227c058bf)) +* **Intercom Node:** Update credential to new style ([#11485](https://github.com/n8n-io/n8n/issues/11485)) ([b137e13](https://github.com/n8n-io/n8n/commit/b137e13845f0714ebf7421c837f5ab104b66709b)) + + + +# [1.66.0](https://github.com/n8n-io/n8n/compare/n8n@1.65.0...n8n@1.66.0) (2024-10-31) + + +### Bug Fixes + +* **Asana Node:** Fix issue with pagination ([#11415](https://github.com/n8n-io/n8n/issues/11415)) ([04c075a](https://github.com/n8n-io/n8n/commit/04c075a46bcc7b1964397f0244b0fde99476212d)) +* **core:** Add 'user_id' to `license-community-plus-registered` telemetry event ([#11430](https://github.com/n8n-io/n8n/issues/11430)) ([7a8dafe](https://github.com/n8n-io/n8n/commit/7a8dafe9902fbc0d5001c50579c34959b95211ab)) +* **core:** Add safeguard for command publishing ([#11337](https://github.com/n8n-io/n8n/issues/11337)) ([656439e](https://github.com/n8n-io/n8n/commit/656439e87138f9f96dea5a683cfdac3f661ffefb)) +* **core:** Ensure `LoggerProxy` is not scoped ([#11379](https://github.com/n8n-io/n8n/issues/11379)) ([f4ea943](https://github.com/n8n-io/n8n/commit/f4ea943c9cb2321e41705de6c5c27535a0f5eae0)) +* **core:** Ensure `remove-triggers-and-pollers` command is not debounced ([#11486](https://github.com/n8n-io/n8n/issues/11486)) ([529d4fc](https://github.com/n8n-io/n8n/commit/529d4fc3ef5206bd1b02d27634342cc50b45997e)) +* **core:** Ensure job processor does not reprocess amended executions ([#11438](https://github.com/n8n-io/n8n/issues/11438)) ([c152a3a](https://github.com/n8n-io/n8n/commit/c152a3ac56f140a39eea4771a94f5a3082118df7)) +* **core:** Fix Message Event Bus Metrics not counting up for labeled metrics ([#11396](https://github.com/n8n-io/n8n/issues/11396)) ([7fc3b25](https://github.com/n8n-io/n8n/commit/7fc3b25d21c6c4f1802f34b1ae065a65cac3001b)) +* **core:** Fix resolving of $fromAI expression via `evaluateExpression` ([#11397](https://github.com/n8n-io/n8n/issues/11397)) ([2e64464](https://github.com/n8n-io/n8n/commit/2e6446454defbd3e5a47b66e6fd46d4f1b9fbd0f)) +* **core:** Make execution and its data creation atomic ([#11392](https://github.com/n8n-io/n8n/issues/11392)) ([ed30d43](https://github.com/n8n-io/n8n/commit/ed30d43236bf3c6b657022636a02a41be01aa152)) +* **core:** On unhandled rejections, extract the original exception correctly ([#11389](https://github.com/n8n-io/n8n/issues/11389)) ([8608bae](https://github.com/n8n-io/n8n/commit/8608baeb7ec302daddc8adca6e39778dcf7b2eda)) +* **editor:** Fix TypeError: Cannot read properties of undefined (reading '0') ([#11399](https://github.com/n8n-io/n8n/issues/11399)) ([ae37c52](https://github.com/n8n-io/n8n/commit/ae37c520a91c75e353e818944b36a3619c0d8b4a)) +* **editor:** Add Retry button for AI Assistant errors ([#11345](https://github.com/n8n-io/n8n/issues/11345)) ([7699240](https://github.com/n8n-io/n8n/commit/7699240073122cdef31cf109fd37fa66961f588a)) +* **editor:** Change tooltip for workflow with execute workflow trigger ([#11374](https://github.com/n8n-io/n8n/issues/11374)) ([dcd6038](https://github.com/n8n-io/n8n/commit/dcd6038c3085135803cdaa546a239359a6d449eb)) +* **editor:** Ensure toasts show above modal overlays ([#11410](https://github.com/n8n-io/n8n/issues/11410)) ([351134f](https://github.com/n8n-io/n8n/commit/351134f786af933f5f310bf8d9897269387635a0)) +* **editor:** Fit view consistently after nodes are initialized on new canvas ([#11457](https://github.com/n8n-io/n8n/issues/11457)) ([497d637](https://github.com/n8n-io/n8n/commit/497d637fc5308b9c4a06bc764152fde1f1a9c130)) +* **editor:** Fix adding connections when initializing workspace in templates view on new canvas ([#11451](https://github.com/n8n-io/n8n/issues/11451)) ([ea47b02](https://github.com/n8n-io/n8n/commit/ea47b025fb16c967d4fc73dcacc6e260d2aecd61)) +* **editor:** Fix rendering of AI logs ([#11450](https://github.com/n8n-io/n8n/issues/11450)) ([73b0a80](https://github.com/n8n-io/n8n/commit/73b0a80ac92b4f4b5a300d0ec1c833b4395a222a)) +* **editor:** Hide data mapping tooltip in credential edit modal ([#11356](https://github.com/n8n-io/n8n/issues/11356)) ([ff14dcb](https://github.com/n8n-io/n8n/commit/ff14dcb3a1ddaea4eca7c1ecd2e92c0abb0c413c)) +* **editor:** Prevent running workflow that has issues if listening to webhook ([#11402](https://github.com/n8n-io/n8n/issues/11402)) ([8b0a48f](https://github.com/n8n-io/n8n/commit/8b0a48f53010378e497e4cc362fda75a958cf363)) +* **editor:** Run external hooks after settings have been initialized ([#11423](https://github.com/n8n-io/n8n/issues/11423)) ([0ab24c8](https://github.com/n8n-io/n8n/commit/0ab24c814abd1787268750ba808993ab2735ac52)) +* **editor:** Support middle click to scroll when using a mouse on new canvas ([#11384](https://github.com/n8n-io/n8n/issues/11384)) ([46f3b4a](https://github.com/n8n-io/n8n/commit/46f3b4a258f89f02e0d2bd1eef25a22e3a721167)) +* **HTTP Request Tool Node:** Fix HTML response optimization issue ([#11439](https://github.com/n8n-io/n8n/issues/11439)) ([cf37e94](https://github.com/n8n-io/n8n/commit/cf37e94dd875e9f6ab1f189146fb34e7296af93c)) +* **n8n Form Node:** Form Trigger does not wait in multi-form mode ([#11404](https://github.com/n8n-io/n8n/issues/11404)) ([151f4dd](https://github.com/n8n-io/n8n/commit/151f4dd7b8f800af424f8ae64cb8238975fb3cb8)) +* Update required node js version in CONTRIBUTING.md ([#11437](https://github.com/n8n-io/n8n/issues/11437)) ([4f511aa](https://github.com/n8n-io/n8n/commit/4f511aab68651caa8fe47f70cd7cdb88bb06a3e2)) + + +### Features + +* **Anthropic Chat Model Node:** Add model claude-3-5-sonnet-20241022 ([#11465](https://github.com/n8n-io/n8n/issues/11465)) ([f6c8890](https://github.com/n8n-io/n8n/commit/f6c8890a8069de221b9b96e735418ecc9624cf7b)) +* **core:** Handle nodes with multiple inputs and connections during partial executions ([#11376](https://github.com/n8n-io/n8n/issues/11376)) ([cb7c4d2](https://github.com/n8n-io/n8n/commit/cb7c4d29a6f042b590822e5b9c67fff0a8f0863d)) +* **editor:** Add descriptive header to projects /workflow ([#11203](https://github.com/n8n-io/n8n/issues/11203)) ([5d19e8f](https://github.com/n8n-io/n8n/commit/5d19e8f2b45dc1abc5a8253f9e3a0fdacb1ebd91)) +* **editor:** Improve placeholder for vector store tool ([#11483](https://github.com/n8n-io/n8n/issues/11483)) ([629e092](https://github.com/n8n-io/n8n/commit/629e09240785bc648ff6575f97910fbb4e77cdab)) +* **editor:** Remove edge execution animation on new canvas ([#11446](https://github.com/n8n-io/n8n/issues/11446)) ([a701d87](https://github.com/n8n-io/n8n/commit/a701d87f5ba94ffc811e424b60e188b26ac6c1c5)) +* **editor:** Update ownership pills ([#11155](https://github.com/n8n-io/n8n/issues/11155)) ([8147038](https://github.com/n8n-io/n8n/commit/8147038cf87dca657602e617e49698065bf1a63f)) + + + +# [1.65.0](https://github.com/n8n-io/n8n/compare/n8n@1.64.0...n8n@1.65.0) (2024-10-24) + + +### Bug Fixes + +* **AI Agent Node:** Preserve `intermediateSteps` when using output parser with non-tool agent ([#11363](https://github.com/n8n-io/n8n/issues/11363)) ([e61a853](https://github.com/n8n-io/n8n/commit/e61a8535aa39653b9a87575ea911a65318282167)) +* **API:** `PUT /credentials/:id` should move the specified credential, not the first one in the database ([#11365](https://github.com/n8n-io/n8n/issues/11365)) ([e6b2f8e](https://github.com/n8n-io/n8n/commit/e6b2f8e7e6ebbb6e3776a976297d519e99ac6c64)) +* **API:** Correct credential schema for response in `POST /credentials` ([#11340](https://github.com/n8n-io/n8n/issues/11340)) ([f495875](https://github.com/n8n-io/n8n/commit/f4958756b4976e0b608b9155dab84564f7e8804e)) +* **core:** Account for waiting jobs during shutdown ([#11338](https://github.com/n8n-io/n8n/issues/11338)) ([c863abd](https://github.com/n8n-io/n8n/commit/c863abd08300b53ea898fc4d06aae97dec7afa9b)) +* **core:** Add missing primary key to execution annotation tags table ([#11168](https://github.com/n8n-io/n8n/issues/11168)) ([b4b543d](https://github.com/n8n-io/n8n/commit/b4b543d41daa07753eca24ab93bf7445f672361d)) +* **core:** Change dedupe value column type from varchar(255) to text ([#11357](https://github.com/n8n-io/n8n/issues/11357)) ([7a71cff](https://github.com/n8n-io/n8n/commit/7a71cff4d75fe4e7282a398b4843428e0161ba8c)) +* **core:** Do not debounce webhooks, triggers and pollers activation ([#11306](https://github.com/n8n-io/n8n/issues/11306)) ([64bddf8](https://github.com/n8n-io/n8n/commit/64bddf86536ddd688638a643d24f80c947a12f31)) +* **core:** Enforce nodejs version consistently ([#11323](https://github.com/n8n-io/n8n/issues/11323)) ([0fa2e8c](https://github.com/n8n-io/n8n/commit/0fa2e8ca85005362d9043d82469f3c3525f4c4ef)) +* **core:** Fix memory issue with empty model response ([#11300](https://github.com/n8n-io/n8n/issues/11300)) ([216b119](https://github.com/n8n-io/n8n/commit/216b119350949de70f15cf2d61f474770803ad7a)) +* **core:** Fix race condition when resolving post-execute promise ([#11360](https://github.com/n8n-io/n8n/issues/11360)) ([4f1816e](https://github.com/n8n-io/n8n/commit/4f1816e03db00219bc2e723e3048848aef7f8fe1)) +* **core:** Sanitise IdP provided information in SAML test pages ([#11171](https://github.com/n8n-io/n8n/issues/11171)) ([74fc388](https://github.com/n8n-io/n8n/commit/74fc3889b946e8f224e65ef8d3d44125404aa4fc)) +* Don't show pin button in input panel when there's binary data ([#11267](https://github.com/n8n-io/n8n/issues/11267)) ([c0b5b92](https://github.com/n8n-io/n8n/commit/c0b5b92f62a2d7ba60492eb27daced268b654fe9)) +* **editor:** Add Personal project to main navigation ([#11161](https://github.com/n8n-io/n8n/issues/11161)) ([1f441f9](https://github.com/n8n-io/n8n/commit/1f441f97528f58e905eaf8930577bbcd08debf06)) +* **editor:** Fix Cannot read properties of undefined (reading 'finished') ([#11367](https://github.com/n8n-io/n8n/issues/11367)) ([475d72e](https://github.com/n8n-io/n8n/commit/475d72e0bc9e13c6dc56129902f6f89c67547f78)) +* **editor:** Fix delete all existing executions ([#11352](https://github.com/n8n-io/n8n/issues/11352)) ([3ec103f](https://github.com/n8n-io/n8n/commit/3ec103f8baaa89e579844947d945f00bec9e498e)) +* **editor:** Fix pin data button disappearing after reload ([#11198](https://github.com/n8n-io/n8n/issues/11198)) ([3b2f63e](https://github.com/n8n-io/n8n/commit/3b2f63e248cd0cba04087e2f40e13d670073707d)) +* **editor:** Fix RunData non-binary pagination when binary data is present ([#11309](https://github.com/n8n-io/n8n/issues/11309)) ([901888d](https://github.com/n8n-io/n8n/commit/901888d5b1027098653540c72f787f176941f35a)) +* **editor:** Fix sorting problem in older browsers that don't support `toSorted` ([#11204](https://github.com/n8n-io/n8n/issues/11204)) ([c728a2f](https://github.com/n8n-io/n8n/commit/c728a2ffe01f510a237979a54897c4680a407800)) +* **editor:** Follow-up fixes to projects side menu ([#11327](https://github.com/n8n-io/n8n/issues/11327)) ([4dde772](https://github.com/n8n-io/n8n/commit/4dde772814c55e66efcc9b369ae443328af21b14)) +* **editor:** Keep always focus on the first item on the node's search panel ([#11193](https://github.com/n8n-io/n8n/issues/11193)) ([c57cac9](https://github.com/n8n-io/n8n/commit/c57cac9e4d447c3a4240a565f9f2de8aa3b7c513)) +* **editor:** Open Community+ enrollment modal only for the instance owner ([#11292](https://github.com/n8n-io/n8n/issues/11292)) ([76724c3](https://github.com/n8n-io/n8n/commit/76724c3be6e001792433045c2b2aac0ef16d4b8a)) +* **editor:** Record sessionStarted telemetry event in Setting Store ([#11334](https://github.com/n8n-io/n8n/issues/11334)) ([1b734dd](https://github.com/n8n-io/n8n/commit/1b734dd9f42885594ce02400cfb395a4f5e7e088)) +* Ensure NDV params don't get cut off early and scrolled to the top ([#11252](https://github.com/n8n-io/n8n/issues/11252)) ([054fe97](https://github.com/n8n-io/n8n/commit/054fe9745ff6864f9088aa4cd66ed9e7869520d5)) +* **HTTP Request Tool Node:** Fix the undefined response issue when authentication is enabled ([#11343](https://github.com/n8n-io/n8n/issues/11343)) ([094ec68](https://github.com/n8n-io/n8n/commit/094ec68d4c00848013aa4eec4ac5efbd2c92afc5)) +* Include error in the message in JS task runner sandbox ([#11359](https://github.com/n8n-io/n8n/issues/11359)) ([0708b3a](https://github.com/n8n-io/n8n/commit/0708b3a1f8097af829c92fe106ea6ba375d6c500)) +* **Microsoft SQL Node:** Fix execute query to allow for non select query to run ([#11335](https://github.com/n8n-io/n8n/issues/11335)) ([ba158b4](https://github.com/n8n-io/n8n/commit/ba158b4f8533bd3430db8766d4921f75db5c1a11)) +* **OpenAI Chat Model Node, Ollama Chat Model Node:** Change default model to a more up-to-date option ([#11293](https://github.com/n8n-io/n8n/issues/11293)) ([0be04c6](https://github.com/n8n-io/n8n/commit/0be04c6348d8c059a96c3d37a6d6cd587bfb97f3)) +* **Pinecone Vector Store Node:** Prevent populating of vectors after manually stopping the execution ([#11288](https://github.com/n8n-io/n8n/issues/11288)) ([fbae17d](https://github.com/n8n-io/n8n/commit/fbae17d8fb35a5197fa183e3639bb36762dc73d2)) +* **Postgres Node:** Special datetime values cause errors ([#11225](https://github.com/n8n-io/n8n/issues/11225)) ([3c57f46](https://github.com/n8n-io/n8n/commit/3c57f46aaeb968d2974f2dc9790317a6a6fab624)) +* Resend invite operation on users list ([#11351](https://github.com/n8n-io/n8n/issues/11351)) ([e4218de](https://github.com/n8n-io/n8n/commit/e4218debd18812fa3aa508339afd3de03c4d69dc)) +* **SSH Node:** Cleanup temporary binary files as soon as possible ([#11305](https://github.com/n8n-io/n8n/issues/11305)) ([08a7b5b](https://github.com/n8n-io/n8n/commit/08a7b5b7425663ec6593114921c2e22ab37d039e)) + + +### Features + +* Add report bug buttons ([#11304](https://github.com/n8n-io/n8n/issues/11304)) ([296f68f](https://github.com/n8n-io/n8n/commit/296f68f041b93fd32ac7be2b53c2b41d58c2998a)) +* **AI Agent Node:** Make tools optional when using OpenAI model with Tools agent ([#11212](https://github.com/n8n-io/n8n/issues/11212)) ([fed7c3e](https://github.com/n8n-io/n8n/commit/fed7c3ec1fb0553adaa9a933f91aabfd54fe83a3)) +* **core:** introduce JWT API keys for the public API ([#11005](https://github.com/n8n-io/n8n/issues/11005)) ([679fa4a](https://github.com/n8n-io/n8n/commit/679fa4a10a85fc96e12ca66fe12cdb32368bc12b)) +* **core:** Enforce config file permissions on startup ([#11328](https://github.com/n8n-io/n8n/issues/11328)) ([c078a51](https://github.com/n8n-io/n8n/commit/c078a516bec857831cc904ef807d0791b889f3a2)) +* **core:** Handle cycles in workflows when partially executing them ([#11187](https://github.com/n8n-io/n8n/issues/11187)) ([321d6de](https://github.com/n8n-io/n8n/commit/321d6deef18806d88d97afef2f2c6f29e739ccb4)) +* **editor:** Separate node output execution tooltip from status icon ([#11196](https://github.com/n8n-io/n8n/issues/11196)) ([cd15e95](https://github.com/n8n-io/n8n/commit/cd15e959c7af82a7d8c682e94add2b2640624a70)) +* **GitHub Node:** Add workflow resource operations ([#10744](https://github.com/n8n-io/n8n/issues/10744)) ([d309112](https://github.com/n8n-io/n8n/commit/d3091126472faa2c8f270650e54027d19dc56bb6)) +* **n8n Form Page Node:** New node ([#10390](https://github.com/n8n-io/n8n/issues/10390)) ([643d66c](https://github.com/n8n-io/n8n/commit/643d66c0ae084a0d93dac652703adc0a32cab8de)) +* **n8n Google My Business Node:** New node ([#10504](https://github.com/n8n-io/n8n/issues/10504)) ([bf28fbe](https://github.com/n8n-io/n8n/commit/bf28fbefe5e8ba648cba1555a2d396b75ee32bbb)) +* Run `mfa.beforeSetup` hook before enabling MFA ([#11116](https://github.com/n8n-io/n8n/issues/11116)) ([25c1c32](https://github.com/n8n-io/n8n/commit/25c1c3218cf1075ca3abd961236f3b2fbd9d6ba9)) +* **Structured Output Parser Node:** Refactor Output Parsers and Improve Error Handling ([#11148](https://github.com/n8n-io/n8n/issues/11148)) ([45274f2](https://github.com/n8n-io/n8n/commit/45274f2e7f081e194e330e1c9e6a5c26fca0b141)) + + + # [1.64.0](https://github.com/n8n-io/n8n/compare/n8n@1.63.0...n8n@1.64.0) (2024-10-16) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4a67bf00ac..9ed101af7d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -19,6 +19,7 @@ Great that you are here and you want to contribute to n8n - [Actual n8n setup](#actual-n8n-setup) - [Start](#start) - [Development cycle](#development-cycle) + - [Community PR Guidelines](#community-pr-guidelines) - [Test suite](#test-suite) - [Unit tests](#unit-tests) - [E2E tests](#e2e-tests) @@ -68,7 +69,7 @@ If you already have VS Code and Docker installed, you can click [here](https://v #### Node.js -[Node.js](https://nodejs.org/en/) version 18.10 or newer is required for development purposes. +[Node.js](https://nodejs.org/en/) version 20.15 or newer is required for development purposes. #### pnpm @@ -191,6 +192,51 @@ automatically build your code, restart the backend and refresh the frontend ``` 1. Commit code and [create a pull request](https://docs.github.com/en/github/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request-from-a-fork) +--- + +### Community PR Guidelines + +#### **1. Change Request/Comment** + +Please address the requested changes or provide feedback within 14 days. If there is no response or updates to the pull request during this time, it will be automatically closed. The PR can be reopened once the requested changes are applied. + +#### **2. General Requirements** + +- **Follow the Style Guide:** + - Ensure your code adheres to n8n's coding standards and conventions (e.g., formatting, naming, indentation). Use linting tools where applicable. +- **TypeScript Compliance:** + - Do not use `ts-ignore` . + - Ensure code adheres to TypeScript rules. +- **Avoid Repetitive Code:** + - Reuse existing components, parameters, and logic wherever possible instead of redefining or duplicating them. + - For nodes: Use the same parameter across multiple operations rather than defining a new parameter for each operation (if applicable). +- **Testing Requirements:** + - PRs **must include tests**: + - Unit tests + - Workflow tests for nodes (example [here](https://github.com/n8n-io/n8n/tree/master/packages/nodes-base/nodes/Switch/V3/test)) + - UI tests (if applicable) +- **Typos:** + - Use a spell-checking tool, such as [**Code Spell Checker**](https://marketplace.visualstudio.com/items?itemName=streetsidesoftware.code-spell-checker), to avoid typos. + +#### **3. PR Specific Requirements** + +- **Small PRs Only:** + - Focus on a single feature or fix per PR. +- **Naming Convention:** + - Follow [n8n's PR Title Conventions](https://github.com/n8n-io/n8n/blob/master/.github/pull_request_title_conventions.md#L36). +- **New Nodes:** + - PRs that introduce new nodes will be **auto-closed** unless they are explicitly requested by the n8n team and aligned with an agreed project scope. However, you can still explore [building your own nodes](https://docs.n8n.io/integrations/creating-nodes/) , as n8n offers the flexibility to create your own custom nodes. +- **Typo-Only PRs:** + - Typos are not sufficient justification for a PR and will be rejected. + +#### **4. Workflow Summary for Non-Compliant PRs** + +- **No Tests:** If tests are not provided, the PR will be auto-closed after **14 days**. +- **Non-Small PRs:** Large or multifaceted PRs will be returned for segmentation. +- **New Nodes/Typo PRs:** Automatically rejected if not aligned with project scope or guidelines. + +--- + ### Test suite #### Unit tests diff --git a/LICENSE.md b/LICENSE.md index aab68b6d93..f85f59baa9 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -3,9 +3,11 @@ Portions of this software are licensed as follows: - Content of branches other than the main branch (i.e. "master") are not licensed. -- Source code files that contain ".ee." in their filename are NOT licensed under the Sustainable Use License. - To use source code files that contain ".ee." in their filename you must hold a valid n8n Enterprise License - specifically allowing you access to such source code files and as defined in "LICENSE_EE.md". +- Source code files that contain ".ee." in their filename or ".ee" in their dirname are NOT licensed under + the Sustainable Use License. + To use source code files that contain ".ee." in their filename or ".ee" in their dirname you must hold a + valid n8n Enterprise License specifically allowing you access to such source code files and as defined + in "LICENSE_EE.md". - All third party components incorporated into the n8n Software are licensed under the original license provided by the owner of the applicable component. - Content outside of the above mentioned files or restrictions is available under the "Sustainable Use diff --git a/README.md b/README.md index d51ac596ca..cc7d596f61 100644 --- a/README.md +++ b/README.md @@ -1,104 +1,67 @@ -![n8n.io - Workflow Automation](https://user-images.githubusercontent.com/65276001/173571060-9f2f6d7b-bac0-43b6-bdb2-001da9694058.png) +![Banner image](https://user-images.githubusercontent.com/10284570/173569848-c624317f-42b1-45a6-ab09-f0ea3c247648.png) -# n8n - Workflow automation tool +# n8n - Secure Workflow Automation for Technical Teams -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 -functions, logic and apps. n8n's node-based approach makes it highly versatile, enabling you to connect -anything to everything. +n8n is a workflow automation platform that gives technical teams the flexibility of code with the speed of no-code. With 400+ integrations, native AI capabilities, and a fair-code license, n8n lets you build powerful automations while maintaining full control over your data and deployments. -![n8n.io - Screenshot](https://raw.githubusercontent.com/n8n-io/n8n/master/assets/n8n-screenshot.png) +![n8n.io - Screenshot](https://raw.githubusercontent.com/n8n-io/n8n/master/assets/n8n-screenshot-readme.png) -## Demo +## Key Capabilities -[:tv: A short video (< 5 min)](https://www.youtube.com/watch?v=1MwSoB0gnM4) that goes over key concepts of -creating workflows in n8n. +- **Code When You Need It**: Write JavaScript/Python, add npm packages, or use the visual interface +- **AI-Native Platform**: Build AI agent workflows based on LangChain with your own data and models +- **Full Control**: Self-host with our fair-code license or use our [cloud offering](https://app.n8n.cloud/login) +- **Enterprise-Ready**: Advanced permissions, SSO, and air-gapped deployments +- **Active Community**: 400+ integrations and 900+ ready-to-use [templates](https://n8n.io/workflows) -## Available integrations +## Quick Start -n8n has 200+ different nodes to automate workflows. The list can be found on: -[https://n8n.io/integrations](https://n8n.io/integrations) - -## Documentation - -The official n8n documentation can be found on our [documentation website](https://docs.n8n.io) - -Additional information and example workflows on the [n8n.io website](https://n8n.io) - -The release notes can be found [here](https://docs.n8n.io/release-notes/) and the list of breaking -changes [here](https://github.com/n8n-io/n8n/blob/master/packages/cli/BREAKING-CHANGES.md). - -## Usage - -- :books: Learn - [how to **use** it from the command line](https://docs.n8n.io/reference/cli-commands/) -- :whale: Learn - [how to run n8n in **Docker**](https://docs.n8n.io/hosting/installation/docker/) - -## Start - -You can try n8n without installing it using npx. You must have [Node.js](https://nodejs.org/en/) installed. -From the terminal, run: +Try n8n instantly with [npx](https://docs.n8n.io/hosting/installation/npm/) (requires [Node.js](https://nodejs.org/en/)): `npx n8n` -This command will download everything that is needed to start n8n. You can then access n8n and start building workflows by opening [http://localhost:5678](http://localhost:5678). +Or deploy with [Docker](https://docs.n8n.io/hosting/installation/docker/): -## n8n cloud +`docker run -it --rm --name n8n -p 5678:5678 docker.n8n.io/n8nio/n8n` -Sign-up for an [n8n cloud](https://www.n8n.io/cloud/) account. +Access the editor at http://localhost:5678 -While n8n cloud and n8n are the same in terms of features, n8n cloud provides certain conveniences such as: +## Resources -- Not having to set up and maintain your n8n instance -- Managed OAuth for authentication -- Easily upgrading to the newer n8n versions - -## Build with LangChain and AI in n8n (beta) - -With n8n's LangChain nodes you can build AI-powered functionality within your workflows. The LangChain nodes are configurable, meaning you can choose your preferred agent, LLM, memory, and so on. Alongside the LangChain nodes, you can connect any n8n node as normal: this means you can integrate your LangChain logic with other data sources and services. - -Learn more in the [documentation](https://docs.n8n.io/langchain/). - -- [LangChain nodes package](https://www.npmjs.com/package/@n8n/n8n-nodes-langchain) -- [Chatbot package](https://www.npmjs.com/package/@n8n/chat) +- 📚 [Documentation](https://docs.n8n.io) +- 🔧 [400+ Integrations](https://n8n.io/integrations) +- 💡 [Example Workflows](https://n8n.io/workflows) +- 🤖 [AI & LangChain Guide](https://docs.n8n.io/langchain/) +- 👥 [Community Forum](https://community.n8n.io) +- 📖 [Community Tutorials](https://community.n8n.io/c/tutorials/28) ## Support -If you have problems or questions go to our forum, we will then try to help you asap: - -[https://community.n8n.io](https://community.n8n.io) - -## Jobs - -If you are interested in working for n8n and so shape the future of the project check out our -[job posts](https://apply.workable.com/n8n/) - -## What does n8n mean and how do you pronounce it? - -**Short answer:** It means "nodemation" and it is pronounced as n-eight-n. - -**Long answer:** "I get that question quite often (more often than I expected) so I decided it is probably -best to answer it here. While looking for a good name for the project with a free domain I realized very -quickly that all the good ones I could think of were already taken. So, in the end, I chose nodemation. -'node-' in the sense that it uses a Node-View and that it uses Node.js and '-mation' for 'automation' which is -what the project is supposed to help with. However, I did not like how long the name was and I could not -imagine writing something that long every time in the CLI. That is when I then ended up on 'n8n'." - **Jan -Oberhauser, Founder and CEO, n8n.io** - -## Development setup - -Have you found a bug :bug: ? Or maybe you have a nice feature :sparkles: to contribute ? The -[CONTRIBUTING guide](https://github.com/n8n-io/n8n/blob/master/CONTRIBUTING.md) will help you get your -development environment ready in minutes. +Need help? Our community forum is the place to get support and connect with other users: +[community.n8n.io](https://community.n8n.io) ## License -n8n is [fair-code](https://faircode.io) distributed under the -[**Sustainable Use License**](https://github.com/n8n-io/n8n/blob/master/LICENSE.md) and the -[**n8n Enterprise License**](https://github.com/n8n-io/n8n/blob/master/LICENSE_EE.md). +n8n is [fair-code](https://faircode.io) distributed under the [Sustainable Use License](https://github.com/n8n-io/n8n/blob/master/LICENSE.md) and [n8n Enterprise License](https://github.com/n8n-io/n8n/blob/master/LICENSE_EE.md). -Proprietary licenses are available for enterprise customers. [Get in touch](mailto:license@n8n.io) +- **Source Available**: Always visible source code +- **Self-Hostable**: Deploy anywhere +- **Extensible**: Add your own nodes and functionality -Additional information about the license model can be found in the -[docs](https://docs.n8n.io/reference/license/). +[Enterprise licenses](mailto:license@n8n.io) available for additional features and support. + +Additional information about the license model can be found in the [docs](https://docs.n8n.io/reference/license/). + +## Contributing + +Found a bug 🐛 or have a feature idea ✨? Check our [Contributing Guide](https://github.com/n8n-io/n8n/blob/master/CONTRIBUTING.md) to get started. + +## Join the Team + +Want to shape the future of automation? Check out our [job posts](https://n8n.io/careers) and join our team! + +## What does n8n mean? + +**Short answer:** It means "nodemation" and is pronounced as n-eight-n. + +**Long answer:** "I get that question quite often (more often than I expected) so I decided it is probably best to answer it here. While looking for a good name for the project with a free domain I realized very quickly that all the good ones I could think of were already taken. So, in the end, I chose nodemation. 'node-' in the sense that it uses a Node-View and that it uses Node.js and '-mation' for 'automation' which is what the project is supposed to help with. However, I did not like how long the name was and I could not imagine writing something that long every time in the CLI. That is when I then ended up on 'n8n'." - **Jan Oberhauser, Founder and CEO, n8n.io** diff --git a/assets/n8n-screenshot-readme.png b/assets/n8n-screenshot-readme.png new file mode 100644 index 0000000000..d6b5faef71 Binary files /dev/null and b/assets/n8n-screenshot-readme.png differ diff --git a/cypress/composables/executions.ts b/cypress/composables/executions.ts new file mode 100644 index 0000000000..cd07eb7e6a --- /dev/null +++ b/cypress/composables/executions.ts @@ -0,0 +1,29 @@ +/** + * Getters + */ + +export const getExecutionsSidebar = () => cy.getByTestId('executions-sidebar'); + +export const getWorkflowExecutionPreviewIframe = () => cy.getByTestId('workflow-preview-iframe'); + +export const getExecutionPreviewBody = () => + getWorkflowExecutionPreviewIframe() + .its('0.contentDocument.body') + .then((el) => cy.wrap(el)); + +export const getExecutionPreviewBodyNodes = () => + getExecutionPreviewBody().findChildByTestId('canvas-node'); + +export const getExecutionPreviewBodyNodesByName = (name: string) => + getExecutionPreviewBody().findChildByTestId('canvas-node').filter(`[data-name="${name}"]`).eq(0); + +export function getExecutionPreviewOutputPanelRelatedExecutionLink() { + return getExecutionPreviewBody().findChildByTestId('related-execution-link'); +} + +/** + * Actions + */ + +export const openExecutionPreviewNode = (name: string) => + getExecutionPreviewBodyNodesByName(name).dblclick(); diff --git a/cypress/composables/modals/chat-modal.ts b/cypress/composables/modals/chat-modal.ts index 254d811a18..220c363dd1 100644 --- a/cypress/composables/modals/chat-modal.ts +++ b/cypress/composables/modals/chat-modal.ts @@ -3,7 +3,7 @@ */ export function getManualChatModal() { - return cy.getByTestId('lmChat-modal'); + return cy.getByTestId('canvas-chat'); } export function getManualChatInput() { @@ -19,11 +19,11 @@ export function getManualChatMessages() { } export function getManualChatModalCloseButton() { - return getManualChatModal().get('.el-dialog__close'); + return cy.getByTestId('workflow-chat-button'); } export function getManualChatModalLogs() { - return getManualChatModal().getByTestId('lm-chat-logs'); + return cy.getByTestId('canvas-chat-logs'); } export function getManualChatDialog() { return getManualChatModal().getByTestId('workflow-lm-chat-dialog'); diff --git a/cypress/composables/modals/credential-modal.ts b/cypress/composables/modals/credential-modal.ts index 8ce6a86049..53ba0c3a28 100644 --- a/cypress/composables/modals/credential-modal.ts +++ b/cypress/composables/modals/credential-modal.ts @@ -35,7 +35,12 @@ export function setCredentialConnectionParameterInputByName(name: string, value: } export function saveCredential() { - getCredentialSaveButton().click({ force: true }); + getCredentialSaveButton() + .click({ force: true }) + .within(() => { + cy.get('button').should('not.exist'); + }); + getCredentialSaveButton().should('have.text', 'Saved'); } export function closeCredentialModal() { diff --git a/cypress/composables/ndv.ts b/cypress/composables/ndv.ts index 5b3690e6a6..4819e7fccc 100644 --- a/cypress/composables/ndv.ts +++ b/cypress/composables/ndv.ts @@ -40,10 +40,50 @@ export function getOutputPanelDataContainer() { return getOutputPanel().getByTestId('ndv-data-container'); } +export function getOutputTableRows() { + return getOutputPanelDataContainer().find('table tr'); +} + +export function getOutputTableRow(row: number) { + return getOutputTableRows().eq(row); +} + +export function getOutputTableHeaders() { + return getOutputPanelDataContainer().find('table thead th'); +} + +export function getOutputTableHeaderByText(text: string) { + return getOutputTableHeaders().contains(text); +} + +export function getOutputTbodyCell(row: number, col: number) { + return getOutputTableRows().eq(row).find('td').eq(col); +} + +export function getOutputRunSelector() { + return getOutputPanel().findChildByTestId('run-selector'); +} + +export function getOutputRunSelectorInput() { + return getOutputRunSelector().find('input'); +} + export function getOutputPanelTable() { return getOutputPanelDataContainer().get('table'); } +export function getRunDataInfoCallout() { + return cy.getByTestId('run-data-callout'); +} + +export function getOutputPanelItemsCount() { + return getOutputPanel().getByTestId('ndv-items-count'); +} + +export function getOutputPanelRelatedExecutionLink() { + return getOutputPanel().getByTestId('related-execution-link'); +} + /** * Actions */ @@ -82,3 +122,8 @@ export function setParameterSelectByContent(name: string, content: string) { getParameterInputByName(name).realClick(); getVisibleSelect().find('.option-headline').contains(content).click(); } + +export function changeOutputRunSelector(runName: string) { + getOutputRunSelector().click(); + getVisibleSelect().find('.el-select-dropdown__item').contains(runName).click(); +} diff --git a/cypress/composables/projects.ts b/cypress/composables/projects.ts index da9c6fcc65..09d7a341ce 100644 --- a/cypress/composables/projects.ts +++ b/cypress/composables/projects.ts @@ -6,11 +6,40 @@ const credentialsModal = new CredentialsModal(); export const getHomeButton = () => cy.getByTestId('project-home-menu-item'); export const getMenuItems = () => cy.getByTestId('project-menu-item'); -export const getAddProjectButton = () => - cy.getByTestId('add-project-menu-item').should('contain', 'Add project').should('be.visible'); +export const getAddProjectButton = () => { + cy.getByTestId('universal-add').should('be.visible').click(); + cy.getByTestId('universal-add') + .find('.el-sub-menu__title') + .as('menuitem') + .should('have.attr', 'aria-describedby'); + + cy.get('@menuitem') + .invoke('attr', 'aria-describedby') + .then((el) => cy.get(`[id="${el}"]`)) + .as('submenu'); + + cy.get('@submenu').within((submenu) => + cy + .wrap(submenu) + .getByTestId('navigation-menu-item') + .should('be.visible') + .filter(':contains("Project")') + .as('button'), + ); + + return cy.get('@button'); +}; +export const getAddFirstProjectButton = () => cy.getByTestId('add-first-project-button'); +export const getIconPickerButton = () => cy.getByTestId('icon-picker-button'); +export const getIconPickerTab = (tab: string) => cy.getByTestId('icon-picker-tabs').contains(tab); +export const getIconPickerIcons = () => cy.getByTestId('icon-picker-icon'); +export const getIconPickerEmojis = () => cy.getByTestId('icon-picker-emoji'); +// export const getAddProjectButton = () => +// cy.getByTestId('universal-add').should('contain', 'Add project').should('be.visible'); export const getProjectTabs = () => cy.getByTestId('project-tabs').find('a'); export const getProjectTabWorkflows = () => getProjectTabs().filter('a[href$="/workflows"]'); export const getProjectTabCredentials = () => getProjectTabs().filter('a[href$="/credentials"]'); +export const getProjectTabExecutions = () => getProjectTabs().filter('a[href$="/executions"]'); export const getProjectTabSettings = () => getProjectTabs().filter('a[href$="/settings"]'); export const getProjectSettingsNameInput = () => cy.getByTestId('project-settings-name-input').find('input'); diff --git a/cypress/composables/workflow.ts b/cypress/composables/workflow.ts index 394a35af18..bc27048219 100644 --- a/cypress/composables/workflow.ts +++ b/cypress/composables/workflow.ts @@ -69,6 +69,25 @@ export function getNodeCreatorPlusButton() { return cy.getByTestId('node-creator-plus-button'); } +export function getCanvasNodes() { + return cy.ifCanvasVersion( + () => cy.getByTestId('canvas-node'), + () => cy.getByTestId('canvas-node').not('[data-node-type="n8n-nodes-internal.addNodes"]'), + ); +} + +export function getCanvasNodeByName(nodeName: string) { + return getCanvasNodes().filter(`:contains(${nodeName})`); +} + +export function getSaveButton() { + return cy.getByTestId('workflow-save-button'); +} + +export function getZoomToFitButton() { + return cy.getByTestId('zoom-to-fit'); +} + /** * Actions */ @@ -163,3 +182,24 @@ export function clickManualChatButton() { export function openNode(nodeName: string) { getNodeByName(nodeName).dblclick(); } + +export function saveWorkflowOnButtonClick() { + cy.intercept('POST', '/rest/workflows').as('createWorkflow'); + getSaveButton().should('contain', 'Save'); + getSaveButton().click(); + getSaveButton().should('contain', 'Saved'); + cy.url().should('not.have.string', '/new'); +} + +export function pasteWorkflow(workflow: object) { + cy.get('body').paste(JSON.stringify(workflow)); +} + +export function clickZoomToFit() { + getZoomToFitButton().click(); +} + +export function deleteNode(name: string) { + getCanvasNodeByName(name).first().click(); + cy.get('body').type('{del}'); +} diff --git a/cypress/e2e/10-undo-redo.cy.ts b/cypress/e2e/10-undo-redo.cy.ts index 7e3b5ef8ad..f54c2de9fa 100644 --- a/cypress/e2e/10-undo-redo.cy.ts +++ b/cypress/e2e/10-undo-redo.cy.ts @@ -20,6 +20,7 @@ describe('Undo/Redo', () => { WorkflowPage.actions.visit(); }); + // FIXME: Canvas V2: Fix redo connections it('should undo/redo adding node in the middle', () => { WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); @@ -114,6 +115,7 @@ describe('Undo/Redo', () => { WorkflowPage.getters.nodeConnections().should('have.length', 0); }); + // FIXME: Canvas V2: Fix moving of nodes via e2e tests it('should undo/redo moving nodes', () => { WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); @@ -146,18 +148,14 @@ describe('Undo/Redo', () => { it('should undo/redo deleting a connection using context menu', () => { WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); - WorkflowPage.getters.nodeConnections().realHover(); - cy.get('.connection-actions .delete') - .filter(':visible') - .should('be.visible') - .click({ force: true }); + WorkflowPage.actions.deleteNodeBetweenNodes(SCHEDULE_TRIGGER_NODE_NAME, CODE_NODE_NAME); WorkflowPage.getters.nodeConnections().should('have.length', 0); WorkflowPage.actions.hitUndo(); WorkflowPage.getters.nodeConnections().should('have.length', 1); WorkflowPage.actions.hitRedo(); WorkflowPage.getters.nodeConnections().should('have.length', 0); }); - + // FIXME: Canvas V2: Fix disconnecting by moving it('should undo/redo deleting a connection by moving it away', () => { WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); @@ -206,6 +204,7 @@ describe('Undo/Redo', () => { WorkflowPage.getters.disabledNodes().should('have.length', 2); }); + // FIXME: Canvas V2: Fix undo renaming node it('should undo/redo renaming node using keyboard shortcut', () => { WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); @@ -244,6 +243,7 @@ describe('Undo/Redo', () => { }); }); + // FIXME: Canvas V2: Figure out why moving doesn't work from e2e it('should undo/redo multiple steps', () => { WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); diff --git a/cypress/e2e/11-inline-expression-editor.cy.ts b/cypress/e2e/11-inline-expression-editor.cy.ts index 945c62821b..a762135a65 100644 --- a/cypress/e2e/11-inline-expression-editor.cy.ts +++ b/cypress/e2e/11-inline-expression-editor.cy.ts @@ -129,7 +129,7 @@ describe('Inline expression editor', () => { // Run workflow ndv.actions.close(); - WorkflowPage.actions.executeNode('No Operation'); + WorkflowPage.actions.executeNode('No Operation', { anchor: 'topLeft' }); WorkflowPage.actions.openNode('Hacker News'); WorkflowPage.actions.openInlineExpressionEditor(); diff --git a/cypress/e2e/12-canvas-actions.cy.ts b/cypress/e2e/12-canvas-actions.cy.ts index 8a42521d84..e9244a1d12 100644 --- a/cypress/e2e/12-canvas-actions.cy.ts +++ b/cypress/e2e/12-canvas-actions.cy.ts @@ -16,6 +16,7 @@ describe('Canvas Actions', () => { WorkflowPage.actions.visit(); }); + // FIXME: Canvas V2: Missing execute button if no nodes it('should render canvas', () => { WorkflowPage.getters.nodeViewRoot().should('be.visible'); WorkflowPage.getters.canvasPlusButton().should('be.visible'); @@ -25,10 +26,11 @@ describe('Canvas Actions', () => { WorkflowPage.getters.executeWorkflowButton().should('be.visible'); }); + // FIXME: Canvas V2: Fix changing of connection it('should connect and disconnect a simple node', () => { WorkflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME); WorkflowPage.getters.nodeViewBackground().click(600, 200, { force: true }); - cy.get('.jtk-connector').should('have.length', 1); + WorkflowPage.getters.nodeConnections().should('have.length', 1); WorkflowPage.getters.nodeViewBackground().click(600, 400, { force: true }); WorkflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME); @@ -40,16 +42,16 @@ describe('Canvas Actions', () => { ); WorkflowPage.getters - .canvasNodeInputEndpointByName(`${EDIT_FIELDS_SET_NODE_NAME}1`) - .should('have.class', 'jtk-endpoint-connected'); + .getConnectionBetweenNodes(MANUAL_TRIGGER_NODE_DISPLAY_NAME, `${EDIT_FIELDS_SET_NODE_NAME}1`) + .should('be.visible'); - cy.get('.jtk-connector').should('have.length', 1); + WorkflowPage.getters.nodeConnections().should('have.length', 1); // Disconnect Set1 cy.drag( WorkflowPage.getters.getEndpointSelector('input', `${EDIT_FIELDS_SET_NODE_NAME}1`), [-200, 100], ); - cy.get('.jtk-connector').should('have.length', 0); + WorkflowPage.getters.nodeConnections().should('have.length', 0); }); it('should add first step', () => { @@ -74,7 +76,7 @@ describe('Canvas Actions', () => { it('should add a connected node using plus endpoint', () => { WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME); - cy.get('.plus-endpoint').should('be.visible').click(); + WorkflowPage.getters.canvasNodePlusEndpointByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click(); WorkflowPage.getters.nodeCreatorSearchBar().should('be.visible'); WorkflowPage.getters.nodeCreatorSearchBar().type(CODE_NODE_NAME); WorkflowPage.getters.nodeCreatorSearchBar().type('{enter}'); @@ -85,7 +87,7 @@ describe('Canvas Actions', () => { it('should add a connected node dragging from node creator', () => { WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME); - cy.get('.plus-endpoint').should('be.visible').click(); + WorkflowPage.getters.canvasNodePlusEndpointByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click(); WorkflowPage.getters.nodeCreatorSearchBar().should('be.visible'); WorkflowPage.getters.nodeCreatorSearchBar().type(CODE_NODE_NAME); cy.drag(WorkflowPage.getters.nodeCreatorNodeItems().first(), [100, 100], { @@ -99,7 +101,7 @@ describe('Canvas Actions', () => { it('should open a category when trying to drag and drop it on the canvas', () => { WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME); - cy.get('.plus-endpoint').should('be.visible').click(); + WorkflowPage.getters.canvasNodePlusEndpointByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click(); WorkflowPage.getters.nodeCreatorSearchBar().should('be.visible'); WorkflowPage.getters.nodeCreatorSearchBar().type(CODE_NODE_NAME); cy.drag(WorkflowPage.getters.nodeCreatorActionItems().first(), [100, 100], { @@ -114,7 +116,7 @@ describe('Canvas Actions', () => { it('should add disconnected node if nothing is selected', () => { WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME); // Deselect nodes - WorkflowPage.getters.nodeViewBackground().click({ force: true }); + WorkflowPage.getters.nodeView().click({ force: true }); WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); WorkflowPage.getters.canvasNodes().should('have.length', 2); WorkflowPage.getters.nodeConnections().should('have.length', 0); @@ -136,10 +138,10 @@ describe('Canvas Actions', () => { WorkflowPage.getters.nodeConnections().should('have.length', 3); WorkflowPage.getters.canvasNodeByName(EDIT_FIELDS_SET_NODE_NAME).then(($editFieldsNode) => { - const editFieldsNodeLeft = parseFloat($editFieldsNode.css('left')); + const editFieldsNodeLeft = WorkflowPage.getters.getNodeLeftPosition($editFieldsNode); WorkflowPage.getters.canvasNodeByName(HTTP_REQUEST_NODE_NAME).then(($httpNode) => { - const httpNodeLeft = parseFloat($httpNode.css('left')); + const httpNodeLeft = WorkflowPage.getters.getNodeLeftPosition($httpNode); expect(httpNodeLeft).to.be.lessThan(editFieldsNodeLeft); }); }); @@ -159,10 +161,12 @@ describe('Canvas Actions', () => { WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click(); WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); WorkflowPage.getters.nodeConnections().first().realHover(); - cy.get('.connection-actions .delete').first().click({ force: true }); + WorkflowPage.actions.deleteNodeBetweenNodes(MANUAL_TRIGGER_NODE_DISPLAY_NAME, CODE_NODE_NAME); + WorkflowPage.getters.nodeConnections().should('have.length', 0); }); + // FIXME: Canvas V2: Fix disconnecting of connection by dragging it it('should delete a connection by moving it away from endpoint', () => { WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME); WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click(); @@ -216,10 +220,10 @@ describe('Canvas Actions', () => { WorkflowPage.actions.hitSelectAll(); WorkflowPage.actions.hitCopy(); - successToast().should('contain', 'Copied!'); + successToast().should('contain', 'Copied to clipboard'); WorkflowPage.actions.copyNode(CODE_NODE_NAME); - successToast().should('contain', 'Copied!'); + successToast().should('contain', 'Copied to clipboard'); }); it('should select/deselect all nodes', () => { @@ -231,17 +235,31 @@ describe('Canvas Actions', () => { WorkflowPage.getters.selectedNodes().should('have.length', 0); }); + // FIXME: Canvas V2: Selection via arrow keys is broken it('should select nodes using arrow keys', () => { WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME); WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click(); WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); cy.wait(500); cy.get('body').type('{leftArrow}'); - WorkflowPage.getters.canvasNodes().first().should('have.class', 'jtk-drag-selected'); + const selectedCanvasNodes = () => + cy.ifCanvasVersion( + () => WorkflowPage.getters.canvasNodes(), + () => WorkflowPage.getters.canvasNodes().parent(), + ); + + cy.ifCanvasVersion( + () => selectedCanvasNodes().first().should('have.class', 'jtk-drag-selected'), + () => selectedCanvasNodes().first().should('have.class', 'selected'), + ); cy.get('body').type('{rightArrow}'); - WorkflowPage.getters.canvasNodes().last().should('have.class', 'jtk-drag-selected'); + cy.ifCanvasVersion( + () => selectedCanvasNodes().last().should('have.class', 'jtk-drag-selected'), + () => selectedCanvasNodes().last().should('have.class', 'selected'), + ); }); + // FIXME: Canvas V2: Selection via shift and arrow keys is broken it('should select nodes using shift and arrow keys', () => { WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME); WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click(); @@ -251,6 +269,7 @@ describe('Canvas Actions', () => { WorkflowPage.getters.selectedNodes().should('have.length', 2); }); + // FIXME: Canvas V2: Fix select & deselect it('should not break lasso selection when dragging node action buttons', () => { WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME); WorkflowPage.getters @@ -262,6 +281,7 @@ describe('Canvas Actions', () => { WorkflowPage.actions.testLassoSelection([100, 100], [200, 200]); }); + // FIXME: Canvas V2: Fix select & deselect it('should not break lasso selection with multiple clicks on node action buttons', () => { WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME); WorkflowPage.actions.testLassoSelection([100, 100], [200, 200]); diff --git a/cypress/e2e/12-canvas.cy.ts b/cypress/e2e/12-canvas.cy.ts index 4c7cccaafe..ecfb325de2 100644 --- a/cypress/e2e/12-canvas.cy.ts +++ b/cypress/e2e/12-canvas.cy.ts @@ -9,6 +9,7 @@ import { } from './../constants'; import { NDV, WorkflowExecutionsTab } from '../pages'; import { WorkflowPage as WorkflowPageClass } from '../pages/workflow'; +import { isCanvasV2 } from '../utils/workflowUtils'; const WorkflowPage = new WorkflowPageClass(); const ExecutionsTab = new WorkflowExecutionsTab(); @@ -52,15 +53,15 @@ describe('Canvas Node Manipulation and Navigation', () => { cy.reload(); 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'); + WorkflowPage.getters + .getConnectionBetweenNodes(`${EDIT_FIELDS_SET_NODE_NAME}3`, `${SWITCH_NODE_NAME}1`) + .should('exist'); // Make sure all connections are there after reload for (let i = 0; i < desiredOutputs; i++) { const setName = `${EDIT_FIELDS_SET_NODE_NAME}${i > 0 ? i : ''}`; WorkflowPage.getters - .canvasNodeInputEndpointByName(setName) - .should('have.class', 'jtk-endpoint-connected'); + .getConnectionBetweenNodes(`${SWITCH_NODE_NAME}`, setName) + .should('exist'); } }); @@ -69,9 +70,7 @@ describe('Canvas Node Manipulation and Navigation', () => { WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click(); for (let i = 0; i < 2; i++) { WorkflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME, true); - WorkflowPage.getters - .nodeViewBackground() - .click((i + 1) * 200, (i + 1) * 200, { force: true }); + WorkflowPage.getters.nodeView().click((i + 1) * 200, (i + 1) * 200, { force: true }); } WorkflowPage.actions.zoomToFit(); @@ -84,8 +83,6 @@ describe('Canvas Node Manipulation and Navigation', () => { WorkflowPage.getters.getEndpointSelector('input', `${EDIT_FIELDS_SET_NODE_NAME}1`), ); - cy.get('.rect-input-endpoint.jtk-endpoint-connected').should('have.length', 2); - // Connect Set1 and Set2 to merge cy.draganddrop( WorkflowPage.getters.getEndpointSelector('plus', EDIT_FIELDS_SET_NODE_NAME), @@ -95,20 +92,36 @@ describe('Canvas Node Manipulation and Navigation', () => { WorkflowPage.getters.getEndpointSelector('plus', `${EDIT_FIELDS_SET_NODE_NAME}1`), WorkflowPage.getters.getEndpointSelector('input', MERGE_NODE_NAME, 1), ); - - cy.get('.rect-input-endpoint.jtk-endpoint-connected').should('have.length', 4); + const checkConnections = () => { + WorkflowPage.getters + .getConnectionBetweenNodes( + MANUAL_TRIGGER_NODE_DISPLAY_NAME, + `${EDIT_FIELDS_SET_NODE_NAME}1`, + ) + .should('exist'); + WorkflowPage.getters + .getConnectionBetweenNodes(EDIT_FIELDS_SET_NODE_NAME, MERGE_NODE_NAME) + .should('exist'); + WorkflowPage.getters + .getConnectionBetweenNodes(`${EDIT_FIELDS_SET_NODE_NAME}1`, MERGE_NODE_NAME) + .should('exist'); + }; + checkConnections(); // Make sure all connections are there after save & reload WorkflowPage.actions.saveWorkflowOnButtonClick(); cy.reload(); cy.waitForLoad(); - - cy.get('.rect-input-endpoint.jtk-endpoint-connected').should('have.length', 4); + checkConnections(); + // cy.get('.rect-input-endpoint.jtk-endpoint-connected').should('have.length', 4); WorkflowPage.actions.executeWorkflow(); WorkflowPage.getters.stopExecutionButton().should('not.exist'); // If the merged set nodes are connected and executed correctly, there should be 2 items in the output of merge node - cy.get('[data-label="2 items"]').should('be.visible'); + cy.ifCanvasVersion( + () => cy.get('[data-label="2 items"]').should('be.visible'), + () => cy.getByTestId('canvas-node-output-handle').contains('2 items').should('be.visible'), + ); }); it('should add nodes and check execution success', () => { @@ -120,16 +133,42 @@ describe('Canvas Node Manipulation and Navigation', () => { WorkflowPage.actions.zoomToFit(); WorkflowPage.actions.executeWorkflow(); - cy.get('.jtk-connector.success').should('have.length', 3); - cy.get('.data-count').should('have.length', 4); - cy.get('.plus-draggable-endpoint').should('have.class', 'ep-success'); + cy.ifCanvasVersion( + () => cy.get('.jtk-connector.success').should('have.length', 3), + () => cy.get('[data-edge-status=success]').should('have.length', 3), + ); + cy.ifCanvasVersion( + () => cy.get('.data-count').should('have.length', 4), + () => cy.getByTestId('canvas-node-status-success').should('have.length', 4), + ); + + cy.ifCanvasVersion( + () => cy.get('.plus-draggable-endpoint').should('have.class', 'ep-success'), + () => cy.getByTestId('canvas-handle-plus').should('have.attr', 'data-plus-type', 'success'), + ); WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); WorkflowPage.actions.zoomToFit(); - cy.get('.plus-draggable-endpoint').filter(':visible').should('not.have.class', 'ep-success'); - cy.get('.jtk-connector.success').should('have.length', 3); - cy.get('.jtk-connector').should('have.length', 4); + cy.ifCanvasVersion( + () => + cy + .get('.plus-draggable-endpoint') + .filter(':visible') + .should('not.have.class', 'ep-success'), + () => + cy.getByTestId('canvas-handle-plus').should('not.have.attr', 'data-plus-type', 'success'), + ); + + cy.ifCanvasVersion( + () => cy.get('.jtk-connector.success').should('have.length', 3), + // The new version of the canvas correctly shows executed data being passed to the input of the next node + () => cy.get('[data-edge-status=success]').should('have.length', 4), + ); + cy.ifCanvasVersion( + () => cy.get('.data-count').should('have.length', 4), + () => cy.getByTestId('canvas-node-status-success').should('have.length', 4), + ); }); it('should delete node using context menu', () => { @@ -194,19 +233,29 @@ describe('Canvas Node Manipulation and Navigation', () => { WorkflowPage.getters.canvasNodes().should('have.length', 0); }); + // FIXME: Canvas V2: Figure out how to test moving of the node it('should move node', () => { WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME); WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click(); WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); + WorkflowPage.actions.zoomToFit(); WorkflowPage.getters .canvasNodes() .last() .then(($node) => { const { left, top } = $node.position(); - cy.drag('[data-test-id="canvas-node"].jtk-drag-selected', [50, 150], { - clickToFinish: true, - }); + + if (isCanvasV2()) { + cy.drag('.vue-flow__node', [300, 300], { + realMouse: true, + }); + } else { + cy.drag('[data-test-id="canvas-node"].jtk-drag-selected', [50, 150], { + clickToFinish: true, + }); + } + WorkflowPage.getters .canvasNodes() .last() @@ -218,91 +267,80 @@ describe('Canvas Node Manipulation and Navigation', () => { }); }); - it('should zoom in', () => { - WorkflowPage.getters.zoomInButton().should('be.visible').click(); - WorkflowPage.getters - .nodeView() - .should( - 'have.css', - 'transform', - `matrix(${ZOOM_IN_X1_FACTOR}, 0, 0, ${ZOOM_IN_X1_FACTOR}, 0, 0)`, + describe('Canvas Zoom Functionality', () => { + const getContainer = () => + cy.ifCanvasVersion( + () => WorkflowPage.getters.nodeView(), + () => WorkflowPage.getters.canvasViewport(), ); - WorkflowPage.getters.zoomInButton().click(); - WorkflowPage.getters - .nodeView() - .should( - 'have.css', - 'transform', - `matrix(${ZOOM_IN_X2_FACTOR}, 0, 0, ${ZOOM_IN_X2_FACTOR}, 0, 0)`, - ); - }); + const checkZoomLevel = (expectedFactor: number) => { + return getContainer().should(($nodeView) => { + const newTransform = $nodeView.css('transform'); + const newScale = parseFloat(newTransform.split(',')[0].slice(7)); - it('should zoom out', () => { - WorkflowPage.getters.zoomOutButton().should('be.visible').click(); - WorkflowPage.getters - .nodeView() - .should( - 'have.css', - 'transform', - `matrix(${ZOOM_OUT_X1_FACTOR}, 0, 0, ${ZOOM_OUT_X1_FACTOR}, 0, 0)`, - ); - WorkflowPage.getters.zoomOutButton().click(); - WorkflowPage.getters - .nodeView() - .should( - 'have.css', - 'transform', - `matrix(${ZOOM_OUT_X2_FACTOR}, 0, 0, ${ZOOM_OUT_X2_FACTOR}, 0, 0)`, - ); - }); + expect(newScale).to.be.closeTo(expectedFactor, 0.2); + }); + }; - it('should zoom using scroll or pinch gesture', () => { - WorkflowPage.actions.pinchToZoom(1, 'zoomIn'); - WorkflowPage.getters - .nodeView() - .should( - 'have.css', - 'transform', - `matrix(${PINCH_ZOOM_IN_FACTOR}, 0, 0, ${PINCH_ZOOM_IN_FACTOR}, 0, 0)`, + const zoomAndCheck = (action: 'zoomIn' | 'zoomOut', expectedFactor: number) => { + WorkflowPage.getters[`${action}Button`]().click(); + checkZoomLevel(expectedFactor); + }; + + it('should zoom in', () => { + WorkflowPage.getters.zoomInButton().should('be.visible'); + getContainer().then(($nodeView) => { + const initialTransform = $nodeView.css('transform'); + const initialScale = + initialTransform === 'none' ? 1 : parseFloat(initialTransform.split(',')[0].slice(7)); + + zoomAndCheck('zoomIn', initialScale * ZOOM_IN_X1_FACTOR); + zoomAndCheck('zoomIn', initialScale * ZOOM_IN_X2_FACTOR); + }); + }); + + it('should zoom out', () => { + zoomAndCheck('zoomOut', ZOOM_OUT_X1_FACTOR); + zoomAndCheck('zoomOut', ZOOM_OUT_X2_FACTOR); + }); + + it('should zoom using scroll or pinch gesture', () => { + WorkflowPage.actions.pinchToZoom(1, 'zoomIn'); + + // V2 Canvas is using the same zoom factor for both pinch and scroll + cy.ifCanvasVersion( + () => checkZoomLevel(PINCH_ZOOM_IN_FACTOR), + () => checkZoomLevel(ZOOM_IN_X1_FACTOR), ); - WorkflowPage.actions.pinchToZoom(1, 'zoomOut'); - // 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'); + checkZoomLevel(1); // Zoom in 1x + Zoom out 1x should reset to default (=1) - WorkflowPage.actions.pinchToZoom(1, 'zoomOut'); - WorkflowPage.getters - .nodeView() - .should( - 'have.css', - 'transform', - `matrix(${PINCH_ZOOM_OUT_FACTOR}, 0, 0, ${PINCH_ZOOM_OUT_FACTOR}, 0, 0)`, + WorkflowPage.actions.pinchToZoom(1, 'zoomOut'); + + cy.ifCanvasVersion( + () => checkZoomLevel(PINCH_ZOOM_OUT_FACTOR), + () => checkZoomLevel(ZOOM_OUT_X1_FACTOR), ); - }); + }); - it('should reset zoom', () => { - // Reset zoom should not appear until zoom level changed - WorkflowPage.getters.resetZoomButton().should('not.exist'); - WorkflowPage.getters.zoomInButton().click(); - WorkflowPage.getters.resetZoomButton().should('be.visible').click(); - WorkflowPage.getters - .nodeView() - .should( - 'have.css', - 'transform', - `matrix(${DEFAULT_ZOOM_FACTOR}, 0, 0, ${DEFAULT_ZOOM_FACTOR}, 0, 0)`, - ); - }); + it('should reset zoom', () => { + WorkflowPage.getters.resetZoomButton().should('not.exist'); + WorkflowPage.getters.zoomInButton().click(); + WorkflowPage.getters.resetZoomButton().should('be.visible').click(); + checkZoomLevel(DEFAULT_ZOOM_FACTOR); + }); - it('should zoom to fit', () => { - WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME); - WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); - WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); - WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); - // At this point last added node should be off-screen - WorkflowPage.getters.canvasNodes().last().should('not.be.visible'); - WorkflowPage.getters.zoomToFitButton().click(); - WorkflowPage.getters.canvasNodes().last().should('be.visible'); + it('should zoom to fit', () => { + WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME); + WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); + WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); + WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); + // At this point last added node should be off-screen + WorkflowPage.getters.canvasNodes().last().should('not.be.visible'); + WorkflowPage.getters.zoomToFitButton().click(); + WorkflowPage.getters.canvasNodes().last().should('be.visible'); + }); }); it('should disable node (context menu or shortcut)', () => { @@ -426,9 +464,9 @@ describe('Canvas Node Manipulation and Navigation', () => { cy.reload(); cy.waitForLoad(); WorkflowPage.getters.canvasNodes().should('have.length', 2); - cy.get('.rect-input-endpoint.jtk-endpoint-connected').should('have.length', 1); + WorkflowPage.getters.nodeConnections().should('have.length', 1); }); - + // FIXME: Canvas V2: Credentials should show issue on the first open it('should remove unknown credentials on pasting workflow', () => { cy.fixture('workflow-with-unknown-credentials.json').then((data) => { cy.get('body').paste(JSON.stringify(data)); @@ -441,6 +479,7 @@ describe('Canvas Node Manipulation and Navigation', () => { }); }); + // FIXME: Canvas V2: Unknown nodes should still render connection endpoints it('should render connections correctly if unkown nodes are present', () => { const unknownNodeName = 'Unknown node'; cy.createFixtureWorkflow('workflow-with-unknown-nodes.json', 'Unknown nodes'); diff --git a/cypress/e2e/13-pinning.cy.ts b/cypress/e2e/13-pinning.cy.ts index 4f48fa4529..2d3351f8aa 100644 --- a/cypress/e2e/13-pinning.cy.ts +++ b/cypress/e2e/13-pinning.cy.ts @@ -112,7 +112,7 @@ describe('Data pinning', () => { 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.actions.openContextMenu(EDIT_FIELDS_SET_NODE_NAME, { method: 'overflow-button' }); workflowPage.getters .contextMenuAction('toggle_pin') .parent() diff --git a/cypress/e2e/14-mapping.cy.ts b/cypress/e2e/14-mapping.cy.ts index 3bbbd0b293..46448b4966 100644 --- a/cypress/e2e/14-mapping.cy.ts +++ b/cypress/e2e/14-mapping.cy.ts @@ -41,7 +41,9 @@ describe('Data mapping', () => { ndv.actions.mapDataFromHeader(1, 'value'); ndv.getters.inlineExpressionEditorInput().should('have.text', '{{ $json.timestamp }}'); ndv.getters.inlineExpressionEditorInput().type('{esc}'); - ndv.getters.parameterExpressionPreview('value').should('include.text', '2024'); + ndv.getters + .parameterExpressionPreview('value') + .should('include.text', new Date().getFullYear()); ndv.actions.mapDataFromHeader(2, 'value'); ndv.getters @@ -185,7 +187,6 @@ describe('Data mapping', () => { workflowPage.actions.openNode('Set1'); ndv.actions.executePrevious(); - ndv.actions.expandSchemaViewNode(SCHEDULE_TRIGGER_NODE_NAME); const dataPill = ndv.getters .inputDataContainer() diff --git a/cypress/e2e/16-form-trigger-node.cy.ts b/cypress/e2e/16-form-trigger-node.cy.ts index 0162479f7c..ed901107ea 100644 --- a/cypress/e2e/16-form-trigger-node.cy.ts +++ b/cypress/e2e/16-form-trigger-node.cy.ts @@ -16,12 +16,14 @@ describe('n8n Form Trigger', () => { ndv.getters.parameterInput('formDescription').type('Test Form Description'); ndv.getters.parameterInput('fieldLabel').type('Test Field 1'); ndv.getters.backToCanvas().click(); - workflowPage.getters.nodeIssuesByName('n8n Form Trigger').should('not.exist'); + workflowPage.getters.nodeIssuesByName('On form submission').should('not.exist'); }); it('should fill up form fields', () => { - workflowPage.actions.addInitialNodeToCanvas('n8n Form Trigger'); - workflowPage.getters.canvasNodes().first().dblclick(); + workflowPage.actions.addInitialNodeToCanvas('n8n Form Trigger', { + isTrigger: true, + action: 'On new n8n Form event', + }); ndv.getters.parameterInput('formTitle').type('Test Form'); ndv.getters.parameterInput('formDescription').type('Test Form Description'); //fill up first field of type number @@ -42,8 +44,7 @@ describe('n8n Form Trigger', () => { ':nth-child(3) > .border-top-dashed > .parameter-input-list-wrapper > :nth-child(1) > .parameter-item', ) .find('input[placeholder*="e.g. What is your name?"]') - .type('Test Field 3') - .blur(); + .type('Test Field 3'); cy.get( ':nth-child(3) > .border-top-dashed > .parameter-input-list-wrapper > :nth-child(2) > .parameter-item', ).click(); @@ -54,27 +55,24 @@ describe('n8n Form Trigger', () => { ':nth-child(4) > .border-top-dashed > .parameter-input-list-wrapper > :nth-child(1) > .parameter-item', ) .find('input[placeholder*="e.g. What is your name?"]') - .type('Test Field 4') - .blur(); + .type('Test Field 4'); cy.get( ':nth-child(4) > .border-top-dashed > .parameter-input-list-wrapper > :nth-child(2) > .parameter-item', ).click(); getVisibleSelect().contains('Dropdown').click(); - cy.get( - '.border-top-dashed > :nth-child(2) > :nth-child(3) > .multi-parameter > .fixed-collection-parameter > :nth-child(2) > .button', - ).click(); - cy.get( - ':nth-child(4) > :nth-child(1) > :nth-child(2) > :nth-child(3) > .multi-parameter > .fixed-collection-parameter > .fixed-collection-parameter-property > :nth-child(1) > :nth-child(1)', - ) - .find('input') - .type('Option 1') - .blur(); - cy.get( - ':nth-child(4) > :nth-child(1) > :nth-child(2) > :nth-child(3) > .multi-parameter > .fixed-collection-parameter > .fixed-collection-parameter-property > :nth-child(1) > :nth-child(2)', - ) - .find('input') - .type('Option 2') - .blur(); + cy.contains('button', 'Add Field Option').click(); + cy.contains('label', 'Field Options') + .parent() + .nextAll() + .find('[data-test-id="parameter-input-field"]') + .eq(0) + .type('Option 1'); + cy.contains('label', 'Field Options') + .parent() + .nextAll() + .find('[data-test-id="parameter-input-field"]') + .eq(1) + .type('Option 2'); //add optional submitted message cy.get('.param-options').click(); @@ -92,10 +90,9 @@ describe('n8n Form Trigger', () => { .children() .children() .first() - .clear() .type('Your test form was successfully submitted'); ndv.getters.backToCanvas().click(); - workflowPage.getters.nodeIssuesByName('n8n Form Trigger').should('not.exist'); + workflowPage.getters.nodeIssuesByName('On form submission').should('not.exist'); }); }); diff --git a/cypress/e2e/17-sharing.cy.ts b/cypress/e2e/17-sharing.cy.ts index e2af15f101..30a990fb28 100644 --- a/cypress/e2e/17-sharing.cy.ts +++ b/cypress/e2e/17-sharing.cy.ts @@ -1,3 +1,4 @@ +import { saveCredential } from '../composables/modals/credential-modal'; import * as projects from '../composables/projects'; import { INSTANCE_MEMBERS, INSTANCE_OWNER, INSTANCE_ADMIN, NOTION_NODE_NAME } from '../constants'; import { @@ -88,7 +89,7 @@ describe('Sharing', { disableAutoLogin: true }, () => { cy.visit(workflowsPage.url); workflowsPage.getters.workflowCards().should('have.length', 1); - workflowsPage.getters.workflowCard('Workflow W1').click(); + workflowsPage.getters.workflowCardContent('Workflow W1').click(); workflowPage.actions.addNodeToCanvas('Airtable', true, true); ndv.getters.credentialInput().find('input').should('have.value', 'Credential C2'); ndv.actions.close(); @@ -104,7 +105,7 @@ describe('Sharing', { disableAutoLogin: true }, () => { cy.visit(workflowsPage.url); workflowsPage.getters.workflowCards().should('have.length', 2); - workflowsPage.getters.workflowCard('Workflow W1').click(); + workflowsPage.getters.workflowCardContent('Workflow W1').click(); workflowPage.actions.addNodeToCanvas('Airtable', true, true); ndv.getters.credentialInput().find('input').should('have.value', 'Credential C2'); ndv.actions.close(); @@ -133,7 +134,7 @@ describe('Sharing', { disableAutoLogin: true }, () => { cy.visit(workflowsPage.url); workflowsPage.getters.workflowCards().should('have.length', 2); - workflowsPage.getters.workflowCard('Workflow W1').click(); + workflowsPage.getters.workflowCardContent('Workflow W1').click(); workflowPage.actions.openNode('Notion'); ndv.getters .credentialInput() @@ -144,7 +145,7 @@ describe('Sharing', { disableAutoLogin: true }, () => { cy.waitForLoad(); cy.visit(workflowsPage.url); - workflowsPage.getters.workflowCard('Workflow W2').click('top'); + workflowsPage.getters.workflowCardContent('Workflow W2').click('top'); workflowPage.actions.executeWorkflow(); }); @@ -225,7 +226,7 @@ describe('Sharing', { disableAutoLogin: true }, () => { .filter(':contains("Development")') .should('have.length', 1) .click(); - credentialsModal.getters.saveButton().click(); + saveCredential(); credentialsModal.actions.close(); projects.getProjectTabWorkflows().click(); @@ -251,13 +252,13 @@ describe('Sharing', { disableAutoLogin: true }, () => { credentialsModal.actions.changeTab('Sharing'); credentialsModal.getters.usersSelect().click(); getVisibleSelect().find('li').should('have.length', 4).first().click(); - credentialsModal.getters.saveButton().click(); + saveCredential(); credentialsModal.actions.close(); credentialsPage.getters .credentialCards() .should('have.length', 2) - .filter(':contains("Owned by me")') + .filter(':contains("Personal")') .should('have.length', 1); }); }); @@ -351,7 +352,7 @@ describe('Credential Usage in Cross Shared Workflows', () => { credentialsPage.getters.emptyListCreateCredentialButton().click(); credentialsModal.actions.createNewCredential('Notion API'); cy.visit(workflowsPage.url); - workflowsPage.getters.workflowCard(workflowName).click(); + workflowsPage.getters.workflowCardContent(workflowName).click(); workflowPage.actions.addNodeToCanvas(NOTION_NODE_NAME, true, true); // Only the own credential the shared one (+ the 'Create new' option) @@ -396,7 +397,7 @@ describe('Credential Usage in Cross Shared Workflows', () => { credentialsPage.getters.createCredentialButton().click(); credentialsModal.actions.createNewCredential('Notion API'); cy.visit(workflowsPage.url); - workflowsPage.getters.workflowCard(workflowName).click(); + workflowsPage.getters.workflowCardContent(workflowName).click(); workflowPage.actions.addNodeToCanvas(NOTION_NODE_NAME, true, true); // Only the personal credentials of the workflow owner and the global owner diff --git a/cypress/e2e/18-user-management.cy.ts b/cypress/e2e/18-user-management.cy.ts index fe91a72935..d4eb5841cf 100644 --- a/cypress/e2e/18-user-management.cy.ts +++ b/cypress/e2e/18-user-management.cy.ts @@ -148,24 +148,9 @@ describe('User Management', { disableAutoLogin: true }, () => { 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('/static/logo/channel/dev-dark.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('/static/logo/channel/dev.svg'); - }); }); it('should delete user and their data', () => { diff --git a/cypress/e2e/19-execution.cy.ts b/cypress/e2e/19-execution.cy.ts index 81e11b1b63..5be2399253 100644 --- a/cypress/e2e/19-execution.cy.ts +++ b/cypress/e2e/19-execution.cy.ts @@ -1,6 +1,7 @@ import { SCHEDULE_TRIGGER_NODE_NAME, EDIT_FIELDS_SET_NODE_NAME } from '../constants'; import { NDV, WorkflowExecutionsTab, WorkflowPage as WorkflowPageClass } from '../pages'; import { clearNotifications, errorToast, successToast } from '../pages/notifications'; +import { isCanvasV2 } from '../utils/workflowUtils'; const workflowPage = new WorkflowPageClass(); const executionsTab = new WorkflowExecutionsTab(); @@ -117,15 +118,22 @@ describe('Execution', () => { .canvasNodeByName('Manual') .within(() => cy.get('.fa-check')) .should('exist'); - workflowPage.getters - .canvasNodeByName('Wait') - .within(() => cy.get('.fa-sync-alt').should('not.visible')); + + if (isCanvasV2()) { + workflowPage.getters + .canvasNodeByName('Wait') + .within(() => cy.get('.fa-sync-alt').should('not.exist')); + } else { + workflowPage.getters + .canvasNodeByName('Wait') + .within(() => cy.get('.fa-sync-alt').should('not.be.visible')); + } + workflowPage.getters .canvasNodeByName('Set') .within(() => cy.get('.fa-check').should('not.exist')); successToast().should('be.visible'); - clearNotifications(); // Clear execution data workflowPage.getters.clearExecutionDataButton().should('be.visible'); @@ -206,6 +214,7 @@ describe('Execution', () => { workflowPage.getters.clearExecutionDataButton().should('not.exist'); }); + // FIXME: Canvas V2: Webhook should show waiting state but it doesn't it('should test webhook workflow stop', () => { cy.createFixtureWorkflow('Webhook_wait_set.json'); @@ -267,9 +276,17 @@ describe('Execution', () => { .canvasNodeByName('Webhook') .within(() => cy.get('.fa-check')) .should('exist'); - workflowPage.getters - .canvasNodeByName('Wait') - .within(() => cy.get('.fa-sync-alt').should('not.visible')); + + if (isCanvasV2()) { + workflowPage.getters + .canvasNodeByName('Wait') + .within(() => cy.get('.fa-sync-alt').should('not.exist')); + } else { + workflowPage.getters + .canvasNodeByName('Wait') + .within(() => cy.get('.fa-sync-alt').should('not.be.visible')); + } + workflowPage.getters .canvasNodeByName('Set') .within(() => cy.get('.fa-check').should('not.exist')); @@ -295,6 +312,7 @@ describe('Execution', () => { }); }); + // FIXME: Canvas V2: Missing pinned states for `edge-label-wrapper` describe('connections should be colored differently for pinned data', () => { beforeEach(() => { cy.createFixtureWorkflow('Schedule_pinned.json'); diff --git a/cypress/e2e/2-credentials.cy.ts b/cypress/e2e/2-credentials.cy.ts index dbc613bd64..cda01c71a3 100644 --- a/cypress/e2e/2-credentials.cy.ts +++ b/cypress/e2e/2-credentials.cy.ts @@ -1,5 +1,6 @@ import { type ICredentialType } from 'n8n-workflow'; +import { getCredentialSaveButton, saveCredential } from '../composables/modals/credential-modal'; import { AGENT_NODE_NAME, AI_TOOL_HTTP_NODE_NAME, @@ -15,7 +16,7 @@ import { TRELLO_NODE_NAME, } from '../constants'; import { CredentialsModal, CredentialsPage, NDV, WorkflowPage } from '../pages'; -import { successToast } from '../pages/notifications'; +import { errorToast, successToast } from '../pages/notifications'; import { getVisibleSelect } from '../utils'; const credentialsPage = new CredentialsPage(); @@ -26,6 +27,22 @@ const nodeDetailsView = new NDV(); const NEW_CREDENTIAL_NAME = 'Something else'; const NEW_CREDENTIAL_NAME2 = 'Something else entirely'; +function createNotionCredential() { + workflowPage.actions.addNodeToCanvas(NOTION_NODE_NAME); + workflowPage.actions.openNode(NOTION_NODE_NAME); + workflowPage.getters.nodeCredentialsSelect().click(); + getVisibleSelect().find('li').last().click(); + credentialsModal.actions.fillCredentialsForm(); + cy.get('body').type('{esc}'); + workflowPage.actions.deleteNode(NOTION_NODE_NAME); +} + +function deleteSelectedCredential() { + workflowPage.getters.nodeCredentialsEditButton().click(); + credentialsModal.getters.deleteButton().click(); + cy.get('.el-message-box').find('button').contains('Yes').click(); +} + describe('Credentials', () => { beforeEach(() => { cy.visit(credentialsPage.url); @@ -178,7 +195,7 @@ describe('Credentials', () => { credentialsModal.getters.credentialsEditModal().should('be.visible'); credentialsModal.getters.name().click(); credentialsModal.actions.renameCredential(NEW_CREDENTIAL_NAME); - credentialsModal.getters.saveButton().click(); + saveCredential(); credentialsModal.getters.closeButton().click(); workflowPage.getters .nodeCredentialsSelect() @@ -196,7 +213,7 @@ describe('Credentials', () => { credentialsModal.getters.credentialsEditModal().should('be.visible'); credentialsModal.getters.name().click(); credentialsModal.actions.renameCredential(NEW_CREDENTIAL_NAME2); - credentialsModal.getters.saveButton().click(); + saveCredential(); credentialsModal.getters.closeButton().click(); workflowPage.getters .nodeCredentialsSelect() @@ -221,7 +238,7 @@ describe('Credentials', () => { credentialsModal.getters.credentialsEditModal().should('be.visible'); credentialsModal.getters.name().click(); credentialsModal.actions.renameCredential(NEW_CREDENTIAL_NAME); - credentialsModal.getters.saveButton().click(); + saveCredential(); credentialsModal.getters.closeButton().click(); workflowPage.getters .nodeCredentialsSelect() @@ -229,6 +246,40 @@ describe('Credentials', () => { .should('have.value', NEW_CREDENTIAL_NAME); }); + it('should set a default credential when adding nodes', () => { + workflowPage.actions.visit(); + + createNotionCredential(); + + workflowPage.actions.addNodeToCanvas(NOTION_NODE_NAME, true, true); + workflowPage.getters + .nodeCredentialsSelect() + .find('input') + .should('have.value', NEW_NOTION_ACCOUNT_NAME); + + deleteSelectedCredential(); + }); + + it('should set a default credential when editing a node', () => { + workflowPage.actions.visit(); + + createNotionCredential(); + + workflowPage.actions.addNodeToCanvas(HTTP_REQUEST_NODE_NAME, true, true); + nodeDetailsView.getters.parameterInput('authentication').click(); + getVisibleSelect().find('li').contains('Predefined').click(); + + nodeDetailsView.getters.parameterInput('nodeCredentialType').click(); + getVisibleSelect().find('li').contains('Notion API').click(); + + workflowPage.getters + .nodeCredentialsSelect() + .find('input') + .should('have.value', NEW_NOTION_ACCOUNT_NAME); + + deleteSelectedCredential(); + }); + it('should setup generic authentication for HTTP node', () => { workflowPage.actions.visit(); workflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME); @@ -278,4 +329,26 @@ describe('Credentials', () => { credentialsModal.getters.credentialAuthTypeRadioButtons().first().click(); nodeDetailsView.getters.copyInput().should('not.exist'); }); + + it('ADO-2583 should show notifications above credential modal overlay', () => { + // check error notifications because they are sticky + cy.intercept('POST', '/rest/credentials', { forceNetworkError: true }); + credentialsPage.getters.createCredentialButton().click(); + + credentialsModal.getters.newCredentialModal().should('be.visible'); + credentialsModal.getters.newCredentialTypeSelect().should('be.visible'); + credentialsModal.getters.newCredentialTypeOption('Notion API').click(); + + credentialsModal.getters.newCredentialTypeButton().click(); + credentialsModal.getters.connectionParameter('Internal Integration Secret').type('1234567890'); + + credentialsModal.actions.setName('My awesome Notion account'); + getCredentialSaveButton().click(); + + errorToast().should('have.length', 1); + errorToast().should('be.visible'); + + errorToast().should('have.css', 'z-index', '2100'); + cy.get('.el-overlay').should('have.css', 'z-index', '2001'); + }); }); diff --git a/cypress/e2e/2270-ADO-opening-webhook-ndv-marks-workflow-as-unsaved.cy.ts b/cypress/e2e/2270-ADO-opening-webhook-ndv-marks-workflow-as-unsaved.cy.ts new file mode 100644 index 0000000000..eede668e1e --- /dev/null +++ b/cypress/e2e/2270-ADO-opening-webhook-ndv-marks-workflow-as-unsaved.cy.ts @@ -0,0 +1,21 @@ +import { WEBHOOK_NODE_NAME } from '../constants'; +import { NDV, WorkflowPage } from '../pages'; + +const workflowPage = new WorkflowPage(); +const ndv = new NDV(); + +describe('ADO-2270 Save button resets on webhook node open', () => { + it('should not reset the save button if webhook node is opened and closed', () => { + workflowPage.actions.visit(); + workflowPage.actions.addInitialNodeToCanvas(WEBHOOK_NODE_NAME); + workflowPage.getters.saveButton().click(); + workflowPage.actions.openNode(WEBHOOK_NODE_NAME); + + ndv.actions.close(); + + cy.ifCanvasVersion( + () => cy.getByTestId('workflow-save-button').should('not.contain', 'Saved'), + () => cy.getByTestId('workflow-save-button').should('contain', 'Saved'), + ); + }); +}); diff --git a/cypress/e2e/233-AI-switch-to-logs-on-error.cy.ts b/cypress/e2e/233-AI-switch-to-logs-on-error.cy.ts index eca3af81fb..79f33b841c 100644 --- a/cypress/e2e/233-AI-switch-to-logs-on-error.cy.ts +++ b/cypress/e2e/233-AI-switch-to-logs-on-error.cy.ts @@ -90,6 +90,14 @@ function createRunDataWithError(inputMessage: string) { routine: 'InitPostgres', } as unknown as Error, } as ExecutionError, + metadata: { + subRun: [ + { + node: 'Postgres Chat Memory', + runIndex: 0, + }, + ], + }, }), createMockNodeExecutionData(AGENT_NODE_NAME, { executionStatus: 'error', @@ -124,14 +132,6 @@ function createRunDataWithError(inputMessage: string) { description: 'Internal error', message: 'Internal error', } as unknown as ExecutionError, - metadata: { - subRun: [ - { - node: 'Postgres Chat Memory', - runIndex: 0, - }, - ], - }, }), ]; } diff --git a/cypress/e2e/2372-ado-prevent-clipping-params.cy.ts b/cypress/e2e/2372-ado-prevent-clipping-params.cy.ts new file mode 100644 index 0000000000..260c6e48c9 --- /dev/null +++ b/cypress/e2e/2372-ado-prevent-clipping-params.cy.ts @@ -0,0 +1,86 @@ +import { NDV, WorkflowPage } from '../pages'; + +const workflowPage = new WorkflowPage(); +const ndv = new NDV(); + +describe('ADO-2362 ADO-2350 NDV Prevent clipping long parameters and scrolling to expression', () => { + it('should show last parameters and open at scroll top of parameters', () => { + workflowPage.actions.visit(); + cy.createFixtureWorkflow('Test-workflow-with-long-parameters.json'); + workflowPage.actions.openNode('Schedule Trigger'); + + ndv.getters.inlineExpressionEditorInput().should('be.visible'); + + ndv.actions.close(); + + workflowPage.actions.openNode('Edit Fields1'); + + // first parameter should be visible + ndv.getters.inputLabel().eq(0).should('include.text', 'Mode'); + ndv.getters.inputLabel().eq(0).should('be.visible'); + + ndv.getters.inlineExpressionEditorInput().should('have.length', 2); + + // last parameter in view should be visible + ndv.getters.inlineExpressionEditorInput().eq(0).should('have.text', 'should be visible!'); + ndv.getters.inlineExpressionEditorInput().eq(0).should('be.visible'); + + // next parameter in view should not be visible + ndv.getters.inlineExpressionEditorInput().eq(1).should('have.text', 'not visible'); + ndv.getters.inlineExpressionEditorInput().eq(1).should('not.be.visible'); + + ndv.actions.close(); + workflowPage.actions.openNode('Schedule Trigger'); + + // first parameter (notice) should be visible + ndv.getters.nthParam(0).should('include.text', 'This workflow will run on the schedule '); + ndv.getters.inputLabel().eq(0).should('be.visible'); + + ndv.getters.inlineExpressionEditorInput().should('have.length', 2); + + // last parameter in view should be visible + ndv.getters.inlineExpressionEditorInput().eq(0).should('have.text', 'should be visible'); + ndv.getters.inlineExpressionEditorInput().eq(0).should('be.visible'); + + // next parameter in view should not be visible + ndv.getters.inlineExpressionEditorInput().eq(1).should('have.text', 'not visible'); + ndv.getters.inlineExpressionEditorInput().eq(1).should('not.be.visible'); + + ndv.actions.close(); + workflowPage.actions.openNode('Slack'); + + // first field (credentials) should be visible + ndv.getters.nodeCredentialsLabel().should('be.visible'); + + // last parameter in view should be visible + ndv.getters.inlineExpressionEditorInput().eq(0).should('have.text', 'should be visible'); + ndv.getters.inlineExpressionEditorInput().eq(0).should('be.visible'); + + // next parameter in view should not be visible + ndv.getters.inlineExpressionEditorInput().eq(1).should('have.text', 'not visible'); + ndv.getters.inlineExpressionEditorInput().eq(1).should('not.be.visible'); + }); + + it('NODE-1272 ensure expressions scrolled to top, not middle', () => { + workflowPage.actions.visit(); + cy.createFixtureWorkflow('Test-workflow-with-long-parameters.json'); + workflowPage.actions.openNode('With long expression'); + + ndv.getters.inlineExpressionEditorInput().eq(0).should('be.visible'); + // should be scrolled at top + ndv.getters + .inlineExpressionEditorInput() + .eq(0) + .find('.cm-line') + .eq(0) + .should('have.text', '1 visible!'); + ndv.getters.inlineExpressionEditorInput().eq(0).find('.cm-line').eq(0).should('be.visible'); + ndv.getters + .inlineExpressionEditorInput() + .eq(0) + .find('.cm-line') + .eq(6) + .should('have.text', '7 not visible!'); + ndv.getters.inlineExpressionEditorInput().eq(0).find('.cm-line').eq(6).should('not.be.visible'); + }); +}); diff --git a/cypress/e2e/24-ndv-paired-item.cy.ts b/cypress/e2e/24-ndv-paired-item.cy.ts index 1261a0fcd1..49257a8a12 100644 --- a/cypress/e2e/24-ndv-paired-item.cy.ts +++ b/cypress/e2e/24-ndv-paired-item.cy.ts @@ -31,29 +31,31 @@ describe('NDV', () => { ndv.getters.inputTableRow(1).invoke('attr', 'data-test-id').should('equal', 'hovering-item'); - ndv.getters.inputTableRow(1).realHover(); + ndv.actions.dragMainPanelToRight(); + ndv.getters.inputTableRow(1).realMouseMove(10, 1); ndv.getters.outputTableRow(4).invoke('attr', 'data-test-id').should('equal', 'hovering-item'); - ndv.getters.inputTableRow(2).realHover(); + ndv.getters.inputTableRow(2).realMouseMove(10, 1); ndv.getters.outputTableRow(2).invoke('attr', 'data-test-id').should('equal', 'hovering-item'); - ndv.getters.inputTableRow(3).realHover(); + ndv.getters.inputTableRow(3).realMouseMove(10, 1); ndv.getters.outputTableRow(6).invoke('attr', 'data-test-id').should('equal', 'hovering-item'); // output to input - ndv.getters.outputTableRow(1).realHover(); + ndv.actions.dragMainPanelToLeft(); + ndv.getters.outputTableRow(1).realMouseMove(10, 1); ndv.getters.inputTableRow(4).invoke('attr', 'data-test-id').should('equal', 'hovering-item'); - ndv.getters.outputTableRow(4).realHover(); + ndv.getters.outputTableRow(4).realMouseMove(10, 1); ndv.getters.inputTableRow(1).invoke('attr', 'data-test-id').should('equal', 'hovering-item'); - ndv.getters.outputTableRow(2).realHover(); + ndv.getters.outputTableRow(2).realMouseMove(10, 1); ndv.getters.inputTableRow(2).invoke('attr', 'data-test-id').should('equal', 'hovering-item'); - ndv.getters.outputTableRow(6).realHover(); + ndv.getters.outputTableRow(6).realMouseMove(10, 1); ndv.getters.inputTableRow(3).invoke('attr', 'data-test-id').should('equal', 'hovering-item'); - ndv.getters.outputTableRow(1).realHover(); + ndv.getters.outputTableRow(1).realMouseMove(10, 1); ndv.getters.inputTableRow(4).invoke('attr', 'data-test-id').should('equal', 'hovering-item'); }); @@ -75,31 +77,32 @@ describe('NDV', () => { ndv.actions.switchInputMode('Table'); ndv.actions.switchOutputMode('Table'); - ndv.getters.backToCanvas().realHover(); // reset to default hover + ndv.getters.backToCanvas().realMouseMove(10, 1); // reset to default hover ndv.getters.outputHoveringItem().should('not.exist'); ndv.getters.parameterExpressionPreview('value').should('include.text', '1111'); ndv.actions.selectInputNode('Set1'); - ndv.getters.backToCanvas().realHover(); // reset to default hover + ndv.getters.backToCanvas().realMouseMove(10, 1); // reset to default hover ndv.getters.inputTableRow(1).should('have.text', '1000'); ndv.getters.inputTableRow(1).invoke('attr', 'data-test-id').should('equal', 'hovering-item'); - ndv.getters.inputTableRow(1).realHover(); - cy.wait(50); + ndv.actions.dragMainPanelToRight(); + ndv.getters.inputTbodyCell(1, 0).realMouseMove(10, 1); ndv.getters.outputHoveringItem().should('have.text', '1000'); ndv.getters.parameterExpressionPreview('value').should('include.text', '1000'); ndv.actions.selectInputNode('Sort'); + ndv.actions.dragMainPanelToLeft(); ndv.actions.changeOutputRunSelector('1 of 2 (6 items)'); - ndv.getters.backToCanvas().realHover(); // reset to default hover + ndv.getters.backToCanvas().realMouseMove(10, 1); // reset to default hover ndv.getters.inputTableRow(1).should('have.text', '1111'); ndv.getters.inputTableRow(1).invoke('attr', 'data-test-id').should('equal', 'hovering-item'); - ndv.getters.inputTableRow(1).realHover(); - cy.wait(50); + ndv.actions.dragMainPanelToRight(); + ndv.getters.inputTbodyCell(1, 0).realMouseMove(10, 1); ndv.getters.outputHoveringItem().should('have.text', '1111'); ndv.getters.parameterExpressionPreview('value').should('include.text', '1111'); }); @@ -132,20 +135,22 @@ describe('NDV', () => { ndv.getters.inputTableRow(1).should('have.text', '1111'); ndv.getters.inputTableRow(1).invoke('attr', 'data-test-id').should('equal', 'hovering-item'); + + ndv.actions.dragMainPanelToLeft(); ndv.getters.outputTableRow(1).should('have.text', '1111'); - ndv.getters.outputTableRow(1).realHover(); + ndv.getters.outputTableRow(1).realMouseMove(10, 1); ndv.getters.outputTableRow(3).should('have.text', '4444'); - ndv.getters.outputTableRow(3).realHover(); + ndv.getters.outputTableRow(3).realMouseMove(10, 1); ndv.getters.inputTableRow(3).should('have.text', '4444'); ndv.getters.inputTableRow(3).invoke('attr', 'data-test-id').should('equal', 'hovering-item'); ndv.actions.changeOutputRunSelector('2 of 2 (6 items)'); - cy.wait(50); ndv.getters.inputTableRow(1).should('have.text', '1000'); - ndv.getters.inputTableRow(1).realHover(); + ndv.actions.dragMainPanelToRight(); + ndv.getters.inputTableRow(1).realMouseMove(10, 1); ndv.getters.outputTableRow(1).should('have.text', '1000'); ndv.getters @@ -155,7 +160,8 @@ describe('NDV', () => { .should('equal', 'hovering-item'); ndv.getters.outputTableRow(3).should('have.text', '2000'); - ndv.getters.outputTableRow(3).realHover(); + ndv.actions.dragMainPanelToLeft(); + ndv.getters.outputTableRow(3).realMouseMove(10, 1); ndv.getters.inputTableRow(3).should('have.text', '2000'); @@ -175,14 +181,15 @@ describe('NDV', () => { ndv.actions.switchOutputBranch('False Branch (2 items)'); ndv.getters.outputTableRow(1).should('have.text', '8888'); - ndv.getters.outputTableRow(1).realHover(); + ndv.actions.dragMainPanelToLeft(); + ndv.getters.outputTableRow(1).realMouseMove(10, 1); ndv.getters.inputTableRow(5).should('have.text', '8888'); ndv.getters.inputTableRow(5).invoke('attr', 'data-test-id').should('equal', 'hovering-item'); ndv.getters.outputTableRow(2).should('have.text', '9999'); - ndv.getters.outputTableRow(2).realHover(); + ndv.getters.outputTableRow(2).realMouseMove(10, 1); ndv.getters.inputTableRow(6).should('have.text', '9999'); @@ -192,29 +199,35 @@ describe('NDV', () => { workflowPage.actions.openNode('Set5'); + ndv.actions.dragMainPanelToRight(); ndv.actions.switchInputBranch('True Branch'); + + ndv.actions.dragMainPanelToLeft(); ndv.actions.changeOutputRunSelector('1 of 2 (2 items)'); ndv.getters.outputTableRow(1).should('have.text', '8888'); - ndv.getters.outputTableRow(1).realHover(); - cy.wait(100); + ndv.getters.outputTableRow(1).realMouseMove(10, 1); ndv.getters.inputHoveringItem().should('not.exist'); ndv.getters.inputTableRow(1).should('have.text', '1111'); - ndv.getters.inputTableRow(1).realHover(); - cy.wait(100); + + ndv.actions.dragMainPanelToRight(); + ndv.getters.inputTableRow(1).realMouseMove(10, 1); ndv.getters.outputHoveringItem().should('not.exist'); ndv.actions.switchInputBranch('False Branch'); ndv.getters.inputTableRow(1).should('have.text', '8888'); - ndv.getters.inputTableRow(1).realHover(); + ndv.actions.dragMainPanelToRight(); + ndv.getters.inputTableRow(1).realMouseMove(10, 1); + ndv.actions.dragMainPanelToLeft(); ndv.actions.changeOutputRunSelector('2 of 2 (4 items)'); ndv.getters.outputTableRow(1).should('have.text', '1111'); - ndv.getters.outputTableRow(1).realHover(); + ndv.getters.outputTableRow(1).realMouseMove(10, 1); ndv.actions.changeOutputRunSelector('1 of 2 (2 items)'); ndv.getters.inputTableRow(1).should('have.text', '8888'); - ndv.getters.inputTableRow(1).realHover(); + ndv.actions.dragMainPanelToRight(); + ndv.getters.inputTableRow(1).realMouseMove(10, 1); ndv.getters.outputHoveringItem().should('have.text', '8888'); // todo there's a bug here need to fix ADO-534 // ndv.getters.outputHoveringItem().should('not.exist'); diff --git a/cypress/e2e/27-two-factor-authentication.cy.ts b/cypress/e2e/27-two-factor-authentication.cy.ts index dc62a0c58c..05949a188c 100644 --- a/cypress/e2e/27-two-factor-authentication.cy.ts +++ b/cypress/e2e/27-two-factor-authentication.cy.ts @@ -49,33 +49,47 @@ describe('Two-factor authentication', { disableAutoLogin: true }, () => { cy.intercept('GET', '/rest/mfa/qr').as('getMfaQrCode'); }); - it('Should be able to login with MFA token', () => { + it('Should be able to login with MFA code', () => { const { email, password } = user; signinPage.actions.loginWithEmailAndPassword(email, password); personalSettingsPage.actions.enableMfa(); mainSidebar.actions.signout(); - const token = generateOTPToken(user.mfaSecret); - mfaLoginPage.actions.loginWithMfaToken(email, password, token); + const mfaCode = generateOTPToken(user.mfaSecret); + mfaLoginPage.actions.loginWithMfaCode(email, password, mfaCode); mainSidebar.actions.signout(); }); - it('Should be able to login with recovery code', () => { + it('Should be able to login with MFA recovery code', () => { const { email, password } = user; signinPage.actions.loginWithEmailAndPassword(email, password); personalSettingsPage.actions.enableMfa(); mainSidebar.actions.signout(); - mfaLoginPage.actions.loginWithRecoveryCode(email, password, user.mfaRecoveryCodes[0]); + mfaLoginPage.actions.loginWithMfaRecoveryCode(email, password, user.mfaRecoveryCodes[0]); mainSidebar.actions.signout(); }); - it('Should be able to disable MFA in account', () => { + it('Should be able to disable MFA in account with MFA code', () => { const { email, password } = user; signinPage.actions.loginWithEmailAndPassword(email, password); personalSettingsPage.actions.enableMfa(); mainSidebar.actions.signout(); - const token = generateOTPToken(user.mfaSecret); - mfaLoginPage.actions.loginWithMfaToken(email, password, token); - personalSettingsPage.actions.disableMfa(); + const mfaCode = generateOTPToken(user.mfaSecret); + mfaLoginPage.actions.loginWithMfaCode(email, password, mfaCode); + const disableToken = generateOTPToken(user.mfaSecret); + personalSettingsPage.actions.disableMfa(disableToken); + personalSettingsPage.getters.enableMfaButton().should('exist'); + mainSidebar.actions.signout(); + }); + + it('Should be able to disable MFA in account with recovery code', () => { + const { email, password } = user; + signinPage.actions.loginWithEmailAndPassword(email, password); + personalSettingsPage.actions.enableMfa(); + mainSidebar.actions.signout(); + const mfaCode = generateOTPToken(user.mfaSecret); + mfaLoginPage.actions.loginWithMfaCode(email, password, mfaCode); + personalSettingsPage.actions.disableMfa(user.mfaRecoveryCodes[0]); + personalSettingsPage.getters.enableMfaButton().should('exist'); mainSidebar.actions.signout(); }); }); diff --git a/cypress/e2e/28-debug.cy.ts b/cypress/e2e/28-debug.cy.ts index bc1f03c162..9149e2e478 100644 --- a/cypress/e2e/28-debug.cy.ts +++ b/cypress/e2e/28-debug.cy.ts @@ -87,11 +87,28 @@ describe('Debug', () => { confirmDialog.get('.btn--confirm').click(); cy.url().should('include', '/debug'); - workflowPage.getters.canvasNodes().first().should('have.descendants', '.node-pin-data-icon'); - workflowPage.getters - .canvasNodes() - .not(':first') - .should('not.have.descendants', '.node-pin-data-icon'); + cy.ifCanvasVersion( + () => { + workflowPage.getters + .canvasNodes() + .first() + .should('have.descendants', '.node-pin-data-icon'); + workflowPage.getters + .canvasNodes() + .not(':first') + .should('not.have.descendants', '.node-pin-data-icon'); + }, + () => { + workflowPage.getters + .canvasNodes() + .first() + .should('have.descendants', '[data-test-id="canvas-node-status-pinned"]'); + workflowPage.getters + .canvasNodes() + .not(':first') + .should('not.have.descendants', '[data-test-id="canvas-node-status-pinned"]'); + }, + ); cy.reload(true); cy.wait(['@getExecution']); @@ -114,10 +131,22 @@ describe('Debug', () => { confirmDialog.get('.btn--confirm').click(); cy.url().should('include', '/debug'); - workflowPage.getters.canvasNodes().last().find('.node-info-icon').should('be.empty'); + cy.ifCanvasVersion( + () => { + workflowPage.getters.canvasNodes().last().find('.node-info-icon').should('be.empty'); + }, + () => { + workflowPage.getters + .canvasNodes() + .last() + .find('[class*="statusIcons"]') + .should('not.exist'); + }, + ); workflowPage.getters.canvasNodes().first().dblclick(); - ndv.getters.pinDataButton().click(); + ndv.actions.unPinData(); + ndv.actions.close(); workflowPage.actions.saveWorkflowUsingKeyboardShortcut(); diff --git a/cypress/e2e/29-templates.cy.ts b/cypress/e2e/29-templates.cy.ts index 5b52889c94..727b0cbe3f 100644 --- a/cypress/e2e/29-templates.cy.ts +++ b/cypress/e2e/29-templates.cy.ts @@ -129,7 +129,6 @@ describe('Workflow templates', () => { workflowPage.actions.shouldHaveWorkflowName('Demo: ' + OnboardingWorkflow.name); workflowPage.getters.canvasNodes().should('have.length', 4); workflowPage.getters.stickies().should('have.length', 1); - workflowPage.getters.canvasNodes().first().should('have.descendants', '.node-pin-data-icon'); }); it('can import template', () => { @@ -142,6 +141,7 @@ describe('Workflow templates', () => { }); it('should save template id with the workflow', () => { + cy.intercept('POST', '/rest/workflows').as('saveWorkflow'); templatesPage.actions.importTemplate(); cy.visit(templatesPage.url); @@ -159,10 +159,8 @@ describe('Workflow templates', () => { workflowPage.actions.hitSelectAll(); workflowPage.actions.hitCopy(); - cy.grantBrowserPermissions('clipboardReadWrite', 'clipboardSanitizedWrite'); - // Check workflow JSON by copying it to clipboard - cy.readClipboard().then((workflowJSON) => { - expect(workflowJSON).to.contain('"templateId": "1"'); + cy.wait('@saveWorkflow').then((interception) => { + expect(interception.request.body.meta.templateId).to.equal('1'); }); }); diff --git a/cypress/e2e/2929-ado-can-load-old-switch-node-workflows.cy.ts b/cypress/e2e/2929-ado-can-load-old-switch-node-workflows.cy.ts new file mode 100644 index 0000000000..39edc54163 --- /dev/null +++ b/cypress/e2e/2929-ado-can-load-old-switch-node-workflows.cy.ts @@ -0,0 +1,17 @@ +import { + deleteNode, + getCanvasNodes, + navigateToNewWorkflowPage, + pasteWorkflow, +} from '../composables/workflow'; +import Workflow from '../fixtures/Switch_node_with_null_connection.json'; + +describe('ADO-2929 can load Switch nodes', () => { + it('can load workflows with Switch nodes with null at connection index', () => { + navigateToNewWorkflowPage(); + pasteWorkflow(Workflow); + getCanvasNodes().should('have.length', 3); + deleteNode('Switch'); + getCanvasNodes().should('have.length', 2); + }); +}); diff --git a/cypress/e2e/30-langchain.cy.ts b/cypress/e2e/30-langchain.cy.ts index 0deec76e9f..96a03be961 100644 --- a/cypress/e2e/30-langchain.cy.ts +++ b/cypress/e2e/30-langchain.cy.ts @@ -14,7 +14,6 @@ import { } from './../constants'; import { closeManualChatModal, - getManualChatDialog, getManualChatMessages, getManualChatModal, getManualChatModalLogs, @@ -27,6 +26,8 @@ import { clickCreateNewCredential, clickExecuteNode, clickGetBackToCanvas, + getRunDataInfoCallout, + getOutputPanelTable, toggleParameterCheckboxInputByName, } from '../composables/ndv'; import { @@ -44,6 +45,7 @@ import { openNode, getConnectionBySourceAndTarget, } from '../composables/workflow'; +import { NDV, WorkflowPage } from '../pages'; import { createMockNodeExecutionData, runMockWorkflowExecution } from '../utils'; describe('Langchain Integration', () => { @@ -167,7 +169,7 @@ describe('Langchain Integration', () => { lastNodeExecuted: BASIC_LLM_CHAIN_NODE_NAME, }); - getManualChatDialog().should('contain', outputMessage); + getManualChatMessages().should('contain', outputMessage); }); it('should be able to open and execute Agent node', () => { @@ -207,7 +209,7 @@ describe('Langchain Integration', () => { lastNodeExecuted: AGENT_NODE_NAME, }); - getManualChatDialog().should('contain', outputMessage); + getManualChatMessages().should('contain', outputMessage); }); it('should add and use Manual Chat Trigger node together with Agent node', () => { @@ -228,99 +230,98 @@ describe('Langchain Integration', () => { clickManualChatButton(); - getManualChatModalLogs().should('not.exist'); - const inputMessage = 'Hello!'; const outputMessage = 'Hi there! How can I assist you today?'; + const 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, + }, + }, + }, + }, + }, + metadata: { + subRun: [{ node: AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, runIndex: 0 }], + }, + 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?' }, + }, + }), + ]; runMockWorkflowExecution({ 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 }], - }, - }), - ], + runData, lastNodeExecuted: AGENT_NODE_NAME, }); @@ -333,6 +334,8 @@ describe('Langchain Integration', () => { getManualChatModalLogsEntries().should('have.length', 1); closeManualChatModal(); + getManualChatModalLogs().should('not.exist'); + getManualChatModal().should('not.exist'); }); it('should auto-add chat trigger and basic LLM chain when adding LLM node', () => { @@ -357,4 +360,162 @@ describe('Langchain Integration', () => { getConnectionBySourceAndTarget(CHAT_TRIGGER_NODE_DISPLAY_NAME, AGENT_NODE_NAME).should('exist'); getNodes().should('have.length', 3); }); + it('should not auto-add nodes if ChatTrigger is already present', () => { + addNodeToCanvas(MANUAL_CHAT_TRIGGER_NODE_NAME, true); + addNodeToCanvas(AGENT_NODE_NAME, true); + + addNodeToCanvas(AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, true); + getConnectionBySourceAndTarget(CHAT_TRIGGER_NODE_DISPLAY_NAME, AGENT_NODE_NAME).should('exist'); + getNodes().should('have.length', 3); + }); + it('should render runItems for sub-nodes and allow switching between them', () => { + const workflowPage = new WorkflowPage(); + const ndv = new NDV(); + + cy.visit(workflowPage.url); + cy.createFixtureWorkflow('In_memory_vector_store_fake_embeddings.json'); + workflowPage.actions.zoomToFit(); + + workflowPage.actions.executeNode('Populate VS'); + cy.get('[data-label="25 items"]').should('exist'); + + const assertInputOutputText = (text: string, assertion: 'exist' | 'not.exist') => { + ndv.getters.outputPanel().contains(text).should(assertion); + ndv.getters.inputPanel().contains(text).should(assertion); + }; + + workflowPage.actions.openNode('Character Text Splitter'); + ndv.getters.outputRunSelector().should('exist'); + ndv.getters.inputRunSelector().should('exist'); + ndv.getters.inputRunSelector().find('input').should('include.value', '3 of 3'); + ndv.getters.outputRunSelector().find('input').should('include.value', '3 of 3'); + assertInputOutputText('Kyiv', 'exist'); + assertInputOutputText('Berlin', 'not.exist'); + assertInputOutputText('Prague', 'not.exist'); + + ndv.actions.changeOutputRunSelector('2 of 3'); + assertInputOutputText('Berlin', 'exist'); + assertInputOutputText('Kyiv', 'not.exist'); + assertInputOutputText('Prague', 'not.exist'); + + ndv.actions.changeOutputRunSelector('1 of 3'); + assertInputOutputText('Prague', 'exist'); + assertInputOutputText('Berlin', 'not.exist'); + assertInputOutputText('Kyiv', 'not.exist'); + + ndv.actions.toggleInputRunLinking(); + ndv.actions.changeOutputRunSelector('2 of 3'); + ndv.getters.inputRunSelector().find('input').should('include.value', '1 of 3'); + ndv.getters.outputRunSelector().find('input').should('include.value', '2 of 3'); + ndv.getters.inputPanel().contains('Prague').should('exist'); + ndv.getters.inputPanel().contains('Berlin').should('not.exist'); + + ndv.getters.outputPanel().contains('Berlin').should('exist'); + ndv.getters.outputPanel().contains('Prague').should('not.exist'); + + ndv.actions.toggleInputRunLinking(); + ndv.getters.inputRunSelector().find('input').should('include.value', '1 of 3'); + ndv.getters.outputRunSelector().find('input').should('include.value', '1 of 3'); + assertInputOutputText('Prague', 'exist'); + assertInputOutputText('Berlin', 'not.exist'); + assertInputOutputText('Kyiv', 'not.exist'); + }); + + it('should show tool info notice if no existing tools were used during execution', () => { + addNodeToCanvas(MANUAL_CHAT_TRIGGER_NODE_NAME, true); + addNodeToCanvas(AGENT_NODE_NAME, true); + + addLanguageModelNodeToParent( + AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, + AGENT_NODE_NAME, + true, + ); + + clickCreateNewCredential(); + setCredentialValues({ + apiKey: 'sk_test_123', + }); + clickGetBackToCanvas(); + + addToolNodeToParent(AI_TOOL_CALCULATOR_NODE_NAME, AGENT_NODE_NAME); + clickGetBackToCanvas(); + openNode(AGENT_NODE_NAME); + + const inputMessage = 'Hello!'; + const outputMessage = 'Hi there! How can I assist you today?'; + + clickExecuteNode(); + + runMockWorkflowExecution({ + trigger: () => sendManualChatMessage(inputMessage), + 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, + }); + closeManualChatModal(); + openNode(AGENT_NODE_NAME); + + getRunDataInfoCallout().should('exist'); + }); + + it('should not show tool info notice if tools were used during execution', () => { + addNodeToCanvas(MANUAL_CHAT_TRIGGER_NODE_NAME, true); + addNodeToCanvas(AGENT_NODE_NAME, true, true); + getRunDataInfoCallout().should('not.exist'); + clickGetBackToCanvas(); + + addLanguageModelNodeToParent( + AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, + AGENT_NODE_NAME, + true, + ); + + clickCreateNewCredential(); + setCredentialValues({ + apiKey: 'sk_test_123', + }); + clickGetBackToCanvas(); + + addToolNodeToParent(AI_TOOL_CALCULATOR_NODE_NAME, AGENT_NODE_NAME); + clickGetBackToCanvas(); + openNode(AGENT_NODE_NAME); + + getRunDataInfoCallout().should('not.exist'); + + const inputMessage = 'Hello!'; + const outputMessage = 'Hi there! How can I assist you today?'; + + clickExecuteNode(); + + runMockWorkflowExecution({ + trigger: () => sendManualChatMessage(inputMessage), + runData: [ + createMockNodeExecutionData(AGENT_NODE_NAME, { + jsonData: { + main: { output: outputMessage }, + }, + metadata: { + subRun: [{ node: AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, runIndex: 0 }], + }, + }), + createMockNodeExecutionData(AI_TOOL_CALCULATOR_NODE_NAME, {}), + ], + lastNodeExecuted: AGENT_NODE_NAME, + }); + + closeManualChatModal(); + openNode(AGENT_NODE_NAME); + // This waits to ensure the output panel is rendered + getOutputPanelTable(); + + getRunDataInfoCallout().should('not.exist'); + }); }); diff --git a/cypress/e2e/31-demo.cy.ts b/cypress/e2e/31-demo.cy.ts index 32307361fd..7f13d1659a 100644 --- a/cypress/e2e/31-demo.cy.ts +++ b/cypress/e2e/31-demo.cy.ts @@ -1,21 +1,29 @@ -import workflow from '../fixtures/Manual_wait_set.json'; +import { getOutputTableRow } from '../composables/ndv'; +import { getCanvasNodes, openNode } from '../composables/workflow'; +import SIMPLE_WORKFLOW from '../fixtures/Manual_wait_set.json'; +import WORKFLOW_WITH_PINNED from '../fixtures/Webhook_set_pinned.json'; import { importWorkflow, visitDemoPage } from '../pages/demo'; import { errorToast } from '../pages/notifications'; -import { WorkflowPage } from '../pages/workflow'; - -const workflowPage = new WorkflowPage(); describe('Demo', () => { beforeEach(() => { cy.overrideSettings({ previewMode: true }); - cy.signout(); }); it('can import template', () => { visitDemoPage(); errorToast().should('not.exist'); - importWorkflow(workflow); - workflowPage.getters.canvasNodes().should('have.length', 3); + importWorkflow(SIMPLE_WORKFLOW); + getCanvasNodes().should('have.length', 3); + }); + + it('can import workflow with pin data', () => { + visitDemoPage(); + importWorkflow(WORKFLOW_WITH_PINNED); + getCanvasNodes().should('have.length', 2); + openNode('Webhook'); + getOutputTableRow(0).should('include.text', 'headers'); + getOutputTableRow(1).should('include.text', 'dragons'); }); it('can override theme to dark', () => { diff --git a/cypress/e2e/34-template-credentials-setup.cy.ts b/cypress/e2e/34-template-credentials-setup.cy.ts index 815f4b1ceb..386c83eb0a 100644 --- a/cypress/e2e/34-template-credentials-setup.cy.ts +++ b/cypress/e2e/34-template-credentials-setup.cy.ts @@ -56,10 +56,10 @@ describe('Template credentials setup', () => { it('can be opened from template collection page', () => { visitTemplateCollectionPage(testData.ecommerceStarterPack); templateCredentialsSetupPage.enableTemplateCredentialSetupFeatureFlag(); - clickUseWorkflowButtonByTitle('Promote new Shopify products on Twitter and Telegram'); + clickUseWorkflowButtonByTitle('Promote new Shopify products'); templateCredentialsSetupPage.getters - .title("Set up 'Promote new Shopify products on Twitter and Telegram' template") + .title("Set up 'Promote new Shopify products' template") .should('be.visible'); }); @@ -67,7 +67,7 @@ describe('Template credentials setup', () => { templateCredentialsSetupPage.visitTemplateCredentialSetupPage(testTemplate.id); templateCredentialsSetupPage.getters - .title("Set up 'Promote new Shopify products on Twitter and Telegram' template") + .title("Set up 'Promote new Shopify products' template") .should('be.visible'); templateCredentialsSetupPage.getters diff --git a/cypress/e2e/39-projects.cy.ts b/cypress/e2e/39-projects.cy.ts index 4e3bb583df..efd18bca74 100644 --- a/cypress/e2e/39-projects.cy.ts +++ b/cypress/e2e/39-projects.cy.ts @@ -15,6 +15,7 @@ import { NDV, MainSidebar, } from '../pages'; +import { clearNotifications, successToast } from '../pages/notifications'; import { getVisibleDropdown, getVisibleModalOverlay, getVisibleSelect } from '../utils'; const workflowsPage = new WorkflowsPage(); @@ -50,7 +51,7 @@ describe('Projects', { disableAutoLogin: true }, () => { }); projects.getHomeButton().click(); - projects.getProjectTabs().should('have.length', 2); + projects.getProjectTabs().should('have.length', 3); projects.getProjectTabCredentials().click(); credentialsPage.getters.credentialCards().should('not.have.length'); @@ -100,7 +101,7 @@ describe('Projects', { disableAutoLogin: true }, () => { projects.getMenuItems().first().click(); workflowsPage.getters.workflowCards().should('not.have.length'); - projects.getProjectTabs().should('have.length', 3); + projects.getProjectTabs().should('have.length', 4); workflowsPage.getters.newWorkflowButtonCard().click(); @@ -175,7 +176,7 @@ describe('Projects', { disableAutoLogin: true }, () => { let menuItems = cy.getByTestId('menu-item'); menuItems.filter('[class*=active_]').should('have.length', 1); - menuItems.filter(':contains("Home")[class*=active_]').should('exist'); + menuItems.filter(':contains("Overview")[class*=active_]').should('exist'); projects.getMenuItems().first().click(); @@ -185,7 +186,7 @@ describe('Projects', { disableAutoLogin: true }, () => { menuItems.filter(':contains("Development")[class*=active_]').should('exist'); cy.intercept('GET', '/rest/workflows/*').as('loadWorkflow'); - workflowsPage.getters.workflowCards().first().click(); + workflowsPage.getters.workflowCards().first().findChildByTestId('card-content').click(); cy.wait('@loadWorkflow'); menuItems = cy.getByTestId('menu-item'); @@ -221,7 +222,7 @@ describe('Projects', { disableAutoLogin: true }, () => { menuItems = cy.getByTestId('menu-item'); menuItems.filter('[class*=active_]').should('have.length', 1); - menuItems.filter(':contains("Home")[class*=active_]').should('exist'); + menuItems.filter(':contains("Overview")[class*=active_]').should('exist'); workflowsPage.getters.workflowCards().should('have.length', 2).first().click(); @@ -229,7 +230,7 @@ describe('Projects', { disableAutoLogin: true }, () => { cy.getByTestId('execute-workflow-button').should('be.visible'); menuItems = cy.getByTestId('menu-item'); - menuItems.filter(':contains("Home")[class*=active_]').should('not.exist'); + menuItems.filter(':contains("Overview")[class*=active_]').should('not.exist'); menuItems = cy.getByTestId('menu-item'); menuItems.filter('[class*=active_]').should('have.length', 1); @@ -448,38 +449,48 @@ describe('Projects', { disableAutoLogin: true }, () => { workflowsPage.getters.workflowCards().should('not.have.length'); workflowsPage.getters.newWorkflowButtonCard().click(); projects.createWorkflow('Test_workflow_1.json', 'Workflow in Home project'); + clearNotifications(); projects.getHomeButton().click(); projects.getProjectTabCredentials().should('be.visible').click(); credentialsPage.getters.emptyListCreateCredentialButton().click(); projects.createCredential('Credential in Home project'); + clearNotifications(); + // Create a project and add a credential and a workflow to it projects.createProject('Project 1'); + clearNotifications(); projects.getProjectTabCredentials().click(); credentialsPage.getters.emptyListCreateCredentialButton().click(); projects.createCredential('Credential in Project 1'); + clearNotifications(); projects.getProjectTabWorkflows().click(); workflowsPage.getters.newWorkflowButtonCard().click(); projects.createWorkflow('Test_workflow_1.json', 'Workflow in Project 1'); + clearNotifications(); + // Create another project and add a credential and a workflow to it projects.createProject('Project 2'); + clearNotifications(); projects.getProjectTabCredentials().click(); credentialsPage.getters.emptyListCreateCredentialButton().click(); projects.createCredential('Credential in Project 2'); + clearNotifications(); projects.getProjectTabWorkflows().click(); workflowsPage.getters.newWorkflowButtonCard().click(); projects.createWorkflow('Test_workflow_1.json', 'Workflow in Project 2'); + clearNotifications(); - // Move the workflow owned by me from Home to Project 1 + // Move the workflow Personal from Home to Project 1 projects.getHomeButton().click(); workflowsPage.getters .workflowCards() .should('have.length', 3) - .filter(':contains("Owned by me")') + .filter(':contains("Personal")') .should('exist'); workflowsPage.getters.workflowCardActions('Workflow in Home project').click(); workflowsPage.getters.workflowMoveButton().click(); @@ -487,7 +498,7 @@ describe('Projects', { disableAutoLogin: true }, () => { projects .getResourceMoveModal() .should('be.visible') - .find('button:contains("Move workflow")') + .contains('button', 'Move workflow') .should('be.disabled'); projects.getProjectMoveSelect().click(); getVisibleSelect() @@ -495,12 +506,13 @@ describe('Projects', { disableAutoLogin: true }, () => { .should('have.length', 5) .filter(':contains("Project 1")') .click(); - projects.getResourceMoveModal().find('button:contains("Move workflow")').click(); + projects.getResourceMoveModal().contains('button', 'Move workflow').click(); + clearNotifications(); workflowsPage.getters .workflowCards() .should('have.length', 3) - .filter(':contains("Owned by me")') + .filter(':contains("Personal")') .should('not.exist'); // Move the workflow from Project 1 to Project 2 @@ -512,7 +524,7 @@ describe('Projects', { disableAutoLogin: true }, () => { projects .getResourceMoveModal() .should('be.visible') - .find('button:contains("Move workflow")') + .contains('button', 'Move workflow') .should('be.disabled'); projects.getProjectMoveSelect().click(); getVisibleSelect() @@ -520,18 +532,19 @@ describe('Projects', { disableAutoLogin: true }, () => { .should('have.length', 5) .filter(':contains("Project 2")') .click(); - projects.getResourceMoveModal().find('button:contains("Move workflow")').click(); + projects.getResourceMoveModal().contains('button', 'Move workflow').click(); // Move the workflow from Project 2 to a member user projects.getMenuItems().last().click(); workflowsPage.getters.workflowCards().should('have.length', 2); workflowsPage.getters.workflowCardActions('Workflow in Home project').click(); workflowsPage.getters.workflowMoveButton().click(); + clearNotifications(); projects .getResourceMoveModal() .should('be.visible') - .find('button:contains("Move workflow")') + .contains('button', 'Move workflow') .should('be.disabled'); projects.getProjectMoveSelect().click(); getVisibleSelect() @@ -540,7 +553,7 @@ describe('Projects', { disableAutoLogin: true }, () => { .filter(`:contains("${INSTANCE_MEMBERS[0].email}")`) .click(); - projects.getResourceMoveModal().find('button:contains("Move workflow")').click(); + projects.getResourceMoveModal().contains('button', 'Move workflow').click(); workflowsPage.getters.workflowCards().should('have.length', 1); // Move the workflow from member user back to Home @@ -556,7 +569,7 @@ describe('Projects', { disableAutoLogin: true }, () => { projects .getResourceMoveModal() .should('be.visible') - .find('button:contains("Move workflow")') + .contains('button', 'Move workflow') .should('be.disabled'); projects.getProjectMoveSelect().click(); getVisibleSelect() @@ -565,11 +578,12 @@ describe('Projects', { disableAutoLogin: true }, () => { .filter(`:contains("${INSTANCE_OWNER.email}")`) .click(); - projects.getResourceMoveModal().find('button:contains("Move workflow")').click(); + projects.getResourceMoveModal().contains('button', 'Move workflow').click(); + clearNotifications(); workflowsPage.getters .workflowCards() .should('have.length', 3) - .filter(':contains("Owned by me")') + .filter(':contains("Personal")') .should('have.length', 1); // Move the credential from Project 1 to Project 2 @@ -582,7 +596,7 @@ describe('Projects', { disableAutoLogin: true }, () => { projects .getResourceMoveModal() .should('be.visible') - .find('button:contains("Move credential")') + .contains('button', 'Move credential') .should('be.disabled'); projects.getProjectMoveSelect().click(); getVisibleSelect() @@ -590,8 +604,8 @@ describe('Projects', { disableAutoLogin: true }, () => { .should('have.length', 5) .filter(':contains("Project 2")') .click(); - projects.getResourceMoveModal().find('button:contains("Move credential")').click(); - + projects.getResourceMoveModal().contains('button', 'Move credential').click(); + clearNotifications(); credentialsPage.getters.credentialCards().should('not.have.length'); // Move the credential from Project 2 to admin user @@ -605,7 +619,7 @@ describe('Projects', { disableAutoLogin: true }, () => { projects .getResourceMoveModal() .should('be.visible') - .find('button:contains("Move credential")') + .contains('button', 'Move credential') .should('be.disabled'); projects.getProjectMoveSelect().click(); getVisibleSelect() @@ -613,7 +627,7 @@ describe('Projects', { disableAutoLogin: true }, () => { .should('have.length', 5) .filter(`:contains("${INSTANCE_ADMIN.email}")`) .click(); - projects.getResourceMoveModal().find('button:contains("Move credential")').click(); + projects.getResourceMoveModal().contains('button', 'Move credential').click(); credentialsPage.getters.credentialCards().should('have.length', 1); // Move the credential from admin user back to instance owner @@ -627,7 +641,7 @@ describe('Projects', { disableAutoLogin: true }, () => { projects .getResourceMoveModal() .should('be.visible') - .find('button:contains("Move credential")') + .contains('button', 'Move credential') .should('be.disabled'); projects.getProjectMoveSelect().click(); getVisibleSelect() @@ -635,12 +649,14 @@ describe('Projects', { disableAutoLogin: true }, () => { .should('have.length', 5) .filter(`:contains("${INSTANCE_OWNER.email}")`) .click(); - projects.getResourceMoveModal().find('button:contains("Move credential")').click(); + projects.getResourceMoveModal().contains('button', 'Move credential').click(); + + clearNotifications(); credentialsPage.getters .credentialCards() .should('have.length', 3) - .filter(':contains("Owned by me")') + .filter(':contains("Personal")') .should('have.length', 2); // Move the credential from admin user back to its original project (Project 1) @@ -650,7 +666,7 @@ describe('Projects', { disableAutoLogin: true }, () => { projects .getResourceMoveModal() .should('be.visible') - .find('button:contains("Move credential")') + .contains('button', 'Move credential') .should('be.disabled'); projects.getProjectMoveSelect().click(); getVisibleSelect() @@ -658,7 +674,7 @@ describe('Projects', { disableAutoLogin: true }, () => { .should('have.length', 5) .filter(':contains("Project 1")') .click(); - projects.getResourceMoveModal().find('button:contains("Move credential")').click(); + projects.getResourceMoveModal().contains('button', 'Move credential').click(); projects.getMenuItems().first().click(); projects.getProjectTabCredentials().click(); @@ -681,9 +697,7 @@ describe('Projects', { disableAutoLogin: true }, () => { projects.getHomeButton().click(); workflowsPage.getters.workflowCards().should('not.have.length'); workflowsPage.getters.newWorkflowButtonCard().click(); - workflowsPage.getters.workflowCards().should('not.have.length'); - workflowsPage.getters.newWorkflowButtonCard().click(); workflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME); workflowPage.actions.addNodeToCanvas(NOTION_NODE_NAME, true, true); ndv.getters.backToCanvas().click(); @@ -699,7 +713,7 @@ describe('Projects', { disableAutoLogin: true }, () => { workflowsPage.getters .workflowCards() .should('have.length', 1) - .filter(':contains("Owned by me")') + .filter(':contains("Personal")') .should('exist'); workflowsPage.getters.workflowCardActions('My workflow').click(); workflowsPage.getters.workflowMoveButton().click(); @@ -707,7 +721,7 @@ describe('Projects', { disableAutoLogin: true }, () => { projects .getResourceMoveModal() .should('be.visible') - .find('button:contains("Move workflow")') + .contains('button', 'Move workflow') .should('be.disabled'); projects.getProjectMoveSelect().click(); getVisibleSelect() @@ -715,12 +729,12 @@ describe('Projects', { disableAutoLogin: true }, () => { .should('have.length', 4) .filter(':contains("Project 1")') .click(); - projects.getResourceMoveModal().find('button:contains("Move workflow")').click(); + projects.getResourceMoveModal().contains('button', 'Move workflow').click(); workflowsPage.getters .workflowCards() .should('have.length', 1) - .filter(':contains("Owned by me")') + .filter(':contains("Personal")') .should('not.exist'); //Log out with instance owner and log in with the member user @@ -733,7 +747,7 @@ describe('Projects', { disableAutoLogin: true }, () => { // Open the moved workflow workflowsPage.getters.workflowCards().should('have.length', 1); - workflowsPage.getters.workflowCards().first().click(); + workflowsPage.getters.workflowCards().first().findChildByTestId('card-content').click(); // Check if the credential can be changed workflowPage.getters.canvasNodeByName(NOTION_NODE_NAME).should('be.visible').dblclick(); @@ -769,7 +783,8 @@ describe('Projects', { disableAutoLogin: true }, () => { cy.get('input[name="password"]').type(INSTANCE_MEMBERS[0].password); cy.getByTestId('form-submit-button').click(); - mainSidebar.getters.executions().click(); + projects.getMenuItems().last().click(); + projects.getProjectTabExecutions().click(); cy.getByTestId('global-execution-list-item').first().find('td:last button').click(); getVisibleDropdown() .find('li') @@ -815,4 +830,23 @@ describe('Projects', { disableAutoLogin: true }, () => { .should('not.have.length'); }); }); + + it('should set and update project icon', () => { + const DEFAULT_ICON = 'fa-layer-group'; + const NEW_PROJECT_NAME = 'Test Project'; + + cy.signinAsAdmin(); + cy.visit(workflowsPage.url); + projects.createProject(NEW_PROJECT_NAME); + // New project should have default icon + projects.getIconPickerButton().find('svg').should('have.class', DEFAULT_ICON); + // Choose another icon + projects.getIconPickerButton().click(); + projects.getIconPickerTab('Emojis').click(); + projects.getIconPickerEmojis().first().click(); + // Project should be updated with new icon + successToast().contains('Project icon updated successfully'); + projects.getIconPickerButton().should('contain', '😀'); + projects.getMenuItems().contains(NEW_PROJECT_NAME).should('contain', '😀'); + }); }); diff --git a/cypress/e2e/40-manual-partial-execution.cy.ts b/cypress/e2e/40-manual-partial-execution.cy.ts index 5fe31b56ad..2eb129475f 100644 --- a/cypress/e2e/40-manual-partial-execution.cy.ts +++ b/cypress/e2e/40-manual-partial-execution.cy.ts @@ -23,6 +23,7 @@ describe('Manual partial execution', () => { canvas.actions.openNode('Webhook1'); ndv.getters.nodeRunSuccessIndicator().should('exist'); + ndv.getters.nodeRunTooltipIndicator().should('exist'); ndv.getters.outputRunSelector().should('not.exist'); // single run }); }); diff --git a/cypress/e2e/42-nps-survey.cy.ts b/cypress/e2e/42-nps-survey.cy.ts index e06fe43ba8..11e5ebb88e 100644 --- a/cypress/e2e/42-nps-survey.cy.ts +++ b/cypress/e2e/42-nps-survey.cy.ts @@ -10,7 +10,7 @@ import { WorkflowPage } from '../pages/workflow'; const workflowPage = new WorkflowPage(); -const NOW = 1717771477012; +const NOW = Date.now(); const ONE_DAY = 24 * 60 * 60 * 1000; const THREE_DAYS = ONE_DAY * 3; const SEVEN_DAYS = ONE_DAY * 7; diff --git a/cypress/e2e/43-oauth-flow.cy.ts b/cypress/e2e/43-oauth-flow.cy.ts index 300a202540..d91315627b 100644 --- a/cypress/e2e/43-oauth-flow.cy.ts +++ b/cypress/e2e/43-oauth-flow.cy.ts @@ -1,3 +1,4 @@ +import { getCredentialSaveButton } from '../composables/modals/credential-modal'; import { CredentialsPage, CredentialsModal } from '../pages'; const credentialsPage = new CredentialsPage(); @@ -40,7 +41,7 @@ describe('Credentials', () => { }); // Check that the credential was saved and connected successfully - credentialsModal.getters.saveButton().should('contain.text', 'Saved'); + getCredentialSaveButton().should('contain.text', 'Saved'); credentialsModal.getters.oauthConnectSuccessBanner().should('be.visible'); }); }); diff --git a/cypress/e2e/45-ai-assistant.cy.ts b/cypress/e2e/45-ai-assistant.cy.ts index 3b4f61f660..9d33811363 100644 --- a/cypress/e2e/45-ai-assistant.cy.ts +++ b/cypress/e2e/45-ai-assistant.cy.ts @@ -4,6 +4,7 @@ import { clickCreateNewCredential, openCredentialSelect } from '../composables/n import { GMAIL_NODE_NAME, SCHEDULE_TRIGGER_NODE_NAME } from '../constants'; import { CredentialsModal, CredentialsPage, NDV, WorkflowPage } from '../pages'; import { AIAssistant } from '../pages/features/ai-assistant'; +import { NodeCreator } from '../pages/features/node-creator'; import { getVisibleSelect } from '../utils'; const wf = new WorkflowPage(); @@ -11,6 +12,7 @@ const ndv = new NDV(); const aiAssistant = new AIAssistant(); const credentialsPage = new CredentialsPage(); const credentialsModal = new CredentialsModal(); +const nodeCreatorFeature = new NodeCreator(); describe('AI Assistant::disabled', () => { beforeEach(() => { @@ -222,6 +224,54 @@ describe('AI Assistant::enabled', () => { .should('contain.text', 'item.json.myNewField = 1'); }); + it('Should ignore node execution success and error messages after the node run successfully once', () => { + const getParameter = () => ndv.getters.parameterInput('jsCode').should('be.visible'); + + const getEditor = () => getParameter().find('.cm-content').should('exist'); + + cy.intercept('POST', '/rest/ai/chat', { + statusCode: 200, + fixture: 'aiAssistant/responses/code_diff_suggestion_response.json', + }).as('chatRequest'); + + cy.createFixtureWorkflow('aiAssistant/workflows/test_workflow.json'); + wf.actions.openNode('Code'); + ndv.getters.nodeExecuteButton().click(); + aiAssistant.getters.nodeErrorViewAssistantButton().click({ force: true }); + cy.wait('@chatRequest'); + + cy.intercept('POST', '/rest/ai/chat', { + statusCode: 200, + fixture: 'aiAssistant/responses/node_execution_succeeded_response.json', + }).as('chatRequest2'); + + getEditor() + .type('{selectall}') + .paste( + 'for (const item of $input.all()) {\n item.json.myNewField = 1;\n}\n\nreturn $input.all();', + ); + + ndv.getters.nodeExecuteButton().click(); + + getEditor() + .type('{selectall}') + .paste( + 'for (const item of $input.all()) {\n item.json.myNewField = 1aaaa!;\n}\n\nreturn $input.all();', + ); + + ndv.getters.nodeExecuteButton().click(); + + aiAssistant.getters.chatMessagesAssistant().should('have.length', 3); + + aiAssistant.getters + .chatMessagesAssistant() + .eq(2) + .should( + 'contain.text', + 'Code node ran successfully, did my solution help resolve your issue?\nQuick reply 👇Yes, thanksNo, I am still stuck', + ); + }); + it('should end chat session when `end_session` event is received', () => { cy.intercept('POST', '/rest/ai/chat', { statusCode: 200, @@ -280,6 +330,20 @@ describe('AI Assistant::enabled', () => { wf.getters.isWorkflowSaved(); aiAssistant.getters.placeholderMessage().should('not.exist'); }); + + it('should send message via enter even with global NodeCreator panel opened', () => { + cy.intercept('POST', '/rest/ai/chat', { + statusCode: 200, + fixture: 'aiAssistant/responses/simple_message_response.json', + }).as('chatRequest'); + + wf.actions.addInitialNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME); + aiAssistant.actions.openChat(); + nodeCreatorFeature.actions.openNodeCreator(); + aiAssistant.getters.chatInput().type('Hello{Enter}'); + + aiAssistant.getters.placeholderMessage().should('not.exist'); + }); }); describe('AI Assistant Credential Help', () => { @@ -493,6 +557,8 @@ describe('General help', () => { }).as('chatRequest'); aiAssistant.getters.askAssistantFloatingButton().click(); + wf.getters.zoomToFitButton().click(); + aiAssistant.actions.sendMessage('What is wrong with this workflow?'); cy.wait('@chatRequest'); diff --git a/cypress/e2e/45-workflow-selector-parameter.cy.ts b/cypress/e2e/45-workflow-selector-parameter.cy.ts index a6dc23e6c2..38df9a29b8 100644 --- a/cypress/e2e/45-workflow-selector-parameter.cy.ts +++ b/cypress/e2e/45-workflow-selector-parameter.cy.ts @@ -27,7 +27,7 @@ describe('Workflow Selector Parameter', () => { getVisiblePopper() .should('have.length', 1) .findChildByTestId('rlc-item') - .should('have.length', 2); + .should('have.length', 3); }); it('should show required parameter warning', () => { @@ -44,7 +44,8 @@ describe('Workflow Selector Parameter', () => { getVisiblePopper() .should('have.length', 1) .findChildByTestId('rlc-item') - .should('have.length', 1) + .should('have.length', 2) + .eq(1) .click(); ndv.getters @@ -57,7 +58,7 @@ describe('Workflow Selector Parameter', () => { ndv.getters.resourceLocator('workflowId').should('be.visible'); ndv.getters.resourceLocatorInput('workflowId').click(); - getVisiblePopper().findChildByTestId('rlc-item').first().click(); + getVisiblePopper().findChildByTestId('rlc-item').eq(1).click(); ndv.getters.resourceLocatorInput('workflowId').find('a').should('exist'); cy.getByTestId('radio-button-expression').eq(1).click(); @@ -68,7 +69,7 @@ describe('Workflow Selector Parameter', () => { ndv.getters.resourceLocator('workflowId').should('be.visible'); ndv.getters.resourceLocatorInput('workflowId').click(); - getVisiblePopper().findChildByTestId('rlc-item').first().click(); + getVisiblePopper().findChildByTestId('rlc-item').eq(1).click(); ndv.getters .resourceLocatorModeSelector('workflowId') .find('input') @@ -79,4 +80,28 @@ describe('Workflow Selector Parameter', () => { .find('input') .should('have.value', 'By ID'); }); + + it('should render add resource option and redirect to the correct route when clicked', () => { + cy.window().then((win) => { + cy.stub(win, 'open').as('windowOpen'); + }); + + ndv.getters.resourceLocator('workflowId').should('be.visible'); + ndv.getters.resourceLocatorInput('workflowId').click(); + + getVisiblePopper().findChildByTestId('rlc-item').eq(0).should('exist'); + getVisiblePopper() + .findChildByTestId('rlc-item') + .eq(0) + .find('span') + .should('have.text', 'Create a new sub-workflow'); + + getVisiblePopper().findChildByTestId('rlc-item').eq(0).click(); + + const SAMPLE_SUBWORKFLOW_TEMPLATE_ID = 'VMiAxXa3lCAizGB5f7dVZQSFfg3FtHkdTKvLuupqBls='; + cy.get('@windowOpen').should( + 'be.calledWith', + `/workflows/onboarding/${SAMPLE_SUBWORKFLOW_TEMPLATE_ID}?sampleSubWorkflows=0`, + ); + }); }); diff --git a/cypress/e2e/47-subworkflow-debugging.cy.ts b/cypress/e2e/47-subworkflow-debugging.cy.ts new file mode 100644 index 0000000000..f808bdd044 --- /dev/null +++ b/cypress/e2e/47-subworkflow-debugging.cy.ts @@ -0,0 +1,140 @@ +import { + getExecutionPreviewOutputPanelRelatedExecutionLink, + getExecutionsSidebar, + getWorkflowExecutionPreviewIframe, + openExecutionPreviewNode, +} from '../composables/executions'; +import { + changeOutputRunSelector, + getOutputPanelItemsCount, + getOutputPanelRelatedExecutionLink, + getOutputRunSelectorInput, + getOutputTableHeaders, + getOutputTableRows, + getOutputTbodyCell, +} from '../composables/ndv'; +import { + clickExecuteWorkflowButton, + clickZoomToFit, + getCanvasNodes, + navigateToNewWorkflowPage, + openNode, + pasteWorkflow, + saveWorkflowOnButtonClick, +} from '../composables/workflow'; +import SUBWORKFLOW_DEBUGGING_EXAMPLE from '../fixtures/Subworkflow-debugging-execute-workflow.json'; + +describe('Subworkflow debugging', () => { + beforeEach(() => { + navigateToNewWorkflowPage(); + pasteWorkflow(SUBWORKFLOW_DEBUGGING_EXAMPLE); + saveWorkflowOnButtonClick(); + getCanvasNodes().should('have.length', 11); + clickZoomToFit(); + + clickExecuteWorkflowButton(); + }); + + describe('can inspect sub executed workflow', () => { + it('(Run once with all items/ Wait for Sub-workflow completion) (default behavior)', () => { + openNode('Execute Workflow with param'); + + getOutputPanelItemsCount().should('contain.text', '2 items, 1 sub-execution'); + getOutputPanelRelatedExecutionLink().should('contain.text', 'Inspect Sub-Execution'); + getOutputPanelRelatedExecutionLink().should('have.attr', 'href'); + + // ensure workflow executed and waited on output + getOutputTableHeaders().should('have.length', 2); + getOutputTbodyCell(1, 0).should('have.text', 'world Natalie Moore'); + }); + + it('(Run once for each item/ Wait for Sub-workflow completion)', () => { + openNode('Execute Workflow with param1'); + + getOutputPanelItemsCount().should('contain.text', '2 items, 2 sub-execution'); + getOutputPanelRelatedExecutionLink().should('not.exist'); + + // ensure workflow executed and waited on output + getOutputTableHeaders().should('have.length', 3); + getOutputTbodyCell(1, 0).find('a').should('have.attr', 'href'); + getOutputTbodyCell(1, 1).should('have.text', 'world Natalie Moore'); + }); + + it('(Run once with all items/ Wait for Sub-workflow completion)', () => { + openNode('Execute Workflow with param2'); + + getOutputPanelItemsCount().should('not.exist'); + getOutputPanelRelatedExecutionLink().should('contain.text', 'Inspect Sub-Execution'); + getOutputPanelRelatedExecutionLink().should('have.attr', 'href'); + + // ensure workflow executed but returned same data as input + getOutputRunSelectorInput().should('have.value', '2 of 2 (3 items, 1 sub-execution)'); + getOutputTableHeaders().should('have.length', 6); + getOutputTableHeaders().eq(0).should('have.text', 'uid'); + getOutputTableRows().should('have.length', 4); + getOutputTbodyCell(1, 1).should('include.text', 'Jon_Ebert@yahoo.com'); + + changeOutputRunSelector('1 of 2 (2 items, 1 sub-execution)'); + getOutputRunSelectorInput().should('have.value', '1 of 2 (2 items, 1 sub-execution)'); + getOutputTableHeaders().should('have.length', 6); + getOutputTableHeaders().eq(0).should('have.text', 'uid'); + getOutputTableRows().should('have.length', 3); + getOutputTbodyCell(1, 1).should('include.text', 'Terry.Dach@hotmail.com'); + }); + + it('(Run once for each item/ Wait for Sub-workflow completion)', () => { + openNode('Execute Workflow with param3'); + + // ensure workflow executed but returned same data as input + getOutputRunSelectorInput().should('have.value', '2 of 2 (3 items, 3 sub-executions)'); + getOutputTableHeaders().should('have.length', 7); + getOutputTableHeaders().eq(1).should('have.text', 'uid'); + getOutputTableRows().should('have.length', 4); + getOutputTbodyCell(1, 0).find('a').should('have.attr', 'href'); + getOutputTbodyCell(1, 2).should('include.text', 'Jon_Ebert@yahoo.com'); + + changeOutputRunSelector('1 of 2 (2 items, 2 sub-executions)'); + getOutputRunSelectorInput().should('have.value', '1 of 2 (2 items, 2 sub-executions)'); + getOutputTableHeaders().should('have.length', 7); + getOutputTableHeaders().eq(1).should('have.text', 'uid'); + getOutputTableRows().should('have.length', 3); + + getOutputTbodyCell(1, 0).find('a').should('have.attr', 'href'); + getOutputTbodyCell(1, 2).should('include.text', 'Terry.Dach@hotmail.com'); + }); + }); + + it('can inspect parent executions', () => { + cy.url().then((workflowUrl) => { + openNode('Execute Workflow with param'); + + getOutputPanelItemsCount().should('contain.text', '2 items, 1 sub-execution'); + getOutputPanelRelatedExecutionLink().should('contain.text', 'Inspect Sub-Execution'); + getOutputPanelRelatedExecutionLink().should('have.attr', 'href'); + + // ensure workflow executed and waited on output + getOutputTableHeaders().should('have.length', 2); + getOutputTbodyCell(1, 0).should('have.text', 'world Natalie Moore'); + + // cypress cannot handle new tabs so removing it + getOutputPanelRelatedExecutionLink().invoke('removeAttr', 'target').click(); + + getExecutionsSidebar().should('be.visible'); + getWorkflowExecutionPreviewIframe().should('be.visible'); + openExecutionPreviewNode('Execute Workflow Trigger'); + + getExecutionPreviewOutputPanelRelatedExecutionLink().should( + 'include.text', + 'Inspect Parent Execution', + ); + + getExecutionPreviewOutputPanelRelatedExecutionLink() + .invoke('removeAttr', 'target') + .click({ force: true }); + + cy.url().then((currentUrl) => { + expect(currentUrl === workflowUrl); + }); + }); + }); +}); diff --git a/cypress/e2e/48-subworkflow-inputs.cy.ts b/cypress/e2e/48-subworkflow-inputs.cy.ts new file mode 100644 index 0000000000..0e2755b9f0 --- /dev/null +++ b/cypress/e2e/48-subworkflow-inputs.cy.ts @@ -0,0 +1,288 @@ +import { clickGetBackToCanvas, getOutputTableHeaders } from '../composables/ndv'; +import { + clickZoomToFit, + navigateToNewWorkflowPage, + openNode, + pasteWorkflow, + saveWorkflowOnButtonClick, +} from '../composables/workflow'; +import SUB_WORKFLOW_INPUTS from '../fixtures/Test_Subworkflow-Inputs.json'; +import { NDV, WorkflowsPage, WorkflowPage } from '../pages'; +import { errorToast, successToast } from '../pages/notifications'; +import { getVisiblePopper } from '../utils'; + +const ndv = new NDV(); +const workflowsPage = new WorkflowsPage(); +const workflow = new WorkflowPage(); + +const DEFAULT_WORKFLOW_NAME = 'My workflow'; +const DEFAULT_SUBWORKFLOW_NAME_1 = 'My Sub-Workflow 1'; +const DEFAULT_SUBWORKFLOW_NAME_2 = 'My Sub-Workflow 2'; + +type FieldRow = readonly string[]; + +const exampleFields = [ + ['aNumber', 'Number'], + ['aString', 'String'], + ['aArray', 'Array'], + ['aObject', 'Object'], + ['aAny', 'Allow Any Type'], + // bool last since it's not an inputField so we'll skip it for some cases + ['aBool', 'Boolean'], +] as const; + +/** + * Populate multiValue fixedCollections. Only supports fixedCollections for which all fields can be defined via keyboard typing + * + * @param items - 2D array of items to populate, i.e. [["myField1", "String"], [""] + * @param collectionName - name of the fixedCollection to populate + * @param offset - amount of 'parameter-input's before the fixedCollection under test + * @returns + */ +function populateFixedCollection( + items: readonly FieldRow[], + collectionName: string, + offset: number, +) { + if (items.length === 0) return; + const n = items[0].length; + for (const [i, params] of items.entries()) { + ndv.actions.addItemToFixedCollection(collectionName); + for (const [j, param] of params.entries()) { + ndv.getters + .fixedCollectionParameter(collectionName) + .getByTestId('parameter-input') + .eq(offset + i * n + j) + .type(`${param}{downArrow}{enter}`); + } + } +} + +function makeExample(type: TypeField) { + switch (type) { + case 'String': + return '"example"'; + case 'Number': + return '42'; + case 'Boolean': + return 'true'; + case 'Array': + return '["example", 123, null]'; + case 'Object': + return '{{}"example": [123]}'; + case 'Allow Any Type': + return 'null'; + } +} + +type TypeField = 'Allow Any Type' | 'String' | 'Number' | 'Boolean' | 'Array' | 'Object'; +function populateFields(items: ReadonlyArray) { + populateFixedCollection(items, 'workflowInputs', 1); +} + +function navigateWorkflowSelectionDropdown(index: number, expectedText: string) { + ndv.getters.resourceLocator('workflowId').should('be.visible'); + ndv.getters.resourceLocatorInput('workflowId').click(); + + getVisiblePopper().findChildByTestId('rlc-item').eq(0).should('exist'); + getVisiblePopper() + .findChildByTestId('rlc-item') + .eq(index) + .find('span') + .should('have.text', expectedText) + .click(); +} + +function populateMapperFields(values: readonly string[], offset: number) { + for (const [i, value] of values.entries()) { + cy.getByTestId('parameter-input') + .eq(offset + i) + .type(value); + + // Click on a parent to dismiss the pop up hiding the field below. + cy.getByTestId('parameter-input') + .eq(offset + i) + .parent() + .parent() + .click('topLeft'); + } +} + +// This function starts off in the Child Workflow Input Trigger, assuming we just defined the input fields +// It then navigates back to the parent and validates output +function validateAndReturnToParent(targetChild: string, offset: number, fields: string[]) { + ndv.actions.execute(); + + // + 1 to account for formatting-only column + getOutputTableHeaders().should('have.length', fields.length + 1); + for (const [i, name] of fields.entries()) { + getOutputTableHeaders().eq(i).should('have.text', name); + } + + clickGetBackToCanvas(); + saveWorkflowOnButtonClick(); + + cy.visit(workflowsPage.url); + + workflowsPage.getters.workflowCardContent(DEFAULT_WORKFLOW_NAME).click(); + + openNode('Execute Workflow'); + + // Note that outside of e2e tests this will be pre-selected correctly. + // Due to our workaround to remain in the same tab we need to select the correct tab manually + navigateWorkflowSelectionDropdown(offset, targetChild); + + // This fails, pointing to `usePushConnection` `const triggerNode = subWorkflow?.nodes.find` being `undefined.find()`I + ndv.actions.execute(); + + getOutputTableHeaders().should('have.length', fields.length + 1); + for (const [i, name] of fields.entries()) { + getOutputTableHeaders().eq(i).should('have.text', name); + } + + // todo: verify the fields appear and show the correct types + + // todo: fill in the input fields (and mock previous node data in the json fixture to match) + + // todo: validate the actual output data +} + +function setWorkflowInputFieldValue(index: number, value: string) { + ndv.actions.addItemToFixedCollection('workflowInputs'); + ndv.actions.typeIntoFixedCollectionItem('workflowInputs', index, value); +} + +describe('Sub-workflow creation and typed usage', () => { + beforeEach(() => { + navigateToNewWorkflowPage(); + pasteWorkflow(SUB_WORKFLOW_INPUTS); + saveWorkflowOnButtonClick(); + clickZoomToFit(); + + openNode('Execute Workflow'); + + // Prevent sub-workflow from opening in new window + cy.window().then((win) => { + cy.stub(win, 'open').callsFake((url) => { + cy.visit(url); + }); + }); + navigateWorkflowSelectionDropdown(0, 'Create a new sub-workflow'); + // ************************** + // NAVIGATE TO CHILD WORKFLOW + // ************************** + + openNode('Workflow Input Trigger'); + }); + + it('works with type-checked values', () => { + populateFields(exampleFields); + + validateAndReturnToParent( + DEFAULT_SUBWORKFLOW_NAME_1, + 1, + exampleFields.map((f) => f[0]), + ); + + const values = [ + '-1', // number fields don't support `=` switch to expression, so let's test the Fixed case with it + ...exampleFields.slice(1).map((x) => `={{}{{} $json.a${x[0]}`), // }} are added automatically + ]; + + // this matches with the pinned data provided in the fixture + populateMapperFields(values, 2); + + ndv.actions.execute(); + + // todo: + // - validate output lines up + // - change input to need casts + // - run + // - confirm error + // - switch `attemptToConvertTypes` flag + // - confirm success and changed output + // - change input to be invalid despite cast + // - run + // - confirm error + // - switch type option flags + // - run + // - confirm success + // - turn off attempt to cast flag + // - confirm a value was not cast + }); + + it('works with Fields input source into JSON input source', () => { + ndv.getters.nodeOutputHint().should('exist'); + + populateFields(exampleFields); + + validateAndReturnToParent( + DEFAULT_SUBWORKFLOW_NAME_1, + 1, + exampleFields.map((f) => f[0]), + ); + + cy.window().then((win) => { + cy.stub(win, 'open').callsFake((url) => { + cy.visit(url); + }); + }); + navigateWorkflowSelectionDropdown(0, 'Create a new sub-workflow'); + + openNode('Workflow Input Trigger'); + + cy.getByTestId('parameter-input').eq(0).click(); + + // Todo: Check if there's a better way to interact with option dropdowns + // This PR would add this child testId + getVisiblePopper() + .getByTestId('parameter-input') + .eq(0) + .type('Using JSON Example{downArrow}{enter}'); + + const exampleJson = + '{{}' + exampleFields.map((x) => `"${x[0]}": ${makeExample(x[1])}`).join(',') + '}'; + cy.getByTestId('parameter-input-jsonExample') + .find('.cm-line') + .eq(0) + .type(`{selectAll}{backspace}${exampleJson}{enter}`); + + // first one doesn't work for some reason, might need to wait for something? + ndv.actions.execute(); + + validateAndReturnToParent( + DEFAULT_SUBWORKFLOW_NAME_2, + 2, + exampleFields.map((f) => f[0]), + ); + + // test for either InputSource mode and options combinations: + // + we're showing the notice in the output panel + // + we start with no fields + // + Test Step works and we create the fields + // + create field of each type (string, number, boolean, object, array, any) + // + exit ndv + // + save + // + go back to parent workflow + // - verify fields appear [needs Ivan's PR] + // - link fields [needs Ivan's PR] + // + run parent + // - verify output with `null` defaults exists + // + }); + + it('should show node issue when no fields are defined in manual mode', () => { + ndv.getters.nodeExecuteButton().should('be.disabled'); + ndv.actions.close(); + // Executing the workflow should show an error toast + workflow.actions.executeWorkflow(); + errorToast().should('contain', 'The workflow has issues'); + openNode('Workflow Input Trigger'); + // Add a field to the workflowInputs fixedCollection + setWorkflowInputFieldValue(0, 'test'); + // Executing the workflow should not show error now + ndv.actions.close(); + workflow.actions.executeWorkflow(); + successToast().should('contain', 'Workflow executed successfully'); + }); +}); diff --git a/cypress/e2e/5-ndv.cy.ts b/cypress/e2e/5-ndv.cy.ts index a591d62895..8bad424554 100644 --- a/cypress/e2e/5-ndv.cy.ts +++ b/cypress/e2e/5-ndv.cy.ts @@ -65,26 +65,6 @@ describe('NDV', () => { cy.shouldNotHaveConsoleErrors(); }); - it('should disconect Switch outputs if rules order was changed', () => { - cy.createFixtureWorkflow('NDV-test-switch_reorder.json', 'NDV test switch reorder'); - workflowPage.actions.zoomToFit(); - - workflowPage.actions.executeWorkflow(); - workflowPage.actions.openNode('Merge'); - ndv.getters.outputPanel().contains('2 items').should('exist'); - cy.contains('span', 'first').should('exist'); - ndv.getters.backToCanvas().click(); - - workflowPage.actions.openNode('Switch'); - cy.get('.cm-line').realMouseMove(100, 100); - cy.get('.fa-angle-down').click(); - ndv.getters.backToCanvas().click(); - workflowPage.actions.executeWorkflow(); - workflowPage.actions.openNode('Merge'); - ndv.getters.outputPanel().contains('1 item').should('exist'); - cy.contains('span', 'zero').should('exist'); - }); - it('should show correct validation state for resource locator params', () => { workflowPage.actions.addNodeToCanvas('Typeform', true, true); ndv.getters.container().should('be.visible'); @@ -111,6 +91,7 @@ describe('NDV', () => { cy.get('[class*=hasIssues]').should('have.length', 1); }); + // Correctly failing in V2 - node issues are only shows after execution it('should show all validation errors when opening pasted node', () => { cy.createFixtureWorkflow('Test_workflow_ndv_errors.json', 'Validation errors'); workflowPage.getters.canvasNodes().should('have.have.length', 1); @@ -133,9 +114,10 @@ describe('NDV', () => { "An expression here won't work because it uses .item and n8n can't figure out the matching item.", ); ndv.getters.nodeRunErrorIndicator().should('be.visible'); + ndv.getters.nodeRunTooltipIndicator().should('be.visible'); // The error details should be hidden behind a tooltip - ndv.getters.nodeRunErrorIndicator().should('not.contain', 'Start Time'); - ndv.getters.nodeRunErrorIndicator().should('not.contain', 'Execution Time'); + ndv.getters.nodeRunTooltipIndicator().should('not.contain', 'Start Time'); + ndv.getters.nodeRunTooltipIndicator().should('not.contain', 'Execution Time'); }); it('should save workflow using keyboard shortcut from NDV', () => { @@ -203,7 +185,7 @@ describe('NDV', () => { .contains(key) .should('be.visible'); }); - getObjectValueItem().find('label').click({ force: true }); + getObjectValueItem().find('.toggle').click({ force: true }); expandedObjectProps.forEach((key) => { ndv.getters .outputPanel() @@ -212,9 +194,11 @@ describe('NDV', () => { .should('not.be.visible'); }); }); + it('should not display pagination for schema', () => { setupSchemaWorkflow(); ndv.getters.backToCanvas().click(); + workflowPage.actions.deselectAll(); workflowPage.getters.canvasNodeByName('Set').click(); workflowPage.actions.addNodeToCanvas( 'Customer Datastore (n8n training)', @@ -244,8 +228,8 @@ describe('NDV', () => { ndv.getters.outputPanel().find('[class*=_pagination]').should('not.exist'); ndv.getters .outputPanel() - .find('[data-test-id=run-data-schema-item] [data-test-id=run-data-schema-item]') - .should('have.length', 20); + .find('[data-test-id=run-data-schema-item]') + .should('have.length.above', 10); }); }); @@ -406,8 +390,18 @@ describe('NDV', () => { return cy.get(`[data-node-placement=${position}]`); } + // Correctly failing in V2 - due to floating navigation not updating the selected node it('should traverse floating nodes with mouse', () => { cy.createFixtureWorkflow('Floating_Nodes.json', 'Floating Nodes'); + + cy.ifCanvasVersion( + () => {}, + () => { + // Needed in V2 as all nodes remain selected when clicking on a selected node + workflowPage.actions.deselectAll(); + }, + ); + workflowPage.getters.canvasNodes().first().dblclick(); getFloatingNodeByPosition('inputMain').should('not.exist'); getFloatingNodeByPosition('outputMain').should('exist'); @@ -418,6 +412,7 @@ describe('NDV', () => { getFloatingNodeByPosition('inputMain').should('exist'); getFloatingNodeByPosition('outputMain').should('exist'); ndv.actions.close(); + // These two lines are broken in V2 workflowPage.getters.selectedNodes().should('have.length', 1); workflowPage.getters .selectedNodes() @@ -425,10 +420,8 @@ describe('NDV', () => { .should('contain', `Node ${i + 1}`); workflowPage.getters.selectedNodes().first().dblclick(); }); - getFloatingNodeByPosition('outputMain').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 }); @@ -452,8 +445,17 @@ describe('NDV', () => { .should('contain', MANUAL_TRIGGER_NODE_DISPLAY_NAME); }); + // Correctly failing in V2 - due to floating navigation not updating the selected node it('should traverse floating nodes with keyboard', () => { cy.createFixtureWorkflow('Floating_Nodes.json', 'Floating Nodes'); + cy.ifCanvasVersion( + () => {}, + () => { + // Needed in V2 as all nodes remain selected when clicking on a selected node + workflowPage.actions.deselectAll(); + }, + ); + workflowPage.getters.canvasNodes().first().dblclick(); getFloatingNodeByPosition('inputMain').should('not.exist'); getFloatingNodeByPosition('outputMain').should('exist'); @@ -464,6 +466,7 @@ describe('NDV', () => { getFloatingNodeByPosition('inputMain').should('exist'); getFloatingNodeByPosition('outputMain').should('exist'); ndv.actions.close(); + // These two lines are broken in V2 workflowPage.getters.selectedNodes().should('have.length', 1); workflowPage.getters .selectedNodes() @@ -491,6 +494,7 @@ describe('NDV', () => { getFloatingNodeByPosition('inputSub').should('not.exist'); getFloatingNodeByPosition('outputSub').should('not.exist'); ndv.actions.close(); + // These two lines are broken in V2 workflowPage.getters.selectedNodes().should('have.length', 1); workflowPage.getters .selectedNodes() @@ -617,8 +621,10 @@ describe('NDV', () => { // Should not show run info before execution ndv.getters.nodeRunSuccessIndicator().should('not.exist'); ndv.getters.nodeRunErrorIndicator().should('not.exist'); + ndv.getters.nodeRunTooltipIndicator().should('not.exist'); ndv.getters.nodeExecuteButton().click(); ndv.getters.nodeRunSuccessIndicator().should('exist'); + ndv.getters.nodeRunTooltipIndicator().should('exist'); }); it('should properly show node execution indicator for multiple nodes', () => { @@ -630,6 +636,7 @@ describe('NDV', () => { // Manual tigger node should show success indicator workflowPage.actions.openNode('When clicking ‘Test workflow’'); ndv.getters.nodeRunSuccessIndicator().should('exist'); + ndv.getters.nodeRunTooltipIndicator().should('exist'); // Code node should show error ndv.getters.backToCanvas().click(); workflowPage.actions.openNode('Code'); @@ -713,6 +720,7 @@ describe('NDV', () => { .should('have.value', 'Error fetching options from Notion'); }); + // Correctly failing in V2 - NodeCreator is not opened after clicking on the link it('Should open appropriate node creator after clicking on connection hint link', () => { const nodeCreator = new NodeCreator(); const hintMapper = { @@ -730,6 +738,7 @@ describe('NDV', () => { Object.entries(hintMapper).forEach(([node, group]) => { workflowPage.actions.openNode(node); + // This fails to open the NodeCreator cy.get('[data-action=openSelectiveNodeCreator]').contains('Insert one').click(); nodeCreator.getters.activeSubcategory().should('contain', group); cy.realPress('Escape'); @@ -791,4 +800,60 @@ describe('NDV', () => { .find('[data-test-id=run-data-schema-item]') .should('contain.text', 'onlyOnItem3'); }); + + it('should keep search expanded after Test step node run', () => { + cy.createFixtureWorkflow('Test_ndv_search.json'); + workflowPage.actions.zoomToFit(); + workflowPage.actions.executeWorkflow(); + workflowPage.actions.openNode('Edit Fields'); + ndv.getters.outputPanel().should('be.visible'); + ndv.getters.outputPanel().findChildByTestId('ndv-search').click().type('US'); + ndv.getters.outputTableRow(1).find('mark').should('have.text', 'US'); + + ndv.actions.execute(); + ndv.getters + .outputPanel() + .findChildByTestId('ndv-search') + .should('be.visible') + .should('have.value', 'US'); + }); + + it('should not show items count when seaching in schema view', () => { + cy.createFixtureWorkflow('Test_ndv_search.json'); + workflowPage.actions.zoomToFit(); + workflowPage.actions.openNode('Edit Fields'); + ndv.getters.outputPanel().should('be.visible'); + ndv.actions.execute(); + ndv.actions.switchOutputMode('Schema'); + ndv.getters.outputPanel().find('[data-test-id=ndv-search]').click().type('US'); + ndv.getters.outputPanel().find('[data-test-id=ndv-items-count]').should('not.exist'); + }); + + it('should show additional tooltip when seaching in schema view if no matches', () => { + cy.createFixtureWorkflow('Test_ndv_search.json'); + workflowPage.actions.zoomToFit(); + workflowPage.actions.openNode('Edit Fields'); + ndv.getters.outputPanel().should('be.visible'); + ndv.actions.execute(); + ndv.actions.switchOutputMode('Schema'); + ndv.getters.outputPanel().find('[data-test-id=ndv-search]').click().type('foo'); + ndv.getters + .outputPanel() + .contains('To search field contents rather than just names, use Table or JSON view') + .should('exist'); + }); + + it('ADO-2931 - should handle multiple branches of the same input with the first branch empty correctly', () => { + cy.createFixtureWorkflow('Test_ndv_two_branches_of_same_parent_false_populated.json'); + workflowPage.actions.zoomToFit(); + workflowPage.actions.openNode('DebugHelper'); + ndv.getters.inputPanel().should('be.visible'); + ndv.getters.outputPanel().should('be.visible'); + ndv.actions.execute(); + // This ensures we rendered the inputPanel + ndv.getters + .inputPanel() + .find('[data-test-id=run-data-schema-item]') + .should('contain.text', 'a1'); + }); }); diff --git a/cypress/e2e/6-code-node.cy.ts b/cypress/e2e/6-code-node.cy.ts index 5a6182c25a..5bc7d05ee2 100644 --- a/cypress/e2e/6-code-node.cy.ts +++ b/cypress/e2e/6-code-node.cy.ts @@ -162,21 +162,21 @@ return [] cy.get('#tab-code').should('have.class', 'is-active'); }); - it('should show error based on status code', () => { - const prompt = nanoid(20); - cy.get('#tab-ask-ai').click(); - ndv.actions.executePrevious(); + const handledCodes = [ + { code: 400, message: 'Code generation failed due to an unknown reason' }, + { code: 413, message: 'Your workflow data is too large for AI to process' }, + { code: 429, message: "We've hit our rate limit with our AI partner" }, + { code: 500, message: 'Code generation failed due to an unknown reason' }, + ]; - cy.getByTestId('ask-ai-prompt-input').type(prompt); + handledCodes.forEach(({ code, message }) => { + it(`should show error based on status code ${code}`, () => { + const prompt = nanoid(20); + cy.get('#tab-ask-ai').click(); + ndv.actions.executePrevious(); - const handledCodes = [ - { code: 400, message: 'Code generation failed due to an unknown reason' }, - { code: 413, message: 'Your workflow data is too large for AI to process' }, - { code: 429, message: "We've hit our rate limit with our AI partner" }, - { code: 500, message: 'Code generation failed due to an unknown reason' }, - ]; + cy.getByTestId('ask-ai-prompt-input').type(prompt); - handledCodes.forEach(({ code, message }) => { cy.intercept('POST', '/rest/ai/ask-ai', { statusCode: code, status: code, diff --git a/cypress/fixtures/Ecommerce_starter_pack_template_collection.json b/cypress/fixtures/Ecommerce_starter_pack_template_collection.json index 1f908c587b..d6235b358c 100644 --- a/cypress/fixtures/Ecommerce_starter_pack_template_collection.json +++ b/cypress/fixtures/Ecommerce_starter_pack_template_collection.json @@ -1 +1,1555 @@ -{"collection":{"id":1,"name":"eCommerce Starter Pack","description":"eCommerce operations are complex — but there are many things that you can automate to make your life easier. This collection provides a few ideas to get started.\n\nReduce manual work and the risk of human error by automating processes such as social media promotion of products, updating customer databases, and get notifications for important events.","totalViews":0,"createdAt":"2022-02-17T12:40:50.498Z","nodes":[{"id":20,"name":"n8n-nodes-base.if","defaults":{"name":"IF","color":"#408000"},"displayName":"IF","icon":"fa:map-signs","iconData":{"icon":"map-signs","type":"icon"},"typeVersion":1,"categories":[{"id":9,"name":"Core Nodes"}]},{"id":49,"name":"n8n-nodes-base.telegram","defaults":{"name":"Telegram"},"displayName":"Telegram","icon":"file:telegram.svg","iconData":{"type":"file","fileBuffer":"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgNjYgNjYiIGZpbGw9IiNmZmYiIGZpbGwtcnVsZT0iZXZlbm9kZCIgc3Ryb2tlPSIjMDAwIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjx1c2UgeGxpbms6aHJlZj0iI2EiIHg9Ii41IiB5PSIuNSIvPjxzeW1ib2wgaWQ9ImEiIG92ZXJmbG93PSJ2aXNpYmxlIj48ZyBzdHJva2U9Im5vbmUiIGZpbGwtcnVsZT0ibm9uemVybyI+PHBhdGggZD0iTTAgMzJjMCAxNy42NzMgMTQuMzI3IDMyIDMyIDMyczMyLTE0LjMyNyAzMi0zMlM0OS42NzMgMCAzMiAwIDAgMTQuMzI3IDAgMzIiIGZpbGw9IiMzN2FlZTIiLz48cGF0aCBkPSJNMjEuNjYxIDM0LjMzOGwzLjc5NyAxMC41MDhzLjQ3NS45ODMuOTgzLjk4MyA4LjA2OC03Ljg2NCA4LjA2OC03Ljg2NGw4LjQwNy0xNi4yMzctMjEuMTE5IDkuODk4eiIgZmlsbD0iI2M4ZGFlYSIvPjxwYXRoIGQ9Ik0yNi42OTUgMzcuMDM0bC0uNzI5IDcuNzQ2cy0uMzA1IDIuMzczIDIuMDY4IDBsNC42NDQtNC4yMDMiIGZpbGw9IiNhOWM2ZDgiLz48cGF0aCBkPSJNMjEuNzMgMzQuNzEybC03LjgwOS0yLjU0NXMtLjkzMi0uMzc4LS42MzMtMS4yMzdjLjA2Mi0uMTc3LjE4Ni0uMzI4LjU1OS0uNTg4IDEuNzMxLTEuMjA2IDMyLjAyOC0xMi4wOTYgMzIuMDI4LTEyLjA5NnMuODU2LS4yODggMS4zNjEtLjA5N2MuMjMxLjA4OC4zNzguMTg3LjUwMy41NDguMDQ1LjEzMi4wNzEuNDExLjA2OC42ODktLjAwMy4yMDEtLjAyNy4zODYtLjA0NS42NzgtLjE4NCAyLjk3OC01LjcwNiAyNS4xOTgtNS43MDYgMjUuMTk4cy0uMzMgMS4zLTEuNTE0IDEuMzQ1Yy0uNDMyLjAxNi0uOTU2LS4wNzEtMS41ODItLjYxLTIuMzIzLTEuOTk4LTEwLjM1Mi03LjM5NC0xMi4xMjYtOC41OGEuMzQuMzQgMCAwMS0uMTQ2LS4yMzljLS4wMjUtLjEyNS4xMDgtLjI4LjEwOC0uMjhzMTMuOTgtMTIuNDI3IDE0LjM1Mi0xMy43MzFjLjAyOS0uMTAxLS4wNzktLjE1MS0uMjI2LS4xMDctLjkyOS4zNDItMTcuMDI1IDEwLjUwNi0xOC44MDEgMTEuNjI5LS4xMDQuMDY2LS4zOTUuMDIzLS4zOTUuMDIzIi8+PC9nPjwvc3ltYm9sPjwvc3ZnPg=="},"typeVersion":1,"categories":[{"id":6,"name":"Communication"}]},{"id":107,"name":"n8n-nodes-base.shopifyTrigger","defaults":{"name":"Shopify Trigger"},"displayName":"Shopify Trigger","icon":"file:shopify.svg","iconData":{"type":"file","fileBuffer":"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgNTggNjYiIGZpbGw9IiNmZmYiIGZpbGwtcnVsZT0iZXZlbm9kZCIgc3Ryb2tlPSIjMDAwIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjx1c2UgeGxpbms6aHJlZj0iI2EiIHg9Ii41IiB5PSIuNSIvPjxzeW1ib2wgaWQ9ImEiIG92ZXJmbG93PSJ2aXNpYmxlIj48ZyBzdHJva2U9Im5vbmUiIGZpbGwtcnVsZT0ibm9uemVybyI+PHBhdGggZD0iTTQ5LjI1NSAxMi40ODRhLjYzMy42MzMgMCAwMC0uNTY0LS41MjdjLS4yMjUtLjAzNy01LjE3LS4zNzYtNS4xNy0uMzc2bC0zLjc3LTMuNzdjLS4zNC0uMzc2LTEuMDkyLS4yNjYtMS4zNzYtLjE4OC0uMDM3IDAtLjc1Mi4yMjUtMS45MjIuNjA1LTEuMTM3LTMuMy0zLjE1LTYuMzA2LTYuNjk2LTYuMzA2aC0uMzAzQzI4LjQzOC42MDUgMjcuMTk0IDAgMjYuMTQ0IDBjLTguMjU2LjAzNy0xMi4yIDEwLjMzMy0xMy40MzQgMTUuNTk0bC01Ljc3IDEuNzdjLTEuNzcuNTY0LTEuODM1LjYwNS0yLjA3MyAyLjI5M0wwIDU3LjE3NSAzNi40NjggNjRsMTkuNzYzLTQuMjZjMC0uMDM3LTYuOTQtNDYuODk3LTYuOTc2LTQ3LjI1NXpNMzQuNDMxIDguODZjLS45MTcuMzAzLTEuOTYzLjYwNS0zLjEuOTQ1di0uNjhhMTUuMDMgMTUuMDMgMCAwMC0uNzUyLTQuOTk5YzEuODQ4LjI4NCAzLjEgMi4zNTcgMy44NDMgNC43MzN6bS02LjA2OC00LjI5OGMuNjAzIDEuNzc4Ljg4MyAzLjY1LjgyNiA1LjUyN3YuMzRsLTYuMzc1IDEuOTYzYzEuMjQ4LTQuNjYgMy41NS02Ljk2MiA1LjU1LTcuODN6bS0yLjQ1LTIuMjkzYTEuOTQgMS45NCAwIDAxMS4wNTUuMzM5Yy0yLjY2IDEuMjM4LTUuNDcyIDQuMzY2LTYuNjc4IDEwLjYyN2wtNS4wNDUgMS41NDZDMTYuNjY4IDEwLjAzIDE5Ljk4OCAyLjI2IDI1LjkxIDIuMjZ6IiBmaWxsPSIjOTViZjQ3Ii8+PHBhdGggZD0iTTQ4LjY5MSAxMS45NTdjLS4yMjUtLjAzNy01LjE3LS4zNzYtNS4xNy0uMzc2bC0zLjc3LTMuNzdhLjc1My43NTMgMCAwMC0uNTI3LS4yMjVMMzYuNDcyIDY0bDE5Ljc2My00LjI2LTYuOTgtNDcuMjE4YS42OC42OCAwIDAwLS41NjQtLjU2NHoiIGZpbGw9IiM1ZThlM2UiLz48cGF0aCBkPSJNMjkuNzU4IDIyLjlsLTIuNDU0IDcuMjQyYTExLjM2IDExLjM2IDAgMDAtNC43NTItMS4xMzNjLTMuODQ4IDAtNC4wMzYgMi40MTItNC4wMzYgMy4wMTggMCAzLjI5OCA4LjYzNiA0LjU2NCA4LjYzNiAxMi4zMzMgMCA2LjEtMy44ODUgMTAuMDMtOS4xIDEwLjAzLTYuMjYgMC05LjQ2Ny0zLjg4NS05LjQ2Ny0zLjg4NWwxLjY2NS01LjU1czMuMjggMi44MyA2LjA3MyAyLjgzYTIuNDcgMi40NyAwIDAwMi41NjQtMi40OWMwLTQuMzQtNy4xLTQuNTI3LTcuMS0xMS42MTggMC01Ljk2MiA0LjI5OC0xMS43NyAxMi45MzQtMTEuNzcgMy4zOTQuMDUgNS4wMTggMSA1LjAxOCAxeiIvPjwvZz48L3N5bWJvbD48L3N2Zz4="},"typeVersion":1,"categories":[{"id":2,"name":"Sales"}]},{"id":126,"name":"n8n-nodes-base.mautic","defaults":{"name":"Mautic"},"displayName":"Mautic","icon":"file:mautic.svg","iconData":{"type":"file","fileBuffer":"data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAxNi4wLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DQo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB3aWR0aD0iMzQ5Ljc3OXB4IiBoZWlnaHQ9IjM0OS43NzlweCIgdmlld0JveD0iMCAwIDM0OS43NzkgMzQ5Ljc3OSIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgMzQ5Ljc3OSAzNDkuNzc5Ig0KCSB4bWw6c3BhY2U9InByZXNlcnZlIj4NCjxwYXRoIGZpbGw9IiNGRkZGRkYiIGQ9Ik0xNzQuODksMzQ5Ljc3OUM3OC42MTIsMzQ5Ljc3OSwwLDI3MS40NjIsMCwxNzQuODlTNzguNjEyLDAsMTc0Ljg5LDBjMjMuMjYsMCw0NS45MzEsNC40MTcsNjcuMTI5LDEzLjU0Mw0KCWM1Ljg4OSwyLjY1LDguODMzLDkuNDIyLDYuNDc4LDE1LjYwNWMtMi42NDksNS44ODgtOS40MjEsOC44MzMtMTUuNjA0LDYuNDc3Yy0xOC41NDktNy42NTUtMzcuOTgtMTEuNDgyLTU4LjAwMi0xMS40ODINCgljLTgzLjMyMywwLTE1MS4wNDEsNjcuNzE4LTE1MS4wNDEsMTUxLjA0MVM5MS41NjcsMzI2LjIyNSwxNzQuODksMzI2LjIyNWM4My4zMjMsMCwxNTEuMDQxLTY3LjcxOCwxNTEuMDQxLTE1MS4wNDENCgljMC0xNy45Ni0yLjk0NC0zNS4zMzItOS4xMjctNTEuODE5Yy0yLjM1NS02LjE4MywwLjg4My0xMi45NTUsNy4wNjYtMTUuMzFjNi4xODMtMi4zNTUsMTIuOTU0LDAuODgzLDE1LjMxLDcuMDY2DQoJYzcuMDY2LDE5LjEzOCwxMC42LDM5LjQ1MywxMC42LDYwLjA2M0MzNDkuNzc5LDI3MS4xNjcsMjcxLjQ2MiwzNDkuNzc5LDE3NC44OSwzNDkuNzc5Ii8+DQo8Zz4NCgk8cG9seWdvbiBmaWxsPSIjRkRCOTMzIiBwb2ludHM9IjI1MS40NCwxNTYuOTMgMjI0LjM1NCwxODUuMTk0IDIzOS4zNjksMjQ4LjQ5NiAyNzMuNTIyLDI0OC40OTYgCSIvPg0KPC9nPg0KPHBvbHlnb24gZmlsbD0iI0ZEQjkzMyIgcG9pbnRzPSIyNDAuMjUzLDczLjMxMiAyNDkuNjc0LDgyLjczNCAxNzQuODksMTYxLjkzNSAxMTAuOTk5LDk2LjI3NyA3NC4xOTYsMjQ4LjQ5NiAxMDguMzUsMjQ4LjQ5NiANCgkxMjguNjY1LDE2My45OTYgMTc0Ljg5LDIxNC4zNDMgMjczLjgxNywxMDYuNTgzIDI4My4yMzksMTE2LjI5OSAyOTIuNjYsNjMuMDA3ICIvPg0KPC9zdmc+DQo="},"typeVersion":1,"categories":[{"id":1,"name":"Marketing & Content"},{"id":6,"name":"Communication"}]},{"id":235,"name":"n8n-nodes-base.wooCommerceTrigger","defaults":{"name":"WooCommerce Trigger"},"displayName":"WooCommerce Trigger","icon":"file:wooCommerce.svg","iconData":{"type":"file","fileBuffer":"data:image/svg+xml;base64,PHN2ZyBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWlkWU1pZCIgdmVyc2lvbj0iMS4xIiB2aWV3Qm94PSIwIC01MCAyNTYgMjUzIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KPHRpdGxlPldvb0NvbW1lcmNlIExvZ288L3RpdGxlPgo8bWV0YWRhdGE+CjxyZGY6UkRGPgo8Y2M6V29yayByZGY6YWJvdXQ9IiI+CjxkYzpmb3JtYXQ+aW1hZ2Uvc3ZnK3htbDwvZGM6Zm9ybWF0Pgo8ZGM6dHlwZSByZGY6cmVzb3VyY2U9Imh0dHA6Ly9wdXJsLm9yZy9kYy9kY21pdHlwZS9TdGlsbEltYWdlIi8+CjxkYzp0aXRsZS8+CjwvY2M6V29yaz4KPC9yZGY6UkRGPgo8L21ldGFkYXRhPgo8cGF0aCBkPSJtMjMuNzU5IDBoMjA4LjM4YzEzLjE4NyAwIDIzLjg2MyAxMC42NzUgMjMuODYzIDIzLjg2M3Y3OS41NDJjMCAxMy4xODctMTAuNjc1IDIzLjg2My0yMy44NjMgMjMuODYzaC03NC43MjdsMTAuMjU3IDI1LjExOC00NS4xMDktMjUuMTE4aC05OC42OTVjLTEzLjE4NyAwLTIzLjg2My0xMC42NzUtMjMuODYzLTIzLjg2M3YtNzkuNTQyYy0wLjEwNDY2LTEzLjA4MyAxMC41NzEtMjMuODYzIDIzLjc1OC0yMy44NjN6IiBmaWxsPSIjN2Y1NGIzIi8+CjxwYXRoIGQ9Im0xNC41NzggMjEuNzVjMS40NTY5LTEuOTc3MiAzLjY0MjMtMy4wMTc5IDYuNTU2MS0zLjIyNiA1LjMwNzMtMC40MTYyNiA4LjMyNTIgMi4wODEzIDkuMDUzNyA3LjQ5MjcgMy4yMjYgMjEuNzUgNi43NjQyIDQwLjE2OSAxMC41MTEgNTUuMjU5bDIyLjc5LTQzLjM5NWMyLjA4MTMtMy45NTQ1IDQuNjgyOS02LjAzNTggNy44MDQ5LTYuMjQzOSA0LjU3ODktMC4zMTIyIDcuMzg4NiAyLjYwMTYgOC41MzMzIDguNzQxNSAyLjYwMTYgMTMuODQxIDUuOTMxNyAyNS42IDkuODg2MiAzNS41OSAyLjcwNTctMjYuNDMzIDcuMjg0Ni00NS40NzYgMTMuNzM3LTU3LjIzNiAxLjU2MS0yLjkxMzggMy44NTA0LTQuMzcwNyA2Ljg2ODMtNC41Nzg5IDIuMzkzNS0wLjIwODEzIDQuNTc4OSAwLjUyMDMzIDYuNTU2MSAyLjA4MTMgMS45NzcyIDEuNTYxIDMuMDE3OSAzLjUzODIgMy4yMjYgNS45MzE3IDAuMTA0MDYgMS44NzMyLTAuMjA4MTMgMy40MzQxLTEuMDQwNyA0Ljk5NTEtNC4wNTg1IDcuNDkyNy03LjM4ODYgMjAuMDg1LTEwLjA5NCAzNy41NjctMi42MDE2IDE2Ljk2My0zLjUzODIgMzAuMTc5LTIuOTEzOCAzOS42NDkgMC4yMDgxMyAyLjYwMTYtMC4yMDgxMyA0Ljg5MTEtMS4yNDg4IDYuODY4My0xLjI0ODggMi4yODk0LTMuMTIyIDMuNTM4Mi01LjUxNTQgMy43NDYzLTIuNzA1NyAwLjIwODEzLTUuNTE1NC0xLjA0MDYtOC4yMjExLTMuODUwNC05LjY3OC05Ljg4NjItMTcuMzc5LTI0LjY2My0yMi45OTgtNDQuMzMyLTYuNzY0MiAxMy4zMi0xMS43NTkgMjMuMzExLTE0Ljk4NSAyOS45NzEtNi4xMzk4IDExLjc1OS0xMS4zNDMgMTcuNzk1LTE1LjcxNCAxOC4xMDctMi44MDk4IDAuMjA4MTMtNS4yMDMzLTIuMTg1NC03LjI4NDYtNy4xODA1LTUuMzA3My0xMy42MzMtMTEuMDMxLTM5Ljk2MS0xNy4xNzEtNzguOTg1LTAuNDE2MjYtMi43MDU3IDAuMjA4MTMtNS4wOTkyIDEuNjY1LTYuOTcyNHptMjIzLjY0IDE2LjMzOGMtMy43NDYzLTYuNTU2MS05LjI2MTgtMTAuNTExLTE2LjY1LTEyLjA3Mi0xLjk3NzItMC40MTYyNi0zLjg1MDQtMC42MjQzOS01LjYxOTUtMC42MjQzOS05Ljk5MDIgMC0xOC4xMDcgNS4yMDMzLTI0LjQ1NSAxNS42MS01LjQxMTQgOC44NDU1LTguMTE3MSAxOC42MjgtOC4xMTcxIDI5LjM0NiAwIDguMDEzIDEuNjY1IDE0Ljg4MSA0Ljk5NTEgMjAuNjA1IDMuNzQ2MyA2LjU1NjEgOS4yNjE4IDEwLjUxMSAxNi42NSAxMi4wNzIgMS45NzcyIDAuNDE2MjYgMy44NTA0IDAuNjI0MzkgNS42MTk1IDAuNjI0MzkgMTAuMDk0IDAgMTguMjExLTUuMjAzMyAyNC40NTUtMTUuNjEgNS40MTE0LTguOTQ5NiA4LjExNzEtMTguNzMyIDguMTE3MS0yOS40NSAwLjEwNDA2LTguMTE3MS0xLjY2NS0xNC44ODEtNC45OTUxLTIwLjUwMXptLTEzLjExMiAyOC44MjZjLTEuNDU2OSA2Ljg2ODMtNC4wNTg1IDExLjk2Ny03LjkwODkgMTUuNDAyLTMuMDE3OSAyLjcwNTctNS44Mjc2IDMuODUwNC04LjQyOTMgMy4zMzAxLTIuNDk3Ni0wLjUyMDMzLTQuNTc4OS0yLjcwNTctNi4xMzk4LTYuNzY0Mi0xLjI0ODgtMy4yMjYtMS44NzMyLTYuNDUyLTEuODczMi05LjQ2OTkgMC0yLjYwMTYgMC4yMDgxMy01LjIwMzMgMC43Mjg0Ni03LjU5NjcgMC45MzY1OS00LjI2NjcgMi43MDU3LTguNDI5MyA1LjUxNTQtMTIuMzg0IDMuNDM0MS01LjA5OTIgNy4wNzY0LTcuMTgwNSAxMC44MjMtNi40NTIgMi40OTc2IDAuNTIwMzMgNC41Nzg5IDIuNzA1NyA2LjEzOTggNi43NjQyIDEuMjQ4OCAzLjIyNiAxLjg3MzIgNi40NTIgMS44NzMyIDkuNDY5OSAwIDIuNzA1Ny0wLjIwODEzIDUuMzA3My0wLjcyODQ2IDcuNzAwOHptLTUyLjAzMy0yOC44MjZjLTMuNzQ2My02LjU1NjEtOS4zNjU5LTEwLjUxMS0xNi42NS0xMi4wNzItMS45NzcyLTAuNDE2MjYtMy44NTA0LTAuNjI0MzktNS42MTk1LTAuNjI0MzktOS45OTAyIDAtMTguMTA3IDUuMjAzMy0yNC40NTUgMTUuNjEtNS40MTE0IDguODQ1NS04LjExNzEgMTguNjI4LTguMTE3MSAyOS4zNDYgMCA4LjAxMyAxLjY2NSAxNC44ODEgNC45OTUxIDIwLjYwNSAzLjc0NjMgNi41NTYxIDkuMjYxOCAxMC41MTEgMTYuNjUgMTIuMDcyIDEuOTc3MiAwLjQxNjI2IDMuODUwNCAwLjYyNDM5IDUuNjE5NSAwLjYyNDM5IDEwLjA5NCAwIDE4LjIxMS01LjIwMzMgMjQuNDU1LTE1LjYxIDUuNDExNC04Ljk0OTYgOC4xMTcxLTE4LjczMiA4LjExNzEtMjkuNDUgMC04LjExNzEtMS42NjUtMTQuODgxLTQuOTk1MS0yMC41MDF6bS0xMy4yMTYgMjguODI2Yy0xLjQ1NjkgNi44NjgzLTQuMDU4NSAxMS45NjctNy45MDg5IDE1LjQwMi0zLjAxNzkgMi43MDU3LTUuODI3NiAzLjg1MDQtOC40MjkzIDMuMzMwMS0yLjQ5NzYtMC41MjAzMy00LjU3ODktMi43MDU3LTYuMTM5OC02Ljc2NDItMS4yNDg4LTMuMjI2LTEuODczMi02LjQ1Mi0xLjg3MzItOS40Njk5IDAtMi42MDE2IDAuMjA4MTMtNS4yMDMzIDAuNzI4NDYtNy41OTY3IDAuOTM2NTgtNC4yNjY3IDIuNzA1Ny04LjQyOTMgNS41MTU0LTEyLjM4NCAzLjQzNDEtNS4wOTkyIDcuMDc2NC03LjE4MDUgMTAuODIzLTYuNDUyIDIuNDk3NiAwLjUyMDMzIDQuNTc4OSAyLjcwNTcgNi4xMzk4IDYuNzY0MiAxLjI0ODggMy4yMjYgMS44NzMyIDYuNDUyIDEuODczMiA5LjQ2OTkgMC4xMDQwNiAyLjcwNTctMC4yMDgxMyA1LjMwNzMtMC43Mjg0NiA3LjcwMDh6IiBmaWxsPSIjZmZmIi8+Cjwvc3ZnPgo="},"typeVersion":1,"categories":[{"id":2,"name":"Sales"}]},{"id":325,"name":"n8n-nodes-base.twitter","defaults":{"name":"X"},"displayName":"X (Formerly Twitter)","icon":"file:x.svg","iconData":{"type":"file","fileBuffer":"data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMjQgMjQiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTE4LjI0NCAyLjI1aDMuMzA4bC03LjIyNyA4LjI2IDguNTAyIDExLjI0SDE2LjE3bC01LjIxNC02LjgxN0w0Ljk5IDIxLjc1SDEuNjhsNy43My04LjgzNUwxLjI1NCAyLjI1SDguMDhsNC43MTMgNi4yMzF6bS0xLjE2MSAxNy41MmgxLjgzM0w3LjA4NCA0LjEyNkg1LjExN3oiPjwvcGF0aD48L3N2Zz4K"},"typeVersion":2,"categories":[{"id":1,"name":"Marketing & Content"}]}],"categories":[{"id":2,"name":"Sales"}],"workflows":[{"id":1205,"name":"Promote new Shopify products on Twitter and Telegram","views":485,"recentViews":9850,"totalViews":485,"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"}]},{"id":1456,"name":"Add new customers from WooCommerce to Mautic","views":333,"recentViews":9833,"totalViews":333,"createdAt":"2022-02-17T15:00:40.748Z","description":"This workflow uses a WooCommerce trigger that will run when a new customer has been added, It will then add the customer to Mautic.\n\nTo use this workflow you will need to set the credentials to use for the WooCommerce and Mautic nodes.","workflow":{"id":83,"name":"New WooCommerce Customer to Mautic","nodes":[{"name":"Check for Existing","type":"n8n-nodes-base.mautic","position":[280,480],"parameters":{"options":{"search":"={{$json[\"email\"]}}"},"operation":"getAll","authentication":"oAuth2"},"credentials":{"mauticOAuth2Api":{"id":"54","name":"Mautic account"}},"typeVersion":1,"alwaysOutputData":true},{"name":"If New","type":"n8n-nodes-base.if","position":[460,480],"parameters":{"conditions":{"string":[{"value1":"={{$json[\"id\"]}}","operation":"isEmpty"}]}},"typeVersion":1},{"name":"Create Contact","type":"n8n-nodes-base.mautic","position":[680,320],"parameters":{"email":"={{$node[\"Customer Created\"].json[\"email\"]}}","company":"={{$node[\"Customer Created\"].json[\"billing\"][\"company\"]}}","options":{},"lastName":"={{$node[\"Customer Created\"].json[\"last_name\"]}}","firstName":"={{$node[\"Customer Created\"].json[\"first_name\"]}}","authentication":"oAuth2","additionalFields":{}},"credentials":{"mauticOAuth2Api":{"id":"54","name":"Mautic account"}},"typeVersion":1},{"name":"Update Contact","type":"n8n-nodes-base.mautic","position":[680,580],"parameters":{"options":{},"contactId":"={{$json[\"id\"]}}","operation":"update","updateFields":{"lastName":"={{$node[\"Customer Created or Updated\"].json[\"last_name\"]}}","firstName":"={{$node[\"Customer Created or Updated\"].json[\"first_name\"]}}"},"authentication":"oAuth2"},"credentials":{"mauticOAuth2Api":{"id":"54","name":"Mautic account"}},"typeVersion":1},{"name":"Customer Created or Updated","type":"n8n-nodes-base.wooCommerceTrigger","position":[100,480],"webhookId":"5d89e322-a5e0-4cce-9eab-185e8375175b","parameters":{"event":"customer.updated"},"credentials":{"wooCommerceApi":{"id":"48","name":"WooCommerce account"}},"typeVersion":1}],"active":false,"settings":{"saveManualExecutions":true,"saveExecutionProgress":true,"saveDataSuccessExecution":"all"},"connections":{"If New":{"main":[[{"node":"Create Contact","type":"main","index":0}],[{"node":"Update Contact","type":"main","index":0}]]},"Check for Existing":{"main":[[{"node":"If New","type":"main","index":0}]]},"Customer Created or Updated":{"main":[[{"node":"Check for Existing","type":"main","index":0}]]}}},"workflowInfo":{"nodeCount":6,"nodeTypes":{"n8n-nodes-base.if":{"count":1},"n8n-nodes-base.mautic":{"count":3},"n8n-nodes-base.wooCommerceTrigger":{"count":1}}},"user":{"username":"jon-n8n"},"nodes":[{"id":20,"icon":"fa:map-signs","name":"n8n-nodes-base.if","defaults":{"name":"IF","color":"#408000"},"iconData":{"icon":"map-signs","type":"icon"},"categories":[{"id":9,"name":"Core Nodes"}],"displayName":"IF","typeVersion":1},{"id":42,"icon":"fa:play","name":"n8n-nodes-base.start","defaults":{"name":"Start","color":"#00e000"},"iconData":{"icon":"play","type":"icon"},"categories":[{"id":9,"name":"Core Nodes"}],"displayName":"Start","typeVersion":1},{"id":126,"icon":"file:mautic.svg","name":"n8n-nodes-base.mautic","defaults":{"name":"Mautic"},"iconData":{"type":"file","fileBuffer":"data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAxNi4wLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DQo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB3aWR0aD0iMzQ5Ljc3OXB4IiBoZWlnaHQ9IjM0OS43NzlweCIgdmlld0JveD0iMCAwIDM0OS43NzkgMzQ5Ljc3OSIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgMzQ5Ljc3OSAzNDkuNzc5Ig0KCSB4bWw6c3BhY2U9InByZXNlcnZlIj4NCjxwYXRoIGZpbGw9IiNGRkZGRkYiIGQ9Ik0xNzQuODksMzQ5Ljc3OUM3OC42MTIsMzQ5Ljc3OSwwLDI3MS40NjIsMCwxNzQuODlTNzguNjEyLDAsMTc0Ljg5LDBjMjMuMjYsMCw0NS45MzEsNC40MTcsNjcuMTI5LDEzLjU0Mw0KCWM1Ljg4OSwyLjY1LDguODMzLDkuNDIyLDYuNDc4LDE1LjYwNWMtMi42NDksNS44ODgtOS40MjEsOC44MzMtMTUuNjA0LDYuNDc3Yy0xOC41NDktNy42NTUtMzcuOTgtMTEuNDgyLTU4LjAwMi0xMS40ODINCgljLTgzLjMyMywwLTE1MS4wNDEsNjcuNzE4LTE1MS4wNDEsMTUxLjA0MVM5MS41NjcsMzI2LjIyNSwxNzQuODksMzI2LjIyNWM4My4zMjMsMCwxNTEuMDQxLTY3LjcxOCwxNTEuMDQxLTE1MS4wNDENCgljMC0xNy45Ni0yLjk0NC0zNS4zMzItOS4xMjctNTEuODE5Yy0yLjM1NS02LjE4MywwLjg4My0xMi45NTUsNy4wNjYtMTUuMzFjNi4xODMtMi4zNTUsMTIuOTU0LDAuODgzLDE1LjMxLDcuMDY2DQoJYzcuMDY2LDE5LjEzOCwxMC42LDM5LjQ1MywxMC42LDYwLjA2M0MzNDkuNzc5LDI3MS4xNjcsMjcxLjQ2MiwzNDkuNzc5LDE3NC44OSwzNDkuNzc5Ii8+DQo8Zz4NCgk8cG9seWdvbiBmaWxsPSIjRkRCOTMzIiBwb2ludHM9IjI1MS40NCwxNTYuOTMgMjI0LjM1NCwxODUuMTk0IDIzOS4zNjksMjQ4LjQ5NiAyNzMuNTIyLDI0OC40OTYgCSIvPg0KPC9nPg0KPHBvbHlnb24gZmlsbD0iI0ZEQjkzMyIgcG9pbnRzPSIyNDAuMjUzLDczLjMxMiAyNDkuNjc0LDgyLjczNCAxNzQuODksMTYxLjkzNSAxMTAuOTk5LDk2LjI3NyA3NC4xOTYsMjQ4LjQ5NiAxMDguMzUsMjQ4LjQ5NiANCgkxMjguNjY1LDE2My45OTYgMTc0Ljg5LDIxNC4zNDMgMjczLjgxNywxMDYuNTgzIDI4My4yMzksMTE2LjI5OSAyOTIuNjYsNjMuMDA3ICIvPg0KPC9zdmc+DQo="},"categories":[{"id":1,"name":"Marketing & Content"},{"id":6,"name":"Communication"}],"displayName":"Mautic","typeVersion":1},{"id":235,"icon":"file:wooCommerce.svg","name":"n8n-nodes-base.wooCommerceTrigger","defaults":{"name":"WooCommerce Trigger"},"iconData":{"type":"file","fileBuffer":"data:image/svg+xml;base64,PHN2ZyBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWlkWU1pZCIgdmVyc2lvbj0iMS4xIiB2aWV3Qm94PSIwIC01MCAyNTYgMjUzIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KPHRpdGxlPldvb0NvbW1lcmNlIExvZ288L3RpdGxlPgo8bWV0YWRhdGE+CjxyZGY6UkRGPgo8Y2M6V29yayByZGY6YWJvdXQ9IiI+CjxkYzpmb3JtYXQ+aW1hZ2Uvc3ZnK3htbDwvZGM6Zm9ybWF0Pgo8ZGM6dHlwZSByZGY6cmVzb3VyY2U9Imh0dHA6Ly9wdXJsLm9yZy9kYy9kY21pdHlwZS9TdGlsbEltYWdlIi8+CjxkYzp0aXRsZS8+CjwvY2M6V29yaz4KPC9yZGY6UkRGPgo8L21ldGFkYXRhPgo8cGF0aCBkPSJtMjMuNzU5IDBoMjA4LjM4YzEzLjE4NyAwIDIzLjg2MyAxMC42NzUgMjMuODYzIDIzLjg2M3Y3OS41NDJjMCAxMy4xODctMTAuNjc1IDIzLjg2My0yMy44NjMgMjMuODYzaC03NC43MjdsMTAuMjU3IDI1LjExOC00NS4xMDktMjUuMTE4aC05OC42OTVjLTEzLjE4NyAwLTIzLjg2My0xMC42NzUtMjMuODYzLTIzLjg2M3YtNzkuNTQyYy0wLjEwNDY2LTEzLjA4MyAxMC41NzEtMjMuODYzIDIzLjc1OC0yMy44NjN6IiBmaWxsPSIjN2Y1NGIzIi8+CjxwYXRoIGQ9Im0xNC41NzggMjEuNzVjMS40NTY5LTEuOTc3MiAzLjY0MjMtMy4wMTc5IDYuNTU2MS0zLjIyNiA1LjMwNzMtMC40MTYyNiA4LjMyNTIgMi4wODEzIDkuMDUzNyA3LjQ5MjcgMy4yMjYgMjEuNzUgNi43NjQyIDQwLjE2OSAxMC41MTEgNTUuMjU5bDIyLjc5LTQzLjM5NWMyLjA4MTMtMy45NTQ1IDQuNjgyOS02LjAzNTggNy44MDQ5LTYuMjQzOSA0LjU3ODktMC4zMTIyIDcuMzg4NiAyLjYwMTYgOC41MzMzIDguNzQxNSAyLjYwMTYgMTMuODQxIDUuOTMxNyAyNS42IDkuODg2MiAzNS41OSAyLjcwNTctMjYuNDMzIDcuMjg0Ni00NS40NzYgMTMuNzM3LTU3LjIzNiAxLjU2MS0yLjkxMzggMy44NTA0LTQuMzcwNyA2Ljg2ODMtNC41Nzg5IDIuMzkzNS0wLjIwODEzIDQuNTc4OSAwLjUyMDMzIDYuNTU2MSAyLjA4MTMgMS45NzcyIDEuNTYxIDMuMDE3OSAzLjUzODIgMy4yMjYgNS45MzE3IDAuMTA0MDYgMS44NzMyLTAuMjA4MTMgMy40MzQxLTEuMDQwNyA0Ljk5NTEtNC4wNTg1IDcuNDkyNy03LjM4ODYgMjAuMDg1LTEwLjA5NCAzNy41NjctMi42MDE2IDE2Ljk2My0zLjUzODIgMzAuMTc5LTIuOTEzOCAzOS42NDkgMC4yMDgxMyAyLjYwMTYtMC4yMDgxMyA0Ljg5MTEtMS4yNDg4IDYuODY4My0xLjI0ODggMi4yODk0LTMuMTIyIDMuNTM4Mi01LjUxNTQgMy43NDYzLTIuNzA1NyAwLjIwODEzLTUuNTE1NC0xLjA0MDYtOC4yMjExLTMuODUwNC05LjY3OC05Ljg4NjItMTcuMzc5LTI0LjY2My0yMi45OTgtNDQuMzMyLTYuNzY0MiAxMy4zMi0xMS43NTkgMjMuMzExLTE0Ljk4NSAyOS45NzEtNi4xMzk4IDExLjc1OS0xMS4zNDMgMTcuNzk1LTE1LjcxNCAxOC4xMDctMi44MDk4IDAuMjA4MTMtNS4yMDMzLTIuMTg1NC03LjI4NDYtNy4xODA1LTUuMzA3My0xMy42MzMtMTEuMDMxLTM5Ljk2MS0xNy4xNzEtNzguOTg1LTAuNDE2MjYtMi43MDU3IDAuMjA4MTMtNS4wOTkyIDEuNjY1LTYuOTcyNHptMjIzLjY0IDE2LjMzOGMtMy43NDYzLTYuNTU2MS05LjI2MTgtMTAuNTExLTE2LjY1LTEyLjA3Mi0xLjk3NzItMC40MTYyNi0zLjg1MDQtMC42MjQzOS01LjYxOTUtMC42MjQzOS05Ljk5MDIgMC0xOC4xMDcgNS4yMDMzLTI0LjQ1NSAxNS42MS01LjQxMTQgOC44NDU1LTguMTE3MSAxOC42MjgtOC4xMTcxIDI5LjM0NiAwIDguMDEzIDEuNjY1IDE0Ljg4MSA0Ljk5NTEgMjAuNjA1IDMuNzQ2MyA2LjU1NjEgOS4yNjE4IDEwLjUxMSAxNi42NSAxMi4wNzIgMS45NzcyIDAuNDE2MjYgMy44NTA0IDAuNjI0MzkgNS42MTk1IDAuNjI0MzkgMTAuMDk0IDAgMTguMjExLTUuMjAzMyAyNC40NTUtMTUuNjEgNS40MTE0LTguOTQ5NiA4LjExNzEtMTguNzMyIDguMTE3MS0yOS40NSAwLjEwNDA2LTguMTE3MS0xLjY2NS0xNC44ODEtNC45OTUxLTIwLjUwMXptLTEzLjExMiAyOC44MjZjLTEuNDU2OSA2Ljg2ODMtNC4wNTg1IDExLjk2Ny03LjkwODkgMTUuNDAyLTMuMDE3OSAyLjcwNTctNS44Mjc2IDMuODUwNC04LjQyOTMgMy4zMzAxLTIuNDk3Ni0wLjUyMDMzLTQuNTc4OS0yLjcwNTctNi4xMzk4LTYuNzY0Mi0xLjI0ODgtMy4yMjYtMS44NzMyLTYuNDUyLTEuODczMi05LjQ2OTkgMC0yLjYwMTYgMC4yMDgxMy01LjIwMzMgMC43Mjg0Ni03LjU5NjcgMC45MzY1OS00LjI2NjcgMi43MDU3LTguNDI5MyA1LjUxNTQtMTIuMzg0IDMuNDM0MS01LjA5OTIgNy4wNzY0LTcuMTgwNSAxMC44MjMtNi40NTIgMi40OTc2IDAuNTIwMzMgNC41Nzg5IDIuNzA1NyA2LjEzOTggNi43NjQyIDEuMjQ4OCAzLjIyNiAxLjg3MzIgNi40NTIgMS44NzMyIDkuNDY5OSAwIDIuNzA1Ny0wLjIwODEzIDUuMzA3My0wLjcyODQ2IDcuNzAwOHptLTUyLjAzMy0yOC44MjZjLTMuNzQ2My02LjU1NjEtOS4zNjU5LTEwLjUxMS0xNi42NS0xMi4wNzItMS45NzcyLTAuNDE2MjYtMy44NTA0LTAuNjI0MzktNS42MTk1LTAuNjI0MzktOS45OTAyIDAtMTguMTA3IDUuMjAzMy0yNC40NTUgMTUuNjEtNS40MTE0IDguODQ1NS04LjExNzEgMTguNjI4LTguMTE3MSAyOS4zNDYgMCA4LjAxMyAxLjY2NSAxNC44ODEgNC45OTUxIDIwLjYwNSAzLjc0NjMgNi41NTYxIDkuMjYxOCAxMC41MTEgMTYuNjUgMTIuMDcyIDEuOTc3MiAwLjQxNjI2IDMuODUwNCAwLjYyNDM5IDUuNjE5NSAwLjYyNDM5IDEwLjA5NCAwIDE4LjIxMS01LjIwMzMgMjQuNDU1LTE1LjYxIDUuNDExNC04Ljk0OTYgOC4xMTcxLTE4LjczMiA4LjExNzEtMjkuNDUgMC04LjExNzEtMS42NjUtMTQuODgxLTQuOTk1MS0yMC41MDF6bS0xMy4yMTYgMjguODI2Yy0xLjQ1NjkgNi44NjgzLTQuMDU4NSAxMS45NjctNy45MDg5IDE1LjQwMi0zLjAxNzkgMi43MDU3LTUuODI3NiAzLjg1MDQtOC40MjkzIDMuMzMwMS0yLjQ5NzYtMC41MjAzMy00LjU3ODktMi43MDU3LTYuMTM5OC02Ljc2NDItMS4yNDg4LTMuMjI2LTEuODczMi02LjQ1Mi0xLjg3MzItOS40Njk5IDAtMi42MDE2IDAuMjA4MTMtNS4yMDMzIDAuNzI4NDYtNy41OTY3IDAuOTM2NTgtNC4yNjY3IDIuNzA1Ny04LjQyOTMgNS41MTU0LTEyLjM4NCAzLjQzNDEtNS4wOTkyIDcuMDc2NC03LjE4MDUgMTAuODIzLTYuNDUyIDIuNDk3NiAwLjUyMDMzIDQuNTc4OSAyLjcwNTcgNi4xMzk4IDYuNzY0MiAxLjI0ODggMy4yMjYgMS44NzMyIDYuNDUyIDEuODczMiA5LjQ2OTkgMC4xMDQwNiAyLjcwNTctMC4yMDgxMyA1LjMwNzMtMC43Mjg0NiA3LjcwMDh6IiBmaWxsPSIjZmZmIi8+Cjwvc3ZnPgo="},"categories":[{"id":2,"name":"Sales"}],"displayName":"WooCommerce Trigger","typeVersion":1}],"categories":[{"id":2,"name":"Sales"}],"image":[]},{"id":1459,"name":"Notify on Telegram and Twitter when new order is added in WooCommerce","views":620,"recentViews":9823,"totalViews":620,"createdAt":"2022-02-17T15:02:14.961Z","description":"This workflow uses a WooCommerce trigger that will run a new product has been added, It will then post the product to Telegram and Twitter.\n\nTo use this workflow you will need to set the credentials to use for the WooCommerce, Twitter and Telegram nodes.","workflow":{"id":85,"name":"New WooCommerce Product to Twitter and Telegram","nodes":[{"name":"Twitter","type":"n8n-nodes-base.twitter","position":[720,300],"parameters":{"text":"=✨ New Product Announcement ✨\nWe have just added {{$json[\"name\"]}}, Head to {{$json[\"permalink\"]}} to find out more.","additionalFields":{}},"credentials":{"twitterOAuth1Api":{"id":"37","name":"joffcom"}},"typeVersion":1},{"name":"Telegram","type":"n8n-nodes-base.telegram","position":[720,500],"parameters":{"text":"=✨ New Product Announcement ✨\nWe have just added {{$json[\"name\"]}}, Head to {{$json[\"permalink\"]}} to find out more.","chatId":"123456","additionalFields":{}},"credentials":{"telegramApi":{"id":"56","name":"Telegram account"}},"typeVersion":1},{"name":"WooCommerce Trigger","type":"n8n-nodes-base.wooCommerceTrigger","position":[540,400],"webhookId":"ab7b134b-9b2d-4e0d-b496-1aee30db0808","parameters":{"event":"product.created"},"credentials":{"wooCommerceApi":{"id":"48","name":"WooCommerce account"}},"typeVersion":1}],"active":false,"settings":{},"connections":{"WooCommerce Trigger":{"main":[[{"node":"Twitter","type":"main","index":0},{"node":"Telegram","type":"main","index":0}]]}}},"workflowInfo":{"nodeCount":4,"nodeTypes":{"n8n-nodes-base.twitter":{"count":1},"n8n-nodes-base.telegram":{"count":1},"n8n-nodes-base.wooCommerceTrigger":{"count":1}}},"user":{"username":"jon-n8n"},"nodes":[{"id":42,"icon":"fa:play","name":"n8n-nodes-base.start","defaults":{"name":"Start","color":"#00e000"},"iconData":{"icon":"play","type":"icon"},"categories":[{"id":9,"name":"Core Nodes"}],"displayName":"Start","typeVersion":1},{"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":235,"icon":"file:wooCommerce.svg","name":"n8n-nodes-base.wooCommerceTrigger","defaults":{"name":"WooCommerce Trigger"},"iconData":{"type":"file","fileBuffer":"data:image/svg+xml;base64,PHN2ZyBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWlkWU1pZCIgdmVyc2lvbj0iMS4xIiB2aWV3Qm94PSIwIC01MCAyNTYgMjUzIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KPHRpdGxlPldvb0NvbW1lcmNlIExvZ288L3RpdGxlPgo8bWV0YWRhdGE+CjxyZGY6UkRGPgo8Y2M6V29yayByZGY6YWJvdXQ9IiI+CjxkYzpmb3JtYXQ+aW1hZ2Uvc3ZnK3htbDwvZGM6Zm9ybWF0Pgo8ZGM6dHlwZSByZGY6cmVzb3VyY2U9Imh0dHA6Ly9wdXJsLm9yZy9kYy9kY21pdHlwZS9TdGlsbEltYWdlIi8+CjxkYzp0aXRsZS8+CjwvY2M6V29yaz4KPC9yZGY6UkRGPgo8L21ldGFkYXRhPgo8cGF0aCBkPSJtMjMuNzU5IDBoMjA4LjM4YzEzLjE4NyAwIDIzLjg2MyAxMC42NzUgMjMuODYzIDIzLjg2M3Y3OS41NDJjMCAxMy4xODctMTAuNjc1IDIzLjg2My0yMy44NjMgMjMuODYzaC03NC43MjdsMTAuMjU3IDI1LjExOC00NS4xMDktMjUuMTE4aC05OC42OTVjLTEzLjE4NyAwLTIzLjg2My0xMC42NzUtMjMuODYzLTIzLjg2M3YtNzkuNTQyYy0wLjEwNDY2LTEzLjA4MyAxMC41NzEtMjMuODYzIDIzLjc1OC0yMy44NjN6IiBmaWxsPSIjN2Y1NGIzIi8+CjxwYXRoIGQ9Im0xNC41NzggMjEuNzVjMS40NTY5LTEuOTc3MiAzLjY0MjMtMy4wMTc5IDYuNTU2MS0zLjIyNiA1LjMwNzMtMC40MTYyNiA4LjMyNTIgMi4wODEzIDkuMDUzNyA3LjQ5MjcgMy4yMjYgMjEuNzUgNi43NjQyIDQwLjE2OSAxMC41MTEgNTUuMjU5bDIyLjc5LTQzLjM5NWMyLjA4MTMtMy45NTQ1IDQuNjgyOS02LjAzNTggNy44MDQ5LTYuMjQzOSA0LjU3ODktMC4zMTIyIDcuMzg4NiAyLjYwMTYgOC41MzMzIDguNzQxNSAyLjYwMTYgMTMuODQxIDUuOTMxNyAyNS42IDkuODg2MiAzNS41OSAyLjcwNTctMjYuNDMzIDcuMjg0Ni00NS40NzYgMTMuNzM3LTU3LjIzNiAxLjU2MS0yLjkxMzggMy44NTA0LTQuMzcwNyA2Ljg2ODMtNC41Nzg5IDIuMzkzNS0wLjIwODEzIDQuNTc4OSAwLjUyMDMzIDYuNTU2MSAyLjA4MTMgMS45NzcyIDEuNTYxIDMuMDE3OSAzLjUzODIgMy4yMjYgNS45MzE3IDAuMTA0MDYgMS44NzMyLTAuMjA4MTMgMy40MzQxLTEuMDQwNyA0Ljk5NTEtNC4wNTg1IDcuNDkyNy03LjM4ODYgMjAuMDg1LTEwLjA5NCAzNy41NjctMi42MDE2IDE2Ljk2My0zLjUzODIgMzAuMTc5LTIuOTEzOCAzOS42NDkgMC4yMDgxMyAyLjYwMTYtMC4yMDgxMyA0Ljg5MTEtMS4yNDg4IDYuODY4My0xLjI0ODggMi4yODk0LTMuMTIyIDMuNTM4Mi01LjUxNTQgMy43NDYzLTIuNzA1NyAwLjIwODEzLTUuNTE1NC0xLjA0MDYtOC4yMjExLTMuODUwNC05LjY3OC05Ljg4NjItMTcuMzc5LTI0LjY2My0yMi45OTgtNDQuMzMyLTYuNzY0MiAxMy4zMi0xMS43NTkgMjMuMzExLTE0Ljk4NSAyOS45NzEtNi4xMzk4IDExLjc1OS0xMS4zNDMgMTcuNzk1LTE1LjcxNCAxOC4xMDctMi44MDk4IDAuMjA4MTMtNS4yMDMzLTIuMTg1NC03LjI4NDYtNy4xODA1LTUuMzA3My0xMy42MzMtMTEuMDMxLTM5Ljk2MS0xNy4xNzEtNzguOTg1LTAuNDE2MjYtMi43MDU3IDAuMjA4MTMtNS4wOTkyIDEuNjY1LTYuOTcyNHptMjIzLjY0IDE2LjMzOGMtMy43NDYzLTYuNTU2MS05LjI2MTgtMTAuNTExLTE2LjY1LTEyLjA3Mi0xLjk3NzItMC40MTYyNi0zLjg1MDQtMC42MjQzOS01LjYxOTUtMC42MjQzOS05Ljk5MDIgMC0xOC4xMDcgNS4yMDMzLTI0LjQ1NSAxNS42MS01LjQxMTQgOC44NDU1LTguMTE3MSAxOC42MjgtOC4xMTcxIDI5LjM0NiAwIDguMDEzIDEuNjY1IDE0Ljg4MSA0Ljk5NTEgMjAuNjA1IDMuNzQ2MyA2LjU1NjEgOS4yNjE4IDEwLjUxMSAxNi42NSAxMi4wNzIgMS45NzcyIDAuNDE2MjYgMy44NTA0IDAuNjI0MzkgNS42MTk1IDAuNjI0MzkgMTAuMDk0IDAgMTguMjExLTUuMjAzMyAyNC40NTUtMTUuNjEgNS40MTE0LTguOTQ5NiA4LjExNzEtMTguNzMyIDguMTE3MS0yOS40NSAwLjEwNDA2LTguMTE3MS0xLjY2NS0xNC44ODEtNC45OTUxLTIwLjUwMXptLTEzLjExMiAyOC44MjZjLTEuNDU2OSA2Ljg2ODMtNC4wNTg1IDExLjk2Ny03LjkwODkgMTUuNDAyLTMuMDE3OSAyLjcwNTctNS44Mjc2IDMuODUwNC04LjQyOTMgMy4zMzAxLTIuNDk3Ni0wLjUyMDMzLTQuNTc4OS0yLjcwNTctNi4xMzk4LTYuNzY0Mi0xLjI0ODgtMy4yMjYtMS44NzMyLTYuNDUyLTEuODczMi05LjQ2OTkgMC0yLjYwMTYgMC4yMDgxMy01LjIwMzMgMC43Mjg0Ni03LjU5NjcgMC45MzY1OS00LjI2NjcgMi43MDU3LTguNDI5MyA1LjUxNTQtMTIuMzg0IDMuNDM0MS01LjA5OTIgNy4wNzY0LTcuMTgwNSAxMC44MjMtNi40NTIgMi40OTc2IDAuNTIwMzMgNC41Nzg5IDIuNzA1NyA2LjEzOTggNi43NjQyIDEuMjQ4OCAzLjIyNiAxLjg3MzIgNi40NTIgMS44NzMyIDkuNDY5OSAwIDIuNzA1Ny0wLjIwODEzIDUuMzA3My0wLjcyODQ2IDcuNzAwOHptLTUyLjAzMy0yOC44MjZjLTMuNzQ2My02LjU1NjEtOS4zNjU5LTEwLjUxMS0xNi42NS0xMi4wNzItMS45NzcyLTAuNDE2MjYtMy44NTA0LTAuNjI0MzktNS42MTk1LTAuNjI0MzktOS45OTAyIDAtMTguMTA3IDUuMjAzMy0yNC40NTUgMTUuNjEtNS40MTE0IDguODQ1NS04LjExNzEgMTguNjI4LTguMTE3MSAyOS4zNDYgMCA4LjAxMyAxLjY2NSAxNC44ODEgNC45OTUxIDIwLjYwNSAzLjc0NjMgNi41NTYxIDkuMjYxOCAxMC41MTEgMTYuNjUgMTIuMDcyIDEuOTc3MiAwLjQxNjI2IDMuODUwNCAwLjYyNDM5IDUuNjE5NSAwLjYyNDM5IDEwLjA5NCAwIDE4LjIxMS01LjIwMzMgMjQuNDU1LTE1LjYxIDUuNDExNC04Ljk0OTYgOC4xMTcxLTE4LjczMiA4LjExNzEtMjkuNDUgMC04LjExNzEtMS42NjUtMTQuODgxLTQuOTk1MS0yMC41MDF6bS0xMy4yMTYgMjguODI2Yy0xLjQ1NjkgNi44NjgzLTQuMDU4NSAxMS45NjctNy45MDg5IDE1LjQwMi0zLjAxNzkgMi43MDU3LTUuODI3NiAzLjg1MDQtOC40MjkzIDMuMzMwMS0yLjQ5NzYtMC41MjAzMy00LjU3ODktMi43MDU3LTYuMTM5OC02Ljc2NDItMS4yNDg4LTMuMjI2LTEuODczMi02LjQ1Mi0xLjg3MzItOS40Njk5IDAtMi42MDE2IDAuMjA4MTMtNS4yMDMzIDAuNzI4NDYtNy41OTY3IDAuOTM2NTgtNC4yNjY3IDIuNzA1Ny04LjQyOTMgNS41MTU0LTEyLjM4NCAzLjQzNDEtNS4wOTkyIDcuMDc2NC03LjE4MDUgMTAuODIzLTYuNDUyIDIuNDk3NiAwLjUyMDMzIDQuNTc4OSAyLjcwNTcgNi4xMzk4IDYuNzY0MiAxLjI0ODggMy4yMjYgMS44NzMyIDYuNDUyIDEuODczMiA5LjQ2OTkgMC4xMDQwNiAyLjcwNTctMC4yMDgxMyA1LjMwNzMtMC43Mjg0NiA3LjcwMDh6IiBmaWxsPSIjZmZmIi8+Cjwvc3ZnPgo="},"categories":[{"id":2,"name":"Sales"}],"displayName":"WooCommerce 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":1457,"name":"Notify on Slack when new order is registered in WooCommerce","views":178,"recentViews":9787,"totalViews":178,"createdAt":"2022-02-17T15:01:13.489Z","description":"This workflow uses a WooCommerce trigger that will run when an order has been placed.\n\nIf the value of this is over 100 it will post it to a Slack channel.\n\nTo use this workflow you will need to set the credentials to use for the WooCommerce and Slack nodes, You will also need to pick a channel to post the message to.","workflow":{"id":81,"name":"New WooCommerce order to Slack","nodes":[{"name":"Order Created","type":"n8n-nodes-base.wooCommerceTrigger","position":[340,500],"webhookId":"287b4bf4-67ec-4c97-85d9-c0d3e6f59e6b","parameters":{"event":"order.created"},"credentials":{"wooCommerceApi":{"id":"48","name":"WooCommerce account"}},"typeVersion":1},{"name":"Send to Slack","type":"n8n-nodes-base.slack","position":[780,480],"parameters":{"text":":sparkles: There is a new order :sparkles:","channel":"woo-commerce","blocksUi":{"blocksValues":[]},"attachments":[{"color":"#66FF00","fields":{"item":[{"short":true,"title":"Order ID","value":"={{$json[\"id\"]}}"},{"short":true,"title":"Status","value":"={{$json[\"status\"]}}"},{"short":true,"title":"Total","value":"={{$json[\"currency_symbol\"]}}{{$json[\"total\"]}}"},{"short":false,"title":"Link","value":"={{$node[\"Order Created\"].json[\"_links\"][\"self\"][0][\"href\"]}}"}]},"footer":"=*Ordered:* {{$json[\"date_created\"]}} | *Transaction ID:* {{$json[\"transaction_id\"]}}"}],"otherOptions":{}},"credentials":{"slackApi":{"id":"53","name":"Slack Access Token"}},"typeVersion":1},{"name":"Price over 100","type":"n8n-nodes-base.if","position":[540,500],"parameters":{"conditions":{"number":[{"value1":"={{$json[\"total\"]}}","value2":100,"operation":"largerEqual"}]}},"typeVersion":1}],"active":false,"settings":{"saveManualExecutions":true,"saveExecutionProgress":true,"saveDataSuccessExecution":"all"},"connections":{"Order Created":{"main":[[{"node":"Price over 100","type":"main","index":0}]]},"Price over 100":{"main":[[{"node":"Send to Slack","type":"main","index":0}],[]]}}},"workflowInfo":{"nodeCount":4,"nodeTypes":{"n8n-nodes-base.if":{"count":1},"n8n-nodes-base.slack":{"count":1},"n8n-nodes-base.wooCommerceTrigger":{"count":1}}},"user":{"username":"jon-n8n"},"nodes":[{"id":20,"icon":"fa:map-signs","name":"n8n-nodes-base.if","defaults":{"name":"IF","color":"#408000"},"iconData":{"icon":"map-signs","type":"icon"},"categories":[{"id":9,"name":"Core Nodes"}],"displayName":"IF","typeVersion":1},{"id":40,"icon":"file:slack.svg","name":"n8n-nodes-base.slack","defaults":{"name":"Slack"},"iconData":{"type":"file","fileBuffer":"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgMTUwLjg1MiAxNTAuODUyIiBmaWxsPSIjZmZmIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48dXNlIHhsaW5rOmhyZWY9IiNhIiB4PSIuOTI2IiB5PSIuOTI2Ii8+PHN5bWJvbCBpZD0iYSIgb3ZlcmZsb3c9InZpc2libGUiPjxnIHN0cm9rZS13aWR0aD0iMS44NTIiPjxwYXRoIGQ9Ik00MC43NDEgOTMuNTVjMC04LjczNSA2LjYwNy0xNS43NzIgMTQuODE1LTE1Ljc3MnMxNC44MTUgNy4wMzcgMTQuODE1IDE1Ljc3MnYzOC44MjRjMCA4LjczNy02LjYwNyAxNS43NzQtMTQuODE1IDE1Ljc3NHMtMTQuODE1LTcuMDM3LTE0LjgxNS0xNS43NzJ6IiBmaWxsPSIjZTAxZTVhIiBzdHJva2U9IiNlMDFlNWEiLz48cGF0aCBkPSJNOTMuNTUgMTA3LjQwOGMtOC43MzUgMC0xNS43NzItNi42MDctMTUuNzcyLTE0LjgxNXM3LjAzNy0xNC44MTUgMTUuNzcyLTE0LjgxNWgzOC44MjZjOC43MzUgMCAxNS43NzIgNi42MDcgMTUuNzcyIDE0LjgxNXMtNy4wMzcgMTQuODE1LTE1Ljc3MiAxNC44MTV6IiBmaWxsPSIjZWNiMjJkIiBzdHJva2U9IiNlY2IyMmQiLz48cGF0aCBkPSJNNzcuNzc4IDE1Ljc3MkM3Ny43NzggNy4wMzcgODQuMzg1IDAgOTIuNTkzIDBzMTQuODE1IDcuMDM3IDE0LjgxNSAxNS43NzJ2MzguODI2YzAgOC43MzUtNi42MDcgMTUuNzcyLTE0LjgxNSAxNS43NzJzLTE0LjgxNS03LjAzNy0xNC44MTUtMTUuNzcyeiIgZmlsbD0iIzJmYjY3YyIgc3Ryb2tlPSIjMmZiNjdjIi8+PHBhdGggZD0iTTE1Ljc3MiA3MC4zNzFDNy4wMzcgNzAuMzcxIDAgNjMuNzYzIDAgNTUuNTU2czcuMDM3LTE0LjgxNSAxNS43NzItMTQuODE1aDM4LjgyNmM4LjczNSAwIDE1Ljc3MiA2LjYwNyAxNS43NzIgMTQuODE1cy03LjAzNyAxNC44MTUtMTUuNzcyIDE0LjgxNXoiIGZpbGw9IiMzNmM1ZjEiIHN0cm9rZT0iIzM2YzVmMSIvPjxnIHN0cm9rZS1saW5lam9pbj0ibWl0ZXIiPjxwYXRoIGQ9Ik03Ny43NzggMTMzLjMzM2MwIDguMjA4IDYuNjA3IDE0LjgxNSAxNC44MTUgMTQuODE1czE0LjgxNS02LjYwNyAxNC44MTUtMTQuODE1LTYuNjA3LTE0LjgxNS0xNC44MTUtMTQuODE1SDc3Ljc3OHoiIGZpbGw9IiNlY2IyMmQiIHN0cm9rZT0iI2VjYjIyZCIvPjxwYXRoIGQ9Ik0xMzMuMzM0IDcwLjM3MWgtMTQuODE1VjU1LjU1NmMwLTguMjA3IDYuNjA3LTE0LjgxNSAxNC44MTUtMTQuODE1czE0LjgxNSA2LjYwNyAxNC44MTUgMTQuODE1LTYuNjA3IDE0LjgxNS0xNC44MTUgMTQuODE1eiIgZmlsbD0iIzJmYjY3YyIgc3Ryb2tlPSIjMmZiNjdjIi8+PHBhdGggZD0iTTE0LjgxNSA3Ny43NzhIMjkuNjN2MTQuODE1YzAgOC4yMDctNi42MDcgMTQuODE1LTE0LjgxNSAxNC44MTVTMCAxMDAuOCAwIDkyLjU5M3M2LjYwNy0xNC44MTUgMTQuODE1LTE0LjgxNXoiIGZpbGw9IiNlMDFlNWEiIHN0cm9rZT0iI2UwMWU1YSIvPjxwYXRoIGQ9Ik03MC4zNzEgMTQuODE1VjI5LjYzSDU1LjU1NmMtOC4yMDcgMC0xNC44MTUtNi42MDctMTQuODE1LTE0LjgxNVM0Ny4zNDggMCA1NS41NTYgMHMxNC44MTUgNi42MDcgMTQuODE1IDE0LjgxNXoiIGZpbGw9IiMzNmM1ZjEiIHN0cm9rZT0iIzM2YzVmMSIvPjwvZz48L2c+PC9zeW1ib2w+PC9zdmc+"},"categories":[{"id":6,"name":"Communication"}],"displayName":"Slack","typeVersion":2},{"id":42,"icon":"fa:play","name":"n8n-nodes-base.start","defaults":{"name":"Start","color":"#00e000"},"iconData":{"icon":"play","type":"icon"},"categories":[{"id":9,"name":"Core Nodes"}],"displayName":"Start","typeVersion":1},{"id":235,"icon":"file:wooCommerce.svg","name":"n8n-nodes-base.wooCommerceTrigger","defaults":{"name":"WooCommerce Trigger"},"iconData":{"type":"file","fileBuffer":"data:image/svg+xml;base64,PHN2ZyBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWlkWU1pZCIgdmVyc2lvbj0iMS4xIiB2aWV3Qm94PSIwIC01MCAyNTYgMjUzIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KPHRpdGxlPldvb0NvbW1lcmNlIExvZ288L3RpdGxlPgo8bWV0YWRhdGE+CjxyZGY6UkRGPgo8Y2M6V29yayByZGY6YWJvdXQ9IiI+CjxkYzpmb3JtYXQ+aW1hZ2Uvc3ZnK3htbDwvZGM6Zm9ybWF0Pgo8ZGM6dHlwZSByZGY6cmVzb3VyY2U9Imh0dHA6Ly9wdXJsLm9yZy9kYy9kY21pdHlwZS9TdGlsbEltYWdlIi8+CjxkYzp0aXRsZS8+CjwvY2M6V29yaz4KPC9yZGY6UkRGPgo8L21ldGFkYXRhPgo8cGF0aCBkPSJtMjMuNzU5IDBoMjA4LjM4YzEzLjE4NyAwIDIzLjg2MyAxMC42NzUgMjMuODYzIDIzLjg2M3Y3OS41NDJjMCAxMy4xODctMTAuNjc1IDIzLjg2My0yMy44NjMgMjMuODYzaC03NC43MjdsMTAuMjU3IDI1LjExOC00NS4xMDktMjUuMTE4aC05OC42OTVjLTEzLjE4NyAwLTIzLjg2My0xMC42NzUtMjMuODYzLTIzLjg2M3YtNzkuNTQyYy0wLjEwNDY2LTEzLjA4MyAxMC41NzEtMjMuODYzIDIzLjc1OC0yMy44NjN6IiBmaWxsPSIjN2Y1NGIzIi8+CjxwYXRoIGQ9Im0xNC41NzggMjEuNzVjMS40NTY5LTEuOTc3MiAzLjY0MjMtMy4wMTc5IDYuNTU2MS0zLjIyNiA1LjMwNzMtMC40MTYyNiA4LjMyNTIgMi4wODEzIDkuMDUzNyA3LjQ5MjcgMy4yMjYgMjEuNzUgNi43NjQyIDQwLjE2OSAxMC41MTEgNTUuMjU5bDIyLjc5LTQzLjM5NWMyLjA4MTMtMy45NTQ1IDQuNjgyOS02LjAzNTggNy44MDQ5LTYuMjQzOSA0LjU3ODktMC4zMTIyIDcuMzg4NiAyLjYwMTYgOC41MzMzIDguNzQxNSAyLjYwMTYgMTMuODQxIDUuOTMxNyAyNS42IDkuODg2MiAzNS41OSAyLjcwNTctMjYuNDMzIDcuMjg0Ni00NS40NzYgMTMuNzM3LTU3LjIzNiAxLjU2MS0yLjkxMzggMy44NTA0LTQuMzcwNyA2Ljg2ODMtNC41Nzg5IDIuMzkzNS0wLjIwODEzIDQuNTc4OSAwLjUyMDMzIDYuNTU2MSAyLjA4MTMgMS45NzcyIDEuNTYxIDMuMDE3OSAzLjUzODIgMy4yMjYgNS45MzE3IDAuMTA0MDYgMS44NzMyLTAuMjA4MTMgMy40MzQxLTEuMDQwNyA0Ljk5NTEtNC4wNTg1IDcuNDkyNy03LjM4ODYgMjAuMDg1LTEwLjA5NCAzNy41NjctMi42MDE2IDE2Ljk2My0zLjUzODIgMzAuMTc5LTIuOTEzOCAzOS42NDkgMC4yMDgxMyAyLjYwMTYtMC4yMDgxMyA0Ljg5MTEtMS4yNDg4IDYuODY4My0xLjI0ODggMi4yODk0LTMuMTIyIDMuNTM4Mi01LjUxNTQgMy43NDYzLTIuNzA1NyAwLjIwODEzLTUuNTE1NC0xLjA0MDYtOC4yMjExLTMuODUwNC05LjY3OC05Ljg4NjItMTcuMzc5LTI0LjY2My0yMi45OTgtNDQuMzMyLTYuNzY0MiAxMy4zMi0xMS43NTkgMjMuMzExLTE0Ljk4NSAyOS45NzEtNi4xMzk4IDExLjc1OS0xMS4zNDMgMTcuNzk1LTE1LjcxNCAxOC4xMDctMi44MDk4IDAuMjA4MTMtNS4yMDMzLTIuMTg1NC03LjI4NDYtNy4xODA1LTUuMzA3My0xMy42MzMtMTEuMDMxLTM5Ljk2MS0xNy4xNzEtNzguOTg1LTAuNDE2MjYtMi43MDU3IDAuMjA4MTMtNS4wOTkyIDEuNjY1LTYuOTcyNHptMjIzLjY0IDE2LjMzOGMtMy43NDYzLTYuNTU2MS05LjI2MTgtMTAuNTExLTE2LjY1LTEyLjA3Mi0xLjk3NzItMC40MTYyNi0zLjg1MDQtMC42MjQzOS01LjYxOTUtMC42MjQzOS05Ljk5MDIgMC0xOC4xMDcgNS4yMDMzLTI0LjQ1NSAxNS42MS01LjQxMTQgOC44NDU1LTguMTE3MSAxOC42MjgtOC4xMTcxIDI5LjM0NiAwIDguMDEzIDEuNjY1IDE0Ljg4MSA0Ljk5NTEgMjAuNjA1IDMuNzQ2MyA2LjU1NjEgOS4yNjE4IDEwLjUxMSAxNi42NSAxMi4wNzIgMS45NzcyIDAuNDE2MjYgMy44NTA0IDAuNjI0MzkgNS42MTk1IDAuNjI0MzkgMTAuMDk0IDAgMTguMjExLTUuMjAzMyAyNC40NTUtMTUuNjEgNS40MTE0LTguOTQ5NiA4LjExNzEtMTguNzMyIDguMTE3MS0yOS40NSAwLjEwNDA2LTguMTE3MS0xLjY2NS0xNC44ODEtNC45OTUxLTIwLjUwMXptLTEzLjExMiAyOC44MjZjLTEuNDU2OSA2Ljg2ODMtNC4wNTg1IDExLjk2Ny03LjkwODkgMTUuNDAyLTMuMDE3OSAyLjcwNTctNS44Mjc2IDMuODUwNC04LjQyOTMgMy4zMzAxLTIuNDk3Ni0wLjUyMDMzLTQuNTc4OS0yLjcwNTctNi4xMzk4LTYuNzY0Mi0xLjI0ODgtMy4yMjYtMS44NzMyLTYuNDUyLTEuODczMi05LjQ2OTkgMC0yLjYwMTYgMC4yMDgxMy01LjIwMzMgMC43Mjg0Ni03LjU5NjcgMC45MzY1OS00LjI2NjcgMi43MDU3LTguNDI5MyA1LjUxNTQtMTIuMzg0IDMuNDM0MS01LjA5OTIgNy4wNzY0LTcuMTgwNSAxMC44MjMtNi40NTIgMi40OTc2IDAuNTIwMzMgNC41Nzg5IDIuNzA1NyA2LjEzOTggNi43NjQyIDEuMjQ4OCAzLjIyNiAxLjg3MzIgNi40NTIgMS44NzMyIDkuNDY5OSAwIDIuNzA1Ny0wLjIwODEzIDUuMzA3My0wLjcyODQ2IDcuNzAwOHptLTUyLjAzMy0yOC44MjZjLTMuNzQ2My02LjU1NjEtOS4zNjU5LTEwLjUxMS0xNi42NS0xMi4wNzItMS45NzcyLTAuNDE2MjYtMy44NTA0LTAuNjI0MzktNS42MTk1LTAuNjI0MzktOS45OTAyIDAtMTguMTA3IDUuMjAzMy0yNC40NTUgMTUuNjEtNS40MTE0IDguODQ1NS04LjExNzEgMTguNjI4LTguMTE3MSAyOS4zNDYgMCA4LjAxMyAxLjY2NSAxNC44ODEgNC45OTUxIDIwLjYwNSAzLjc0NjMgNi41NTYxIDkuMjYxOCAxMC41MTEgMTYuNjUgMTIuMDcyIDEuOTc3MiAwLjQxNjI2IDMuODUwNCAwLjYyNDM5IDUuNjE5NSAwLjYyNDM5IDEwLjA5NCAwIDE4LjIxMS01LjIwMzMgMjQuNDU1LTE1LjYxIDUuNDExNC04Ljk0OTYgOC4xMTcxLTE4LjczMiA4LjExNzEtMjkuNDUgMC04LjExNzEtMS42NjUtMTQuODgxLTQuOTk1MS0yMC41MDF6bS0xMy4yMTYgMjguODI2Yy0xLjQ1NjkgNi44NjgzLTQuMDU4NSAxMS45NjctNy45MDg5IDE1LjQwMi0zLjAxNzkgMi43MDU3LTUuODI3NiAzLjg1MDQtOC40MjkzIDMuMzMwMS0yLjQ5NzYtMC41MjAzMy00LjU3ODktMi43MDU3LTYuMTM5OC02Ljc2NDItMS4yNDg4LTMuMjI2LTEuODczMi02LjQ1Mi0xLjg3MzItOS40Njk5IDAtMi42MDE2IDAuMjA4MTMtNS4yMDMzIDAuNzI4NDYtNy41OTY3IDAuOTM2NTgtNC4yNjY3IDIuNzA1Ny04LjQyOTMgNS41MTU0LTEyLjM4NCAzLjQzNDEtNS4wOTkyIDcuMDc2NC03LjE4MDUgMTAuODIzLTYuNDUyIDIuNDk3NiAwLjUyMDMzIDQuNTc4OSAyLjcwNTcgNi4xMzk4IDYuNzY0MiAxLjI0ODggMy4yMjYgMS44NzMyIDYuNDUyIDEuODczMiA5LjQ2OTkgMC4xMDQwNiAyLjcwNTctMC4yMDgxMyA1LjMwNzMtMC43Mjg0NiA3LjcwMDh6IiBmaWxsPSIjZmZmIi8+Cjwvc3ZnPgo="},"categories":[{"id":2,"name":"Sales"}],"displayName":"WooCommerce Trigger","typeVersion":1}],"categories":[{"id":2,"name":"Sales"}],"image":[]},{"id":1765,"name":"Get Slack notifications when new product published on WooCommerce","views":79,"recentViews":9577,"totalViews":79,"createdAt":"2022-08-12T12:36:53.409Z","description":"This workflow let's a bot in Slack notify a specific channel when a new product in WooCommerce is published and live on the site. \n\n## Prerequisites\n\n[WooCommerce](https://docs.n8n.io/integrations/builtin/trigger-nodes/n8n-nodes-base.woocommercetrigger/) account\n[Slack](https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.slack/) and a [Slack bot](https://slack.com/help/articles/115005265703-Create-a-bot-for-your-workspace)\n\n## How it works\n\n1. Listen for WooCommerce product creation\n2. If permalink starts with https://[your-url-here].com/product/\n3. Slack bot notifies channel that a new product has been added. \n\nPlease note, you must update the URL in the IF node to match your url. If your WooCommerce doesn't use the slug /product/, that will need to be updated too. \n","workflow":{"id":1016,"name":"Woocommerce to slack: notify new product created","tags":[{"id":"5","name":"FVF","createdAt":"2022-07-30T07:43:44.795Z","updatedAt":"2022-07-30T07:43:44.795Z"}],"nodes":[{"name":"If URL has /product/","type":"n8n-nodes-base.if","position":[640,300],"parameters":{"conditions":{"string":[{"value1":"={{$json[\"permalink\"]}}","value2":"https://[add-your-url-here]/product/","operation":"startsWith"}]}},"typeVersion":1},{"name":"Send message to slack","type":"n8n-nodes-base.slack","position":[920,260],"parameters":{"text":":new: A new product has been added! :new:","channel":"newproducts","blocksUi":{"blocksValues":[]},"attachments":[{"color":"#66FF00","fields":{"item":[{"short":false,"title":"Name","value":"={{$json[\"name\"]}}"},{"short":true,"title":"Price","value":"={{$json[\"regular_price\"]}}"},{"short":true,"title":"Sale Price","value":"={{$json[\"sale_price\"]}}"},{"short":false,"title":"Link","value":"={{$json[\"permalink\"]}}"}]},"footer":"=Added: {{$json[\"date_created\"]}}"}],"otherOptions":{}},"credentials":{"slackApi":{"id":"21","name":"FVF bot"}},"typeVersion":1},{"name":"On product creation","type":"n8n-nodes-base.wooCommerceTrigger","position":[460,300],"webhookId":"267c4855-6227-4d33-867e-74600097473e","parameters":{"event":"product.created"},"credentials":{"wooCommerceApi":{"id":"20","name":"WooCommerce account FVF"}},"typeVersion":1}],"active":true,"settings":{},"connections":{"On product creation":{"main":[[{"node":"If URL has /product/","type":"main","index":0}]]},"If URL has /product/":{"main":[[{"node":"Send message to slack","type":"main","index":0}]]}}},"workflowInfo":{"nodeCount":4,"nodeTypes":{"n8n-nodes-base.if":{"count":1},"n8n-nodes-base.slack":{"count":1},"n8n-nodes-base.wooCommerceTrigger":{"count":1}}},"user":{"username":"n8n-team"},"nodes":[{"id":20,"icon":"fa:map-signs","name":"n8n-nodes-base.if","defaults":{"name":"IF","color":"#408000"},"iconData":{"icon":"map-signs","type":"icon"},"categories":[{"id":9,"name":"Core Nodes"}],"displayName":"IF","typeVersion":1},{"id":40,"icon":"file:slack.svg","name":"n8n-nodes-base.slack","defaults":{"name":"Slack"},"iconData":{"type":"file","fileBuffer":"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgMTUwLjg1MiAxNTAuODUyIiBmaWxsPSIjZmZmIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48dXNlIHhsaW5rOmhyZWY9IiNhIiB4PSIuOTI2IiB5PSIuOTI2Ii8+PHN5bWJvbCBpZD0iYSIgb3ZlcmZsb3c9InZpc2libGUiPjxnIHN0cm9rZS13aWR0aD0iMS44NTIiPjxwYXRoIGQ9Ik00MC43NDEgOTMuNTVjMC04LjczNSA2LjYwNy0xNS43NzIgMTQuODE1LTE1Ljc3MnMxNC44MTUgNy4wMzcgMTQuODE1IDE1Ljc3MnYzOC44MjRjMCA4LjczNy02LjYwNyAxNS43NzQtMTQuODE1IDE1Ljc3NHMtMTQuODE1LTcuMDM3LTE0LjgxNS0xNS43NzJ6IiBmaWxsPSIjZTAxZTVhIiBzdHJva2U9IiNlMDFlNWEiLz48cGF0aCBkPSJNOTMuNTUgMTA3LjQwOGMtOC43MzUgMC0xNS43NzItNi42MDctMTUuNzcyLTE0LjgxNXM3LjAzNy0xNC44MTUgMTUuNzcyLTE0LjgxNWgzOC44MjZjOC43MzUgMCAxNS43NzIgNi42MDcgMTUuNzcyIDE0LjgxNXMtNy4wMzcgMTQuODE1LTE1Ljc3MiAxNC44MTV6IiBmaWxsPSIjZWNiMjJkIiBzdHJva2U9IiNlY2IyMmQiLz48cGF0aCBkPSJNNzcuNzc4IDE1Ljc3MkM3Ny43NzggNy4wMzcgODQuMzg1IDAgOTIuNTkzIDBzMTQuODE1IDcuMDM3IDE0LjgxNSAxNS43NzJ2MzguODI2YzAgOC43MzUtNi42MDcgMTUuNzcyLTE0LjgxNSAxNS43NzJzLTE0LjgxNS03LjAzNy0xNC44MTUtMTUuNzcyeiIgZmlsbD0iIzJmYjY3YyIgc3Ryb2tlPSIjMmZiNjdjIi8+PHBhdGggZD0iTTE1Ljc3MiA3MC4zNzFDNy4wMzcgNzAuMzcxIDAgNjMuNzYzIDAgNTUuNTU2czcuMDM3LTE0LjgxNSAxNS43NzItMTQuODE1aDM4LjgyNmM4LjczNSAwIDE1Ljc3MiA2LjYwNyAxNS43NzIgMTQuODE1cy03LjAzNyAxNC44MTUtMTUuNzcyIDE0LjgxNXoiIGZpbGw9IiMzNmM1ZjEiIHN0cm9rZT0iIzM2YzVmMSIvPjxnIHN0cm9rZS1saW5lam9pbj0ibWl0ZXIiPjxwYXRoIGQ9Ik03Ny43NzggMTMzLjMzM2MwIDguMjA4IDYuNjA3IDE0LjgxNSAxNC44MTUgMTQuODE1czE0LjgxNS02LjYwNyAxNC44MTUtMTQuODE1LTYuNjA3LTE0LjgxNS0xNC44MTUtMTQuODE1SDc3Ljc3OHoiIGZpbGw9IiNlY2IyMmQiIHN0cm9rZT0iI2VjYjIyZCIvPjxwYXRoIGQ9Ik0xMzMuMzM0IDcwLjM3MWgtMTQuODE1VjU1LjU1NmMwLTguMjA3IDYuNjA3LTE0LjgxNSAxNC44MTUtMTQuODE1czE0LjgxNSA2LjYwNyAxNC44MTUgMTQuODE1LTYuNjA3IDE0LjgxNS0xNC44MTUgMTQuODE1eiIgZmlsbD0iIzJmYjY3YyIgc3Ryb2tlPSIjMmZiNjdjIi8+PHBhdGggZD0iTTE0LjgxNSA3Ny43NzhIMjkuNjN2MTQuODE1YzAgOC4yMDctNi42MDcgMTQuODE1LTE0LjgxNSAxNC44MTVTMCAxMDAuOCAwIDkyLjU5M3M2LjYwNy0xNC44MTUgMTQuODE1LTE0LjgxNXoiIGZpbGw9IiNlMDFlNWEiIHN0cm9rZT0iI2UwMWU1YSIvPjxwYXRoIGQ9Ik03MC4zNzEgMTQuODE1VjI5LjYzSDU1LjU1NmMtOC4yMDcgMC0xNC44MTUtNi42MDctMTQuODE1LTE0LjgxNVM0Ny4zNDggMCA1NS41NTYgMHMxNC44MTUgNi42MDcgMTQuODE1IDE0LjgxNXoiIGZpbGw9IiMzNmM1ZjEiIHN0cm9rZT0iIzM2YzVmMSIvPjwvZz48L2c+PC9zeW1ib2w+PC9zdmc+"},"categories":[{"id":6,"name":"Communication"}],"displayName":"Slack","typeVersion":2},{"id":42,"icon":"fa:play","name":"n8n-nodes-base.start","defaults":{"name":"Start","color":"#00e000"},"iconData":{"icon":"play","type":"icon"},"categories":[{"id":9,"name":"Core Nodes"}],"displayName":"Start","typeVersion":1},{"id":235,"icon":"file:wooCommerce.svg","name":"n8n-nodes-base.wooCommerceTrigger","defaults":{"name":"WooCommerce Trigger"},"iconData":{"type":"file","fileBuffer":"data:image/svg+xml;base64,PHN2ZyBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWlkWU1pZCIgdmVyc2lvbj0iMS4xIiB2aWV3Qm94PSIwIC01MCAyNTYgMjUzIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KPHRpdGxlPldvb0NvbW1lcmNlIExvZ288L3RpdGxlPgo8bWV0YWRhdGE+CjxyZGY6UkRGPgo8Y2M6V29yayByZGY6YWJvdXQ9IiI+CjxkYzpmb3JtYXQ+aW1hZ2Uvc3ZnK3htbDwvZGM6Zm9ybWF0Pgo8ZGM6dHlwZSByZGY6cmVzb3VyY2U9Imh0dHA6Ly9wdXJsLm9yZy9kYy9kY21pdHlwZS9TdGlsbEltYWdlIi8+CjxkYzp0aXRsZS8+CjwvY2M6V29yaz4KPC9yZGY6UkRGPgo8L21ldGFkYXRhPgo8cGF0aCBkPSJtMjMuNzU5IDBoMjA4LjM4YzEzLjE4NyAwIDIzLjg2MyAxMC42NzUgMjMuODYzIDIzLjg2M3Y3OS41NDJjMCAxMy4xODctMTAuNjc1IDIzLjg2My0yMy44NjMgMjMuODYzaC03NC43MjdsMTAuMjU3IDI1LjExOC00NS4xMDktMjUuMTE4aC05OC42OTVjLTEzLjE4NyAwLTIzLjg2My0xMC42NzUtMjMuODYzLTIzLjg2M3YtNzkuNTQyYy0wLjEwNDY2LTEzLjA4MyAxMC41NzEtMjMuODYzIDIzLjc1OC0yMy44NjN6IiBmaWxsPSIjN2Y1NGIzIi8+CjxwYXRoIGQ9Im0xNC41NzggMjEuNzVjMS40NTY5LTEuOTc3MiAzLjY0MjMtMy4wMTc5IDYuNTU2MS0zLjIyNiA1LjMwNzMtMC40MTYyNiA4LjMyNTIgMi4wODEzIDkuMDUzNyA3LjQ5MjcgMy4yMjYgMjEuNzUgNi43NjQyIDQwLjE2OSAxMC41MTEgNTUuMjU5bDIyLjc5LTQzLjM5NWMyLjA4MTMtMy45NTQ1IDQuNjgyOS02LjAzNTggNy44MDQ5LTYuMjQzOSA0LjU3ODktMC4zMTIyIDcuMzg4NiAyLjYwMTYgOC41MzMzIDguNzQxNSAyLjYwMTYgMTMuODQxIDUuOTMxNyAyNS42IDkuODg2MiAzNS41OSAyLjcwNTctMjYuNDMzIDcuMjg0Ni00NS40NzYgMTMuNzM3LTU3LjIzNiAxLjU2MS0yLjkxMzggMy44NTA0LTQuMzcwNyA2Ljg2ODMtNC41Nzg5IDIuMzkzNS0wLjIwODEzIDQuNTc4OSAwLjUyMDMzIDYuNTU2MSAyLjA4MTMgMS45NzcyIDEuNTYxIDMuMDE3OSAzLjUzODIgMy4yMjYgNS45MzE3IDAuMTA0MDYgMS44NzMyLTAuMjA4MTMgMy40MzQxLTEuMDQwNyA0Ljk5NTEtNC4wNTg1IDcuNDkyNy03LjM4ODYgMjAuMDg1LTEwLjA5NCAzNy41NjctMi42MDE2IDE2Ljk2My0zLjUzODIgMzAuMTc5LTIuOTEzOCAzOS42NDkgMC4yMDgxMyAyLjYwMTYtMC4yMDgxMyA0Ljg5MTEtMS4yNDg4IDYuODY4My0xLjI0ODggMi4yODk0LTMuMTIyIDMuNTM4Mi01LjUxNTQgMy43NDYzLTIuNzA1NyAwLjIwODEzLTUuNTE1NC0xLjA0MDYtOC4yMjExLTMuODUwNC05LjY3OC05Ljg4NjItMTcuMzc5LTI0LjY2My0yMi45OTgtNDQuMzMyLTYuNzY0MiAxMy4zMi0xMS43NTkgMjMuMzExLTE0Ljk4NSAyOS45NzEtNi4xMzk4IDExLjc1OS0xMS4zNDMgMTcuNzk1LTE1LjcxNCAxOC4xMDctMi44MDk4IDAuMjA4MTMtNS4yMDMzLTIuMTg1NC03LjI4NDYtNy4xODA1LTUuMzA3My0xMy42MzMtMTEuMDMxLTM5Ljk2MS0xNy4xNzEtNzguOTg1LTAuNDE2MjYtMi43MDU3IDAuMjA4MTMtNS4wOTkyIDEuNjY1LTYuOTcyNHptMjIzLjY0IDE2LjMzOGMtMy43NDYzLTYuNTU2MS05LjI2MTgtMTAuNTExLTE2LjY1LTEyLjA3Mi0xLjk3NzItMC40MTYyNi0zLjg1MDQtMC42MjQzOS01LjYxOTUtMC42MjQzOS05Ljk5MDIgMC0xOC4xMDcgNS4yMDMzLTI0LjQ1NSAxNS42MS01LjQxMTQgOC44NDU1LTguMTE3MSAxOC42MjgtOC4xMTcxIDI5LjM0NiAwIDguMDEzIDEuNjY1IDE0Ljg4MSA0Ljk5NTEgMjAuNjA1IDMuNzQ2MyA2LjU1NjEgOS4yNjE4IDEwLjUxMSAxNi42NSAxMi4wNzIgMS45NzcyIDAuNDE2MjYgMy44NTA0IDAuNjI0MzkgNS42MTk1IDAuNjI0MzkgMTAuMDk0IDAgMTguMjExLTUuMjAzMyAyNC40NTUtMTUuNjEgNS40MTE0LTguOTQ5NiA4LjExNzEtMTguNzMyIDguMTE3MS0yOS40NSAwLjEwNDA2LTguMTE3MS0xLjY2NS0xNC44ODEtNC45OTUxLTIwLjUwMXptLTEzLjExMiAyOC44MjZjLTEuNDU2OSA2Ljg2ODMtNC4wNTg1IDExLjk2Ny03LjkwODkgMTUuNDAyLTMuMDE3OSAyLjcwNTctNS44Mjc2IDMuODUwNC04LjQyOTMgMy4zMzAxLTIuNDk3Ni0wLjUyMDMzLTQuNTc4OS0yLjcwNTctNi4xMzk4LTYuNzY0Mi0xLjI0ODgtMy4yMjYtMS44NzMyLTYuNDUyLTEuODczMi05LjQ2OTkgMC0yLjYwMTYgMC4yMDgxMy01LjIwMzMgMC43Mjg0Ni03LjU5NjcgMC45MzY1OS00LjI2NjcgMi43MDU3LTguNDI5MyA1LjUxNTQtMTIuMzg0IDMuNDM0MS01LjA5OTIgNy4wNzY0LTcuMTgwNSAxMC44MjMtNi40NTIgMi40OTc2IDAuNTIwMzMgNC41Nzg5IDIuNzA1NyA2LjEzOTggNi43NjQyIDEuMjQ4OCAzLjIyNiAxLjg3MzIgNi40NTIgMS44NzMyIDkuNDY5OSAwIDIuNzA1Ny0wLjIwODEzIDUuMzA3My0wLjcyODQ2IDcuNzAwOHptLTUyLjAzMy0yOC44MjZjLTMuNzQ2My02LjU1NjEtOS4zNjU5LTEwLjUxMS0xNi42NS0xMi4wNzItMS45NzcyLTAuNDE2MjYtMy44NTA0LTAuNjI0MzktNS42MTk1LTAuNjI0MzktOS45OTAyIDAtMTguMTA3IDUuMjAzMy0yNC40NTUgMTUuNjEtNS40MTE0IDguODQ1NS04LjExNzEgMTguNjI4LTguMTE3MSAyOS4zNDYgMCA4LjAxMyAxLjY2NSAxNC44ODEgNC45OTUxIDIwLjYwNSAzLjc0NjMgNi41NTYxIDkuMjYxOCAxMC41MTEgMTYuNjUgMTIuMDcyIDEuOTc3MiAwLjQxNjI2IDMuODUwNCAwLjYyNDM5IDUuNjE5NSAwLjYyNDM5IDEwLjA5NCAwIDE4LjIxMS01LjIwMzMgMjQuNDU1LTE1LjYxIDUuNDExNC04Ljk0OTYgOC4xMTcxLTE4LjczMiA4LjExNzEtMjkuNDUgMC04LjExNzEtMS42NjUtMTQuODgxLTQuOTk1MS0yMC41MDF6bS0xMy4yMTYgMjguODI2Yy0xLjQ1NjkgNi44NjgzLTQuMDU4NSAxMS45NjctNy45MDg5IDE1LjQwMi0zLjAxNzkgMi43MDU3LTUuODI3NiAzLjg1MDQtOC40MjkzIDMuMzMwMS0yLjQ5NzYtMC41MjAzMy00LjU3ODktMi43MDU3LTYuMTM5OC02Ljc2NDItMS4yNDg4LTMuMjI2LTEuODczMi02LjQ1Mi0xLjg3MzItOS40Njk5IDAtMi42MDE2IDAuMjA4MTMtNS4yMDMzIDAuNzI4NDYtNy41OTY3IDAuOTM2NTgtNC4yNjY3IDIuNzA1Ny04LjQyOTMgNS41MTU0LTEyLjM4NCAzLjQzNDEtNS4wOTkyIDcuMDc2NC03LjE4MDUgMTAuODIzLTYuNDUyIDIuNDk3NiAwLjUyMDMzIDQuNTc4OSAyLjcwNTcgNi4xMzk4IDYuNzY0MiAxLjI0ODggMy4yMjYgMS44NzMyIDYuNDUyIDEuODczMiA5LjQ2OTkgMC4xMDQwNiAyLjcwNTctMC4yMDgxMyA1LjMwNzMtMC43Mjg0NiA3LjcwMDh6IiBmaWxsPSIjZmZmIi8+Cjwvc3ZnPgo="},"categories":[{"id":2,"name":"Sales"}],"displayName":"WooCommerce Trigger","typeVersion":1}],"categories":[{"id":2,"name":"Sales"}],"image":[]},{"id":1460,"name":"Notify on Slack when refund is registered in WooCommerce","views":85,"recentViews":9541,"totalViews":85,"createdAt":"2022-02-17T15:02:58.662Z","description":"This workflow uses a WooCommerce trigger that will run when an order has been updated and the status is refunded.\n\nIf the value of this is over 100 it will post it to a Slack channel.\n\nTo use this workflow you will need to set the credentials to use for the WooCommerce and Slack nodes, You will also need to pick a channel to post the message to.","workflow":{"id":82,"name":"New WooCommerce refund to Slack","nodes":[{"name":"Order Updated","type":"n8n-nodes-base.wooCommerceTrigger","position":[320,500],"webhookId":"f7736be3-e978-4a17-b936-7ce9f8ccdb72","parameters":{"event":"order.updated"},"credentials":{"wooCommerceApi":{"id":"48","name":"WooCommerce account"}},"typeVersion":1},{"name":"If Refund and Over 100","type":"n8n-nodes-base.if","position":[540,500],"parameters":{"conditions":{"number":[{"value1":"={{$json[\"total\"]}}","value2":100,"operation":"largerEqual"}],"string":[{"value1":"={{$json[\"status\"]}}","value2":"refunded"}]}},"typeVersion":1},{"name":"Send to Slack","type":"n8n-nodes-base.slack","position":[780,480],"parameters":{"text":":x: A refund has been issued :x:","channel":"woo-commerce","blocksUi":{"blocksValues":[]},"attachments":[{"color":"#FF0000","fields":{"item":[{"short":true,"title":"Order ID","value":"={{$json[\"id\"]}}"},{"short":true,"title":"Status","value":"={{$json[\"status\"]}}"},{"short":true,"title":"Total","value":"={{$json[\"currency_symbol\"]}}{{$json[\"total\"]}}"}]},"footer":"=*Order updated:* {{$json[\"date_modified\"]}}"}],"otherOptions":{}},"credentials":{"slackApi":{"id":"53","name":"Slack Access Token"}},"typeVersion":1}],"active":false,"settings":{"saveManualExecutions":true,"saveExecutionProgress":true,"saveDataSuccessExecution":"all"},"connections":{"Order Updated":{"main":[[{"node":"If Refund and Over 100","type":"main","index":0}]]},"If Refund and Over 100":{"main":[[{"node":"Send to Slack","type":"main","index":0}],[]]}}},"workflowInfo":{"nodeCount":4,"nodeTypes":{"n8n-nodes-base.if":{"count":1},"n8n-nodes-base.slack":{"count":1},"n8n-nodes-base.wooCommerceTrigger":{"count":1}}},"user":{"username":"jon-n8n"},"nodes":[{"id":20,"icon":"fa:map-signs","name":"n8n-nodes-base.if","defaults":{"name":"IF","color":"#408000"},"iconData":{"icon":"map-signs","type":"icon"},"categories":[{"id":9,"name":"Core Nodes"}],"displayName":"IF","typeVersion":1},{"id":40,"icon":"file:slack.svg","name":"n8n-nodes-base.slack","defaults":{"name":"Slack"},"iconData":{"type":"file","fileBuffer":"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgMTUwLjg1MiAxNTAuODUyIiBmaWxsPSIjZmZmIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48dXNlIHhsaW5rOmhyZWY9IiNhIiB4PSIuOTI2IiB5PSIuOTI2Ii8+PHN5bWJvbCBpZD0iYSIgb3ZlcmZsb3c9InZpc2libGUiPjxnIHN0cm9rZS13aWR0aD0iMS44NTIiPjxwYXRoIGQ9Ik00MC43NDEgOTMuNTVjMC04LjczNSA2LjYwNy0xNS43NzIgMTQuODE1LTE1Ljc3MnMxNC44MTUgNy4wMzcgMTQuODE1IDE1Ljc3MnYzOC44MjRjMCA4LjczNy02LjYwNyAxNS43NzQtMTQuODE1IDE1Ljc3NHMtMTQuODE1LTcuMDM3LTE0LjgxNS0xNS43NzJ6IiBmaWxsPSIjZTAxZTVhIiBzdHJva2U9IiNlMDFlNWEiLz48cGF0aCBkPSJNOTMuNTUgMTA3LjQwOGMtOC43MzUgMC0xNS43NzItNi42MDctMTUuNzcyLTE0LjgxNXM3LjAzNy0xNC44MTUgMTUuNzcyLTE0LjgxNWgzOC44MjZjOC43MzUgMCAxNS43NzIgNi42MDcgMTUuNzcyIDE0LjgxNXMtNy4wMzcgMTQuODE1LTE1Ljc3MiAxNC44MTV6IiBmaWxsPSIjZWNiMjJkIiBzdHJva2U9IiNlY2IyMmQiLz48cGF0aCBkPSJNNzcuNzc4IDE1Ljc3MkM3Ny43NzggNy4wMzcgODQuMzg1IDAgOTIuNTkzIDBzMTQuODE1IDcuMDM3IDE0LjgxNSAxNS43NzJ2MzguODI2YzAgOC43MzUtNi42MDcgMTUuNzcyLTE0LjgxNSAxNS43NzJzLTE0LjgxNS03LjAzNy0xNC44MTUtMTUuNzcyeiIgZmlsbD0iIzJmYjY3YyIgc3Ryb2tlPSIjMmZiNjdjIi8+PHBhdGggZD0iTTE1Ljc3MiA3MC4zNzFDNy4wMzcgNzAuMzcxIDAgNjMuNzYzIDAgNTUuNTU2czcuMDM3LTE0LjgxNSAxNS43NzItMTQuODE1aDM4LjgyNmM4LjczNSAwIDE1Ljc3MiA2LjYwNyAxNS43NzIgMTQuODE1cy03LjAzNyAxNC44MTUtMTUuNzcyIDE0LjgxNXoiIGZpbGw9IiMzNmM1ZjEiIHN0cm9rZT0iIzM2YzVmMSIvPjxnIHN0cm9rZS1saW5lam9pbj0ibWl0ZXIiPjxwYXRoIGQ9Ik03Ny43NzggMTMzLjMzM2MwIDguMjA4IDYuNjA3IDE0LjgxNSAxNC44MTUgMTQuODE1czE0LjgxNS02LjYwNyAxNC44MTUtMTQuODE1LTYuNjA3LTE0LjgxNS0xNC44MTUtMTQuODE1SDc3Ljc3OHoiIGZpbGw9IiNlY2IyMmQiIHN0cm9rZT0iI2VjYjIyZCIvPjxwYXRoIGQ9Ik0xMzMuMzM0IDcwLjM3MWgtMTQuODE1VjU1LjU1NmMwLTguMjA3IDYuNjA3LTE0LjgxNSAxNC44MTUtMTQuODE1czE0LjgxNSA2LjYwNyAxNC44MTUgMTQuODE1LTYuNjA3IDE0LjgxNS0xNC44MTUgMTQuODE1eiIgZmlsbD0iIzJmYjY3YyIgc3Ryb2tlPSIjMmZiNjdjIi8+PHBhdGggZD0iTTE0LjgxNSA3Ny43NzhIMjkuNjN2MTQuODE1YzAgOC4yMDctNi42MDcgMTQuODE1LTE0LjgxNSAxNC44MTVTMCAxMDAuOCAwIDkyLjU5M3M2LjYwNy0xNC44MTUgMTQuODE1LTE0LjgxNXoiIGZpbGw9IiNlMDFlNWEiIHN0cm9rZT0iI2UwMWU1YSIvPjxwYXRoIGQ9Ik03MC4zNzEgMTQuODE1VjI5LjYzSDU1LjU1NmMtOC4yMDcgMC0xNC44MTUtNi42MDctMTQuODE1LTE0LjgxNVM0Ny4zNDggMCA1NS41NTYgMHMxNC44MTUgNi42MDcgMTQuODE1IDE0LjgxNXoiIGZpbGw9IiMzNmM1ZjEiIHN0cm9rZT0iIzM2YzVmMSIvPjwvZz48L2c+PC9zeW1ib2w+PC9zdmc+"},"categories":[{"id":6,"name":"Communication"}],"displayName":"Slack","typeVersion":2},{"id":42,"icon":"fa:play","name":"n8n-nodes-base.start","defaults":{"name":"Start","color":"#00e000"},"iconData":{"icon":"play","type":"icon"},"categories":[{"id":9,"name":"Core Nodes"}],"displayName":"Start","typeVersion":1},{"id":235,"icon":"file:wooCommerce.svg","name":"n8n-nodes-base.wooCommerceTrigger","defaults":{"name":"WooCommerce Trigger"},"iconData":{"type":"file","fileBuffer":"data:image/svg+xml;base64,PHN2ZyBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWlkWU1pZCIgdmVyc2lvbj0iMS4xIiB2aWV3Qm94PSIwIC01MCAyNTYgMjUzIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KPHRpdGxlPldvb0NvbW1lcmNlIExvZ288L3RpdGxlPgo8bWV0YWRhdGE+CjxyZGY6UkRGPgo8Y2M6V29yayByZGY6YWJvdXQ9IiI+CjxkYzpmb3JtYXQ+aW1hZ2Uvc3ZnK3htbDwvZGM6Zm9ybWF0Pgo8ZGM6dHlwZSByZGY6cmVzb3VyY2U9Imh0dHA6Ly9wdXJsLm9yZy9kYy9kY21pdHlwZS9TdGlsbEltYWdlIi8+CjxkYzp0aXRsZS8+CjwvY2M6V29yaz4KPC9yZGY6UkRGPgo8L21ldGFkYXRhPgo8cGF0aCBkPSJtMjMuNzU5IDBoMjA4LjM4YzEzLjE4NyAwIDIzLjg2MyAxMC42NzUgMjMuODYzIDIzLjg2M3Y3OS41NDJjMCAxMy4xODctMTAuNjc1IDIzLjg2My0yMy44NjMgMjMuODYzaC03NC43MjdsMTAuMjU3IDI1LjExOC00NS4xMDktMjUuMTE4aC05OC42OTVjLTEzLjE4NyAwLTIzLjg2My0xMC42NzUtMjMuODYzLTIzLjg2M3YtNzkuNTQyYy0wLjEwNDY2LTEzLjA4MyAxMC41NzEtMjMuODYzIDIzLjc1OC0yMy44NjN6IiBmaWxsPSIjN2Y1NGIzIi8+CjxwYXRoIGQ9Im0xNC41NzggMjEuNzVjMS40NTY5LTEuOTc3MiAzLjY0MjMtMy4wMTc5IDYuNTU2MS0zLjIyNiA1LjMwNzMtMC40MTYyNiA4LjMyNTIgMi4wODEzIDkuMDUzNyA3LjQ5MjcgMy4yMjYgMjEuNzUgNi43NjQyIDQwLjE2OSAxMC41MTEgNTUuMjU5bDIyLjc5LTQzLjM5NWMyLjA4MTMtMy45NTQ1IDQuNjgyOS02LjAzNTggNy44MDQ5LTYuMjQzOSA0LjU3ODktMC4zMTIyIDcuMzg4NiAyLjYwMTYgOC41MzMzIDguNzQxNSAyLjYwMTYgMTMuODQxIDUuOTMxNyAyNS42IDkuODg2MiAzNS41OSAyLjcwNTctMjYuNDMzIDcuMjg0Ni00NS40NzYgMTMuNzM3LTU3LjIzNiAxLjU2MS0yLjkxMzggMy44NTA0LTQuMzcwNyA2Ljg2ODMtNC41Nzg5IDIuMzkzNS0wLjIwODEzIDQuNTc4OSAwLjUyMDMzIDYuNTU2MSAyLjA4MTMgMS45NzcyIDEuNTYxIDMuMDE3OSAzLjUzODIgMy4yMjYgNS45MzE3IDAuMTA0MDYgMS44NzMyLTAuMjA4MTMgMy40MzQxLTEuMDQwNyA0Ljk5NTEtNC4wNTg1IDcuNDkyNy03LjM4ODYgMjAuMDg1LTEwLjA5NCAzNy41NjctMi42MDE2IDE2Ljk2My0zLjUzODIgMzAuMTc5LTIuOTEzOCAzOS42NDkgMC4yMDgxMyAyLjYwMTYtMC4yMDgxMyA0Ljg5MTEtMS4yNDg4IDYuODY4My0xLjI0ODggMi4yODk0LTMuMTIyIDMuNTM4Mi01LjUxNTQgMy43NDYzLTIuNzA1NyAwLjIwODEzLTUuNTE1NC0xLjA0MDYtOC4yMjExLTMuODUwNC05LjY3OC05Ljg4NjItMTcuMzc5LTI0LjY2My0yMi45OTgtNDQuMzMyLTYuNzY0MiAxMy4zMi0xMS43NTkgMjMuMzExLTE0Ljk4NSAyOS45NzEtNi4xMzk4IDExLjc1OS0xMS4zNDMgMTcuNzk1LTE1LjcxNCAxOC4xMDctMi44MDk4IDAuMjA4MTMtNS4yMDMzLTIuMTg1NC03LjI4NDYtNy4xODA1LTUuMzA3My0xMy42MzMtMTEuMDMxLTM5Ljk2MS0xNy4xNzEtNzguOTg1LTAuNDE2MjYtMi43MDU3IDAuMjA4MTMtNS4wOTkyIDEuNjY1LTYuOTcyNHptMjIzLjY0IDE2LjMzOGMtMy43NDYzLTYuNTU2MS05LjI2MTgtMTAuNTExLTE2LjY1LTEyLjA3Mi0xLjk3NzItMC40MTYyNi0zLjg1MDQtMC42MjQzOS01LjYxOTUtMC42MjQzOS05Ljk5MDIgMC0xOC4xMDcgNS4yMDMzLTI0LjQ1NSAxNS42MS01LjQxMTQgOC44NDU1LTguMTE3MSAxOC42MjgtOC4xMTcxIDI5LjM0NiAwIDguMDEzIDEuNjY1IDE0Ljg4MSA0Ljk5NTEgMjAuNjA1IDMuNzQ2MyA2LjU1NjEgOS4yNjE4IDEwLjUxMSAxNi42NSAxMi4wNzIgMS45NzcyIDAuNDE2MjYgMy44NTA0IDAuNjI0MzkgNS42MTk1IDAuNjI0MzkgMTAuMDk0IDAgMTguMjExLTUuMjAzMyAyNC40NTUtMTUuNjEgNS40MTE0LTguOTQ5NiA4LjExNzEtMTguNzMyIDguMTE3MS0yOS40NSAwLjEwNDA2LTguMTE3MS0xLjY2NS0xNC44ODEtNC45OTUxLTIwLjUwMXptLTEzLjExMiAyOC44MjZjLTEuNDU2OSA2Ljg2ODMtNC4wNTg1IDExLjk2Ny03LjkwODkgMTUuNDAyLTMuMDE3OSAyLjcwNTctNS44Mjc2IDMuODUwNC04LjQyOTMgMy4zMzAxLTIuNDk3Ni0wLjUyMDMzLTQuNTc4OS0yLjcwNTctNi4xMzk4LTYuNzY0Mi0xLjI0ODgtMy4yMjYtMS44NzMyLTYuNDUyLTEuODczMi05LjQ2OTkgMC0yLjYwMTYgMC4yMDgxMy01LjIwMzMgMC43Mjg0Ni03LjU5NjcgMC45MzY1OS00LjI2NjcgMi43MDU3LTguNDI5MyA1LjUxNTQtMTIuMzg0IDMuNDM0MS01LjA5OTIgNy4wNzY0LTcuMTgwNSAxMC44MjMtNi40NTIgMi40OTc2IDAuNTIwMzMgNC41Nzg5IDIuNzA1NyA2LjEzOTggNi43NjQyIDEuMjQ4OCAzLjIyNiAxLjg3MzIgNi40NTIgMS44NzMyIDkuNDY5OSAwIDIuNzA1Ny0wLjIwODEzIDUuMzA3My0wLjcyODQ2IDcuNzAwOHptLTUyLjAzMy0yOC44MjZjLTMuNzQ2My02LjU1NjEtOS4zNjU5LTEwLjUxMS0xNi42NS0xMi4wNzItMS45NzcyLTAuNDE2MjYtMy44NTA0LTAuNjI0MzktNS42MTk1LTAuNjI0MzktOS45OTAyIDAtMTguMTA3IDUuMjAzMy0yNC40NTUgMTUuNjEtNS40MTE0IDguODQ1NS04LjExNzEgMTguNjI4LTguMTE3MSAyOS4zNDYgMCA4LjAxMyAxLjY2NSAxNC44ODEgNC45OTUxIDIwLjYwNSAzLjc0NjMgNi41NTYxIDkuMjYxOCAxMC41MTEgMTYuNjUgMTIuMDcyIDEuOTc3MiAwLjQxNjI2IDMuODUwNCAwLjYyNDM5IDUuNjE5NSAwLjYyNDM5IDEwLjA5NCAwIDE4LjIxMS01LjIwMzMgMjQuNDU1LTE1LjYxIDUuNDExNC04Ljk0OTYgOC4xMTcxLTE4LjczMiA4LjExNzEtMjkuNDUgMC04LjExNzEtMS42NjUtMTQuODgxLTQuOTk1MS0yMC41MDF6bS0xMy4yMTYgMjguODI2Yy0xLjQ1NjkgNi44NjgzLTQuMDU4NSAxMS45NjctNy45MDg5IDE1LjQwMi0zLjAxNzkgMi43MDU3LTUuODI3NiAzLjg1MDQtOC40MjkzIDMuMzMwMS0yLjQ5NzYtMC41MjAzMy00LjU3ODktMi43MDU3LTYuMTM5OC02Ljc2NDItMS4yNDg4LTMuMjI2LTEuODczMi02LjQ1Mi0xLjg3MzItOS40Njk5IDAtMi42MDE2IDAuMjA4MTMtNS4yMDMzIDAuNzI4NDYtNy41OTY3IDAuOTM2NTgtNC4yNjY3IDIuNzA1Ny04LjQyOTMgNS41MTU0LTEyLjM4NCAzLjQzNDEtNS4wOTkyIDcuMDc2NC03LjE4MDUgMTAuODIzLTYuNDUyIDIuNDk3NiAwLjUyMDMzIDQuNTc4OSAyLjcwNTcgNi4xMzk4IDYuNzY0MiAxLjI0ODggMy4yMjYgMS44NzMyIDYuNDUyIDEuODczMiA5LjQ2OTkgMC4xMDQwNiAyLjcwNTctMC4yMDgxMyA1LjMwNzMtMC43Mjg0NiA3LjcwMDh6IiBmaWxsPSIjZmZmIi8+Cjwvc3ZnPgo="},"categories":[{"id":2,"name":"Sales"}],"displayName":"WooCommerce Trigger","typeVersion":1}],"categories":[{"id":2,"name":"Sales"},{"id":8,"name":"Finance & Accounting"}],"image":[]}],"image":[]}} +{ + "collection": { + "id": 1, + "name": "eCommerce Starter Pack", + "description": "eCommerce operations are complex — but there are many things that you can automate to make your life easier. This collection provides a few ideas to get started.\n\nReduce manual work and the risk of human error by automating processes such as social media promotion of products, updating customer databases, and get notifications for important events.", + "totalViews": 0, + "createdAt": "2022-02-17T12:40:50.498Z", + "nodes": [ + { + "id": 20, + "name": "n8n-nodes-base.if", + "defaults": { + "name": "IF", + "color": "#408000" + }, + "displayName": "IF", + "icon": "fa:map-signs", + "iconData": { + "icon": "map-signs", + "type": "icon" + }, + "typeVersion": 1, + "categories": [ + { + "id": 9, + "name": "Core Nodes" + } + ] + }, + { + "id": 49, + "name": "n8n-nodes-base.telegram", + "defaults": { + "name": "Telegram" + }, + "displayName": "Telegram", + "icon": "file:telegram.svg", + "iconData": { + "type": "file", + "fileBuffer": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgNjYgNjYiIGZpbGw9IiNmZmYiIGZpbGwtcnVsZT0iZXZlbm9kZCIgc3Ryb2tlPSIjMDAwIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjx1c2UgeGxpbms6aHJlZj0iI2EiIHg9Ii41IiB5PSIuNSIvPjxzeW1ib2wgaWQ9ImEiIG92ZXJmbG93PSJ2aXNpYmxlIj48ZyBzdHJva2U9Im5vbmUiIGZpbGwtcnVsZT0ibm9uemVybyI+PHBhdGggZD0iTTAgMzJjMCAxNy42NzMgMTQuMzI3IDMyIDMyIDMyczMyLTE0LjMyNyAzMi0zMlM0OS42NzMgMCAzMiAwIDAgMTQuMzI3IDAgMzIiIGZpbGw9IiMzN2FlZTIiLz48cGF0aCBkPSJNMjEuNjYxIDM0LjMzOGwzLjc5NyAxMC41MDhzLjQ3NS45ODMuOTgzLjk4MyA4LjA2OC03Ljg2NCA4LjA2OC03Ljg2NGw4LjQwNy0xNi4yMzctMjEuMTE5IDkuODk4eiIgZmlsbD0iI2M4ZGFlYSIvPjxwYXRoIGQ9Ik0yNi42OTUgMzcuMDM0bC0uNzI5IDcuNzQ2cy0uMzA1IDIuMzczIDIuMDY4IDBsNC42NDQtNC4yMDMiIGZpbGw9IiNhOWM2ZDgiLz48cGF0aCBkPSJNMjEuNzMgMzQuNzEybC03LjgwOS0yLjU0NXMtLjkzMi0uMzc4LS42MzMtMS4yMzdjLjA2Mi0uMTc3LjE4Ni0uMzI4LjU1OS0uNTg4IDEuNzMxLTEuMjA2IDMyLjAyOC0xMi4wOTYgMzIuMDI4LTEyLjA5NnMuODU2LS4yODggMS4zNjEtLjA5N2MuMjMxLjA4OC4zNzguMTg3LjUwMy41NDguMDQ1LjEzMi4wNzEuNDExLjA2OC42ODktLjAwMy4yMDEtLjAyNy4zODYtLjA0NS42NzgtLjE4NCAyLjk3OC01LjcwNiAyNS4xOTgtNS43MDYgMjUuMTk4cy0uMzMgMS4zLTEuNTE0IDEuMzQ1Yy0uNDMyLjAxNi0uOTU2LS4wNzEtMS41ODItLjYxLTIuMzIzLTEuOTk4LTEwLjM1Mi03LjM5NC0xMi4xMjYtOC41OGEuMzQuMzQgMCAwMS0uMTQ2LS4yMzljLS4wMjUtLjEyNS4xMDgtLjI4LjEwOC0uMjhzMTMuOTgtMTIuNDI3IDE0LjM1Mi0xMy43MzFjLjAyOS0uMTAxLS4wNzktLjE1MS0uMjI2LS4xMDctLjkyOS4zNDItMTcuMDI1IDEwLjUwNi0xOC44MDEgMTEuNjI5LS4xMDQuMDY2LS4zOTUuMDIzLS4zOTUuMDIzIi8+PC9nPjwvc3ltYm9sPjwvc3ZnPg==" + }, + "typeVersion": 1, + "categories": [ + { + "id": 6, + "name": "Communication" + } + ] + }, + { + "id": 107, + "name": "n8n-nodes-base.shopifyTrigger", + "defaults": { + "name": "Shopify Trigger" + }, + "displayName": "Shopify Trigger", + "icon": "file:shopify.svg", + "iconData": { + "type": "file", + "fileBuffer": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgNTggNjYiIGZpbGw9IiNmZmYiIGZpbGwtcnVsZT0iZXZlbm9kZCIgc3Ryb2tlPSIjMDAwIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjx1c2UgeGxpbms6aHJlZj0iI2EiIHg9Ii41IiB5PSIuNSIvPjxzeW1ib2wgaWQ9ImEiIG92ZXJmbG93PSJ2aXNpYmxlIj48ZyBzdHJva2U9Im5vbmUiIGZpbGwtcnVsZT0ibm9uemVybyI+PHBhdGggZD0iTTQ5LjI1NSAxMi40ODRhLjYzMy42MzMgMCAwMC0uNTY0LS41MjdjLS4yMjUtLjAzNy01LjE3LS4zNzYtNS4xNy0uMzc2bC0zLjc3LTMuNzdjLS4zNC0uMzc2LTEuMDkyLS4yNjYtMS4zNzYtLjE4OC0uMDM3IDAtLjc1Mi4yMjUtMS45MjIuNjA1LTEuMTM3LTMuMy0zLjE1LTYuMzA2LTYuNjk2LTYuMzA2aC0uMzAzQzI4LjQzOC42MDUgMjcuMTk0IDAgMjYuMTQ0IDBjLTguMjU2LjAzNy0xMi4yIDEwLjMzMy0xMy40MzQgMTUuNTk0bC01Ljc3IDEuNzdjLTEuNzcuNTY0LTEuODM1LjYwNS0yLjA3MyAyLjI5M0wwIDU3LjE3NSAzNi40NjggNjRsMTkuNzYzLTQuMjZjMC0uMDM3LTYuOTQtNDYuODk3LTYuOTc2LTQ3LjI1NXpNMzQuNDMxIDguODZjLS45MTcuMzAzLTEuOTYzLjYwNS0zLjEuOTQ1di0uNjhhMTUuMDMgMTUuMDMgMCAwMC0uNzUyLTQuOTk5YzEuODQ4LjI4NCAzLjEgMi4zNTcgMy44NDMgNC43MzN6bS02LjA2OC00LjI5OGMuNjAzIDEuNzc4Ljg4MyAzLjY1LjgyNiA1LjUyN3YuMzRsLTYuMzc1IDEuOTYzYzEuMjQ4LTQuNjYgMy41NS02Ljk2MiA1LjU1LTcuODN6bS0yLjQ1LTIuMjkzYTEuOTQgMS45NCAwIDAxMS4wNTUuMzM5Yy0yLjY2IDEuMjM4LTUuNDcyIDQuMzY2LTYuNjc4IDEwLjYyN2wtNS4wNDUgMS41NDZDMTYuNjY4IDEwLjAzIDE5Ljk4OCAyLjI2IDI1LjkxIDIuMjZ6IiBmaWxsPSIjOTViZjQ3Ii8+PHBhdGggZD0iTTQ4LjY5MSAxMS45NTdjLS4yMjUtLjAzNy01LjE3LS4zNzYtNS4xNy0uMzc2bC0zLjc3LTMuNzdhLjc1My43NTMgMCAwMC0uNTI3LS4yMjVMMzYuNDcyIDY0bDE5Ljc2My00LjI2LTYuOTgtNDcuMjE4YS42OC42OCAwIDAwLS41NjQtLjU2NHoiIGZpbGw9IiM1ZThlM2UiLz48cGF0aCBkPSJNMjkuNzU4IDIyLjlsLTIuNDU0IDcuMjQyYTExLjM2IDExLjM2IDAgMDAtNC43NTItMS4xMzNjLTMuODQ4IDAtNC4wMzYgMi40MTItNC4wMzYgMy4wMTggMCAzLjI5OCA4LjYzNiA0LjU2NCA4LjYzNiAxMi4zMzMgMCA2LjEtMy44ODUgMTAuMDMtOS4xIDEwLjAzLTYuMjYgMC05LjQ2Ny0zLjg4NS05LjQ2Ny0zLjg4NWwxLjY2NS01LjU1czMuMjggMi44MyA2LjA3MyAyLjgzYTIuNDcgMi40NyAwIDAwMi41NjQtMi40OWMwLTQuMzQtNy4xLTQuNTI3LTcuMS0xMS42MTggMC01Ljk2MiA0LjI5OC0xMS43NyAxMi45MzQtMTEuNzcgMy4zOTQuMDUgNS4wMTggMSA1LjAxOCAxeiIvPjwvZz48L3N5bWJvbD48L3N2Zz4=" + }, + "typeVersion": 1, + "categories": [ + { + "id": 2, + "name": "Sales" + } + ] + }, + { + "id": 126, + "name": "n8n-nodes-base.mautic", + "defaults": { + "name": "Mautic" + }, + "displayName": "Mautic", + "icon": "file:mautic.svg", + "iconData": { + "type": "file", + "fileBuffer": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAxNi4wLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DQo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB3aWR0aD0iMzQ5Ljc3OXB4IiBoZWlnaHQ9IjM0OS43NzlweCIgdmlld0JveD0iMCAwIDM0OS43NzkgMzQ5Ljc3OSIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgMzQ5Ljc3OSAzNDkuNzc5Ig0KCSB4bWw6c3BhY2U9InByZXNlcnZlIj4NCjxwYXRoIGZpbGw9IiNGRkZGRkYiIGQ9Ik0xNzQuODksMzQ5Ljc3OUM3OC42MTIsMzQ5Ljc3OSwwLDI3MS40NjIsMCwxNzQuODlTNzguNjEyLDAsMTc0Ljg5LDBjMjMuMjYsMCw0NS45MzEsNC40MTcsNjcuMTI5LDEzLjU0Mw0KCWM1Ljg4OSwyLjY1LDguODMzLDkuNDIyLDYuNDc4LDE1LjYwNWMtMi42NDksNS44ODgtOS40MjEsOC44MzMtMTUuNjA0LDYuNDc3Yy0xOC41NDktNy42NTUtMzcuOTgtMTEuNDgyLTU4LjAwMi0xMS40ODINCgljLTgzLjMyMywwLTE1MS4wNDEsNjcuNzE4LTE1MS4wNDEsMTUxLjA0MVM5MS41NjcsMzI2LjIyNSwxNzQuODksMzI2LjIyNWM4My4zMjMsMCwxNTEuMDQxLTY3LjcxOCwxNTEuMDQxLTE1MS4wNDENCgljMC0xNy45Ni0yLjk0NC0zNS4zMzItOS4xMjctNTEuODE5Yy0yLjM1NS02LjE4MywwLjg4My0xMi45NTUsNy4wNjYtMTUuMzFjNi4xODMtMi4zNTUsMTIuOTU0LDAuODgzLDE1LjMxLDcuMDY2DQoJYzcuMDY2LDE5LjEzOCwxMC42LDM5LjQ1MywxMC42LDYwLjA2M0MzNDkuNzc5LDI3MS4xNjcsMjcxLjQ2MiwzNDkuNzc5LDE3NC44OSwzNDkuNzc5Ii8+DQo8Zz4NCgk8cG9seWdvbiBmaWxsPSIjRkRCOTMzIiBwb2ludHM9IjI1MS40NCwxNTYuOTMgMjI0LjM1NCwxODUuMTk0IDIzOS4zNjksMjQ4LjQ5NiAyNzMuNTIyLDI0OC40OTYgCSIvPg0KPC9nPg0KPHBvbHlnb24gZmlsbD0iI0ZEQjkzMyIgcG9pbnRzPSIyNDAuMjUzLDczLjMxMiAyNDkuNjc0LDgyLjczNCAxNzQuODksMTYxLjkzNSAxMTAuOTk5LDk2LjI3NyA3NC4xOTYsMjQ4LjQ5NiAxMDguMzUsMjQ4LjQ5NiANCgkxMjguNjY1LDE2My45OTYgMTc0Ljg5LDIxNC4zNDMgMjczLjgxNywxMDYuNTgzIDI4My4yMzksMTE2LjI5OSAyOTIuNjYsNjMuMDA3ICIvPg0KPC9zdmc+DQo=" + }, + "typeVersion": 1, + "categories": [ + { + "id": 1, + "name": "Marketing & Content" + }, + { + "id": 6, + "name": "Communication" + } + ] + }, + { + "id": 235, + "name": "n8n-nodes-base.wooCommerceTrigger", + "defaults": { + "name": "WooCommerce Trigger" + }, + "displayName": "WooCommerce Trigger", + "icon": "file:wooCommerce.svg", + "iconData": { + "type": "file", + "fileBuffer": "data:image/svg+xml;base64,PHN2ZyBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWlkWU1pZCIgdmVyc2lvbj0iMS4xIiB2aWV3Qm94PSIwIC01MCAyNTYgMjUzIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KPHRpdGxlPldvb0NvbW1lcmNlIExvZ288L3RpdGxlPgo8bWV0YWRhdGE+CjxyZGY6UkRGPgo8Y2M6V29yayByZGY6YWJvdXQ9IiI+CjxkYzpmb3JtYXQ+aW1hZ2Uvc3ZnK3htbDwvZGM6Zm9ybWF0Pgo8ZGM6dHlwZSByZGY6cmVzb3VyY2U9Imh0dHA6Ly9wdXJsLm9yZy9kYy9kY21pdHlwZS9TdGlsbEltYWdlIi8+CjxkYzp0aXRsZS8+CjwvY2M6V29yaz4KPC9yZGY6UkRGPgo8L21ldGFkYXRhPgo8cGF0aCBkPSJtMjMuNzU5IDBoMjA4LjM4YzEzLjE4NyAwIDIzLjg2MyAxMC42NzUgMjMuODYzIDIzLjg2M3Y3OS41NDJjMCAxMy4xODctMTAuNjc1IDIzLjg2My0yMy44NjMgMjMuODYzaC03NC43MjdsMTAuMjU3IDI1LjExOC00NS4xMDktMjUuMTE4aC05OC42OTVjLTEzLjE4NyAwLTIzLjg2My0xMC42NzUtMjMuODYzLTIzLjg2M3YtNzkuNTQyYy0wLjEwNDY2LTEzLjA4MyAxMC41NzEtMjMuODYzIDIzLjc1OC0yMy44NjN6IiBmaWxsPSIjN2Y1NGIzIi8+CjxwYXRoIGQ9Im0xNC41NzggMjEuNzVjMS40NTY5LTEuOTc3MiAzLjY0MjMtMy4wMTc5IDYuNTU2MS0zLjIyNiA1LjMwNzMtMC40MTYyNiA4LjMyNTIgMi4wODEzIDkuMDUzNyA3LjQ5MjcgMy4yMjYgMjEuNzUgNi43NjQyIDQwLjE2OSAxMC41MTEgNTUuMjU5bDIyLjc5LTQzLjM5NWMyLjA4MTMtMy45NTQ1IDQuNjgyOS02LjAzNTggNy44MDQ5LTYuMjQzOSA0LjU3ODktMC4zMTIyIDcuMzg4NiAyLjYwMTYgOC41MzMzIDguNzQxNSAyLjYwMTYgMTMuODQxIDUuOTMxNyAyNS42IDkuODg2MiAzNS41OSAyLjcwNTctMjYuNDMzIDcuMjg0Ni00NS40NzYgMTMuNzM3LTU3LjIzNiAxLjU2MS0yLjkxMzggMy44NTA0LTQuMzcwNyA2Ljg2ODMtNC41Nzg5IDIuMzkzNS0wLjIwODEzIDQuNTc4OSAwLjUyMDMzIDYuNTU2MSAyLjA4MTMgMS45NzcyIDEuNTYxIDMuMDE3OSAzLjUzODIgMy4yMjYgNS45MzE3IDAuMTA0MDYgMS44NzMyLTAuMjA4MTMgMy40MzQxLTEuMDQwNyA0Ljk5NTEtNC4wNTg1IDcuNDkyNy03LjM4ODYgMjAuMDg1LTEwLjA5NCAzNy41NjctMi42MDE2IDE2Ljk2My0zLjUzODIgMzAuMTc5LTIuOTEzOCAzOS42NDkgMC4yMDgxMyAyLjYwMTYtMC4yMDgxMyA0Ljg5MTEtMS4yNDg4IDYuODY4My0xLjI0ODggMi4yODk0LTMuMTIyIDMuNTM4Mi01LjUxNTQgMy43NDYzLTIuNzA1NyAwLjIwODEzLTUuNTE1NC0xLjA0MDYtOC4yMjExLTMuODUwNC05LjY3OC05Ljg4NjItMTcuMzc5LTI0LjY2My0yMi45OTgtNDQuMzMyLTYuNzY0MiAxMy4zMi0xMS43NTkgMjMuMzExLTE0Ljk4NSAyOS45NzEtNi4xMzk4IDExLjc1OS0xMS4zNDMgMTcuNzk1LTE1LjcxNCAxOC4xMDctMi44MDk4IDAuMjA4MTMtNS4yMDMzLTIuMTg1NC03LjI4NDYtNy4xODA1LTUuMzA3My0xMy42MzMtMTEuMDMxLTM5Ljk2MS0xNy4xNzEtNzguOTg1LTAuNDE2MjYtMi43MDU3IDAuMjA4MTMtNS4wOTkyIDEuNjY1LTYuOTcyNHptMjIzLjY0IDE2LjMzOGMtMy43NDYzLTYuNTU2MS05LjI2MTgtMTAuNTExLTE2LjY1LTEyLjA3Mi0xLjk3NzItMC40MTYyNi0zLjg1MDQtMC42MjQzOS01LjYxOTUtMC42MjQzOS05Ljk5MDIgMC0xOC4xMDcgNS4yMDMzLTI0LjQ1NSAxNS42MS01LjQxMTQgOC44NDU1LTguMTE3MSAxOC42MjgtOC4xMTcxIDI5LjM0NiAwIDguMDEzIDEuNjY1IDE0Ljg4MSA0Ljk5NTEgMjAuNjA1IDMuNzQ2MyA2LjU1NjEgOS4yNjE4IDEwLjUxMSAxNi42NSAxMi4wNzIgMS45NzcyIDAuNDE2MjYgMy44NTA0IDAuNjI0MzkgNS42MTk1IDAuNjI0MzkgMTAuMDk0IDAgMTguMjExLTUuMjAzMyAyNC40NTUtMTUuNjEgNS40MTE0LTguOTQ5NiA4LjExNzEtMTguNzMyIDguMTE3MS0yOS40NSAwLjEwNDA2LTguMTE3MS0xLjY2NS0xNC44ODEtNC45OTUxLTIwLjUwMXptLTEzLjExMiAyOC44MjZjLTEuNDU2OSA2Ljg2ODMtNC4wNTg1IDExLjk2Ny03LjkwODkgMTUuNDAyLTMuMDE3OSAyLjcwNTctNS44Mjc2IDMuODUwNC04LjQyOTMgMy4zMzAxLTIuNDk3Ni0wLjUyMDMzLTQuNTc4OS0yLjcwNTctNi4xMzk4LTYuNzY0Mi0xLjI0ODgtMy4yMjYtMS44NzMyLTYuNDUyLTEuODczMi05LjQ2OTkgMC0yLjYwMTYgMC4yMDgxMy01LjIwMzMgMC43Mjg0Ni03LjU5NjcgMC45MzY1OS00LjI2NjcgMi43MDU3LTguNDI5MyA1LjUxNTQtMTIuMzg0IDMuNDM0MS01LjA5OTIgNy4wNzY0LTcuMTgwNSAxMC44MjMtNi40NTIgMi40OTc2IDAuNTIwMzMgNC41Nzg5IDIuNzA1NyA2LjEzOTggNi43NjQyIDEuMjQ4OCAzLjIyNiAxLjg3MzIgNi40NTIgMS44NzMyIDkuNDY5OSAwIDIuNzA1Ny0wLjIwODEzIDUuMzA3My0wLjcyODQ2IDcuNzAwOHptLTUyLjAzMy0yOC44MjZjLTMuNzQ2My02LjU1NjEtOS4zNjU5LTEwLjUxMS0xNi42NS0xMi4wNzItMS45NzcyLTAuNDE2MjYtMy44NTA0LTAuNjI0MzktNS42MTk1LTAuNjI0MzktOS45OTAyIDAtMTguMTA3IDUuMjAzMy0yNC40NTUgMTUuNjEtNS40MTE0IDguODQ1NS04LjExNzEgMTguNjI4LTguMTE3MSAyOS4zNDYgMCA4LjAxMyAxLjY2NSAxNC44ODEgNC45OTUxIDIwLjYwNSAzLjc0NjMgNi41NTYxIDkuMjYxOCAxMC41MTEgMTYuNjUgMTIuMDcyIDEuOTc3MiAwLjQxNjI2IDMuODUwNCAwLjYyNDM5IDUuNjE5NSAwLjYyNDM5IDEwLjA5NCAwIDE4LjIxMS01LjIwMzMgMjQuNDU1LTE1LjYxIDUuNDExNC04Ljk0OTYgOC4xMTcxLTE4LjczMiA4LjExNzEtMjkuNDUgMC04LjExNzEtMS42NjUtMTQuODgxLTQuOTk1MS0yMC41MDF6bS0xMy4yMTYgMjguODI2Yy0xLjQ1NjkgNi44NjgzLTQuMDU4NSAxMS45NjctNy45MDg5IDE1LjQwMi0zLjAxNzkgMi43MDU3LTUuODI3NiAzLjg1MDQtOC40MjkzIDMuMzMwMS0yLjQ5NzYtMC41MjAzMy00LjU3ODktMi43MDU3LTYuMTM5OC02Ljc2NDItMS4yNDg4LTMuMjI2LTEuODczMi02LjQ1Mi0xLjg3MzItOS40Njk5IDAtMi42MDE2IDAuMjA4MTMtNS4yMDMzIDAuNzI4NDYtNy41OTY3IDAuOTM2NTgtNC4yNjY3IDIuNzA1Ny04LjQyOTMgNS41MTU0LTEyLjM4NCAzLjQzNDEtNS4wOTkyIDcuMDc2NC03LjE4MDUgMTAuODIzLTYuNDUyIDIuNDk3NiAwLjUyMDMzIDQuNTc4OSAyLjcwNTcgNi4xMzk4IDYuNzY0MiAxLjI0ODggMy4yMjYgMS44NzMyIDYuNDUyIDEuODczMiA5LjQ2OTkgMC4xMDQwNiAyLjcwNTctMC4yMDgxMyA1LjMwNzMtMC43Mjg0NiA3LjcwMDh6IiBmaWxsPSIjZmZmIi8+Cjwvc3ZnPgo=" + }, + "typeVersion": 1, + "categories": [ + { + "id": 2, + "name": "Sales" + } + ] + }, + { + "id": 325, + "name": "n8n-nodes-base.twitter", + "defaults": { + "name": "X" + }, + "displayName": "X (Formerly Twitter)", + "icon": "file:x.svg", + "iconData": { + "type": "file", + "fileBuffer": "data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMjQgMjQiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTE4LjI0NCAyLjI1aDMuMzA4bC03LjIyNyA4LjI2IDguNTAyIDExLjI0SDE2LjE3bC01LjIxNC02LjgxN0w0Ljk5IDIxLjc1SDEuNjhsNy43My04LjgzNUwxLjI1NCAyLjI1SDguMDhsNC43MTMgNi4yMzF6bS0xLjE2MSAxNy41MmgxLjgzM0w3LjA4NCA0LjEyNkg1LjExN3oiPjwvcGF0aD48L3N2Zz4K" + }, + "typeVersion": 2, + "categories": [ + { + "id": 1, + "name": "Marketing & Content" + } + ] + } + ], + "categories": [ + { + "id": 2, + "name": "Sales" + } + ], + "workflows": [ + { + "id": 1205, + "name": "Promote new Shopify products", + "views": 485, + "recentViews": 9850, + "totalViews": 485, + "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" + } + ] + }, + { + "id": 1456, + "name": "Add new customers from WooCommerce to Mautic", + "views": 333, + "recentViews": 9833, + "totalViews": 333, + "createdAt": "2022-02-17T15:00:40.748Z", + "description": "This workflow uses a WooCommerce trigger that will run when a new customer has been added, It will then add the customer to Mautic.\n\nTo use this workflow you will need to set the credentials to use for the WooCommerce and Mautic nodes.", + "workflow": { + "id": 83, + "name": "New WooCommerce Customer to Mautic", + "nodes": [ + { + "name": "Check for Existing", + "type": "n8n-nodes-base.mautic", + "position": [ + 280, + 480 + ], + "parameters": { + "options": { + "search": "={{$json[\"email\"]}}" + }, + "operation": "getAll", + "authentication": "oAuth2" + }, + "credentials": { + "mauticOAuth2Api": { + "id": "54", + "name": "Mautic account" + } + }, + "typeVersion": 1, + "alwaysOutputData": true + }, + { + "name": "If New", + "type": "n8n-nodes-base.if", + "position": [ + 460, + 480 + ], + "parameters": { + "conditions": { + "string": [ + { + "value1": "={{$json[\"id\"]}}", + "operation": "isEmpty" + } + ] + } + }, + "typeVersion": 1 + }, + { + "name": "Create Contact", + "type": "n8n-nodes-base.mautic", + "position": [ + 680, + 320 + ], + "parameters": { + "email": "={{$node[\"Customer Created\"].json[\"email\"]}}", + "company": "={{$node[\"Customer Created\"].json[\"billing\"][\"company\"]}}", + "options": {}, + "lastName": "={{$node[\"Customer Created\"].json[\"last_name\"]}}", + "firstName": "={{$node[\"Customer Created\"].json[\"first_name\"]}}", + "authentication": "oAuth2", + "additionalFields": {} + }, + "credentials": { + "mauticOAuth2Api": { + "id": "54", + "name": "Mautic account" + } + }, + "typeVersion": 1 + }, + { + "name": "Update Contact", + "type": "n8n-nodes-base.mautic", + "position": [ + 680, + 580 + ], + "parameters": { + "options": {}, + "contactId": "={{$json[\"id\"]}}", + "operation": "update", + "updateFields": { + "lastName": "={{$node[\"Customer Created or Updated\"].json[\"last_name\"]}}", + "firstName": "={{$node[\"Customer Created or Updated\"].json[\"first_name\"]}}" + }, + "authentication": "oAuth2" + }, + "credentials": { + "mauticOAuth2Api": { + "id": "54", + "name": "Mautic account" + } + }, + "typeVersion": 1 + }, + { + "name": "Customer Created or Updated", + "type": "n8n-nodes-base.wooCommerceTrigger", + "position": [ + 100, + 480 + ], + "webhookId": "5d89e322-a5e0-4cce-9eab-185e8375175b", + "parameters": { + "event": "customer.updated" + }, + "credentials": { + "wooCommerceApi": { + "id": "48", + "name": "WooCommerce account" + } + }, + "typeVersion": 1 + } + ], + "active": false, + "settings": { + "saveManualExecutions": true, + "saveExecutionProgress": true, + "saveDataSuccessExecution": "all" + }, + "connections": { + "If New": { + "main": [ + [ + { + "node": "Create Contact", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Update Contact", + "type": "main", + "index": 0 + } + ] + ] + }, + "Check for Existing": { + "main": [ + [ + { + "node": "If New", + "type": "main", + "index": 0 + } + ] + ] + }, + "Customer Created or Updated": { + "main": [ + [ + { + "node": "Check for Existing", + "type": "main", + "index": 0 + } + ] + ] + } + } + }, + "workflowInfo": { + "nodeCount": 6, + "nodeTypes": { + "n8n-nodes-base.if": { + "count": 1 + }, + "n8n-nodes-base.mautic": { + "count": 3 + }, + "n8n-nodes-base.wooCommerceTrigger": { + "count": 1 + } + } + }, + "user": { + "username": "jon-n8n" + }, + "nodes": [ + { + "id": 20, + "icon": "fa:map-signs", + "name": "n8n-nodes-base.if", + "defaults": { + "name": "IF", + "color": "#408000" + }, + "iconData": { + "icon": "map-signs", + "type": "icon" + }, + "categories": [ + { + "id": 9, + "name": "Core Nodes" + } + ], + "displayName": "IF", + "typeVersion": 1 + }, + { + "id": 42, + "icon": "fa:play", + "name": "n8n-nodes-base.start", + "defaults": { + "name": "Start", + "color": "#00e000" + }, + "iconData": { + "icon": "play", + "type": "icon" + }, + "categories": [ + { + "id": 9, + "name": "Core Nodes" + } + ], + "displayName": "Start", + "typeVersion": 1 + }, + { + "id": 126, + "icon": "file:mautic.svg", + "name": "n8n-nodes-base.mautic", + "defaults": { + "name": "Mautic" + }, + "iconData": { + "type": "file", + "fileBuffer": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAxNi4wLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DQo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB3aWR0aD0iMzQ5Ljc3OXB4IiBoZWlnaHQ9IjM0OS43NzlweCIgdmlld0JveD0iMCAwIDM0OS43NzkgMzQ5Ljc3OSIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgMzQ5Ljc3OSAzNDkuNzc5Ig0KCSB4bWw6c3BhY2U9InByZXNlcnZlIj4NCjxwYXRoIGZpbGw9IiNGRkZGRkYiIGQ9Ik0xNzQuODksMzQ5Ljc3OUM3OC42MTIsMzQ5Ljc3OSwwLDI3MS40NjIsMCwxNzQuODlTNzguNjEyLDAsMTc0Ljg5LDBjMjMuMjYsMCw0NS45MzEsNC40MTcsNjcuMTI5LDEzLjU0Mw0KCWM1Ljg4OSwyLjY1LDguODMzLDkuNDIyLDYuNDc4LDE1LjYwNWMtMi42NDksNS44ODgtOS40MjEsOC44MzMtMTUuNjA0LDYuNDc3Yy0xOC41NDktNy42NTUtMzcuOTgtMTEuNDgyLTU4LjAwMi0xMS40ODINCgljLTgzLjMyMywwLTE1MS4wNDEsNjcuNzE4LTE1MS4wNDEsMTUxLjA0MVM5MS41NjcsMzI2LjIyNSwxNzQuODksMzI2LjIyNWM4My4zMjMsMCwxNTEuMDQxLTY3LjcxOCwxNTEuMDQxLTE1MS4wNDENCgljMC0xNy45Ni0yLjk0NC0zNS4zMzItOS4xMjctNTEuODE5Yy0yLjM1NS02LjE4MywwLjg4My0xMi45NTUsNy4wNjYtMTUuMzFjNi4xODMtMi4zNTUsMTIuOTU0LDAuODgzLDE1LjMxLDcuMDY2DQoJYzcuMDY2LDE5LjEzOCwxMC42LDM5LjQ1MywxMC42LDYwLjA2M0MzNDkuNzc5LDI3MS4xNjcsMjcxLjQ2MiwzNDkuNzc5LDE3NC44OSwzNDkuNzc5Ii8+DQo8Zz4NCgk8cG9seWdvbiBmaWxsPSIjRkRCOTMzIiBwb2ludHM9IjI1MS40NCwxNTYuOTMgMjI0LjM1NCwxODUuMTk0IDIzOS4zNjksMjQ4LjQ5NiAyNzMuNTIyLDI0OC40OTYgCSIvPg0KPC9nPg0KPHBvbHlnb24gZmlsbD0iI0ZEQjkzMyIgcG9pbnRzPSIyNDAuMjUzLDczLjMxMiAyNDkuNjc0LDgyLjczNCAxNzQuODksMTYxLjkzNSAxMTAuOTk5LDk2LjI3NyA3NC4xOTYsMjQ4LjQ5NiAxMDguMzUsMjQ4LjQ5NiANCgkxMjguNjY1LDE2My45OTYgMTc0Ljg5LDIxNC4zNDMgMjczLjgxNywxMDYuNTgzIDI4My4yMzksMTE2LjI5OSAyOTIuNjYsNjMuMDA3ICIvPg0KPC9zdmc+DQo=" + }, + "categories": [ + { + "id": 1, + "name": "Marketing & Content" + }, + { + "id": 6, + "name": "Communication" + } + ], + "displayName": "Mautic", + "typeVersion": 1 + }, + { + "id": 235, + "icon": "file:wooCommerce.svg", + "name": "n8n-nodes-base.wooCommerceTrigger", + "defaults": { + "name": "WooCommerce Trigger" + }, + "iconData": { + "type": "file", + "fileBuffer": "data:image/svg+xml;base64,PHN2ZyBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWlkWU1pZCIgdmVyc2lvbj0iMS4xIiB2aWV3Qm94PSIwIC01MCAyNTYgMjUzIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KPHRpdGxlPldvb0NvbW1lcmNlIExvZ288L3RpdGxlPgo8bWV0YWRhdGE+CjxyZGY6UkRGPgo8Y2M6V29yayByZGY6YWJvdXQ9IiI+CjxkYzpmb3JtYXQ+aW1hZ2Uvc3ZnK3htbDwvZGM6Zm9ybWF0Pgo8ZGM6dHlwZSByZGY6cmVzb3VyY2U9Imh0dHA6Ly9wdXJsLm9yZy9kYy9kY21pdHlwZS9TdGlsbEltYWdlIi8+CjxkYzp0aXRsZS8+CjwvY2M6V29yaz4KPC9yZGY6UkRGPgo8L21ldGFkYXRhPgo8cGF0aCBkPSJtMjMuNzU5IDBoMjA4LjM4YzEzLjE4NyAwIDIzLjg2MyAxMC42NzUgMjMuODYzIDIzLjg2M3Y3OS41NDJjMCAxMy4xODctMTAuNjc1IDIzLjg2My0yMy44NjMgMjMuODYzaC03NC43MjdsMTAuMjU3IDI1LjExOC00NS4xMDktMjUuMTE4aC05OC42OTVjLTEzLjE4NyAwLTIzLjg2My0xMC42NzUtMjMuODYzLTIzLjg2M3YtNzkuNTQyYy0wLjEwNDY2LTEzLjA4MyAxMC41NzEtMjMuODYzIDIzLjc1OC0yMy44NjN6IiBmaWxsPSIjN2Y1NGIzIi8+CjxwYXRoIGQ9Im0xNC41NzggMjEuNzVjMS40NTY5LTEuOTc3MiAzLjY0MjMtMy4wMTc5IDYuNTU2MS0zLjIyNiA1LjMwNzMtMC40MTYyNiA4LjMyNTIgMi4wODEzIDkuMDUzNyA3LjQ5MjcgMy4yMjYgMjEuNzUgNi43NjQyIDQwLjE2OSAxMC41MTEgNTUuMjU5bDIyLjc5LTQzLjM5NWMyLjA4MTMtMy45NTQ1IDQuNjgyOS02LjAzNTggNy44MDQ5LTYuMjQzOSA0LjU3ODktMC4zMTIyIDcuMzg4NiAyLjYwMTYgOC41MzMzIDguNzQxNSAyLjYwMTYgMTMuODQxIDUuOTMxNyAyNS42IDkuODg2MiAzNS41OSAyLjcwNTctMjYuNDMzIDcuMjg0Ni00NS40NzYgMTMuNzM3LTU3LjIzNiAxLjU2MS0yLjkxMzggMy44NTA0LTQuMzcwNyA2Ljg2ODMtNC41Nzg5IDIuMzkzNS0wLjIwODEzIDQuNTc4OSAwLjUyMDMzIDYuNTU2MSAyLjA4MTMgMS45NzcyIDEuNTYxIDMuMDE3OSAzLjUzODIgMy4yMjYgNS45MzE3IDAuMTA0MDYgMS44NzMyLTAuMjA4MTMgMy40MzQxLTEuMDQwNyA0Ljk5NTEtNC4wNTg1IDcuNDkyNy03LjM4ODYgMjAuMDg1LTEwLjA5NCAzNy41NjctMi42MDE2IDE2Ljk2My0zLjUzODIgMzAuMTc5LTIuOTEzOCAzOS42NDkgMC4yMDgxMyAyLjYwMTYtMC4yMDgxMyA0Ljg5MTEtMS4yNDg4IDYuODY4My0xLjI0ODggMi4yODk0LTMuMTIyIDMuNTM4Mi01LjUxNTQgMy43NDYzLTIuNzA1NyAwLjIwODEzLTUuNTE1NC0xLjA0MDYtOC4yMjExLTMuODUwNC05LjY3OC05Ljg4NjItMTcuMzc5LTI0LjY2My0yMi45OTgtNDQuMzMyLTYuNzY0MiAxMy4zMi0xMS43NTkgMjMuMzExLTE0Ljk4NSAyOS45NzEtNi4xMzk4IDExLjc1OS0xMS4zNDMgMTcuNzk1LTE1LjcxNCAxOC4xMDctMi44MDk4IDAuMjA4MTMtNS4yMDMzLTIuMTg1NC03LjI4NDYtNy4xODA1LTUuMzA3My0xMy42MzMtMTEuMDMxLTM5Ljk2MS0xNy4xNzEtNzguOTg1LTAuNDE2MjYtMi43MDU3IDAuMjA4MTMtNS4wOTkyIDEuNjY1LTYuOTcyNHptMjIzLjY0IDE2LjMzOGMtMy43NDYzLTYuNTU2MS05LjI2MTgtMTAuNTExLTE2LjY1LTEyLjA3Mi0xLjk3NzItMC40MTYyNi0zLjg1MDQtMC42MjQzOS01LjYxOTUtMC42MjQzOS05Ljk5MDIgMC0xOC4xMDcgNS4yMDMzLTI0LjQ1NSAxNS42MS01LjQxMTQgOC44NDU1LTguMTE3MSAxOC42MjgtOC4xMTcxIDI5LjM0NiAwIDguMDEzIDEuNjY1IDE0Ljg4MSA0Ljk5NTEgMjAuNjA1IDMuNzQ2MyA2LjU1NjEgOS4yNjE4IDEwLjUxMSAxNi42NSAxMi4wNzIgMS45NzcyIDAuNDE2MjYgMy44NTA0IDAuNjI0MzkgNS42MTk1IDAuNjI0MzkgMTAuMDk0IDAgMTguMjExLTUuMjAzMyAyNC40NTUtMTUuNjEgNS40MTE0LTguOTQ5NiA4LjExNzEtMTguNzMyIDguMTE3MS0yOS40NSAwLjEwNDA2LTguMTE3MS0xLjY2NS0xNC44ODEtNC45OTUxLTIwLjUwMXptLTEzLjExMiAyOC44MjZjLTEuNDU2OSA2Ljg2ODMtNC4wNTg1IDExLjk2Ny03LjkwODkgMTUuNDAyLTMuMDE3OSAyLjcwNTctNS44Mjc2IDMuODUwNC04LjQyOTMgMy4zMzAxLTIuNDk3Ni0wLjUyMDMzLTQuNTc4OS0yLjcwNTctNi4xMzk4LTYuNzY0Mi0xLjI0ODgtMy4yMjYtMS44NzMyLTYuNDUyLTEuODczMi05LjQ2OTkgMC0yLjYwMTYgMC4yMDgxMy01LjIwMzMgMC43Mjg0Ni03LjU5NjcgMC45MzY1OS00LjI2NjcgMi43MDU3LTguNDI5MyA1LjUxNTQtMTIuMzg0IDMuNDM0MS01LjA5OTIgNy4wNzY0LTcuMTgwNSAxMC44MjMtNi40NTIgMi40OTc2IDAuNTIwMzMgNC41Nzg5IDIuNzA1NyA2LjEzOTggNi43NjQyIDEuMjQ4OCAzLjIyNiAxLjg3MzIgNi40NTIgMS44NzMyIDkuNDY5OSAwIDIuNzA1Ny0wLjIwODEzIDUuMzA3My0wLjcyODQ2IDcuNzAwOHptLTUyLjAzMy0yOC44MjZjLTMuNzQ2My02LjU1NjEtOS4zNjU5LTEwLjUxMS0xNi42NS0xMi4wNzItMS45NzcyLTAuNDE2MjYtMy44NTA0LTAuNjI0MzktNS42MTk1LTAuNjI0MzktOS45OTAyIDAtMTguMTA3IDUuMjAzMy0yNC40NTUgMTUuNjEtNS40MTE0IDguODQ1NS04LjExNzEgMTguNjI4LTguMTE3MSAyOS4zNDYgMCA4LjAxMyAxLjY2NSAxNC44ODEgNC45OTUxIDIwLjYwNSAzLjc0NjMgNi41NTYxIDkuMjYxOCAxMC41MTEgMTYuNjUgMTIuMDcyIDEuOTc3MiAwLjQxNjI2IDMuODUwNCAwLjYyNDM5IDUuNjE5NSAwLjYyNDM5IDEwLjA5NCAwIDE4LjIxMS01LjIwMzMgMjQuNDU1LTE1LjYxIDUuNDExNC04Ljk0OTYgOC4xMTcxLTE4LjczMiA4LjExNzEtMjkuNDUgMC04LjExNzEtMS42NjUtMTQuODgxLTQuOTk1MS0yMC41MDF6bS0xMy4yMTYgMjguODI2Yy0xLjQ1NjkgNi44NjgzLTQuMDU4NSAxMS45NjctNy45MDg5IDE1LjQwMi0zLjAxNzkgMi43MDU3LTUuODI3NiAzLjg1MDQtOC40MjkzIDMuMzMwMS0yLjQ5NzYtMC41MjAzMy00LjU3ODktMi43MDU3LTYuMTM5OC02Ljc2NDItMS4yNDg4LTMuMjI2LTEuODczMi02LjQ1Mi0xLjg3MzItOS40Njk5IDAtMi42MDE2IDAuMjA4MTMtNS4yMDMzIDAuNzI4NDYtNy41OTY3IDAuOTM2NTgtNC4yNjY3IDIuNzA1Ny04LjQyOTMgNS41MTU0LTEyLjM4NCAzLjQzNDEtNS4wOTkyIDcuMDc2NC03LjE4MDUgMTAuODIzLTYuNDUyIDIuNDk3NiAwLjUyMDMzIDQuNTc4OSAyLjcwNTcgNi4xMzk4IDYuNzY0MiAxLjI0ODggMy4yMjYgMS44NzMyIDYuNDUyIDEuODczMiA5LjQ2OTkgMC4xMDQwNiAyLjcwNTctMC4yMDgxMyA1LjMwNzMtMC43Mjg0NiA3LjcwMDh6IiBmaWxsPSIjZmZmIi8+Cjwvc3ZnPgo=" + }, + "categories": [ + { + "id": 2, + "name": "Sales" + } + ], + "displayName": "WooCommerce Trigger", + "typeVersion": 1 + } + ], + "categories": [ + { + "id": 2, + "name": "Sales" + } + ], + "image": [] + }, + { + "id": 1459, + "name": "Notify on Telegram and Twitter when new order is added in WooCommerce", + "views": 620, + "recentViews": 9823, + "totalViews": 620, + "createdAt": "2022-02-17T15:02:14.961Z", + "description": "This workflow uses a WooCommerce trigger that will run a new product has been added, It will then post the product to Telegram and Twitter.\n\nTo use this workflow you will need to set the credentials to use for the WooCommerce, Twitter and Telegram nodes.", + "workflow": { + "id": 85, + "name": "New WooCommerce Product to Twitter and Telegram", + "nodes": [ + { + "name": "Twitter", + "type": "n8n-nodes-base.twitter", + "position": [ + 720, + 300 + ], + "parameters": { + "text": "=✨ New Product Announcement ✨\nWe have just added {{$json[\"name\"]}}, Head to {{$json[\"permalink\"]}} to find out more.", + "additionalFields": {} + }, + "credentials": { + "twitterOAuth1Api": { + "id": "37", + "name": "joffcom" + } + }, + "typeVersion": 1 + }, + { + "name": "Telegram", + "type": "n8n-nodes-base.telegram", + "position": [ + 720, + 500 + ], + "parameters": { + "text": "=✨ New Product Announcement ✨\nWe have just added {{$json[\"name\"]}}, Head to {{$json[\"permalink\"]}} to find out more.", + "chatId": "123456", + "additionalFields": {} + }, + "credentials": { + "telegramApi": { + "id": "56", + "name": "Telegram account" + } + }, + "typeVersion": 1 + }, + { + "name": "WooCommerce Trigger", + "type": "n8n-nodes-base.wooCommerceTrigger", + "position": [ + 540, + 400 + ], + "webhookId": "ab7b134b-9b2d-4e0d-b496-1aee30db0808", + "parameters": { + "event": "product.created" + }, + "credentials": { + "wooCommerceApi": { + "id": "48", + "name": "WooCommerce account" + } + }, + "typeVersion": 1 + } + ], + "active": false, + "settings": {}, + "connections": { + "WooCommerce Trigger": { + "main": [ + [ + { + "node": "Twitter", + "type": "main", + "index": 0 + }, + { + "node": "Telegram", + "type": "main", + "index": 0 + } + ] + ] + } + } + }, + "workflowInfo": { + "nodeCount": 4, + "nodeTypes": { + "n8n-nodes-base.twitter": { + "count": 1 + }, + "n8n-nodes-base.telegram": { + "count": 1 + }, + "n8n-nodes-base.wooCommerceTrigger": { + "count": 1 + } + } + }, + "user": { + "username": "jon-n8n" + }, + "nodes": [ + { + "id": 42, + "icon": "fa:play", + "name": "n8n-nodes-base.start", + "defaults": { + "name": "Start", + "color": "#00e000" + }, + "iconData": { + "icon": "play", + "type": "icon" + }, + "categories": [ + { + "id": 9, + "name": "Core Nodes" + } + ], + "displayName": "Start", + "typeVersion": 1 + }, + { + "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": 235, + "icon": "file:wooCommerce.svg", + "name": "n8n-nodes-base.wooCommerceTrigger", + "defaults": { + "name": "WooCommerce Trigger" + }, + "iconData": { + "type": "file", + "fileBuffer": "data:image/svg+xml;base64,PHN2ZyBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWlkWU1pZCIgdmVyc2lvbj0iMS4xIiB2aWV3Qm94PSIwIC01MCAyNTYgMjUzIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KPHRpdGxlPldvb0NvbW1lcmNlIExvZ288L3RpdGxlPgo8bWV0YWRhdGE+CjxyZGY6UkRGPgo8Y2M6V29yayByZGY6YWJvdXQ9IiI+CjxkYzpmb3JtYXQ+aW1hZ2Uvc3ZnK3htbDwvZGM6Zm9ybWF0Pgo8ZGM6dHlwZSByZGY6cmVzb3VyY2U9Imh0dHA6Ly9wdXJsLm9yZy9kYy9kY21pdHlwZS9TdGlsbEltYWdlIi8+CjxkYzp0aXRsZS8+CjwvY2M6V29yaz4KPC9yZGY6UkRGPgo8L21ldGFkYXRhPgo8cGF0aCBkPSJtMjMuNzU5IDBoMjA4LjM4YzEzLjE4NyAwIDIzLjg2MyAxMC42NzUgMjMuODYzIDIzLjg2M3Y3OS41NDJjMCAxMy4xODctMTAuNjc1IDIzLjg2My0yMy44NjMgMjMuODYzaC03NC43MjdsMTAuMjU3IDI1LjExOC00NS4xMDktMjUuMTE4aC05OC42OTVjLTEzLjE4NyAwLTIzLjg2My0xMC42NzUtMjMuODYzLTIzLjg2M3YtNzkuNTQyYy0wLjEwNDY2LTEzLjA4MyAxMC41NzEtMjMuODYzIDIzLjc1OC0yMy44NjN6IiBmaWxsPSIjN2Y1NGIzIi8+CjxwYXRoIGQ9Im0xNC41NzggMjEuNzVjMS40NTY5LTEuOTc3MiAzLjY0MjMtMy4wMTc5IDYuNTU2MS0zLjIyNiA1LjMwNzMtMC40MTYyNiA4LjMyNTIgMi4wODEzIDkuMDUzNyA3LjQ5MjcgMy4yMjYgMjEuNzUgNi43NjQyIDQwLjE2OSAxMC41MTEgNTUuMjU5bDIyLjc5LTQzLjM5NWMyLjA4MTMtMy45NTQ1IDQuNjgyOS02LjAzNTggNy44MDQ5LTYuMjQzOSA0LjU3ODktMC4zMTIyIDcuMzg4NiAyLjYwMTYgOC41MzMzIDguNzQxNSAyLjYwMTYgMTMuODQxIDUuOTMxNyAyNS42IDkuODg2MiAzNS41OSAyLjcwNTctMjYuNDMzIDcuMjg0Ni00NS40NzYgMTMuNzM3LTU3LjIzNiAxLjU2MS0yLjkxMzggMy44NTA0LTQuMzcwNyA2Ljg2ODMtNC41Nzg5IDIuMzkzNS0wLjIwODEzIDQuNTc4OSAwLjUyMDMzIDYuNTU2MSAyLjA4MTMgMS45NzcyIDEuNTYxIDMuMDE3OSAzLjUzODIgMy4yMjYgNS45MzE3IDAuMTA0MDYgMS44NzMyLTAuMjA4MTMgMy40MzQxLTEuMDQwNyA0Ljk5NTEtNC4wNTg1IDcuNDkyNy03LjM4ODYgMjAuMDg1LTEwLjA5NCAzNy41NjctMi42MDE2IDE2Ljk2My0zLjUzODIgMzAuMTc5LTIuOTEzOCAzOS42NDkgMC4yMDgxMyAyLjYwMTYtMC4yMDgxMyA0Ljg5MTEtMS4yNDg4IDYuODY4My0xLjI0ODggMi4yODk0LTMuMTIyIDMuNTM4Mi01LjUxNTQgMy43NDYzLTIuNzA1NyAwLjIwODEzLTUuNTE1NC0xLjA0MDYtOC4yMjExLTMuODUwNC05LjY3OC05Ljg4NjItMTcuMzc5LTI0LjY2My0yMi45OTgtNDQuMzMyLTYuNzY0MiAxMy4zMi0xMS43NTkgMjMuMzExLTE0Ljk4NSAyOS45NzEtNi4xMzk4IDExLjc1OS0xMS4zNDMgMTcuNzk1LTE1LjcxNCAxOC4xMDctMi44MDk4IDAuMjA4MTMtNS4yMDMzLTIuMTg1NC03LjI4NDYtNy4xODA1LTUuMzA3My0xMy42MzMtMTEuMDMxLTM5Ljk2MS0xNy4xNzEtNzguOTg1LTAuNDE2MjYtMi43MDU3IDAuMjA4MTMtNS4wOTkyIDEuNjY1LTYuOTcyNHptMjIzLjY0IDE2LjMzOGMtMy43NDYzLTYuNTU2MS05LjI2MTgtMTAuNTExLTE2LjY1LTEyLjA3Mi0xLjk3NzItMC40MTYyNi0zLjg1MDQtMC42MjQzOS01LjYxOTUtMC42MjQzOS05Ljk5MDIgMC0xOC4xMDcgNS4yMDMzLTI0LjQ1NSAxNS42MS01LjQxMTQgOC44NDU1LTguMTE3MSAxOC42MjgtOC4xMTcxIDI5LjM0NiAwIDguMDEzIDEuNjY1IDE0Ljg4MSA0Ljk5NTEgMjAuNjA1IDMuNzQ2MyA2LjU1NjEgOS4yNjE4IDEwLjUxMSAxNi42NSAxMi4wNzIgMS45NzcyIDAuNDE2MjYgMy44NTA0IDAuNjI0MzkgNS42MTk1IDAuNjI0MzkgMTAuMDk0IDAgMTguMjExLTUuMjAzMyAyNC40NTUtMTUuNjEgNS40MTE0LTguOTQ5NiA4LjExNzEtMTguNzMyIDguMTE3MS0yOS40NSAwLjEwNDA2LTguMTE3MS0xLjY2NS0xNC44ODEtNC45OTUxLTIwLjUwMXptLTEzLjExMiAyOC44MjZjLTEuNDU2OSA2Ljg2ODMtNC4wNTg1IDExLjk2Ny03LjkwODkgMTUuNDAyLTMuMDE3OSAyLjcwNTctNS44Mjc2IDMuODUwNC04LjQyOTMgMy4zMzAxLTIuNDk3Ni0wLjUyMDMzLTQuNTc4OS0yLjcwNTctNi4xMzk4LTYuNzY0Mi0xLjI0ODgtMy4yMjYtMS44NzMyLTYuNDUyLTEuODczMi05LjQ2OTkgMC0yLjYwMTYgMC4yMDgxMy01LjIwMzMgMC43Mjg0Ni03LjU5NjcgMC45MzY1OS00LjI2NjcgMi43MDU3LTguNDI5MyA1LjUxNTQtMTIuMzg0IDMuNDM0MS01LjA5OTIgNy4wNzY0LTcuMTgwNSAxMC44MjMtNi40NTIgMi40OTc2IDAuNTIwMzMgNC41Nzg5IDIuNzA1NyA2LjEzOTggNi43NjQyIDEuMjQ4OCAzLjIyNiAxLjg3MzIgNi40NTIgMS44NzMyIDkuNDY5OSAwIDIuNzA1Ny0wLjIwODEzIDUuMzA3My0wLjcyODQ2IDcuNzAwOHptLTUyLjAzMy0yOC44MjZjLTMuNzQ2My02LjU1NjEtOS4zNjU5LTEwLjUxMS0xNi42NS0xMi4wNzItMS45NzcyLTAuNDE2MjYtMy44NTA0LTAuNjI0MzktNS42MTk1LTAuNjI0MzktOS45OTAyIDAtMTguMTA3IDUuMjAzMy0yNC40NTUgMTUuNjEtNS40MTE0IDguODQ1NS04LjExNzEgMTguNjI4LTguMTE3MSAyOS4zNDYgMCA4LjAxMyAxLjY2NSAxNC44ODEgNC45OTUxIDIwLjYwNSAzLjc0NjMgNi41NTYxIDkuMjYxOCAxMC41MTEgMTYuNjUgMTIuMDcyIDEuOTc3MiAwLjQxNjI2IDMuODUwNCAwLjYyNDM5IDUuNjE5NSAwLjYyNDM5IDEwLjA5NCAwIDE4LjIxMS01LjIwMzMgMjQuNDU1LTE1LjYxIDUuNDExNC04Ljk0OTYgOC4xMTcxLTE4LjczMiA4LjExNzEtMjkuNDUgMC04LjExNzEtMS42NjUtMTQuODgxLTQuOTk1MS0yMC41MDF6bS0xMy4yMTYgMjguODI2Yy0xLjQ1NjkgNi44NjgzLTQuMDU4NSAxMS45NjctNy45MDg5IDE1LjQwMi0zLjAxNzkgMi43MDU3LTUuODI3NiAzLjg1MDQtOC40MjkzIDMuMzMwMS0yLjQ5NzYtMC41MjAzMy00LjU3ODktMi43MDU3LTYuMTM5OC02Ljc2NDItMS4yNDg4LTMuMjI2LTEuODczMi02LjQ1Mi0xLjg3MzItOS40Njk5IDAtMi42MDE2IDAuMjA4MTMtNS4yMDMzIDAuNzI4NDYtNy41OTY3IDAuOTM2NTgtNC4yNjY3IDIuNzA1Ny04LjQyOTMgNS41MTU0LTEyLjM4NCAzLjQzNDEtNS4wOTkyIDcuMDc2NC03LjE4MDUgMTAuODIzLTYuNDUyIDIuNDk3NiAwLjUyMDMzIDQuNTc4OSAyLjcwNTcgNi4xMzk4IDYuNzY0MiAxLjI0ODggMy4yMjYgMS44NzMyIDYuNDUyIDEuODczMiA5LjQ2OTkgMC4xMDQwNiAyLjcwNTctMC4yMDgxMyA1LjMwNzMtMC43Mjg0NiA3LjcwMDh6IiBmaWxsPSIjZmZmIi8+Cjwvc3ZnPgo=" + }, + "categories": [ + { + "id": 2, + "name": "Sales" + } + ], + "displayName": "WooCommerce 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": 1457, + "name": "Notify on Slack when new order is registered in WooCommerce", + "views": 178, + "recentViews": 9787, + "totalViews": 178, + "createdAt": "2022-02-17T15:01:13.489Z", + "description": "This workflow uses a WooCommerce trigger that will run when an order has been placed.\n\nIf the value of this is over 100 it will post it to a Slack channel.\n\nTo use this workflow you will need to set the credentials to use for the WooCommerce and Slack nodes, You will also need to pick a channel to post the message to.", + "workflow": { + "id": 81, + "name": "New WooCommerce order to Slack", + "nodes": [ + { + "name": "Order Created", + "type": "n8n-nodes-base.wooCommerceTrigger", + "position": [ + 340, + 500 + ], + "webhookId": "287b4bf4-67ec-4c97-85d9-c0d3e6f59e6b", + "parameters": { + "event": "order.created" + }, + "credentials": { + "wooCommerceApi": { + "id": "48", + "name": "WooCommerce account" + } + }, + "typeVersion": 1 + }, + { + "name": "Send to Slack", + "type": "n8n-nodes-base.slack", + "position": [ + 780, + 480 + ], + "parameters": { + "text": ":sparkles: There is a new order :sparkles:", + "channel": "woo-commerce", + "blocksUi": { + "blocksValues": [] + }, + "attachments": [ + { + "color": "#66FF00", + "fields": { + "item": [ + { + "short": true, + "title": "Order ID", + "value": "={{$json[\"id\"]}}" + }, + { + "short": true, + "title": "Status", + "value": "={{$json[\"status\"]}}" + }, + { + "short": true, + "title": "Total", + "value": "={{$json[\"currency_symbol\"]}}{{$json[\"total\"]}}" + }, + { + "short": false, + "title": "Link", + "value": "={{$node[\"Order Created\"].json[\"_links\"][\"self\"][0][\"href\"]}}" + } + ] + }, + "footer": "=*Ordered:* {{$json[\"date_created\"]}} | *Transaction ID:* {{$json[\"transaction_id\"]}}" + } + ], + "otherOptions": {} + }, + "credentials": { + "slackApi": { + "id": "53", + "name": "Slack Access Token" + } + }, + "typeVersion": 1 + }, + { + "name": "Price over 100", + "type": "n8n-nodes-base.if", + "position": [ + 540, + 500 + ], + "parameters": { + "conditions": { + "number": [ + { + "value1": "={{$json[\"total\"]}}", + "value2": 100, + "operation": "largerEqual" + } + ] + } + }, + "typeVersion": 1 + } + ], + "active": false, + "settings": { + "saveManualExecutions": true, + "saveExecutionProgress": true, + "saveDataSuccessExecution": "all" + }, + "connections": { + "Order Created": { + "main": [ + [ + { + "node": "Price over 100", + "type": "main", + "index": 0 + } + ] + ] + }, + "Price over 100": { + "main": [ + [ + { + "node": "Send to Slack", + "type": "main", + "index": 0 + } + ], + [] + ] + } + } + }, + "workflowInfo": { + "nodeCount": 4, + "nodeTypes": { + "n8n-nodes-base.if": { + "count": 1 + }, + "n8n-nodes-base.slack": { + "count": 1 + }, + "n8n-nodes-base.wooCommerceTrigger": { + "count": 1 + } + } + }, + "user": { + "username": "jon-n8n" + }, + "nodes": [ + { + "id": 20, + "icon": "fa:map-signs", + "name": "n8n-nodes-base.if", + "defaults": { + "name": "IF", + "color": "#408000" + }, + "iconData": { + "icon": "map-signs", + "type": "icon" + }, + "categories": [ + { + "id": 9, + "name": "Core Nodes" + } + ], + "displayName": "IF", + "typeVersion": 1 + }, + { + "id": 40, + "icon": "file:slack.svg", + "name": "n8n-nodes-base.slack", + "defaults": { + "name": "Slack" + }, + "iconData": { + "type": "file", + "fileBuffer": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgMTUwLjg1MiAxNTAuODUyIiBmaWxsPSIjZmZmIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48dXNlIHhsaW5rOmhyZWY9IiNhIiB4PSIuOTI2IiB5PSIuOTI2Ii8+PHN5bWJvbCBpZD0iYSIgb3ZlcmZsb3c9InZpc2libGUiPjxnIHN0cm9rZS13aWR0aD0iMS44NTIiPjxwYXRoIGQ9Ik00MC43NDEgOTMuNTVjMC04LjczNSA2LjYwNy0xNS43NzIgMTQuODE1LTE1Ljc3MnMxNC44MTUgNy4wMzcgMTQuODE1IDE1Ljc3MnYzOC44MjRjMCA4LjczNy02LjYwNyAxNS43NzQtMTQuODE1IDE1Ljc3NHMtMTQuODE1LTcuMDM3LTE0LjgxNS0xNS43NzJ6IiBmaWxsPSIjZTAxZTVhIiBzdHJva2U9IiNlMDFlNWEiLz48cGF0aCBkPSJNOTMuNTUgMTA3LjQwOGMtOC43MzUgMC0xNS43NzItNi42MDctMTUuNzcyLTE0LjgxNXM3LjAzNy0xNC44MTUgMTUuNzcyLTE0LjgxNWgzOC44MjZjOC43MzUgMCAxNS43NzIgNi42MDcgMTUuNzcyIDE0LjgxNXMtNy4wMzcgMTQuODE1LTE1Ljc3MiAxNC44MTV6IiBmaWxsPSIjZWNiMjJkIiBzdHJva2U9IiNlY2IyMmQiLz48cGF0aCBkPSJNNzcuNzc4IDE1Ljc3MkM3Ny43NzggNy4wMzcgODQuMzg1IDAgOTIuNTkzIDBzMTQuODE1IDcuMDM3IDE0LjgxNSAxNS43NzJ2MzguODI2YzAgOC43MzUtNi42MDcgMTUuNzcyLTE0LjgxNSAxNS43NzJzLTE0LjgxNS03LjAzNy0xNC44MTUtMTUuNzcyeiIgZmlsbD0iIzJmYjY3YyIgc3Ryb2tlPSIjMmZiNjdjIi8+PHBhdGggZD0iTTE1Ljc3MiA3MC4zNzFDNy4wMzcgNzAuMzcxIDAgNjMuNzYzIDAgNTUuNTU2czcuMDM3LTE0LjgxNSAxNS43NzItMTQuODE1aDM4LjgyNmM4LjczNSAwIDE1Ljc3MiA2LjYwNyAxNS43NzIgMTQuODE1cy03LjAzNyAxNC44MTUtMTUuNzcyIDE0LjgxNXoiIGZpbGw9IiMzNmM1ZjEiIHN0cm9rZT0iIzM2YzVmMSIvPjxnIHN0cm9rZS1saW5lam9pbj0ibWl0ZXIiPjxwYXRoIGQ9Ik03Ny43NzggMTMzLjMzM2MwIDguMjA4IDYuNjA3IDE0LjgxNSAxNC44MTUgMTQuODE1czE0LjgxNS02LjYwNyAxNC44MTUtMTQuODE1LTYuNjA3LTE0LjgxNS0xNC44MTUtMTQuODE1SDc3Ljc3OHoiIGZpbGw9IiNlY2IyMmQiIHN0cm9rZT0iI2VjYjIyZCIvPjxwYXRoIGQ9Ik0xMzMuMzM0IDcwLjM3MWgtMTQuODE1VjU1LjU1NmMwLTguMjA3IDYuNjA3LTE0LjgxNSAxNC44MTUtMTQuODE1czE0LjgxNSA2LjYwNyAxNC44MTUgMTQuODE1LTYuNjA3IDE0LjgxNS0xNC44MTUgMTQuODE1eiIgZmlsbD0iIzJmYjY3YyIgc3Ryb2tlPSIjMmZiNjdjIi8+PHBhdGggZD0iTTE0LjgxNSA3Ny43NzhIMjkuNjN2MTQuODE1YzAgOC4yMDctNi42MDcgMTQuODE1LTE0LjgxNSAxNC44MTVTMCAxMDAuOCAwIDkyLjU5M3M2LjYwNy0xNC44MTUgMTQuODE1LTE0LjgxNXoiIGZpbGw9IiNlMDFlNWEiIHN0cm9rZT0iI2UwMWU1YSIvPjxwYXRoIGQ9Ik03MC4zNzEgMTQuODE1VjI5LjYzSDU1LjU1NmMtOC4yMDcgMC0xNC44MTUtNi42MDctMTQuODE1LTE0LjgxNVM0Ny4zNDggMCA1NS41NTYgMHMxNC44MTUgNi42MDcgMTQuODE1IDE0LjgxNXoiIGZpbGw9IiMzNmM1ZjEiIHN0cm9rZT0iIzM2YzVmMSIvPjwvZz48L2c+PC9zeW1ib2w+PC9zdmc+" + }, + "categories": [ + { + "id": 6, + "name": "Communication" + } + ], + "displayName": "Slack", + "typeVersion": 2 + }, + { + "id": 42, + "icon": "fa:play", + "name": "n8n-nodes-base.start", + "defaults": { + "name": "Start", + "color": "#00e000" + }, + "iconData": { + "icon": "play", + "type": "icon" + }, + "categories": [ + { + "id": 9, + "name": "Core Nodes" + } + ], + "displayName": "Start", + "typeVersion": 1 + }, + { + "id": 235, + "icon": "file:wooCommerce.svg", + "name": "n8n-nodes-base.wooCommerceTrigger", + "defaults": { + "name": "WooCommerce Trigger" + }, + "iconData": { + "type": "file", + "fileBuffer": "data:image/svg+xml;base64,PHN2ZyBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWlkWU1pZCIgdmVyc2lvbj0iMS4xIiB2aWV3Qm94PSIwIC01MCAyNTYgMjUzIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KPHRpdGxlPldvb0NvbW1lcmNlIExvZ288L3RpdGxlPgo8bWV0YWRhdGE+CjxyZGY6UkRGPgo8Y2M6V29yayByZGY6YWJvdXQ9IiI+CjxkYzpmb3JtYXQ+aW1hZ2Uvc3ZnK3htbDwvZGM6Zm9ybWF0Pgo8ZGM6dHlwZSByZGY6cmVzb3VyY2U9Imh0dHA6Ly9wdXJsLm9yZy9kYy9kY21pdHlwZS9TdGlsbEltYWdlIi8+CjxkYzp0aXRsZS8+CjwvY2M6V29yaz4KPC9yZGY6UkRGPgo8L21ldGFkYXRhPgo8cGF0aCBkPSJtMjMuNzU5IDBoMjA4LjM4YzEzLjE4NyAwIDIzLjg2MyAxMC42NzUgMjMuODYzIDIzLjg2M3Y3OS41NDJjMCAxMy4xODctMTAuNjc1IDIzLjg2My0yMy44NjMgMjMuODYzaC03NC43MjdsMTAuMjU3IDI1LjExOC00NS4xMDktMjUuMTE4aC05OC42OTVjLTEzLjE4NyAwLTIzLjg2My0xMC42NzUtMjMuODYzLTIzLjg2M3YtNzkuNTQyYy0wLjEwNDY2LTEzLjA4MyAxMC41NzEtMjMuODYzIDIzLjc1OC0yMy44NjN6IiBmaWxsPSIjN2Y1NGIzIi8+CjxwYXRoIGQ9Im0xNC41NzggMjEuNzVjMS40NTY5LTEuOTc3MiAzLjY0MjMtMy4wMTc5IDYuNTU2MS0zLjIyNiA1LjMwNzMtMC40MTYyNiA4LjMyNTIgMi4wODEzIDkuMDUzNyA3LjQ5MjcgMy4yMjYgMjEuNzUgNi43NjQyIDQwLjE2OSAxMC41MTEgNTUuMjU5bDIyLjc5LTQzLjM5NWMyLjA4MTMtMy45NTQ1IDQuNjgyOS02LjAzNTggNy44MDQ5LTYuMjQzOSA0LjU3ODktMC4zMTIyIDcuMzg4NiAyLjYwMTYgOC41MzMzIDguNzQxNSAyLjYwMTYgMTMuODQxIDUuOTMxNyAyNS42IDkuODg2MiAzNS41OSAyLjcwNTctMjYuNDMzIDcuMjg0Ni00NS40NzYgMTMuNzM3LTU3LjIzNiAxLjU2MS0yLjkxMzggMy44NTA0LTQuMzcwNyA2Ljg2ODMtNC41Nzg5IDIuMzkzNS0wLjIwODEzIDQuNTc4OSAwLjUyMDMzIDYuNTU2MSAyLjA4MTMgMS45NzcyIDEuNTYxIDMuMDE3OSAzLjUzODIgMy4yMjYgNS45MzE3IDAuMTA0MDYgMS44NzMyLTAuMjA4MTMgMy40MzQxLTEuMDQwNyA0Ljk5NTEtNC4wNTg1IDcuNDkyNy03LjM4ODYgMjAuMDg1LTEwLjA5NCAzNy41NjctMi42MDE2IDE2Ljk2My0zLjUzODIgMzAuMTc5LTIuOTEzOCAzOS42NDkgMC4yMDgxMyAyLjYwMTYtMC4yMDgxMyA0Ljg5MTEtMS4yNDg4IDYuODY4My0xLjI0ODggMi4yODk0LTMuMTIyIDMuNTM4Mi01LjUxNTQgMy43NDYzLTIuNzA1NyAwLjIwODEzLTUuNTE1NC0xLjA0MDYtOC4yMjExLTMuODUwNC05LjY3OC05Ljg4NjItMTcuMzc5LTI0LjY2My0yMi45OTgtNDQuMzMyLTYuNzY0MiAxMy4zMi0xMS43NTkgMjMuMzExLTE0Ljk4NSAyOS45NzEtNi4xMzk4IDExLjc1OS0xMS4zNDMgMTcuNzk1LTE1LjcxNCAxOC4xMDctMi44MDk4IDAuMjA4MTMtNS4yMDMzLTIuMTg1NC03LjI4NDYtNy4xODA1LTUuMzA3My0xMy42MzMtMTEuMDMxLTM5Ljk2MS0xNy4xNzEtNzguOTg1LTAuNDE2MjYtMi43MDU3IDAuMjA4MTMtNS4wOTkyIDEuNjY1LTYuOTcyNHptMjIzLjY0IDE2LjMzOGMtMy43NDYzLTYuNTU2MS05LjI2MTgtMTAuNTExLTE2LjY1LTEyLjA3Mi0xLjk3NzItMC40MTYyNi0zLjg1MDQtMC42MjQzOS01LjYxOTUtMC42MjQzOS05Ljk5MDIgMC0xOC4xMDcgNS4yMDMzLTI0LjQ1NSAxNS42MS01LjQxMTQgOC44NDU1LTguMTE3MSAxOC42MjgtOC4xMTcxIDI5LjM0NiAwIDguMDEzIDEuNjY1IDE0Ljg4MSA0Ljk5NTEgMjAuNjA1IDMuNzQ2MyA2LjU1NjEgOS4yNjE4IDEwLjUxMSAxNi42NSAxMi4wNzIgMS45NzcyIDAuNDE2MjYgMy44NTA0IDAuNjI0MzkgNS42MTk1IDAuNjI0MzkgMTAuMDk0IDAgMTguMjExLTUuMjAzMyAyNC40NTUtMTUuNjEgNS40MTE0LTguOTQ5NiA4LjExNzEtMTguNzMyIDguMTE3MS0yOS40NSAwLjEwNDA2LTguMTE3MS0xLjY2NS0xNC44ODEtNC45OTUxLTIwLjUwMXptLTEzLjExMiAyOC44MjZjLTEuNDU2OSA2Ljg2ODMtNC4wNTg1IDExLjk2Ny03LjkwODkgMTUuNDAyLTMuMDE3OSAyLjcwNTctNS44Mjc2IDMuODUwNC04LjQyOTMgMy4zMzAxLTIuNDk3Ni0wLjUyMDMzLTQuNTc4OS0yLjcwNTctNi4xMzk4LTYuNzY0Mi0xLjI0ODgtMy4yMjYtMS44NzMyLTYuNDUyLTEuODczMi05LjQ2OTkgMC0yLjYwMTYgMC4yMDgxMy01LjIwMzMgMC43Mjg0Ni03LjU5NjcgMC45MzY1OS00LjI2NjcgMi43MDU3LTguNDI5MyA1LjUxNTQtMTIuMzg0IDMuNDM0MS01LjA5OTIgNy4wNzY0LTcuMTgwNSAxMC44MjMtNi40NTIgMi40OTc2IDAuNTIwMzMgNC41Nzg5IDIuNzA1NyA2LjEzOTggNi43NjQyIDEuMjQ4OCAzLjIyNiAxLjg3MzIgNi40NTIgMS44NzMyIDkuNDY5OSAwIDIuNzA1Ny0wLjIwODEzIDUuMzA3My0wLjcyODQ2IDcuNzAwOHptLTUyLjAzMy0yOC44MjZjLTMuNzQ2My02LjU1NjEtOS4zNjU5LTEwLjUxMS0xNi42NS0xMi4wNzItMS45NzcyLTAuNDE2MjYtMy44NTA0LTAuNjI0MzktNS42MTk1LTAuNjI0MzktOS45OTAyIDAtMTguMTA3IDUuMjAzMy0yNC40NTUgMTUuNjEtNS40MTE0IDguODQ1NS04LjExNzEgMTguNjI4LTguMTE3MSAyOS4zNDYgMCA4LjAxMyAxLjY2NSAxNC44ODEgNC45OTUxIDIwLjYwNSAzLjc0NjMgNi41NTYxIDkuMjYxOCAxMC41MTEgMTYuNjUgMTIuMDcyIDEuOTc3MiAwLjQxNjI2IDMuODUwNCAwLjYyNDM5IDUuNjE5NSAwLjYyNDM5IDEwLjA5NCAwIDE4LjIxMS01LjIwMzMgMjQuNDU1LTE1LjYxIDUuNDExNC04Ljk0OTYgOC4xMTcxLTE4LjczMiA4LjExNzEtMjkuNDUgMC04LjExNzEtMS42NjUtMTQuODgxLTQuOTk1MS0yMC41MDF6bS0xMy4yMTYgMjguODI2Yy0xLjQ1NjkgNi44NjgzLTQuMDU4NSAxMS45NjctNy45MDg5IDE1LjQwMi0zLjAxNzkgMi43MDU3LTUuODI3NiAzLjg1MDQtOC40MjkzIDMuMzMwMS0yLjQ5NzYtMC41MjAzMy00LjU3ODktMi43MDU3LTYuMTM5OC02Ljc2NDItMS4yNDg4LTMuMjI2LTEuODczMi02LjQ1Mi0xLjg3MzItOS40Njk5IDAtMi42MDE2IDAuMjA4MTMtNS4yMDMzIDAuNzI4NDYtNy41OTY3IDAuOTM2NTgtNC4yNjY3IDIuNzA1Ny04LjQyOTMgNS41MTU0LTEyLjM4NCAzLjQzNDEtNS4wOTkyIDcuMDc2NC03LjE4MDUgMTAuODIzLTYuNDUyIDIuNDk3NiAwLjUyMDMzIDQuNTc4OSAyLjcwNTcgNi4xMzk4IDYuNzY0MiAxLjI0ODggMy4yMjYgMS44NzMyIDYuNDUyIDEuODczMiA5LjQ2OTkgMC4xMDQwNiAyLjcwNTctMC4yMDgxMyA1LjMwNzMtMC43Mjg0NiA3LjcwMDh6IiBmaWxsPSIjZmZmIi8+Cjwvc3ZnPgo=" + }, + "categories": [ + { + "id": 2, + "name": "Sales" + } + ], + "displayName": "WooCommerce Trigger", + "typeVersion": 1 + } + ], + "categories": [ + { + "id": 2, + "name": "Sales" + } + ], + "image": [] + }, + { + "id": 1765, + "name": "Get Slack notifications when new product published on WooCommerce", + "views": 79, + "recentViews": 9577, + "totalViews": 79, + "createdAt": "2022-08-12T12:36:53.409Z", + "description": "This workflow let's a bot in Slack notify a specific channel when a new product in WooCommerce is published and live on the site. \n\n## Prerequisites\n\n[WooCommerce](https://docs.n8n.io/integrations/builtin/trigger-nodes/n8n-nodes-base.woocommercetrigger/) account\n[Slack](https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.slack/) and a [Slack bot](https://slack.com/help/articles/115005265703-Create-a-bot-for-your-workspace)\n\n## How it works\n\n1. Listen for WooCommerce product creation\n2. If permalink starts with https://[your-url-here].com/product/\n3. Slack bot notifies channel that a new product has been added. \n\nPlease note, you must update the URL in the IF node to match your url. If your WooCommerce doesn't use the slug /product/, that will need to be updated too. \n", + "workflow": { + "id": 1016, + "name": "Woocommerce to slack: notify new product created", + "tags": [ + { + "id": "5", + "name": "FVF", + "createdAt": "2022-07-30T07:43:44.795Z", + "updatedAt": "2022-07-30T07:43:44.795Z" + } + ], + "nodes": [ + { + "name": "If URL has /product/", + "type": "n8n-nodes-base.if", + "position": [ + 640, + 300 + ], + "parameters": { + "conditions": { + "string": [ + { + "value1": "={{$json[\"permalink\"]}}", + "value2": "https://[add-your-url-here]/product/", + "operation": "startsWith" + } + ] + } + }, + "typeVersion": 1 + }, + { + "name": "Send message to slack", + "type": "n8n-nodes-base.slack", + "position": [ + 920, + 260 + ], + "parameters": { + "text": ":new: A new product has been added! :new:", + "channel": "newproducts", + "blocksUi": { + "blocksValues": [] + }, + "attachments": [ + { + "color": "#66FF00", + "fields": { + "item": [ + { + "short": false, + "title": "Name", + "value": "={{$json[\"name\"]}}" + }, + { + "short": true, + "title": "Price", + "value": "={{$json[\"regular_price\"]}}" + }, + { + "short": true, + "title": "Sale Price", + "value": "={{$json[\"sale_price\"]}}" + }, + { + "short": false, + "title": "Link", + "value": "={{$json[\"permalink\"]}}" + } + ] + }, + "footer": "=Added: {{$json[\"date_created\"]}}" + } + ], + "otherOptions": {} + }, + "credentials": { + "slackApi": { + "id": "21", + "name": "FVF bot" + } + }, + "typeVersion": 1 + }, + { + "name": "On product creation", + "type": "n8n-nodes-base.wooCommerceTrigger", + "position": [ + 460, + 300 + ], + "webhookId": "267c4855-6227-4d33-867e-74600097473e", + "parameters": { + "event": "product.created" + }, + "credentials": { + "wooCommerceApi": { + "id": "20", + "name": "WooCommerce account FVF" + } + }, + "typeVersion": 1 + } + ], + "active": true, + "settings": {}, + "connections": { + "On product creation": { + "main": [ + [ + { + "node": "If URL has /product/", + "type": "main", + "index": 0 + } + ] + ] + }, + "If URL has /product/": { + "main": [ + [ + { + "node": "Send message to slack", + "type": "main", + "index": 0 + } + ] + ] + } + } + }, + "workflowInfo": { + "nodeCount": 4, + "nodeTypes": { + "n8n-nodes-base.if": { + "count": 1 + }, + "n8n-nodes-base.slack": { + "count": 1 + }, + "n8n-nodes-base.wooCommerceTrigger": { + "count": 1 + } + } + }, + "user": { + "username": "n8n-team" + }, + "nodes": [ + { + "id": 20, + "icon": "fa:map-signs", + "name": "n8n-nodes-base.if", + "defaults": { + "name": "IF", + "color": "#408000" + }, + "iconData": { + "icon": "map-signs", + "type": "icon" + }, + "categories": [ + { + "id": 9, + "name": "Core Nodes" + } + ], + "displayName": "IF", + "typeVersion": 1 + }, + { + "id": 40, + "icon": "file:slack.svg", + "name": "n8n-nodes-base.slack", + "defaults": { + "name": "Slack" + }, + "iconData": { + "type": "file", + "fileBuffer": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgMTUwLjg1MiAxNTAuODUyIiBmaWxsPSIjZmZmIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48dXNlIHhsaW5rOmhyZWY9IiNhIiB4PSIuOTI2IiB5PSIuOTI2Ii8+PHN5bWJvbCBpZD0iYSIgb3ZlcmZsb3c9InZpc2libGUiPjxnIHN0cm9rZS13aWR0aD0iMS44NTIiPjxwYXRoIGQ9Ik00MC43NDEgOTMuNTVjMC04LjczNSA2LjYwNy0xNS43NzIgMTQuODE1LTE1Ljc3MnMxNC44MTUgNy4wMzcgMTQuODE1IDE1Ljc3MnYzOC44MjRjMCA4LjczNy02LjYwNyAxNS43NzQtMTQuODE1IDE1Ljc3NHMtMTQuODE1LTcuMDM3LTE0LjgxNS0xNS43NzJ6IiBmaWxsPSIjZTAxZTVhIiBzdHJva2U9IiNlMDFlNWEiLz48cGF0aCBkPSJNOTMuNTUgMTA3LjQwOGMtOC43MzUgMC0xNS43NzItNi42MDctMTUuNzcyLTE0LjgxNXM3LjAzNy0xNC44MTUgMTUuNzcyLTE0LjgxNWgzOC44MjZjOC43MzUgMCAxNS43NzIgNi42MDcgMTUuNzcyIDE0LjgxNXMtNy4wMzcgMTQuODE1LTE1Ljc3MiAxNC44MTV6IiBmaWxsPSIjZWNiMjJkIiBzdHJva2U9IiNlY2IyMmQiLz48cGF0aCBkPSJNNzcuNzc4IDE1Ljc3MkM3Ny43NzggNy4wMzcgODQuMzg1IDAgOTIuNTkzIDBzMTQuODE1IDcuMDM3IDE0LjgxNSAxNS43NzJ2MzguODI2YzAgOC43MzUtNi42MDcgMTUuNzcyLTE0LjgxNSAxNS43NzJzLTE0LjgxNS03LjAzNy0xNC44MTUtMTUuNzcyeiIgZmlsbD0iIzJmYjY3YyIgc3Ryb2tlPSIjMmZiNjdjIi8+PHBhdGggZD0iTTE1Ljc3MiA3MC4zNzFDNy4wMzcgNzAuMzcxIDAgNjMuNzYzIDAgNTUuNTU2czcuMDM3LTE0LjgxNSAxNS43NzItMTQuODE1aDM4LjgyNmM4LjczNSAwIDE1Ljc3MiA2LjYwNyAxNS43NzIgMTQuODE1cy03LjAzNyAxNC44MTUtMTUuNzcyIDE0LjgxNXoiIGZpbGw9IiMzNmM1ZjEiIHN0cm9rZT0iIzM2YzVmMSIvPjxnIHN0cm9rZS1saW5lam9pbj0ibWl0ZXIiPjxwYXRoIGQ9Ik03Ny43NzggMTMzLjMzM2MwIDguMjA4IDYuNjA3IDE0LjgxNSAxNC44MTUgMTQuODE1czE0LjgxNS02LjYwNyAxNC44MTUtMTQuODE1LTYuNjA3LTE0LjgxNS0xNC44MTUtMTQuODE1SDc3Ljc3OHoiIGZpbGw9IiNlY2IyMmQiIHN0cm9rZT0iI2VjYjIyZCIvPjxwYXRoIGQ9Ik0xMzMuMzM0IDcwLjM3MWgtMTQuODE1VjU1LjU1NmMwLTguMjA3IDYuNjA3LTE0LjgxNSAxNC44MTUtMTQuODE1czE0LjgxNSA2LjYwNyAxNC44MTUgMTQuODE1LTYuNjA3IDE0LjgxNS0xNC44MTUgMTQuODE1eiIgZmlsbD0iIzJmYjY3YyIgc3Ryb2tlPSIjMmZiNjdjIi8+PHBhdGggZD0iTTE0LjgxNSA3Ny43NzhIMjkuNjN2MTQuODE1YzAgOC4yMDctNi42MDcgMTQuODE1LTE0LjgxNSAxNC44MTVTMCAxMDAuOCAwIDkyLjU5M3M2LjYwNy0xNC44MTUgMTQuODE1LTE0LjgxNXoiIGZpbGw9IiNlMDFlNWEiIHN0cm9rZT0iI2UwMWU1YSIvPjxwYXRoIGQ9Ik03MC4zNzEgMTQuODE1VjI5LjYzSDU1LjU1NmMtOC4yMDcgMC0xNC44MTUtNi42MDctMTQuODE1LTE0LjgxNVM0Ny4zNDggMCA1NS41NTYgMHMxNC44MTUgNi42MDcgMTQuODE1IDE0LjgxNXoiIGZpbGw9IiMzNmM1ZjEiIHN0cm9rZT0iIzM2YzVmMSIvPjwvZz48L2c+PC9zeW1ib2w+PC9zdmc+" + }, + "categories": [ + { + "id": 6, + "name": "Communication" + } + ], + "displayName": "Slack", + "typeVersion": 2 + }, + { + "id": 42, + "icon": "fa:play", + "name": "n8n-nodes-base.start", + "defaults": { + "name": "Start", + "color": "#00e000" + }, + "iconData": { + "icon": "play", + "type": "icon" + }, + "categories": [ + { + "id": 9, + "name": "Core Nodes" + } + ], + "displayName": "Start", + "typeVersion": 1 + }, + { + "id": 235, + "icon": "file:wooCommerce.svg", + "name": "n8n-nodes-base.wooCommerceTrigger", + "defaults": { + "name": "WooCommerce Trigger" + }, + "iconData": { + "type": "file", + "fileBuffer": "data:image/svg+xml;base64,PHN2ZyBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWlkWU1pZCIgdmVyc2lvbj0iMS4xIiB2aWV3Qm94PSIwIC01MCAyNTYgMjUzIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KPHRpdGxlPldvb0NvbW1lcmNlIExvZ288L3RpdGxlPgo8bWV0YWRhdGE+CjxyZGY6UkRGPgo8Y2M6V29yayByZGY6YWJvdXQ9IiI+CjxkYzpmb3JtYXQ+aW1hZ2Uvc3ZnK3htbDwvZGM6Zm9ybWF0Pgo8ZGM6dHlwZSByZGY6cmVzb3VyY2U9Imh0dHA6Ly9wdXJsLm9yZy9kYy9kY21pdHlwZS9TdGlsbEltYWdlIi8+CjxkYzp0aXRsZS8+CjwvY2M6V29yaz4KPC9yZGY6UkRGPgo8L21ldGFkYXRhPgo8cGF0aCBkPSJtMjMuNzU5IDBoMjA4LjM4YzEzLjE4NyAwIDIzLjg2MyAxMC42NzUgMjMuODYzIDIzLjg2M3Y3OS41NDJjMCAxMy4xODctMTAuNjc1IDIzLjg2My0yMy44NjMgMjMuODYzaC03NC43MjdsMTAuMjU3IDI1LjExOC00NS4xMDktMjUuMTE4aC05OC42OTVjLTEzLjE4NyAwLTIzLjg2My0xMC42NzUtMjMuODYzLTIzLjg2M3YtNzkuNTQyYy0wLjEwNDY2LTEzLjA4MyAxMC41NzEtMjMuODYzIDIzLjc1OC0yMy44NjN6IiBmaWxsPSIjN2Y1NGIzIi8+CjxwYXRoIGQ9Im0xNC41NzggMjEuNzVjMS40NTY5LTEuOTc3MiAzLjY0MjMtMy4wMTc5IDYuNTU2MS0zLjIyNiA1LjMwNzMtMC40MTYyNiA4LjMyNTIgMi4wODEzIDkuMDUzNyA3LjQ5MjcgMy4yMjYgMjEuNzUgNi43NjQyIDQwLjE2OSAxMC41MTEgNTUuMjU5bDIyLjc5LTQzLjM5NWMyLjA4MTMtMy45NTQ1IDQuNjgyOS02LjAzNTggNy44MDQ5LTYuMjQzOSA0LjU3ODktMC4zMTIyIDcuMzg4NiAyLjYwMTYgOC41MzMzIDguNzQxNSAyLjYwMTYgMTMuODQxIDUuOTMxNyAyNS42IDkuODg2MiAzNS41OSAyLjcwNTctMjYuNDMzIDcuMjg0Ni00NS40NzYgMTMuNzM3LTU3LjIzNiAxLjU2MS0yLjkxMzggMy44NTA0LTQuMzcwNyA2Ljg2ODMtNC41Nzg5IDIuMzkzNS0wLjIwODEzIDQuNTc4OSAwLjUyMDMzIDYuNTU2MSAyLjA4MTMgMS45NzcyIDEuNTYxIDMuMDE3OSAzLjUzODIgMy4yMjYgNS45MzE3IDAuMTA0MDYgMS44NzMyLTAuMjA4MTMgMy40MzQxLTEuMDQwNyA0Ljk5NTEtNC4wNTg1IDcuNDkyNy03LjM4ODYgMjAuMDg1LTEwLjA5NCAzNy41NjctMi42MDE2IDE2Ljk2My0zLjUzODIgMzAuMTc5LTIuOTEzOCAzOS42NDkgMC4yMDgxMyAyLjYwMTYtMC4yMDgxMyA0Ljg5MTEtMS4yNDg4IDYuODY4My0xLjI0ODggMi4yODk0LTMuMTIyIDMuNTM4Mi01LjUxNTQgMy43NDYzLTIuNzA1NyAwLjIwODEzLTUuNTE1NC0xLjA0MDYtOC4yMjExLTMuODUwNC05LjY3OC05Ljg4NjItMTcuMzc5LTI0LjY2My0yMi45OTgtNDQuMzMyLTYuNzY0MiAxMy4zMi0xMS43NTkgMjMuMzExLTE0Ljk4NSAyOS45NzEtNi4xMzk4IDExLjc1OS0xMS4zNDMgMTcuNzk1LTE1LjcxNCAxOC4xMDctMi44MDk4IDAuMjA4MTMtNS4yMDMzLTIuMTg1NC03LjI4NDYtNy4xODA1LTUuMzA3My0xMy42MzMtMTEuMDMxLTM5Ljk2MS0xNy4xNzEtNzguOTg1LTAuNDE2MjYtMi43MDU3IDAuMjA4MTMtNS4wOTkyIDEuNjY1LTYuOTcyNHptMjIzLjY0IDE2LjMzOGMtMy43NDYzLTYuNTU2MS05LjI2MTgtMTAuNTExLTE2LjY1LTEyLjA3Mi0xLjk3NzItMC40MTYyNi0zLjg1MDQtMC42MjQzOS01LjYxOTUtMC42MjQzOS05Ljk5MDIgMC0xOC4xMDcgNS4yMDMzLTI0LjQ1NSAxNS42MS01LjQxMTQgOC44NDU1LTguMTE3MSAxOC42MjgtOC4xMTcxIDI5LjM0NiAwIDguMDEzIDEuNjY1IDE0Ljg4MSA0Ljk5NTEgMjAuNjA1IDMuNzQ2MyA2LjU1NjEgOS4yNjE4IDEwLjUxMSAxNi42NSAxMi4wNzIgMS45NzcyIDAuNDE2MjYgMy44NTA0IDAuNjI0MzkgNS42MTk1IDAuNjI0MzkgMTAuMDk0IDAgMTguMjExLTUuMjAzMyAyNC40NTUtMTUuNjEgNS40MTE0LTguOTQ5NiA4LjExNzEtMTguNzMyIDguMTE3MS0yOS40NSAwLjEwNDA2LTguMTE3MS0xLjY2NS0xNC44ODEtNC45OTUxLTIwLjUwMXptLTEzLjExMiAyOC44MjZjLTEuNDU2OSA2Ljg2ODMtNC4wNTg1IDExLjk2Ny03LjkwODkgMTUuNDAyLTMuMDE3OSAyLjcwNTctNS44Mjc2IDMuODUwNC04LjQyOTMgMy4zMzAxLTIuNDk3Ni0wLjUyMDMzLTQuNTc4OS0yLjcwNTctNi4xMzk4LTYuNzY0Mi0xLjI0ODgtMy4yMjYtMS44NzMyLTYuNDUyLTEuODczMi05LjQ2OTkgMC0yLjYwMTYgMC4yMDgxMy01LjIwMzMgMC43Mjg0Ni03LjU5NjcgMC45MzY1OS00LjI2NjcgMi43MDU3LTguNDI5MyA1LjUxNTQtMTIuMzg0IDMuNDM0MS01LjA5OTIgNy4wNzY0LTcuMTgwNSAxMC44MjMtNi40NTIgMi40OTc2IDAuNTIwMzMgNC41Nzg5IDIuNzA1NyA2LjEzOTggNi43NjQyIDEuMjQ4OCAzLjIyNiAxLjg3MzIgNi40NTIgMS44NzMyIDkuNDY5OSAwIDIuNzA1Ny0wLjIwODEzIDUuMzA3My0wLjcyODQ2IDcuNzAwOHptLTUyLjAzMy0yOC44MjZjLTMuNzQ2My02LjU1NjEtOS4zNjU5LTEwLjUxMS0xNi42NS0xMi4wNzItMS45NzcyLTAuNDE2MjYtMy44NTA0LTAuNjI0MzktNS42MTk1LTAuNjI0MzktOS45OTAyIDAtMTguMTA3IDUuMjAzMy0yNC40NTUgMTUuNjEtNS40MTE0IDguODQ1NS04LjExNzEgMTguNjI4LTguMTE3MSAyOS4zNDYgMCA4LjAxMyAxLjY2NSAxNC44ODEgNC45OTUxIDIwLjYwNSAzLjc0NjMgNi41NTYxIDkuMjYxOCAxMC41MTEgMTYuNjUgMTIuMDcyIDEuOTc3MiAwLjQxNjI2IDMuODUwNCAwLjYyNDM5IDUuNjE5NSAwLjYyNDM5IDEwLjA5NCAwIDE4LjIxMS01LjIwMzMgMjQuNDU1LTE1LjYxIDUuNDExNC04Ljk0OTYgOC4xMTcxLTE4LjczMiA4LjExNzEtMjkuNDUgMC04LjExNzEtMS42NjUtMTQuODgxLTQuOTk1MS0yMC41MDF6bS0xMy4yMTYgMjguODI2Yy0xLjQ1NjkgNi44NjgzLTQuMDU4NSAxMS45NjctNy45MDg5IDE1LjQwMi0zLjAxNzkgMi43MDU3LTUuODI3NiAzLjg1MDQtOC40MjkzIDMuMzMwMS0yLjQ5NzYtMC41MjAzMy00LjU3ODktMi43MDU3LTYuMTM5OC02Ljc2NDItMS4yNDg4LTMuMjI2LTEuODczMi02LjQ1Mi0xLjg3MzItOS40Njk5IDAtMi42MDE2IDAuMjA4MTMtNS4yMDMzIDAuNzI4NDYtNy41OTY3IDAuOTM2NTgtNC4yNjY3IDIuNzA1Ny04LjQyOTMgNS41MTU0LTEyLjM4NCAzLjQzNDEtNS4wOTkyIDcuMDc2NC03LjE4MDUgMTAuODIzLTYuNDUyIDIuNDk3NiAwLjUyMDMzIDQuNTc4OSAyLjcwNTcgNi4xMzk4IDYuNzY0MiAxLjI0ODggMy4yMjYgMS44NzMyIDYuNDUyIDEuODczMiA5LjQ2OTkgMC4xMDQwNiAyLjcwNTctMC4yMDgxMyA1LjMwNzMtMC43Mjg0NiA3LjcwMDh6IiBmaWxsPSIjZmZmIi8+Cjwvc3ZnPgo=" + }, + "categories": [ + { + "id": 2, + "name": "Sales" + } + ], + "displayName": "WooCommerce Trigger", + "typeVersion": 1 + } + ], + "categories": [ + { + "id": 2, + "name": "Sales" + } + ], + "image": [] + }, + { + "id": 1460, + "name": "Notify on Slack when refund is registered in WooCommerce", + "views": 85, + "recentViews": 9541, + "totalViews": 85, + "createdAt": "2022-02-17T15:02:58.662Z", + "description": "This workflow uses a WooCommerce trigger that will run when an order has been updated and the status is refunded.\n\nIf the value of this is over 100 it will post it to a Slack channel.\n\nTo use this workflow you will need to set the credentials to use for the WooCommerce and Slack nodes, You will also need to pick a channel to post the message to.", + "workflow": { + "id": 82, + "name": "New WooCommerce refund to Slack", + "nodes": [ + { + "name": "Order Updated", + "type": "n8n-nodes-base.wooCommerceTrigger", + "position": [ + 320, + 500 + ], + "webhookId": "f7736be3-e978-4a17-b936-7ce9f8ccdb72", + "parameters": { + "event": "order.updated" + }, + "credentials": { + "wooCommerceApi": { + "id": "48", + "name": "WooCommerce account" + } + }, + "typeVersion": 1 + }, + { + "name": "If Refund and Over 100", + "type": "n8n-nodes-base.if", + "position": [ + 540, + 500 + ], + "parameters": { + "conditions": { + "number": [ + { + "value1": "={{$json[\"total\"]}}", + "value2": 100, + "operation": "largerEqual" + } + ], + "string": [ + { + "value1": "={{$json[\"status\"]}}", + "value2": "refunded" + } + ] + } + }, + "typeVersion": 1 + }, + { + "name": "Send to Slack", + "type": "n8n-nodes-base.slack", + "position": [ + 780, + 480 + ], + "parameters": { + "text": ":x: A refund has been issued :x:", + "channel": "woo-commerce", + "blocksUi": { + "blocksValues": [] + }, + "attachments": [ + { + "color": "#FF0000", + "fields": { + "item": [ + { + "short": true, + "title": "Order ID", + "value": "={{$json[\"id\"]}}" + }, + { + "short": true, + "title": "Status", + "value": "={{$json[\"status\"]}}" + }, + { + "short": true, + "title": "Total", + "value": "={{$json[\"currency_symbol\"]}}{{$json[\"total\"]}}" + } + ] + }, + "footer": "=*Order updated:* {{$json[\"date_modified\"]}}" + } + ], + "otherOptions": {} + }, + "credentials": { + "slackApi": { + "id": "53", + "name": "Slack Access Token" + } + }, + "typeVersion": 1 + } + ], + "active": false, + "settings": { + "saveManualExecutions": true, + "saveExecutionProgress": true, + "saveDataSuccessExecution": "all" + }, + "connections": { + "Order Updated": { + "main": [ + [ + { + "node": "If Refund and Over 100", + "type": "main", + "index": 0 + } + ] + ] + }, + "If Refund and Over 100": { + "main": [ + [ + { + "node": "Send to Slack", + "type": "main", + "index": 0 + } + ], + [] + ] + } + } + }, + "workflowInfo": { + "nodeCount": 4, + "nodeTypes": { + "n8n-nodes-base.if": { + "count": 1 + }, + "n8n-nodes-base.slack": { + "count": 1 + }, + "n8n-nodes-base.wooCommerceTrigger": { + "count": 1 + } + } + }, + "user": { + "username": "jon-n8n" + }, + "nodes": [ + { + "id": 20, + "icon": "fa:map-signs", + "name": "n8n-nodes-base.if", + "defaults": { + "name": "IF", + "color": "#408000" + }, + "iconData": { + "icon": "map-signs", + "type": "icon" + }, + "categories": [ + { + "id": 9, + "name": "Core Nodes" + } + ], + "displayName": "IF", + "typeVersion": 1 + }, + { + "id": 40, + "icon": "file:slack.svg", + "name": "n8n-nodes-base.slack", + "defaults": { + "name": "Slack" + }, + "iconData": { + "type": "file", + "fileBuffer": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgMTUwLjg1MiAxNTAuODUyIiBmaWxsPSIjZmZmIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48dXNlIHhsaW5rOmhyZWY9IiNhIiB4PSIuOTI2IiB5PSIuOTI2Ii8+PHN5bWJvbCBpZD0iYSIgb3ZlcmZsb3c9InZpc2libGUiPjxnIHN0cm9rZS13aWR0aD0iMS44NTIiPjxwYXRoIGQ9Ik00MC43NDEgOTMuNTVjMC04LjczNSA2LjYwNy0xNS43NzIgMTQuODE1LTE1Ljc3MnMxNC44MTUgNy4wMzcgMTQuODE1IDE1Ljc3MnYzOC44MjRjMCA4LjczNy02LjYwNyAxNS43NzQtMTQuODE1IDE1Ljc3NHMtMTQuODE1LTcuMDM3LTE0LjgxNS0xNS43NzJ6IiBmaWxsPSIjZTAxZTVhIiBzdHJva2U9IiNlMDFlNWEiLz48cGF0aCBkPSJNOTMuNTUgMTA3LjQwOGMtOC43MzUgMC0xNS43NzItNi42MDctMTUuNzcyLTE0LjgxNXM3LjAzNy0xNC44MTUgMTUuNzcyLTE0LjgxNWgzOC44MjZjOC43MzUgMCAxNS43NzIgNi42MDcgMTUuNzcyIDE0LjgxNXMtNy4wMzcgMTQuODE1LTE1Ljc3MiAxNC44MTV6IiBmaWxsPSIjZWNiMjJkIiBzdHJva2U9IiNlY2IyMmQiLz48cGF0aCBkPSJNNzcuNzc4IDE1Ljc3MkM3Ny43NzggNy4wMzcgODQuMzg1IDAgOTIuNTkzIDBzMTQuODE1IDcuMDM3IDE0LjgxNSAxNS43NzJ2MzguODI2YzAgOC43MzUtNi42MDcgMTUuNzcyLTE0LjgxNSAxNS43NzJzLTE0LjgxNS03LjAzNy0xNC44MTUtMTUuNzcyeiIgZmlsbD0iIzJmYjY3YyIgc3Ryb2tlPSIjMmZiNjdjIi8+PHBhdGggZD0iTTE1Ljc3MiA3MC4zNzFDNy4wMzcgNzAuMzcxIDAgNjMuNzYzIDAgNTUuNTU2czcuMDM3LTE0LjgxNSAxNS43NzItMTQuODE1aDM4LjgyNmM4LjczNSAwIDE1Ljc3MiA2LjYwNyAxNS43NzIgMTQuODE1cy03LjAzNyAxNC44MTUtMTUuNzcyIDE0LjgxNXoiIGZpbGw9IiMzNmM1ZjEiIHN0cm9rZT0iIzM2YzVmMSIvPjxnIHN0cm9rZS1saW5lam9pbj0ibWl0ZXIiPjxwYXRoIGQ9Ik03Ny43NzggMTMzLjMzM2MwIDguMjA4IDYuNjA3IDE0LjgxNSAxNC44MTUgMTQuODE1czE0LjgxNS02LjYwNyAxNC44MTUtMTQuODE1LTYuNjA3LTE0LjgxNS0xNC44MTUtMTQuODE1SDc3Ljc3OHoiIGZpbGw9IiNlY2IyMmQiIHN0cm9rZT0iI2VjYjIyZCIvPjxwYXRoIGQ9Ik0xMzMuMzM0IDcwLjM3MWgtMTQuODE1VjU1LjU1NmMwLTguMjA3IDYuNjA3LTE0LjgxNSAxNC44MTUtMTQuODE1czE0LjgxNSA2LjYwNyAxNC44MTUgMTQuODE1LTYuNjA3IDE0LjgxNS0xNC44MTUgMTQuODE1eiIgZmlsbD0iIzJmYjY3YyIgc3Ryb2tlPSIjMmZiNjdjIi8+PHBhdGggZD0iTTE0LjgxNSA3Ny43NzhIMjkuNjN2MTQuODE1YzAgOC4yMDctNi42MDcgMTQuODE1LTE0LjgxNSAxNC44MTVTMCAxMDAuOCAwIDkyLjU5M3M2LjYwNy0xNC44MTUgMTQuODE1LTE0LjgxNXoiIGZpbGw9IiNlMDFlNWEiIHN0cm9rZT0iI2UwMWU1YSIvPjxwYXRoIGQ9Ik03MC4zNzEgMTQuODE1VjI5LjYzSDU1LjU1NmMtOC4yMDcgMC0xNC44MTUtNi42MDctMTQuODE1LTE0LjgxNVM0Ny4zNDggMCA1NS41NTYgMHMxNC44MTUgNi42MDcgMTQuODE1IDE0LjgxNXoiIGZpbGw9IiMzNmM1ZjEiIHN0cm9rZT0iIzM2YzVmMSIvPjwvZz48L2c+PC9zeW1ib2w+PC9zdmc+" + }, + "categories": [ + { + "id": 6, + "name": "Communication" + } + ], + "displayName": "Slack", + "typeVersion": 2 + }, + { + "id": 42, + "icon": "fa:play", + "name": "n8n-nodes-base.start", + "defaults": { + "name": "Start", + "color": "#00e000" + }, + "iconData": { + "icon": "play", + "type": "icon" + }, + "categories": [ + { + "id": 9, + "name": "Core Nodes" + } + ], + "displayName": "Start", + "typeVersion": 1 + }, + { + "id": 235, + "icon": "file:wooCommerce.svg", + "name": "n8n-nodes-base.wooCommerceTrigger", + "defaults": { + "name": "WooCommerce Trigger" + }, + "iconData": { + "type": "file", + "fileBuffer": "data:image/svg+xml;base64,PHN2ZyBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWlkWU1pZCIgdmVyc2lvbj0iMS4xIiB2aWV3Qm94PSIwIC01MCAyNTYgMjUzIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KPHRpdGxlPldvb0NvbW1lcmNlIExvZ288L3RpdGxlPgo8bWV0YWRhdGE+CjxyZGY6UkRGPgo8Y2M6V29yayByZGY6YWJvdXQ9IiI+CjxkYzpmb3JtYXQ+aW1hZ2Uvc3ZnK3htbDwvZGM6Zm9ybWF0Pgo8ZGM6dHlwZSByZGY6cmVzb3VyY2U9Imh0dHA6Ly9wdXJsLm9yZy9kYy9kY21pdHlwZS9TdGlsbEltYWdlIi8+CjxkYzp0aXRsZS8+CjwvY2M6V29yaz4KPC9yZGY6UkRGPgo8L21ldGFkYXRhPgo8cGF0aCBkPSJtMjMuNzU5IDBoMjA4LjM4YzEzLjE4NyAwIDIzLjg2MyAxMC42NzUgMjMuODYzIDIzLjg2M3Y3OS41NDJjMCAxMy4xODctMTAuNjc1IDIzLjg2My0yMy44NjMgMjMuODYzaC03NC43MjdsMTAuMjU3IDI1LjExOC00NS4xMDktMjUuMTE4aC05OC42OTVjLTEzLjE4NyAwLTIzLjg2My0xMC42NzUtMjMuODYzLTIzLjg2M3YtNzkuNTQyYy0wLjEwNDY2LTEzLjA4MyAxMC41NzEtMjMuODYzIDIzLjc1OC0yMy44NjN6IiBmaWxsPSIjN2Y1NGIzIi8+CjxwYXRoIGQ9Im0xNC41NzggMjEuNzVjMS40NTY5LTEuOTc3MiAzLjY0MjMtMy4wMTc5IDYuNTU2MS0zLjIyNiA1LjMwNzMtMC40MTYyNiA4LjMyNTIgMi4wODEzIDkuMDUzNyA3LjQ5MjcgMy4yMjYgMjEuNzUgNi43NjQyIDQwLjE2OSAxMC41MTEgNTUuMjU5bDIyLjc5LTQzLjM5NWMyLjA4MTMtMy45NTQ1IDQuNjgyOS02LjAzNTggNy44MDQ5LTYuMjQzOSA0LjU3ODktMC4zMTIyIDcuMzg4NiAyLjYwMTYgOC41MzMzIDguNzQxNSAyLjYwMTYgMTMuODQxIDUuOTMxNyAyNS42IDkuODg2MiAzNS41OSAyLjcwNTctMjYuNDMzIDcuMjg0Ni00NS40NzYgMTMuNzM3LTU3LjIzNiAxLjU2MS0yLjkxMzggMy44NTA0LTQuMzcwNyA2Ljg2ODMtNC41Nzg5IDIuMzkzNS0wLjIwODEzIDQuNTc4OSAwLjUyMDMzIDYuNTU2MSAyLjA4MTMgMS45NzcyIDEuNTYxIDMuMDE3OSAzLjUzODIgMy4yMjYgNS45MzE3IDAuMTA0MDYgMS44NzMyLTAuMjA4MTMgMy40MzQxLTEuMDQwNyA0Ljk5NTEtNC4wNTg1IDcuNDkyNy03LjM4ODYgMjAuMDg1LTEwLjA5NCAzNy41NjctMi42MDE2IDE2Ljk2My0zLjUzODIgMzAuMTc5LTIuOTEzOCAzOS42NDkgMC4yMDgxMyAyLjYwMTYtMC4yMDgxMyA0Ljg5MTEtMS4yNDg4IDYuODY4My0xLjI0ODggMi4yODk0LTMuMTIyIDMuNTM4Mi01LjUxNTQgMy43NDYzLTIuNzA1NyAwLjIwODEzLTUuNTE1NC0xLjA0MDYtOC4yMjExLTMuODUwNC05LjY3OC05Ljg4NjItMTcuMzc5LTI0LjY2My0yMi45OTgtNDQuMzMyLTYuNzY0MiAxMy4zMi0xMS43NTkgMjMuMzExLTE0Ljk4NSAyOS45NzEtNi4xMzk4IDExLjc1OS0xMS4zNDMgMTcuNzk1LTE1LjcxNCAxOC4xMDctMi44MDk4IDAuMjA4MTMtNS4yMDMzLTIuMTg1NC03LjI4NDYtNy4xODA1LTUuMzA3My0xMy42MzMtMTEuMDMxLTM5Ljk2MS0xNy4xNzEtNzguOTg1LTAuNDE2MjYtMi43MDU3IDAuMjA4MTMtNS4wOTkyIDEuNjY1LTYuOTcyNHptMjIzLjY0IDE2LjMzOGMtMy43NDYzLTYuNTU2MS05LjI2MTgtMTAuNTExLTE2LjY1LTEyLjA3Mi0xLjk3NzItMC40MTYyNi0zLjg1MDQtMC42MjQzOS01LjYxOTUtMC42MjQzOS05Ljk5MDIgMC0xOC4xMDcgNS4yMDMzLTI0LjQ1NSAxNS42MS01LjQxMTQgOC44NDU1LTguMTE3MSAxOC42MjgtOC4xMTcxIDI5LjM0NiAwIDguMDEzIDEuNjY1IDE0Ljg4MSA0Ljk5NTEgMjAuNjA1IDMuNzQ2MyA2LjU1NjEgOS4yNjE4IDEwLjUxMSAxNi42NSAxMi4wNzIgMS45NzcyIDAuNDE2MjYgMy44NTA0IDAuNjI0MzkgNS42MTk1IDAuNjI0MzkgMTAuMDk0IDAgMTguMjExLTUuMjAzMyAyNC40NTUtMTUuNjEgNS40MTE0LTguOTQ5NiA4LjExNzEtMTguNzMyIDguMTE3MS0yOS40NSAwLjEwNDA2LTguMTE3MS0xLjY2NS0xNC44ODEtNC45OTUxLTIwLjUwMXptLTEzLjExMiAyOC44MjZjLTEuNDU2OSA2Ljg2ODMtNC4wNTg1IDExLjk2Ny03LjkwODkgMTUuNDAyLTMuMDE3OSAyLjcwNTctNS44Mjc2IDMuODUwNC04LjQyOTMgMy4zMzAxLTIuNDk3Ni0wLjUyMDMzLTQuNTc4OS0yLjcwNTctNi4xMzk4LTYuNzY0Mi0xLjI0ODgtMy4yMjYtMS44NzMyLTYuNDUyLTEuODczMi05LjQ2OTkgMC0yLjYwMTYgMC4yMDgxMy01LjIwMzMgMC43Mjg0Ni03LjU5NjcgMC45MzY1OS00LjI2NjcgMi43MDU3LTguNDI5MyA1LjUxNTQtMTIuMzg0IDMuNDM0MS01LjA5OTIgNy4wNzY0LTcuMTgwNSAxMC44MjMtNi40NTIgMi40OTc2IDAuNTIwMzMgNC41Nzg5IDIuNzA1NyA2LjEzOTggNi43NjQyIDEuMjQ4OCAzLjIyNiAxLjg3MzIgNi40NTIgMS44NzMyIDkuNDY5OSAwIDIuNzA1Ny0wLjIwODEzIDUuMzA3My0wLjcyODQ2IDcuNzAwOHptLTUyLjAzMy0yOC44MjZjLTMuNzQ2My02LjU1NjEtOS4zNjU5LTEwLjUxMS0xNi42NS0xMi4wNzItMS45NzcyLTAuNDE2MjYtMy44NTA0LTAuNjI0MzktNS42MTk1LTAuNjI0MzktOS45OTAyIDAtMTguMTA3IDUuMjAzMy0yNC40NTUgMTUuNjEtNS40MTE0IDguODQ1NS04LjExNzEgMTguNjI4LTguMTE3MSAyOS4zNDYgMCA4LjAxMyAxLjY2NSAxNC44ODEgNC45OTUxIDIwLjYwNSAzLjc0NjMgNi41NTYxIDkuMjYxOCAxMC41MTEgMTYuNjUgMTIuMDcyIDEuOTc3MiAwLjQxNjI2IDMuODUwNCAwLjYyNDM5IDUuNjE5NSAwLjYyNDM5IDEwLjA5NCAwIDE4LjIxMS01LjIwMzMgMjQuNDU1LTE1LjYxIDUuNDExNC04Ljk0OTYgOC4xMTcxLTE4LjczMiA4LjExNzEtMjkuNDUgMC04LjExNzEtMS42NjUtMTQuODgxLTQuOTk1MS0yMC41MDF6bS0xMy4yMTYgMjguODI2Yy0xLjQ1NjkgNi44NjgzLTQuMDU4NSAxMS45NjctNy45MDg5IDE1LjQwMi0zLjAxNzkgMi43MDU3LTUuODI3NiAzLjg1MDQtOC40MjkzIDMuMzMwMS0yLjQ5NzYtMC41MjAzMy00LjU3ODktMi43MDU3LTYuMTM5OC02Ljc2NDItMS4yNDg4LTMuMjI2LTEuODczMi02LjQ1Mi0xLjg3MzItOS40Njk5IDAtMi42MDE2IDAuMjA4MTMtNS4yMDMzIDAuNzI4NDYtNy41OTY3IDAuOTM2NTgtNC4yNjY3IDIuNzA1Ny04LjQyOTMgNS41MTU0LTEyLjM4NCAzLjQzNDEtNS4wOTkyIDcuMDc2NC03LjE4MDUgMTAuODIzLTYuNDUyIDIuNDk3NiAwLjUyMDMzIDQuNTc4OSAyLjcwNTcgNi4xMzk4IDYuNzY0MiAxLjI0ODggMy4yMjYgMS44NzMyIDYuNDUyIDEuODczMiA5LjQ2OTkgMC4xMDQwNiAyLjcwNTctMC4yMDgxMyA1LjMwNzMtMC43Mjg0NiA3LjcwMDh6IiBmaWxsPSIjZmZmIi8+Cjwvc3ZnPgo=" + }, + "categories": [ + { + "id": 2, + "name": "Sales" + } + ], + "displayName": "WooCommerce Trigger", + "typeVersion": 1 + } + ], + "categories": [ + { + "id": 2, + "name": "Sales" + }, + { + "id": 8, + "name": "Finance & Accounting" + } + ], + "image": [] + } + ], + "image": [] + } +} diff --git a/cypress/fixtures/In_memory_vector_store_fake_embeddings.json b/cypress/fixtures/In_memory_vector_store_fake_embeddings.json new file mode 100644 index 0000000000..1f804bedb3 --- /dev/null +++ b/cypress/fixtures/In_memory_vector_store_fake_embeddings.json @@ -0,0 +1,347 @@ +{ + "name": "fake_embeddings", + "nodes": [ + { + "parameters": {}, + "id": "de3c1210-3be7-49a6-86ef-9435e661f23f", + "name": "When clicking ‘Test workflow’", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [ + 480, + 760 + ] + }, + { + "parameters": { + "jsonMode": "expressionData", + "jsonData": "={{ $('Code').item.json.city }}", + "options": {} + }, + "id": "de3cb132-14ef-426b-ad33-8365a93dd11f", + "name": "Default Data Loader", + "type": "@n8n/n8n-nodes-langchain.documentDefaultDataLoader", + "typeVersion": 1, + "position": [ + 1100, + 900 + ] + }, + { + "parameters": { + "jsCode": "const kyiv = `Kyiv (also Kiev)[a] is the capital and most populous city of Ukraine. It is in north-central Ukraine along the Dnieper River. As of 1 January 2022, its population was 2,952,301,[2] making Kyiv the seventh-most populous city in Europe.[11] Kyiv is an important industrial, scientific, educational, and cultural center in Eastern Europe. It is home to many high-tech industries, higher education institutions, and historical landmarks. The city has an extensive system of public transport and infrastructure, including the Kyiv Metro.\n\nThe city's name is said to derive from the name of Kyi, one of its four legendary founders. During its history, Kyiv, one of the oldest cities in Eastern Europe, passed through several stages of prominence and obscurity. The city probably existed as a commercial center as early as the 5th century. A Slavic settlement on the great trade route between Scandinavia and Constantinople, Kyiv was a tributary of the Khazars,[12] until its capture by the Varangians (Vikings) in the mid-9th century. Under Varangian rule, the city became a capital of Kievan Rus', the first East Slavic state. Completely destroyed during the Mongol invasions in 1240, the city lost most of its influence for the centuries to come. Coming under Lithuania, then Poland and then Russia, the city would grow from a frontier market into an important centre of Orthodox learning in the sixteenth century, and later of industry, commerce, and administration by the nineteenth.[1]\n\nThe city prospered again during the Russian Empire's Industrial Revolution in the late 19th century. In 1918, when the Ukrainian People's Republic declared independence from the Russian Republic after the October Revolution there, Kyiv became its capital. From the end of the Ukrainian-Soviet and Polish-Soviet wars in 1921, Kyiv was a city of the Ukrainian SSR, and made its capital in 1934. The city suffered significant destruction during World War II but quickly recovered in the postwar years, remaining the Soviet Union's third-largest city.\n\nFollowing the collapse of the Soviet Union and Ukrainian independence in 1991, Kyiv remained Ukraine's capital and experienced a steady influx of ethnic Ukrainian migrants from other regions of the country.[13] During the country's transformation to a market economy and electoral democracy, Kyiv has continued to be Ukraine's largest and wealthiest city. Its armament-dependent industrial output fell after the Soviet collapse, adversely affecting science and technology, but new sectors of the economy such as services and finance facilitated Kyiv's growth in salaries and investment, as well as providing continuous funding for the development of housing and urban infrastructure. Kyiv emerged as the most pro-Western region of Ukraine; parties advocating tighter integration with the European Union dominate during elections.`\n\nconst berlin = `Berlin[a] is the capital and largest city of Germany, both by area and by population.[11] Its more than 3.85 million inhabitants[12] make it the European Union's most populous city, as measured by population within city limits.[13] The city is also one of the states of Germany, and is the third smallest state in the country in terms of area. Berlin is surrounded by the state of Brandenburg, and Brandenburg's capital Potsdam is nearby. The urban area of Berlin has a population of over 4.5 million and is therefore the most populous urban area in Germany.[5][14] The Berlin-Brandenburg capital region has around 6.2 million inhabitants and is Germany's second-largest metropolitan region after the Rhine-Ruhr region, and the sixth-biggest metropolitan region by GDP in the European Union.[15]\n\nBerlin was built along the banks of the Spree river, which flows into the Havel in the western borough of Spandau. The city incorporates lakes in the western and southeastern boroughs, the largest of which is Müggelsee. About one-third of the city's area is composed of forests, parks and gardens, rivers, canals, and lakes.[16]\n\nFirst documented in the 13th century[10] and at the crossing of two important historic trade routes,[17] Berlin was designated the capital of the Margraviate of Brandenburg (1417–1701), Kingdom of Prussia (1701–1918), German Empire (1871–1918), Weimar Republic (1919–1933), and Nazi Germany (1933–1945). Berlin has served as a scientific, artistic, and philosophical hub during the Age of Enlightenment, Neoclassicism, and the German revolutions of 1848–1849. During the Gründerzeit, an industrialization-induced economic boom triggered a rapid population increase in Berlin. 1920s Berlin was the third-largest city in the world by population.[18]\n\nAfter World War II and following Berlin's occupation, the city was split into West Berlin and East Berlin, divided by the Berlin Wall.[19] East Berlin was declared the capital of East Germany, while Bonn became the West German capital. Following German reunification in 1990, Berlin once again became the capital of all of Germany. Due to its geographic location and history, Berlin has been called \"the heart of Europe\".[20][21][22]`\n\nconst prague = `Prague (/ˈprɑːɡ/ PRAHG; Czech: Praha [ˈpraɦa] ⓘ)[a] is the capital and largest city of the Czech Republic[9] and the historical capital of Bohemia. Situated on the Vltava river, Prague is home to about 1.4 million people.\n\nPrague is a political, cultural, and economic hub of Central Europe, with a rich history and Romanesque, Gothic, Renaissance and Baroque architectures. It was the capital of the Kingdom of Bohemia and residence of several Holy Roman Emperors, most notably Charles IV (r. 1346–1378) and Rudolf II (r. 1575–1611).[9] It was an important city to the Habsburg monarchy and Austria-Hungary. The city played major roles in the Bohemian and the Protestant Reformations, the Thirty Years' War and in 20th-century history as the capital of Czechoslovakia between the World Wars and the post-war Communist era.[10]\n\nPrague is home to a number of cultural attractions including Prague Castle, Charles Bridge, Old Town Square with the Prague astronomical clock, the Jewish Quarter, Petřín hill and Vyšehrad. Since 1992, the historic center of Prague has been included in the UNESCO list of World Heritage Sites.\n\nThe city has more than ten major museums, along with numerous theatres, galleries, cinemas, and other historical exhibits. An extensive modern public transportation system connects the city. It is home to a wide range of public and private schools, including Charles University in Prague, the oldest university in Central Europe.\n\nPrague is classified as an \"Alpha-\" global city according to GaWC studies.[11] In 2019, the city was ranked as 69th most livable city in the world by Mercer.[12] In the same year, the PICSA Index ranked the city as 13th most livable city in the world.[13] Its rich history makes it a popular tourist destination and as of 2017, the city receives more than 8.5 million international visitors annually. In 2017, Prague was listed as the fifth most visited European city after London, Paris, Rome, and Istanbul.[14]`\n\nreturn [prague, berlin, kyiv].map(i => ({ city: i}))" + }, + "id": "ce9d517e-2dd9-45e4-a566-79bd79cd809b", + "name": "Code", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 740, + 760 + ] + }, + { + "parameters": { + "chunkSize": 300 + }, + "id": "ebe5f3a5-4d90-4a33-bf48-f160f0e83967", + "name": "Character Text Splitter", + "type": "@n8n/n8n-nodes-langchain.textSplitterCharacterTextSplitter", + "typeVersion": 1, + "position": [ + 1100, + 1060 + ] + }, + { + "parameters": { + "code": { + "supplyData": { + "code": "const { FakeEmbeddings } = require('@langchain/core/utils/testing');\n\nreturn new FakeEmbeddings();" + } + }, + "outputs": { + "output": [ + { + "type": "ai_embedding" + } + ] + } + }, + "id": "0eac6c5b-89a9-48a4-bd21-19f2b20c3424", + "name": "Fake Embeddings 3", + "type": "@n8n/n8n-nodes-langchain.code", + "typeVersion": 1, + "position": [ + 660, + 1220 + ] + }, + { + "parameters": { + "mode": "load", + "prompt": "Tester", + "topK": 3 + }, + "id": "8c9b39bf-59d6-4769-98e1-54988d9d6b53", + "name": "Get All VS", + "type": "@n8n/n8n-nodes-langchain.vectorStoreInMemory", + "typeVersion": 1, + "position": [ + 680, + 1080 + ] + }, + { + "parameters": { + "code": { + "supplyData": { + "code": "const { FakeEmbeddings } = require('@langchain/core/utils/testing');\n\nreturn new FakeEmbeddings();" + } + }, + "outputs": { + "output": [ + { + "type": "ai_embedding" + } + ] + } + }, + "id": "e46004ec-baf6-425c-9897-3faec9e29676", + "name": "Fake Embeddings", + "type": "@n8n/n8n-nodes-langchain.code", + "typeVersion": 1, + "position": [ + 920, + 900 + ] + }, + { + "parameters": { + "promptType": "define", + "text": "Just testing", + "options": {} + }, + "id": "b132b323-a813-469c-859b-f1b3ede743a3", + "name": "Question and Answer Chain", + "type": "@n8n/n8n-nodes-langchain.chainRetrievalQa", + "typeVersion": 1.3, + "position": [ + 1680, + 780 + ] + }, + { + "parameters": {}, + "id": "b9c412e5-d739-4c82-9a2e-6c0af0cae8f9", + "name": "Vector Store Retriever", + "type": "@n8n/n8n-nodes-langchain.retrieverVectorStore", + "typeVersion": 1, + "position": [ + 1760, + 920 + ] + }, + { + "parameters": { + "code": { + "supplyData": { + "code": "const { FakeChatModel } = require('@langchain/core/utils/testing');\n\nreturn new FakeChatModel({});" + } + }, + "outputs": { + "output": [ + { + "type": "ai_languageModel" + } + ] + } + }, + "id": "962b4b87-ffd6-4ab8-8776-6e9c0920930a", + "name": "Fake Language Model", + "type": "@n8n/n8n-nodes-langchain.code", + "typeVersion": 1, + "position": [ + 1620, + 920 + ] + }, + { + "parameters": { + "code": { + "supplyData": { + "code": "const { FakeEmbeddings } = require('@langchain/core/utils/testing');\n\nreturn new FakeEmbeddings();" + } + }, + "outputs": { + "output": [ + { + "type": "ai_embedding" + } + ] + } + }, + "id": "c78be34f-6459-4414-86bd-f2670ece129d", + "name": "Fake Embeddings 2", + "type": "@n8n/n8n-nodes-langchain.code", + "typeVersion": 1, + "position": [ + 1700, + 1200 + ] + }, + { + "parameters": {}, + "id": "3cee9727-6b97-477c-8277-e8883a98786d", + "name": "Retriever VS", + "type": "@n8n/n8n-nodes-langchain.vectorStoreInMemory", + "typeVersion": 1, + "position": [ + 1700, + 1060 + ] + }, + { + "parameters": { + "mode": "insert" + }, + "id": "5793ec6b-ac00-4a5d-a79c-ff557143e46b", + "name": "Populate VS", + "type": "@n8n/n8n-nodes-langchain.vectorStoreInMemory", + "typeVersion": 1, + "position": [ + 980, + 760 + ] + } + ], + "pinData": {}, + "connections": { + "When clicking ‘Test workflow’": { + "main": [ + [ + { + "node": "Code", + "type": "main", + "index": 0 + }, + { + "node": "Get All VS", + "type": "main", + "index": 0 + } + ] + ] + }, + "Default Data Loader": { + "ai_document": [ + [ + { + "node": "Populate VS", + "type": "ai_document", + "index": 0 + } + ] + ] + }, + "Code": { + "main": [ + [ + { + "node": "Populate VS", + "type": "main", + "index": 0 + } + ] + ] + }, + "Character Text Splitter": { + "ai_textSplitter": [ + [ + { + "node": "Default Data Loader", + "type": "ai_textSplitter", + "index": 0 + } + ] + ] + }, + "Fake Embeddings 3": { + "ai_embedding": [ + [ + { + "node": "Get All VS", + "type": "ai_embedding", + "index": 0 + } + ] + ] + }, + "Fake Embeddings": { + "ai_embedding": [ + [ + { + "node": "Populate VS", + "type": "ai_embedding", + "index": 0 + } + ] + ] + }, + "Vector Store Retriever": { + "ai_retriever": [ + [ + { + "node": "Question and Answer Chain", + "type": "ai_retriever", + "index": 0 + } + ] + ] + }, + "Fake Language Model": { + "ai_languageModel": [ + [ + { + "node": "Question and Answer Chain", + "type": "ai_languageModel", + "index": 0 + } + ] + ] + }, + "Fake Embeddings 2": { + "ai_embedding": [ + [ + { + "node": "Retriever VS", + "type": "ai_embedding", + "index": 0 + } + ] + ] + }, + "Retriever VS": { + "ai_vectorStore": [ + [ + { + "node": "Vector Store Retriever", + "type": "ai_vectorStore", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": { + "executionOrder": "v1" + }, + "versionId": "4ad44cc6-d5f7-48af-8455-c3957baba04c", + "meta": { + "templateCredsSetupCompleted": true, + "instanceId": "27cc9b56542ad45b38725555722c50a1c3fee1670bbb67980558314ee08517c4" + }, + "id": "ZjxsuN0rMHRVCb2c", + "tags": [] +} diff --git a/cypress/fixtures/Subworkflow-debugging-execute-workflow.json b/cypress/fixtures/Subworkflow-debugging-execute-workflow.json new file mode 100644 index 0000000000..c336a80b41 --- /dev/null +++ b/cypress/fixtures/Subworkflow-debugging-execute-workflow.json @@ -0,0 +1,354 @@ +{ + "meta": { + "instanceId": "08ce71ad998aeaade0abedb8dd96153d8eaa03fcb84cfccc1530095bf9ee478e" + }, + "nodes": [ + { + "parameters": {}, + "id": "4535ce3e-280e-49b0-8854-373472ec86d1", + "name": "When clicking ‘Test workflow’", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [80, 860] + }, + { + "parameters": { + "category": "randomData", + "randomDataSeed": "0", + "randomDataCount": 2 + }, + "id": "d7fba18a-d51f-4509-af45-68cd9425ac6b", + "name": "DebugHelper1", + "type": "n8n-nodes-base.debugHelper", + "typeVersion": 1, + "position": [280, 860] + }, + { + "parameters": { + "source": "parameter", + "workflowJson": "{\n \"meta\": {\n \"instanceId\": \"a786b722078489c1fa382391a9f3476c2784761624deb2dfb4634827256d51a0\"\n },\n \"nodes\": [\n {\n \"parameters\": {},\n \"type\": \"n8n-nodes-base.executeWorkflowTrigger\",\n \"typeVersion\": 1,\n \"position\": [\n 0,\n 0\n ],\n \"id\": \"00600a51-e63a-4b6e-93f5-f01d50a21e0c\",\n \"name\": \"Execute Workflow Trigger\"\n },\n {\n \"parameters\": {\n \"assignments\": {\n \"assignments\": [\n {\n \"id\": \"87ff01af-2e28-48da-ae6c-304040200b15\",\n \"name\": \"hello\",\n \"value\": \"=world {{ $json.firstname }} {{ $json.lastname }}\",\n \"type\": \"string\"\n }\n ]\n },\n \"includeOtherFields\": false,\n \"options\": {}\n },\n \"type\": \"n8n-nodes-base.set\",\n \"typeVersion\": 3.4,\n \"position\": [\n 280,\n 0\n ],\n \"id\": \"642219a1-d655-4a30-af5c-fcccbb690322\",\n \"name\": \"Edit Fields\"\n }\n ],\n \"connections\": {\n \"Execute Workflow Trigger\": {\n \"main\": [\n [\n {\n \"node\": \"Edit Fields\",\n \"type\": \"main\",\n \"index\": 0\n }\n ]\n ]\n }\n },\n \"pinData\": {}\n}", + "mode": "each", + "options": { + "waitForSubWorkflow": false + } + }, + "type": "n8n-nodes-base.executeWorkflow", + "typeVersion": 1.1, + "position": [680, 1540], + "id": "f90a25da-dd89-4bf8-8f5b-bf8ee1de0b70", + "name": "Execute Workflow with param3" + }, + { + "parameters": { + "assignments": { + "assignments": [ + { + "id": "c93f26bd-3489-467b-909e-6462e1463707", + "name": "uid", + "value": "={{ $json.uid }}", + "type": "string" + }, + { + "id": "3dd706ce-d925-4219-8531-ad12369972fe", + "name": "email", + "value": "={{ $json.email }}", + "type": "string" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [900, 1540], + "id": "3be57648-3be8-4b0f-abfa-8fdcafee804d", + "name": "Edit Fields8" + }, + { + "parameters": { + "source": "parameter", + "workflowJson": "{\n \"meta\": {\n \"instanceId\": \"a786b722078489c1fa382391a9f3476c2784761624deb2dfb4634827256d51a0\"\n },\n \"nodes\": [\n {\n \"parameters\": {},\n \"type\": \"n8n-nodes-base.executeWorkflowTrigger\",\n \"typeVersion\": 1,\n \"position\": [\n 0,\n 0\n ],\n \"id\": \"00600a51-e63a-4b6e-93f5-f01d50a21e0c\",\n \"name\": \"Execute Workflow Trigger\"\n },\n {\n \"parameters\": {\n \"assignments\": {\n \"assignments\": [\n {\n \"id\": \"87ff01af-2e28-48da-ae6c-304040200b15\",\n \"name\": \"hello\",\n \"value\": \"=world {{ $json.firstname }} {{ $json.lastname }}\",\n \"type\": \"string\"\n }\n ]\n },\n \"includeOtherFields\": false,\n \"options\": {}\n },\n \"type\": \"n8n-nodes-base.set\",\n \"typeVersion\": 3.4,\n \"position\": [\n 280,\n 0\n ],\n \"id\": \"642219a1-d655-4a30-af5c-fcccbb690322\",\n \"name\": \"Edit Fields\"\n }\n ],\n \"connections\": {\n \"Execute Workflow Trigger\": {\n \"main\": [\n [\n {\n \"node\": \"Edit Fields\",\n \"type\": \"main\",\n \"index\": 0\n }\n ]\n ]\n }\n },\n \"pinData\": {}\n}", + "options": { + "waitForSubWorkflow": false + } + }, + "type": "n8n-nodes-base.executeWorkflow", + "typeVersion": 1.1, + "position": [620, 1220], + "id": "dabc2356-3660-4d17-b305-936a002029ba", + "name": "Execute Workflow with param2" + }, + { + "parameters": { + "assignments": { + "assignments": [ + { + "id": "c93f26bd-3489-467b-909e-6462e1463707", + "name": "uid", + "value": "={{ $json.uid }}", + "type": "string" + }, + { + "id": "3dd706ce-d925-4219-8531-ad12369972fe", + "name": "email", + "value": "={{ $json.email }}", + "type": "string" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [840, 1220], + "id": "9d2a9dda-e2a1-43e8-a66f-a8a555692e5f", + "name": "Edit Fields7" + }, + { + "parameters": { + "source": "parameter", + "workflowJson": "{\n \"meta\": {\n \"instanceId\": \"a786b722078489c1fa382391a9f3476c2784761624deb2dfb4634827256d51a0\"\n },\n \"nodes\": [\n {\n \"parameters\": {},\n \"type\": \"n8n-nodes-base.executeWorkflowTrigger\",\n \"typeVersion\": 1,\n \"position\": [\n 0,\n 0\n ],\n \"id\": \"00600a51-e63a-4b6e-93f5-f01d50a21e0c\",\n \"name\": \"Execute Workflow Trigger\"\n },\n {\n \"parameters\": {\n \"assignments\": {\n \"assignments\": [\n {\n \"id\": \"87ff01af-2e28-48da-ae6c-304040200b15\",\n \"name\": \"hello\",\n \"value\": \"=world {{ $json.firstname }} {{ $json.lastname }}\",\n \"type\": \"string\"\n }\n ]\n },\n \"includeOtherFields\": false,\n \"options\": {}\n },\n \"type\": \"n8n-nodes-base.set\",\n \"typeVersion\": 3.4,\n \"position\": [\n 280,\n 0\n ],\n \"id\": \"642219a1-d655-4a30-af5c-fcccbb690322\",\n \"name\": \"Edit Fields\"\n }\n ],\n \"connections\": {\n \"Execute Workflow Trigger\": {\n \"main\": [\n [\n {\n \"node\": \"Edit Fields\",\n \"type\": \"main\",\n \"index\": 0\n }\n ]\n ]\n }\n },\n \"pinData\": {}\n}", + "mode": "each", + "options": { + "waitForSubWorkflow": true + } + }, + "type": "n8n-nodes-base.executeWorkflow", + "typeVersion": 1.1, + "position": [560, 900], + "id": "07e47f60-622a-484c-ab24-35f6f2280595", + "name": "Execute Workflow with param1" + }, + { + "parameters": { + "assignments": { + "assignments": [ + { + "id": "c93f26bd-3489-467b-909e-6462e1463707", + "name": "uid", + "value": "={{ $json.uid }}", + "type": "string" + }, + { + "id": "3dd706ce-d925-4219-8531-ad12369972fe", + "name": "email", + "value": "={{ $json.email }}", + "type": "string" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [760, 900], + "id": "80563d0a-0bab-444f-a04c-4041a505d78b", + "name": "Edit Fields6" + }, + { + "parameters": { + "source": "parameter", + "workflowJson": "{\n \"meta\": {\n \"instanceId\": \"a786b722078489c1fa382391a9f3476c2784761624deb2dfb4634827256d51a0\"\n },\n \"nodes\": [\n {\n \"parameters\": {},\n \"type\": \"n8n-nodes-base.executeWorkflowTrigger\",\n \"typeVersion\": 1,\n \"position\": [\n 0,\n 0\n ],\n \"id\": \"00600a51-e63a-4b6e-93f5-f01d50a21e0c\",\n \"name\": \"Execute Workflow Trigger\"\n },\n {\n \"parameters\": {\n \"assignments\": {\n \"assignments\": [\n {\n \"id\": \"87ff01af-2e28-48da-ae6c-304040200b15\",\n \"name\": \"hello\",\n \"value\": \"=world {{ $json.firstname }} {{ $json.lastname }}\",\n \"type\": \"string\"\n }\n ]\n },\n \"includeOtherFields\": false,\n \"options\": {}\n },\n \"type\": \"n8n-nodes-base.set\",\n \"typeVersion\": 3.4,\n \"position\": [\n 280,\n 0\n ],\n \"id\": \"642219a1-d655-4a30-af5c-fcccbb690322\",\n \"name\": \"Edit Fields\"\n }\n ],\n \"connections\": {\n \"Execute Workflow Trigger\": {\n \"main\": [\n [\n {\n \"node\": \"Edit Fields\",\n \"type\": \"main\",\n \"index\": 0\n }\n ]\n ]\n }\n },\n \"pinData\": {}\n}", + "options": { + "waitForSubWorkflow": true + } + }, + "type": "n8n-nodes-base.executeWorkflow", + "typeVersion": 1.1, + "position": [560, 580], + "id": "f04af481-f4d9-4d91-a60a-a377580e8393", + "name": "Execute Workflow with param" + }, + { + "parameters": { + "assignments": { + "assignments": [ + { + "id": "c93f26bd-3489-467b-909e-6462e1463707", + "name": "uid", + "value": "={{ $json.uid }}", + "type": "string" + }, + { + "id": "3dd706ce-d925-4219-8531-ad12369972fe", + "name": "email", + "value": "={{ $json.email }}", + "type": "string" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [760, 580], + "id": "80c10607-a0ac-4090-86a1-890da0a2aa52", + "name": "Edit Fields2" + }, + { + "parameters": { + "content": "## Execute Workflow (Run once with all items/ DONT Wait for Sub-workflow completion)", + "height": 254.84308966329985, + "width": 457.58120569815793 + }, + "id": "534ef523-3453-4a16-9ff0-8ac9f025d47d", + "name": "Sticky Note5", + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [500, 1080] + }, + { + "parameters": { + "content": "## Execute Workflow (Run once with for each item/ DONT Wait for Sub-workflow completion) ", + "height": 284.59778445962905, + "width": 457.58120569815793 + }, + "id": "838f0fa3-5ee4-4d1a-afb8-42e009f1aa9e", + "name": "Sticky Note4", + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [580, 1400] + }, + { + "parameters": { + "category": "randomData", + "randomDataSeed": "1", + "randomDataCount": 3 + }, + "id": "86699a49-2aa7-488e-8ea9-828404c98f08", + "name": "DebugHelper", + "type": "n8n-nodes-base.debugHelper", + "typeVersion": 1, + "position": [320, 1120] + }, + { + "parameters": { + "content": "## Execute Workflow (Run once with for each item/ Wait for Sub-workflow completion) ", + "height": 284.59778445962905, + "width": 457.58120569815793 + }, + "id": "885d35f0-8ae6-45ec-821b-a82c27e7577a", + "name": "Sticky Note3", + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [480, 760] + }, + { + "parameters": { + "content": "## Execute Workflow (Run once with all items/ Wait for Sub-workflow completion) (default behavior)", + "height": 254.84308966329985, + "width": 457.58120569815793 + }, + "id": "505bd7f2-767e-41b8-9325-77300aed5883", + "name": "Sticky Note2", + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [460, 460] + } + ], + "connections": { + "When clicking ‘Test workflow’": { + "main": [ + [ + { + "node": "DebugHelper1", + "type": "main", + "index": 0 + }, + { + "node": "DebugHelper", + "type": "main", + "index": 0 + } + ] + ] + }, + "DebugHelper1": { + "main": [ + [ + { + "node": "Execute Workflow with param3", + "type": "main", + "index": 0 + }, + { + "node": "Execute Workflow with param2", + "type": "main", + "index": 0 + }, + { + "node": "Execute Workflow with param1", + "type": "main", + "index": 0 + }, + { + "node": "Execute Workflow with param", + "type": "main", + "index": 0 + } + ] + ] + }, + "Execute Workflow with param3": { + "main": [ + [ + { + "node": "Edit Fields8", + "type": "main", + "index": 0 + } + ] + ] + }, + "Execute Workflow with param2": { + "main": [ + [ + { + "node": "Edit Fields7", + "type": "main", + "index": 0 + } + ] + ] + }, + "Execute Workflow with param1": { + "main": [ + [ + { + "node": "Edit Fields6", + "type": "main", + "index": 0 + } + ] + ] + }, + "Execute Workflow with param": { + "main": [ + [ + { + "node": "Edit Fields2", + "type": "main", + "index": 0 + } + ] + ] + }, + "DebugHelper": { + "main": [ + [ + { + "node": "Execute Workflow with param2", + "type": "main", + "index": 0 + }, + { + "node": "Execute Workflow with param3", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "pinData": {} +} diff --git a/cypress/fixtures/Switch_node_with_null_connection.json b/cypress/fixtures/Switch_node_with_null_connection.json new file mode 100644 index 0000000000..325e097bd0 --- /dev/null +++ b/cypress/fixtures/Switch_node_with_null_connection.json @@ -0,0 +1,85 @@ +{ + "nodes": [ + { + "parameters": {}, + "id": "418350b8-b402-4d3b-93ba-3794d36c1ad5", + "name": "When clicking \"Test workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [440, 380] + }, + { + "parameters": { + "rules": { + "values": [ + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict" + }, + "conditions": [ + { + "leftValue": "", + "rightValue": "", + "operator": { + "type": "string", + "operation": "equals" + } + } + ], + "combinator": "and" + } + }, + {}, + {} + ] + }, + "options": {} + }, + "id": "b67ad46f-6b0d-4ff4-b2d2-dfbde44e287c", + "name": "Switch", + "type": "n8n-nodes-base.switch", + "typeVersion": 3, + "position": [660, 380] + }, + { + "parameters": { + "options": {} + }, + "id": "24731c11-e2a4-4854-81a6-277ce72e8a93", + "name": "Edit Fields", + "type": "n8n-nodes-base.set", + "typeVersion": 3.3, + "position": [840, 480] + } + ], + "connections": { + "When clicking \"Test workflow\"": { + "main": [ + [ + { + "node": "Switch", + "type": "main", + "index": 0 + } + ] + ] + }, + "Switch": { + "main": [ + null, + null, + [ + { + "node": "Edit Fields", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "pinData": {} +} diff --git a/cypress/fixtures/Test-workflow-with-long-parameters.json b/cypress/fixtures/Test-workflow-with-long-parameters.json new file mode 100644 index 0000000000..d4d052f6f0 --- /dev/null +++ b/cypress/fixtures/Test-workflow-with-long-parameters.json @@ -0,0 +1,150 @@ +{ + "meta": { + "instanceId": "777c68374367604fdf2a0bcfe9b1b574575ddea61aa8268e4bf034434bd7c894" + }, + "nodes": [ + { + "parameters": { + "assignments": { + "assignments": [ + { + "id": "0effebfc-fa8c-4d41-8a37-6d5695dfc9ee", + "name": "test", + "value": "test", + "type": "string" + }, + { + "id": "beb8723f-6333-4186-ab88-41d4e2338866", + "name": "test", + "value": "test", + "type": "string" + }, + { + "id": "85095836-4e94-442f-9270-e1a89008c129", + "name": "test", + "value": "test", + "type": "string" + }, + { + "id": "b6163f8a-bca6-4364-8b38-182df37c55cd", + "name": "=should be visible!", + "value": "=not visible", + "type": "string" + } + ] + }, + "options": {} + }, + "id": "950fcdc1-9e92-410f-8377-d4240e9bf6ff", + "name": "Edit Fields1", + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [ + 680, + 460 + ] + }, + { + "parameters": { + "messageType": "block", + "blocksUi": "blocks", + "text": "=should be visible", + "otherOptions": { + "sendAsUser": "=not visible" + } + }, + "id": "dcf7410d-0f8e-4cdb-9819-ae275558bdaa", + "name": "Slack", + "type": "n8n-nodes-base.slack", + "typeVersion": 2.2, + "position": [ + 900, + 460 + ], + "webhookId": "002b502e-31e5-4fdb-ac43-a56cfde8f82a" + }, + { + "parameters": { + "rule": { + "interval": [ + {}, + { + "field": "=should be visible" + }, + { + "field": "=not visible" + } + ] + } + }, + "id": "4c948a3f-19d4-4b08-a8be-f7d2964a21f4", + "name": "Schedule Trigger", + "type": "n8n-nodes-base.scheduleTrigger", + "typeVersion": 1.2, + "position": [ + 460, + 460 + ] + }, + { + "parameters": { + "assignments": { + "assignments": [ + { + "id": "5dcaab37-1146-49c6-97a3-3b2f73483270", + "name": "object", + "value": "=1 visible!\n2 {\n3 \"str\": \"two\",\n4 \"str_date\": \"{{ $now }}\",\n5 \"str_int\": \"1\",\n6 \"str_float\": \"1.234\",\n7 not visible!\n \"str_bool\": \"true\",\n \"str_email\": \"david@thedavid.com\",\n \"str_with_email\":\"My email is david@n8n.io\",\n \"str_json_single\":\"{'one':'two'}\",\n \"str_json_double\":\"{\\\"one\\\":\\\"two\\\"}\",\n \"bool\": true,\n \"list\": [1, 2, 3],\n \"decimal\": 1.234,\n \"timestamp1\": 1708695471,\n \"timestamp2\": 1708695471000,\n \"timestamp3\": 1708695471000000,\n \"num_one\": 1\n}", + "type": "object" + } + ] + }, + "includeOtherFields": true, + "options": {} + }, + "id": "a41dfb0d-38aa-42d2-b3e2-1854090bd319", + "name": "With long expression", + "type": "n8n-nodes-base.set", + "typeVersion": 3.3, + "position": [ + 1100, + 460 + ] + } + ], + "connections": { + "Edit Fields1": { + "main": [ + [ + { + "node": "Slack", + "type": "main", + "index": 0 + } + ] + ] + }, + "Slack": { + "main": [ + [ + { + "node": "With long expression", + "type": "main", + "index": 0 + } + ] + ] + }, + "Schedule Trigger": { + "main": [ + [ + { + "node": "Edit Fields1", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "pinData": {} +} diff --git a/cypress/fixtures/Test_Subworkflow-Inputs.json b/cypress/fixtures/Test_Subworkflow-Inputs.json new file mode 100644 index 0000000000..aeb4d601fd --- /dev/null +++ b/cypress/fixtures/Test_Subworkflow-Inputs.json @@ -0,0 +1,70 @@ +{ + "meta": { + "instanceId": "4d0676b62208d810ef035130bbfc9fd3afdc78d963ea8ccb9514dc89066efc94" + }, + "nodes": [ + { + "parameters": {}, + "id": "bb7f8bb3-840a-464c-a7de-d3a80538c2be", + "name": "When clicking ‘Test workflow’", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [0, 0] + }, + { + "parameters": { + "workflowId": {}, + "workflowInputs": { + "mappingMode": "defineBelow", + "value": {}, + "matchingColumns": [], + "schema": [], + "ignoreTypeMismatchErrors": false, + "attemptToConvertTypes": false, + "convertFieldsToString": true + }, + "options": {} + }, + "type": "n8n-nodes-base.executeWorkflow", + "typeVersion": 1.2, + "position": [500, 240], + "id": "6b6e2e34-c6ab-4083-b8e3-6b0d56be5453", + "name": "Execute Workflow" + } + ], + "connections": { + "When clicking ‘Test workflow’": { + "main": [ + [ + { + "node": "Execute Workflow", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "pinData": { + "When clicking ‘Test workflow’": [ + { + "aaString": "A String", + "aaNumber": 1, + "aaArray": [1, true, "3"], + "aaObject": { + "aKey": -1 + }, + "aaAny": {} + }, + { + "aaString": "Another String", + "aaNumber": 2, + "aaArray": [], + "aaObject": { + "aDifferentKey": -1 + }, + "aaAny": [] + } + ] + } +} diff --git a/cypress/fixtures/Test_Template_1.json b/cypress/fixtures/Test_Template_1.json index f15970677e..1995beca52 100644 --- a/cypress/fixtures/Test_Template_1.json +++ b/cypress/fixtures/Test_Template_1.json @@ -1,7 +1,7 @@ { "workflow": { "id": 1205, - "name": "Promote new Shopify products on Twitter and Telegram", + "name": "Promote new Shopify products", "views": 478, "recentViews": 9880, "totalViews": 478, diff --git a/cypress/fixtures/Test_ndv_search.json b/cypress/fixtures/Test_ndv_search.json new file mode 100644 index 0000000000..996b558e5a --- /dev/null +++ b/cypress/fixtures/Test_ndv_search.json @@ -0,0 +1,135 @@ +{ + "name": "NDV search bugs (introduced by schema view?)", + "nodes": [ + { + "parameters": {}, + "id": "55635c7b-92ee-4d2d-a0c0-baff9ab071da", + "name": "When clicking ‘Test workflow’", + "type": "n8n-nodes-base.manualTrigger", + "position": [ + 800, + 380 + ], + "typeVersion": 1 + }, + { + "parameters": { + "operation": "getAllPeople" + }, + "id": "4737af43-e49b-4c92-b76f-32605c047114", + "name": "Customer Datastore (n8n training)", + "type": "n8n-nodes-base.n8nTrainingCustomerDatastore", + "typeVersion": 1, + "position": [ + 1020, + 380 + ] + }, + { + "parameters": { + "assignments": { + "assignments": [] + }, + "includeOtherFields": true, + "options": {} + }, + "id": "8cc9b374-1856-4f3f-9315-08e6e27840d8", + "name": "Edit Fields", + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [ + 1240, + 380 + ] + } + ], + "pinData": { + "Customer Datastore (n8n training)": [ + { + "json": { + "id": "23423532", + "name": "Jay Gatsby", + "email": "gatsby@west-egg.com", + "notes": "Keeps asking about a green light??", + "country": "US", + "created": "1925-04-10" + } + }, + { + "json": { + "id": "23423533", + "name": "José Arcadio Buendía", + "email": "jab@macondo.co", + "notes": "Lots of people named after him. Very confusing", + "country": "CO", + "created": "1967-05-05" + } + }, + { + "json": { + "id": "23423534", + "name": "Max Sendak", + "email": "info@in-and-out-of-weeks.org", + "notes": "Keeps rolling his terrible eyes", + "country": "US", + "created": "1963-04-09" + } + }, + { + "json": { + "id": "23423535", + "name": "Zaphod Beeblebrox", + "email": "captain@heartofgold.com", + "notes": "Felt like I was talking to more than one person", + "country": null, + "created": "1979-10-12" + } + }, + { + "json": { + "id": "23423536", + "name": "Edmund Pevensie", + "email": "edmund@narnia.gov", + "notes": "Passionate sailor", + "country": "UK", + "created": "1950-10-16" + } + } + ] + }, + "connections": { + "When clicking ‘Test workflow’": { + "main": [ + [ + { + "node": "Customer Datastore (n8n training)", + "type": "main", + "index": 0 + } + ] + ] + }, + "Customer Datastore (n8n training)": { + "main": [ + [ + { + "node": "Edit Fields", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": { + "executionOrder": "v1" + }, + "versionId": "20178044-fb64-4443-88dd-e941517520d0", + "meta": { + "templateCredsSetupCompleted": true, + "instanceId": "be251a83c052a9862eeac953816fbb1464f89dfbf79d7ac490a8e336a8cc8bfd" + }, + "id": "aBVnTRON9Y2cSmse", + "tags": [] +} \ No newline at end of file diff --git a/cypress/fixtures/Test_ndv_two_branches_of_same_parent_false_populated.json b/cypress/fixtures/Test_ndv_two_branches_of_same_parent_false_populated.json new file mode 100644 index 0000000000..056a35a786 --- /dev/null +++ b/cypress/fixtures/Test_ndv_two_branches_of_same_parent_false_populated.json @@ -0,0 +1,94 @@ +{ + "nodes": [ + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 2 + }, + "conditions": [ + { + "id": "6f0cf983-824b-4339-a5de-6b374a23b4b0", + "leftValue": "={{ $json.a }}", + "rightValue": 3, + "operator": { + "type": "number", + "operation": "equals" + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "type": "n8n-nodes-base.if", + "typeVersion": 2.2, + "position": [220, 0], + "id": "1755282a-ec4a-4d02-a833-0316ca413cc4", + "name": "If" + }, + { + "parameters": {}, + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [0, 0], + "id": "de1e7acf-12d8-4e56-ba42-709ffb397db2", + "name": "When clicking ‘Test workflow’" + }, + { + "parameters": { + "category": "randomData" + }, + "type": "n8n-nodes-base.debugHelper", + "typeVersion": 1, + "position": [580, 0], + "id": "86440d33-f833-453c-bcaa-fff7e0083501", + "name": "DebugHelper", + "alwaysOutputData": true + } + ], + "connections": { + "If": { + "main": [ + [ + { + "node": "DebugHelper", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "DebugHelper", + "type": "main", + "index": 0 + } + ] + ] + }, + "When clicking ‘Test workflow’": { + "main": [ + [ + { + "node": "If", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "pinData": { + "When clicking ‘Test workflow’": [ + { + "a": 1 + }, + { + "a": 2 + } + ] + } +} diff --git a/cypress/fixtures/aiAssistant/responses/node_execution_succeeded_response.json b/cypress/fixtures/aiAssistant/responses/node_execution_succeeded_response.json new file mode 100644 index 0000000000..62caf2a6b5 --- /dev/null +++ b/cypress/fixtures/aiAssistant/responses/node_execution_succeeded_response.json @@ -0,0 +1,22 @@ +{ + "sessionId": "1", + "messages": [ + { + "role": "assistant", + "type": "message", + "text": "**Code** node ran successfully, did my solution help resolve your issue?", + "quickReplies": [ + { + "text": "Yes, thanks", + "type": "all-good", + "isFeedback": true + }, + { + "text": "No, I am still stuck", + "type": "still-stuck", + "isFeedback": true + } + ] + } + ] +} diff --git a/cypress/fixtures/templates_search/sales_templates_search_response.json b/cypress/fixtures/templates_search/sales_templates_search_response.json index 4efbb3585b..d4f90991b3 100644 --- a/cypress/fixtures/templates_search/sales_templates_search_response.json +++ b/cypress/fixtures/templates_search/sales_templates_search_response.json @@ -1202,7 +1202,7 @@ }, { "id": 1205, - "name": "Promote New Shopify Products on Social Media (Twitter and Telegram)", + "name": "Promote New Shopify Products", "totalViews": 219, "recentViews": 0, "user": { diff --git a/cypress/package.json b/cypress/package.json index 02e2a74c03..26b585408b 100644 --- a/cypress/package.json +++ b/cypress/package.json @@ -6,6 +6,7 @@ "cypress:install": "cypress install", "test:e2e:ui": "scripts/run-e2e.js ui", "test:e2e:dev": "scripts/run-e2e.js dev", + "test:e2e:dev:v2": "scripts/run-e2e.js dev:v2", "test:e2e:all": "scripts/run-e2e.js all", "format": "biome format --write .", "format:check": "biome ci .", @@ -26,6 +27,7 @@ "cypress": "^13.14.2", "cypress-otp": "^1.0.3", "cypress-real-events": "^1.13.0", + "flatted": "catalog:", "lodash": "catalog:", "nanoid": "catalog:", "start-server-and-test": "^2.0.8" diff --git a/cypress/pages/credentials.ts b/cypress/pages/credentials.ts index 9b20b48ec4..08b2fee9c7 100644 --- a/cypress/pages/credentials.ts +++ b/cypress/pages/credentials.ts @@ -5,7 +5,11 @@ export class CredentialsPage extends BasePage { getters = { emptyListCreateCredentialButton: () => cy.getByTestId('empty-resources-list').find('button'), - createCredentialButton: () => cy.getByTestId('resources-list-add'), + createCredentialButton: () => { + cy.getByTestId('add-resource').should('be.visible').click(); + cy.getByTestId('add-resource').getByTestId('action-credential').should('be.visible'); + return cy.getByTestId('add-resource').getByTestId('action-credential'); + }, searchInput: () => cy.getByTestId('resources-list-search'), emptyList: () => cy.getByTestId('resources-list-empty'), credentialCards: () => cy.getByTestId('resources-list-item'), diff --git a/cypress/pages/mfa-login.ts b/cypress/pages/mfa-login.ts index 66fc197e3f..7e679804ff 100644 --- a/cypress/pages/mfa-login.ts +++ b/cypress/pages/mfa-login.ts @@ -8,18 +8,18 @@ export class MfaLoginPage extends BasePage { getters = { form: () => cy.getByTestId('mfa-login-form'), - token: () => cy.getByTestId('token'), - recoveryCode: () => cy.getByTestId('recoveryCode'), + mfaCode: () => cy.getByTestId('mfaCode'), + mfaRecoveryCode: () => cy.getByTestId('mfaRecoveryCode'), enterRecoveryCodeButton: () => cy.getByTestId('mfa-enter-recovery-code-button'), }; actions = { - loginWithMfaToken: (email: string, password: string, mfaToken: string) => { + loginWithMfaCode: (email: string, password: string, mfaCode: string) => { const signinPage = new SigninPage(); const workflowsPage = new WorkflowsPage(); cy.session( - [mfaToken], + [mfaCode], () => { cy.visit(signinPage.url); @@ -30,7 +30,7 @@ export class MfaLoginPage extends BasePage { }); this.getters.form().within(() => { - this.getters.token().type(mfaToken); + this.getters.mfaCode().type(mfaCode); }); // we should be redirected to /workflows @@ -43,12 +43,12 @@ export class MfaLoginPage extends BasePage { }, ); }, - loginWithRecoveryCode: (email: string, password: string, recoveryCode: string) => { + loginWithMfaRecoveryCode: (email: string, password: string, mfaRecoveryCode: string) => { const signinPage = new SigninPage(); const workflowsPage = new WorkflowsPage(); cy.session( - [recoveryCode], + [mfaRecoveryCode], () => { cy.visit(signinPage.url); @@ -61,7 +61,7 @@ export class MfaLoginPage extends BasePage { this.getters.enterRecoveryCodeButton().click(); this.getters.form().within(() => { - this.getters.recoveryCode().type(recoveryCode); + this.getters.mfaRecoveryCode().type(mfaRecoveryCode); }); // we should be redirected to /workflows diff --git a/cypress/pages/modals/credentials-modal.ts b/cypress/pages/modals/credentials-modal.ts index cd3ded63f8..b8907386a0 100644 --- a/cypress/pages/modals/credentials-modal.ts +++ b/cypress/pages/modals/credentials-modal.ts @@ -1,3 +1,4 @@ +import { getCredentialSaveButton, saveCredential } from '../../composables/modals/credential-modal'; import { getVisibleSelect } from '../../utils'; import { BasePage } from '../base'; @@ -13,8 +14,6 @@ export class CredentialsModal extends BasePage { this.getters.credentialInputs().find(`:contains('${fieldName}') .n8n-input input`), name: () => cy.getByTestId('credential-name'), nameInput: () => cy.getByTestId('credential-name').find('input'), - // Saving of the credentials takes a while on the CI so we need to increase the timeout - saveButton: () => cy.getByTestId('credential-save-button', { timeout: 5000 }), deleteButton: () => cy.getByTestId('credential-delete-button'), closeButton: () => this.getters.editCredentialModal().find('.el-dialog__close').first(), oauthConnectButton: () => cy.getByTestId('oauth-connect-button'), @@ -41,17 +40,17 @@ export class CredentialsModal extends BasePage { }, save: (test = false) => { cy.intercept('POST', '/rest/credentials').as('saveCredential'); - this.getters.saveButton().click({ force: true }); + saveCredential(); cy.wait('@saveCredential'); if (test) cy.wait('@testCredential'); - this.getters.saveButton().should('contain.text', 'Saved'); + getCredentialSaveButton().should('contain.text', 'Saved'); }, saveSharing: () => { cy.intercept('PUT', '/rest/credentials/*/share').as('shareCredential'); - this.getters.saveButton().click({ force: true }); + saveCredential(); cy.wait('@shareCredential'); - this.getters.saveButton().should('contain.text', 'Saved'); + getCredentialSaveButton().should('contain.text', 'Saved'); }, close: () => { this.getters.closeButton().click(); @@ -65,7 +64,7 @@ export class CredentialsModal extends BasePage { .each(($el) => { cy.wrap($el).type('test'); }); - this.getters.saveButton().click(); + saveCredential(); if (closeModal) { this.getters.closeButton().click(); } diff --git a/cypress/pages/ndv.ts b/cypress/pages/ndv.ts index cae1fb47b0..1926ef0ad1 100644 --- a/cypress/pages/ndv.ts +++ b/cypress/pages/ndv.ts @@ -20,7 +20,8 @@ export class NDV extends BasePage { outputDataContainer: () => this.getters.outputPanel().findChildByTestId('ndv-data-container'), outputDisplayMode: () => this.getters.outputPanel().findChildByTestId('ndv-run-data-display-mode').first(), - pinDataButton: () => cy.getByTestId('ndv-pin-data'), + pinDataButton: () => this.getters.outputPanel().findChildByTestId('ndv-pin-data'), + unpinDataLink: () => this.getters.outputPanel().findChildByTestId('ndv-unpin-data'), editPinnedDataButton: () => cy.getByTestId('ndv-edit-pinned-data'), pinnedDataEditor: () => this.getters.outputPanel().find('.cm-editor .cm-scroller .cm-content'), runDataPaneHeader: () => cy.getByTestId('run-data-pane-header'), @@ -63,6 +64,7 @@ export class NDV extends BasePage { nodeRenameInput: () => cy.getByTestId('node-rename-input'), executePrevious: () => cy.getByTestId('execute-previous-node'), httpRequestNotice: () => cy.getByTestId('node-parameters-http-notice'), + nodeCredentialsLabel: () => cy.getByTestId('credentials-label'), nthParam: (n: number) => cy.getByTestId('node-parameters').find('.parameter-item').eq(n), inputRunSelector: () => this.getters.inputPanel().findChildByTestId('run-selector'), inputLinkRun: () => this.getters.inputPanel().findChildByTestId('link-run'), @@ -130,8 +132,9 @@ export class NDV extends BasePage { codeEditorFullscreenButton: () => cy.getByTestId('code-editor-fullscreen-button'), codeEditorDialog: () => cy.getByTestId('code-editor-fullscreen'), codeEditorFullscreen: () => this.getters.codeEditorDialog().find('.cm-content'), - nodeRunSuccessIndicator: () => cy.getByTestId('node-run-info-success'), - nodeRunErrorIndicator: () => cy.getByTestId('node-run-info-danger'), + nodeRunTooltipIndicator: () => cy.getByTestId('node-run-info'), + nodeRunSuccessIndicator: () => cy.getByTestId('node-run-status-success'), + nodeRunErrorIndicator: () => cy.getByTestId('node-run-status-danger'), nodeRunErrorMessage: () => cy.getByTestId('node-error-message'), nodeRunErrorDescription: () => cy.getByTestId('node-error-description'), fixedCollectionParameter: (paramName: string) => @@ -146,6 +149,9 @@ export class NDV extends BasePage { pinData: () => { this.getters.pinDataButton().click({ force: true }); }, + unPinData: () => { + this.getters.unpinDataLink().click({ force: true }); + }, editPinnedData: () => { this.getters.editPinnedDataButton().click(); }, @@ -221,9 +227,6 @@ export class NDV extends BasePage { this.getters.inputSelect().find('.el-select').click(); this.getters.inputOption().contains(nodeName).click(); }, - expandSchemaViewNode: (nodeName: string) => { - this.getters.schemaViewNodeName().contains(nodeName).click(); - }, addDefaultPinnedData: () => { this.actions.editPinnedData(); this.actions.savePinnedData(); @@ -317,6 +320,17 @@ export class NDV extends BasePage { addItemToFixedCollection: (paramName: string) => { this.getters.fixedCollectionParameter(paramName).getByTestId('fixed-collection-add').click(); }, + typeIntoFixedCollectionItem: (fixedCollectionName: string, index: number, content: string) => { + this.getters.fixedCollectionParameter(fixedCollectionName).within(() => { + cy.getByTestId('parameter-input').eq(index).type(content); + }); + }, + dragMainPanelToLeft: () => { + cy.drag('[data-test-id=panel-drag-button]', [-1000, 0], { moveTwice: true }); + }, + dragMainPanelToRight: () => { + cy.drag('[data-test-id=panel-drag-button]', [1000, 0], { moveTwice: true }); + }, }; } diff --git a/cypress/pages/settings-personal.ts b/cypress/pages/settings-personal.ts index 4574f95691..5602bd7e92 100644 --- a/cypress/pages/settings-personal.ts +++ b/cypress/pages/settings-personal.ts @@ -22,6 +22,8 @@ export class PersonalSettingsPage extends BasePage { saveSettingsButton: () => cy.getByTestId('save-settings-button'), enableMfaButton: () => cy.getByTestId('enable-mfa-button'), disableMfaButton: () => cy.getByTestId('disable-mfa-button'), + mfaCodeOrMfaRecoveryCodeInput: () => cy.getByTestId('mfa-code-or-recovery-code-input'), + mfaSaveButton: () => cy.getByTestId('mfa-save-button'), themeSelector: () => cy.getByTestId('theme-select'), selectOptionsVisible: () => cy.get('.el-select-dropdown:visible .el-select-dropdown__item'), }; @@ -83,9 +85,11 @@ export class PersonalSettingsPage extends BasePage { mfaSetupModal.getters.saveButton().click(); }); }, - disableMfa: () => { + disableMfa: (mfaCodeOrRecoveryCode: string) => { cy.visit(this.url); this.getters.disableMfaButton().click(); + this.getters.mfaCodeOrMfaRecoveryCodeInput().type(mfaCodeOrRecoveryCode); + this.getters.mfaSaveButton().click(); }, }; } diff --git a/cypress/pages/workflow-executions-tab.ts b/cypress/pages/workflow-executions-tab.ts index 5e8c36c055..be022e6cdf 100644 --- a/cypress/pages/workflow-executions-tab.ts +++ b/cypress/pages/workflow-executions-tab.ts @@ -30,6 +30,12 @@ export class WorkflowExecutionsTab extends BasePage { actions = { toggleNodeEnabled: (nodeName: string) => { + cy.ifCanvasVersion( + () => {}, + () => { + cy.get('body').click(); // Cancel selection if it exists + }, + ); workflowPage.getters.canvasNodeByName(nodeName).click(); cy.get('body').type('d', { force: true }); }, diff --git a/cypress/pages/workflow.ts b/cypress/pages/workflow.ts index 89186ee34e..e99b01aa46 100644 --- a/cypress/pages/workflow.ts +++ b/cypress/pages/workflow.ts @@ -1,8 +1,9 @@ import { BasePage } from './base'; import { NodeCreator } from './features/node-creator'; import { META_KEY } from '../constants'; +import type { OpenContextMenuOptions } from '../types'; import { getVisibleSelect } from '../utils'; -import { getUniqueWorkflowName } from '../utils/workflowUtils'; +import { getUniqueWorkflowName, isCanvasV2 } from '../utils/workflowUtils'; const nodeCreator = new NodeCreator(); export class WorkflowPage extends BasePage { @@ -17,7 +18,8 @@ export class WorkflowPage extends BasePage { workflowTagsContainer: () => cy.getByTestId('workflow-tags-container'), workflowTagsInput: () => this.getters.workflowTagsContainer().then(($el) => cy.wrap($el.find('input').first())), - tagPills: () => cy.get('[data-test-id="workflow-tags-container"] span.el-tag'), + tagPills: () => + cy.get('[data-test-id="workflow-tags-container"] span.el-tag:not(.count-container)'), nthTagPill: (n: number) => cy.get(`[data-test-id="workflow-tags-container"] span.el-tag:nth-child(${n})`), tagsDropdown: () => cy.getByTestId('workflow-tags-dropdown'), @@ -27,7 +29,15 @@ export class WorkflowPage extends BasePage { nodeCreatorSearchBar: () => cy.getByTestId('node-creator-search-bar'), nodeCreatorPlusButton: () => cy.getByTestId('node-creator-plus-button'), canvasPlusButton: () => cy.getByTestId('canvas-plus-button'), - canvasNodes: () => cy.getByTestId('canvas-node'), + canvasNodes: () => + cy.ifCanvasVersion( + () => cy.getByTestId('canvas-node'), + () => + cy + .getByTestId('canvas-node') + .not('[data-node-type="n8n-nodes-internal.addNodes"]') + .not('[data-node-type="n8n-nodes-base.stickyNote"]'), + ), canvasNodeByName: (nodeName: string) => this.getters.canvasNodes().filter(`:contains(${nodeName})`), nodeIssuesByName: (nodeName: string) => @@ -37,6 +47,17 @@ export class WorkflowPage extends BasePage { .should('have.length.greaterThan', 0) .findChildByTestId('node-issues'), getEndpointSelector: (type: 'input' | 'output' | 'plus', nodeName: string, index = 0) => { + if (isCanvasV2()) { + if (type === 'input') { + return `[data-test-id="canvas-node-input-handle"][data-node-name="${nodeName}"][data-handle-index="${index}"]`; + } + if (type === 'output') { + return `[data-test-id="canvas-node-output-handle"][data-node-name="${nodeName}"][data-handle-index="${index}"]`; + } + if (type === 'plus') { + return `[data-test-id="canvas-node-output-handle"][data-node-name="${nodeName}"][data-handle-index="${index}"] [data-test-id="canvas-handle-plus"] .clickable`; + } + } return `[data-endpoint-name='${nodeName}'][data-endpoint-type='${type}'][data-input-index='${index}']`; }, canvasNodeInputEndpointByName: (nodeName: string, index = 0) => { @@ -46,7 +67,15 @@ export class WorkflowPage extends BasePage { return cy.get(this.getters.getEndpointSelector('output', nodeName, index)); }, canvasNodePlusEndpointByName: (nodeName: string, index = 0) => { - return cy.get(this.getters.getEndpointSelector('plus', nodeName, index)); + return cy.ifCanvasVersion( + () => cy.get(this.getters.getEndpointSelector('plus', nodeName, index)), + () => + cy + .get( + `[data-test-id="canvas-node-output-handle"][data-node-name="${nodeName}"] [data-test-id="canvas-handle-plus"] .clickable`, + ) + .eq(index), + ); }, activatorSwitch: () => cy.getByTestId('workflow-activate-switch'), workflowMenu: () => cy.getByTestId('workflow-menu'), @@ -56,13 +85,29 @@ export class WorkflowPage extends BasePage { expressionModalInput: () => cy.getByTestId('expression-modal-input').find('[role=textbox]'), expressionModalOutput: () => cy.getByTestId('expression-modal-output'), - nodeViewRoot: () => cy.getByTestId('node-view-root'), + nodeViewRoot: () => + cy.ifCanvasVersion( + () => cy.getByTestId('node-view-root'), + () => this.getters.nodeView(), + ), copyPasteInput: () => cy.getByTestId('hidden-copy-paste'), - nodeConnections: () => cy.get('.jtk-connector'), + nodeConnections: () => + cy.ifCanvasVersion( + () => cy.get('.jtk-connector'), + () => cy.getByTestId('edge-label-wrapper'), + ), zoomToFitButton: () => cy.getByTestId('zoom-to-fit'), nodeEndpoints: () => cy.get('.jtk-endpoint-connected'), - disabledNodes: () => cy.get('.node-box.disabled'), - selectedNodes: () => this.getters.canvasNodes().filter('.jtk-drag-selected'), + disabledNodes: () => + cy.ifCanvasVersion( + () => cy.get('.node-box.disabled'), + () => cy.get('[data-test-id*="node"][class*="disabled"]'), + ), + selectedNodes: () => + cy.ifCanvasVersion( + () => this.getters.canvasNodes().filter('.jtk-drag-selected'), + () => this.getters.canvasNodes().parent().filter('.selected'), + ), // Workflow menu items workflowMenuItemDuplicate: () => cy.getByTestId('workflow-menu-item-duplicate'), workflowMenuItemDownload: () => cy.getByTestId('workflow-menu-item-download'), @@ -92,8 +137,21 @@ export class WorkflowPage extends BasePage { shareButton: () => cy.getByTestId('workflow-share-button'), duplicateWorkflowModal: () => cy.getByTestId('duplicate-modal'), - nodeViewBackground: () => cy.getByTestId('node-view-background'), - nodeView: () => cy.getByTestId('node-view'), + nodeViewBackground: () => + cy.ifCanvasVersion( + () => cy.getByTestId('node-view-background'), + () => cy.getByTestId('canvas'), + ), + nodeView: () => + cy.ifCanvasVersion( + () => cy.getByTestId('node-view'), + () => cy.get('[data-test-id="canvas-wrapper"]'), + ), + canvasViewport: () => + cy.ifCanvasVersion( + () => cy.getByTestId('node-view'), + () => cy.get('.vue-flow__transformationpane.vue-flow__container'), + ), inlineExpressionEditorInput: () => cy.getByTestId('inline-expression-editor-input').find('[role=textbox]'), inlineExpressionEditorOutput: () => cy.getByTestId('inline-expression-editor-output'), @@ -115,12 +173,26 @@ export class WorkflowPage extends BasePage { ndvParameters: () => cy.getByTestId('parameter-item'), nodeCredentialsLabel: () => cy.getByTestId('credentials-label'), getConnectionBetweenNodes: (sourceNodeName: string, targetNodeName: string) => - cy.get( - `.jtk-connector[data-source-node="${sourceNodeName}"][data-target-node="${targetNodeName}"]`, + cy.ifCanvasVersion( + () => + cy.get( + `.jtk-connector[data-source-node="${sourceNodeName}"][data-target-node="${targetNodeName}"]`, + ), + () => + cy.get( + `[data-test-id="edge-label-wrapper"][data-source-node-name="${sourceNodeName}"][data-target-node-name="${targetNodeName}"]`, + ), ), getConnectionActionsBetweenNodes: (sourceNodeName: string, targetNodeName: string) => - cy.get( - `.connection-actions[data-source-node="${sourceNodeName}"][data-target-node="${targetNodeName}"]`, + cy.ifCanvasVersion( + () => + cy.get( + `.connection-actions[data-source-node="${sourceNodeName}"][data-target-node="${targetNodeName}"]`, + ), + () => + cy.get( + `[data-test-id="edge-label-wrapper"][data-source-node-name="${sourceNodeName}"][data-target-node-name="${targetNodeName}"] [data-test-id="canvas-edge-toolbar"]`, + ), ), addStickyButton: () => cy.getByTestId('add-sticky-button'), stickies: () => cy.getByTestId('sticky'), @@ -128,6 +200,18 @@ export class WorkflowPage extends BasePage { workflowHistoryButton: () => cy.getByTestId('workflow-history-button'), colors: () => cy.getByTestId('color'), contextMenuAction: (action: string) => cy.getByTestId(`context-menu-item-${action}`), + getNodeLeftPosition: (element: JQuery) => { + if (isCanvasV2()) { + return parseFloat(element.parent().css('transform').split(',')[4]); + } + return parseFloat(element.css('left')); + }, + getNodeTopPosition: (element: JQuery) => { + if (isCanvasV2()) { + return parseFloat(element.parent().css('transform').split(',')[5]); + } + return parseFloat(element.css('top')); + }, }; actions = { @@ -193,14 +277,14 @@ export class WorkflowPage extends BasePage { }, openContextMenu: ( nodeTypeName?: string, - method: 'right-click' | 'overflow-button' = 'right-click', + { method = 'right-click', anchor = 'center' }: OpenContextMenuOptions = {}, ) => { const target = nodeTypeName ? this.getters.canvasNodeByName(nodeTypeName) : this.getters.nodeViewBackground(); if (method === 'right-click') { - target.rightclick(nodeTypeName ? 'center' : 'topLeft', { force: true }); + target.rightclick(nodeTypeName ? anchor : 'topLeft', { force: true }); } else { target.realHover(); target.find('[data-test-id="overflow-node-button"]').click({ force: true }); @@ -217,8 +301,8 @@ export class WorkflowPage extends BasePage { this.actions.openContextMenu(nodeTypeName); this.actions.contextMenuAction('delete'); }, - executeNode: (nodeTypeName: string) => { - this.actions.openContextMenu(nodeTypeName); + executeNode: (nodeTypeName: string, options?: OpenContextMenuOptions) => { + this.actions.openContextMenu(nodeTypeName, options); this.actions.contextMenuAction('execute'); }, addStickyFromContextMenu: () => { @@ -245,7 +329,7 @@ export class WorkflowPage extends BasePage { this.actions.contextMenuAction('toggle_pin'); }, openNodeFromContextMenu: (nodeTypeName: string) => { - this.actions.openContextMenu(nodeTypeName, 'overflow-button'); + this.actions.openContextMenu(nodeTypeName, { method: 'overflow-button' }); this.actions.contextMenuAction('open'); }, selectAllFromContextMenu: () => { @@ -253,8 +337,14 @@ export class WorkflowPage extends BasePage { this.actions.contextMenuAction('select_all'); }, deselectAll: () => { - this.actions.openContextMenu(); - this.actions.contextMenuAction('deselect_all'); + cy.ifCanvasVersion( + () => { + this.actions.openContextMenu(); + this.actions.contextMenuAction('deselect_all'); + }, + // rightclick doesn't work with vueFlow canvas + () => this.getters.nodeViewBackground().click('topLeft'), + ); }, openExpressionEditorModal: () => { cy.contains('Expression').invoke('show').click(); @@ -332,7 +422,7 @@ export class WorkflowPage extends BasePage { pinchToZoom: (steps: number, mode: 'zoomIn' | 'zoomOut' = 'zoomIn') => { cy.window().then((win) => { // Pinch-to-zoom simulates a 'wheel' event with ctrlKey: true (same as zooming by scrolling) - this.getters.nodeViewBackground().trigger('wheel', { + this.getters.nodeView().trigger('wheel', { force: true, bubbles: true, ctrlKey: true, @@ -391,9 +481,12 @@ export class WorkflowPage extends BasePage { action?: string, ) => { this.getters.getConnectionBetweenNodes(sourceNodeName, targetNodeName).first().realHover(); - this.getters - .getConnectionActionsBetweenNodes(sourceNodeName, targetNodeName) - .find('.add') + const connectionsBetweenNodes = () => + this.getters.getConnectionActionsBetweenNodes(sourceNodeName, targetNodeName); + cy.ifCanvasVersion( + () => connectionsBetweenNodes().find('.add'), + () => connectionsBetweenNodes().get('[data-test-id="add-connection-button"]'), + ) .first() .click({ force: true }); @@ -401,9 +494,12 @@ export class WorkflowPage extends BasePage { }, deleteNodeBetweenNodes: (sourceNodeName: string, targetNodeName: string) => { this.getters.getConnectionBetweenNodes(sourceNodeName, targetNodeName).first().realHover(); - this.getters - .getConnectionActionsBetweenNodes(sourceNodeName, targetNodeName) - .find('.delete') + const connectionsBetweenNodes = () => + this.getters.getConnectionActionsBetweenNodes(sourceNodeName, targetNodeName); + cy.ifCanvasVersion( + () => connectionsBetweenNodes().find('.delete'), + () => connectionsBetweenNodes().get('[data-test-id="delete-connection-button"]'), + ) .first() .click({ force: true }); }, diff --git a/cypress/pages/workflows.ts b/cypress/pages/workflows.ts index 5829ecb863..a58911a355 100644 --- a/cypress/pages/workflows.ts +++ b/cypress/pages/workflows.ts @@ -7,7 +7,10 @@ export class WorkflowsPage extends BasePage { newWorkflowButtonCard: () => cy.getByTestId('new-workflow-card'), newWorkflowTemplateCard: () => cy.getByTestId('new-workflow-template-card'), searchBar: () => cy.getByTestId('resources-list-search'), - createWorkflowButton: () => cy.getByTestId('resources-list-add'), + createWorkflowButton: () => { + cy.getByTestId('add-resource-workflow').should('be.visible'); + return cy.getByTestId('add-resource-workflow'); + }, workflowCards: () => cy.getByTestId('resources-list-item'), workflowCard: (workflowName: string) => this.getters @@ -16,6 +19,8 @@ export class WorkflowsPage extends BasePage { .parents('[data-test-id="resources-list-item"]'), workflowTags: (workflowName: string) => this.getters.workflowCard(workflowName).findChildByTestId('workflow-card-tags'), + workflowCardContent: (workflowName: string) => + this.getters.workflowCard(workflowName).findChildByTestId('card-content'), workflowActivator: (workflowName: string) => this.getters.workflowCard(workflowName).findChildByTestId('workflow-card-activator'), workflowActivatorStatus: (workflowName: string) => diff --git a/cypress/scripts/run-e2e.js b/cypress/scripts/run-e2e.js index 8096a70caf..6819d6c824 100755 --- a/cypress/scripts/run-e2e.js +++ b/cypress/scripts/run-e2e.js @@ -57,6 +57,17 @@ switch (scenario) { }, }); break; + case 'dev:v2': + runTests({ + startCommand: 'develop', + url: 'http://localhost:8080/favicon.ico', + testCommand: 'cypress open', + customEnv: { + CYPRESS_NODE_VIEW_VERSION: 2, + CYPRESS_BASE_URL: 'http://localhost:8080', + }, + }); + break; case 'all': const specSuiteFilter = process.argv[3]; const specParam = specSuiteFilter ? ` --spec **/*${specSuiteFilter}*` : ''; diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index 35f100fded..c414c9fea9 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -10,7 +10,7 @@ import { N8N_AUTH_COOKIE, } from '../constants'; import { WorkflowPage } from '../pages'; -import { getUniqueWorkflowName } from '../utils/workflowUtils'; +import { getUniqueWorkflowName, isCanvasV2 } from '../utils/workflowUtils'; Cypress.Commands.add('setAppDate', (targetDate: number | Date) => { cy.window().then((win) => { @@ -26,6 +26,10 @@ Cypress.Commands.add('getByTestId', (selector, ...args) => { return cy.get(`[data-test-id="${selector}"]`, ...args); }); +Cypress.Commands.add('ifCanvasVersion', (getterV1, getterV2) => { + return isCanvasV2() ? getterV2() : getterV1(); +}); + Cypress.Commands.add( 'createFixtureWorkflow', (fixtureKey: string, workflowName = getUniqueWorkflowName()) => { @@ -70,6 +74,15 @@ Cypress.Commands.add('signin', ({ email, password }) => { }) .then((response) => { Cypress.env('currentUserId', response.body.data.id); + + // @TODO Remove this once the switcher is removed + cy.window().then((win) => { + win.localStorage.setItem('NodeView.migrated', 'true'); + win.localStorage.setItem('NodeView.switcher.discovered.beta', 'true'); + + const nodeViewVersion = Cypress.env('NODE_VIEW_VERSION'); + win.localStorage.setItem('NodeView.version', nodeViewVersion ?? '1'); + }); }); }); }); @@ -169,6 +182,16 @@ Cypress.Commands.add('drag', (selector, pos, options) => { pageY: newPosition.y, force: true, }); + if (options?.moveTwice) { + // first move like hover to trigger object to be visible + // like in main panel in ndv + element.trigger('mousemove', { + which: 1, + pageX: newPosition.x, + pageY: newPosition.y, + force: true, + }); + } if (options?.clickToFinish) { // Click to finish the drag // For some reason, mouseup isn't working when moving nodes diff --git a/cypress/support/index.ts b/cypress/support/index.ts index a5f1caf5b2..864e64f1c4 100644 --- a/cypress/support/index.ts +++ b/cypress/support/index.ts @@ -1,7 +1,7 @@ // Load type definitions that come with Cypress module /// -import type { FrontendSettings } from '@n8n/api-types'; +import type { FrontendSettings, PushPayload, PushType } from '@n8n/api-types'; Cypress.Keyboard.defaults({ keystrokeDelay: 0, @@ -28,6 +28,7 @@ declare global { selector: string, ...args: Array | undefined> ): Chainable>; + ifCanvasVersion(getterV1: () => T1, getterV2: () => T2): T1 | T2; findChildByTestId(childTestId: string): Chainable>; /** * Creates a workflow from the given fixture and optionally renames it. @@ -58,14 +59,20 @@ declare global { drag( selector: string | Chainable>, target: [number, number], - options?: { abs?: boolean; index?: number; realMouse?: boolean; clickToFinish?: boolean }, + options?: { + abs?: boolean; + index?: number; + realMouse?: boolean; + clickToFinish?: boolean; + moveTwice?: boolean; + }, ): void; draganddrop( draggableSelector: string, droppableSelector: string, options?: Partial, ): void; - push(type: string, data: unknown): void; + push(type: Type, data: PushPayload): void; shouldNotHaveConsoleErrors(): void; window(): Chainable< AUTWindow & { diff --git a/cypress/types.ts b/cypress/types.ts index 6186c4201d..63f2ddb99e 100644 --- a/cypress/types.ts +++ b/cypress/types.ts @@ -22,3 +22,8 @@ export interface ExecutionResponse { results: Execution[]; }; } + +export type OpenContextMenuOptions = { + method?: 'right-click' | 'overflow-button'; + anchor?: 'topRight' | 'topLeft' | 'center' | 'bottomRight' | 'bottomLeft'; +}; diff --git a/cypress/utils/executions.ts b/cypress/utils/executions.ts index 0b4814fdc9..11eb5bba2c 100644 --- a/cypress/utils/executions.ts +++ b/cypress/utils/executions.ts @@ -1,4 +1,5 @@ -import type { IDataObject, IPinData, ITaskData, ITaskDataConnections } from 'n8n-workflow'; +import { stringify } from 'flatted'; +import type { IDataObject, ITaskData, ITaskDataConnections } from 'n8n-workflow'; import { nanoid } from 'nanoid'; import { clickExecuteWorkflowButton } from '../composables/workflow'; @@ -16,7 +17,7 @@ export function createMockNodeExecutionData( return { [name]: { startTime: new Date().getTime(), - executionTime: 0, + executionTime: 1, executionStatus, data: jsonData ? Object.keys(jsonData).reduce((acc, key) => { @@ -33,61 +34,23 @@ export function createMockNodeExecutionData( }, {} as ITaskDataConnections) : data, source: [null], + inputOverride, ...rest, }, }; } -export function createMockWorkflowExecutionData({ - executionId, - runData, - pinData = {}, - lastNodeExecuted, -}: { - executionId: string; - runData: Record; - pinData?: IPinData; - lastNodeExecuted: string; -}) { - return { - executionId, - data: { - data: { - startData: {}, - resultData: { - runData, - pinData, - lastNodeExecuted, - }, - executionData: { - contextData: {}, - nodeExecutionStack: [], - metadata: {}, - waitingExecution: {}, - waitingExecutionSource: {}, - }, - }, - mode: 'manual', - startedAt: new Date().toISOString(), - stoppedAt: new Date().toISOString(), - status: 'success', - finished: true, - }, - }; -} - export function runMockWorkflowExecution({ trigger, lastNodeExecuted, runData, - workflowExecutionData, }: { trigger?: () => void; lastNodeExecuted: string; runData: Array>; - workflowExecutionData?: ReturnType; }) { - const executionId = nanoid(8); + const workflowId = nanoid(); + const executionId = Math.floor(Math.random() * 1_000_000).toString(); cy.intercept('POST', '/rest/workflows/**/run?**', { statusCode: 201, @@ -124,13 +87,24 @@ export function runMockWorkflowExecution({ resolvedRunData[nodeName] = nodeExecution[nodeName]; }); - cy.push( - 'executionFinished', - createMockWorkflowExecutionData({ - executionId, - lastNodeExecuted, - runData: resolvedRunData, - ...workflowExecutionData, + cy.push('executionFinished', { + executionId, + workflowId, + status: 'success', + rawData: stringify({ + startData: {}, + resultData: { + runData, + pinData: {}, + lastNodeExecuted, + }, + executionData: { + contextData: {}, + nodeExecutionStack: [], + metadata: {}, + waitingExecution: {}, + waitingExecutionSource: {}, + }, }), - ); + }); } diff --git a/cypress/utils/workflowUtils.ts b/cypress/utils/workflowUtils.ts index 5001dbe1b6..0c91a097bd 100644 --- a/cypress/utils/workflowUtils.ts +++ b/cypress/utils/workflowUtils.ts @@ -3,3 +3,7 @@ import { nanoid } from 'nanoid'; export function getUniqueWorkflowName(workflowNamePrefix?: string) { return workflowNamePrefix ? `${workflowNamePrefix} ${nanoid(12)}` : nanoid(12); } + +export function isCanvasV2() { + return Cypress.env('NODE_VIEW_VERSION') === 2; +} diff --git a/docker/images/n8n-custom/Dockerfile b/docker/images/n8n-custom/Dockerfile index a533cdbdab..13592140a4 100644 --- a/docker/images/n8n-custom/Dockerfile +++ b/docker/images/n8n-custom/Dockerfile @@ -33,27 +33,22 @@ COPY docker/images/n8n/docker-entrypoint.sh / # Setup the Task Runner Launcher ARG TARGETPLATFORM -ARG LAUNCHER_VERSION=0.1.1 -ENV N8N_RUNNERS_USE_LAUNCHER=true \ - N8N_RUNNERS_LAUNCHER_PATH=/usr/local/bin/task-runner-launcher +ARG LAUNCHER_VERSION=1.1.0 COPY docker/images/n8n/n8n-task-runners.json /etc/n8n-task-runners.json -# First, download, verify, then extract the launcher binary -# Second, chmod with 4555 to allow the use of setuid -# Third, create a new user and group to execute the Task Runners under +# Download, verify, then extract the launcher binary RUN \ - if [[ "$TARGETPLATFORM" = "linux/amd64" ]]; then export ARCH_NAME="x86_64"; \ - elif [[ "$TARGETPLATFORM" = "linux/arm64" ]]; then export ARCH_NAME="aarch64"; fi; \ + if [[ "$TARGETPLATFORM" = "linux/amd64" ]]; then export ARCH_NAME="amd64"; \ + elif [[ "$TARGETPLATFORM" = "linux/arm64" ]]; then export ARCH_NAME="arm64"; fi; \ mkdir /launcher-temp && \ cd /launcher-temp && \ - wget https://github.com/n8n-io/task-runner-launcher/releases/download/${LAUNCHER_VERSION}/task-runner-launcher-$ARCH_NAME-unknown-linux-musl.zip && \ - wget https://github.com/n8n-io/task-runner-launcher/releases/download/${LAUNCHER_VERSION}/task-runner-launcher-$ARCH_NAME-unknown-linux-musl.sha256 && \ - sha256sum -c task-runner-launcher-$ARCH_NAME-unknown-linux-musl.sha256 && \ - unzip -d $(dirname ${N8N_RUNNERS_LAUNCHER_PATH}) task-runner-launcher-$ARCH_NAME-unknown-linux-musl.zip task-runner-launcher && \ + wget https://github.com/n8n-io/task-runner-launcher/releases/download/${LAUNCHER_VERSION}/task-runner-launcher-${LAUNCHER_VERSION}-linux-${ARCH_NAME}.tar.gz && \ + wget https://github.com/n8n-io/task-runner-launcher/releases/download/${LAUNCHER_VERSION}/task-runner-launcher-${LAUNCHER_VERSION}-linux-${ARCH_NAME}.tar.gz.sha256 && \ + # The .sha256 does not contain the filename --> Form the correct checksum file + echo "$(cat task-runner-launcher-${LAUNCHER_VERSION}-linux-${ARCH_NAME}.tar.gz.sha256) task-runner-launcher-${LAUNCHER_VERSION}-linux-${ARCH_NAME}.tar.gz" > checksum.sha256 && \ + sha256sum -c checksum.sha256 && \ + tar xvf task-runner-launcher-${LAUNCHER_VERSION}-linux-${ARCH_NAME}.tar.gz --directory=/usr/local/bin && \ cd - && \ - rm -r /launcher-temp && \ - chmod 4555 ${N8N_RUNNERS_LAUNCHER_PATH} && \ - addgroup -g 2000 task-runner && \ - adduser -D -u 2000 -g "Task Runner User" -G task-runner task-runner + rm -r /launcher-temp RUN \ cd /usr/local/lib/node_modules/n8n && \ diff --git a/docker/images/n8n/Dockerfile b/docker/images/n8n/Dockerfile index 08e031cf5f..10720c63f2 100644 --- a/docker/images/n8n/Dockerfile +++ b/docker/images/n8n/Dockerfile @@ -24,27 +24,22 @@ RUN set -eux; \ # Setup the Task Runner Launcher ARG TARGETPLATFORM -ARG LAUNCHER_VERSION=0.1.1 -ENV N8N_RUNNERS_USE_LAUNCHER=true \ - N8N_RUNNERS_LAUNCHER_PATH=/usr/local/bin/task-runner-launcher +ARG LAUNCHER_VERSION=1.1.0 COPY n8n-task-runners.json /etc/n8n-task-runners.json -# First, download, verify, then extract the launcher binary -# Second, chmod with 4555 to allow the use of setuid -# Third, create a new user and group to execute the Task Runners under +# Download, verify, then extract the launcher binary RUN \ - if [[ "$TARGETPLATFORM" = "linux/amd64" ]]; then export ARCH_NAME="x86_64"; \ - elif [[ "$TARGETPLATFORM" = "linux/arm64" ]]; then export ARCH_NAME="aarch64"; fi; \ + if [[ "$TARGETPLATFORM" = "linux/amd64" ]]; then export ARCH_NAME="amd64"; \ + elif [[ "$TARGETPLATFORM" = "linux/arm64" ]]; then export ARCH_NAME="arm64"; fi; \ mkdir /launcher-temp && \ cd /launcher-temp && \ - wget https://github.com/n8n-io/task-runner-launcher/releases/download/${LAUNCHER_VERSION}/task-runner-launcher-$ARCH_NAME-unknown-linux-musl.zip && \ - wget https://github.com/n8n-io/task-runner-launcher/releases/download/${LAUNCHER_VERSION}/task-runner-launcher-$ARCH_NAME-unknown-linux-musl.sha256 && \ - sha256sum -c task-runner-launcher-$ARCH_NAME-unknown-linux-musl.sha256 && \ - unzip -d $(dirname ${N8N_RUNNERS_LAUNCHER_PATH}) task-runner-launcher-$ARCH_NAME-unknown-linux-musl.zip task-runner-launcher && \ + wget https://github.com/n8n-io/task-runner-launcher/releases/download/${LAUNCHER_VERSION}/task-runner-launcher-${LAUNCHER_VERSION}-linux-${ARCH_NAME}.tar.gz && \ + wget https://github.com/n8n-io/task-runner-launcher/releases/download/${LAUNCHER_VERSION}/task-runner-launcher-${LAUNCHER_VERSION}-linux-${ARCH_NAME}.tar.gz.sha256 && \ + # The .sha256 does not contain the filename --> Form the correct checksum file + echo "$(cat task-runner-launcher-${LAUNCHER_VERSION}-linux-${ARCH_NAME}.tar.gz.sha256) task-runner-launcher-${LAUNCHER_VERSION}-linux-${ARCH_NAME}.tar.gz" > checksum.sha256 && \ + sha256sum -c checksum.sha256 && \ + tar xvf task-runner-launcher-${LAUNCHER_VERSION}-linux-${ARCH_NAME}.tar.gz --directory=/usr/local/bin && \ cd - && \ - rm -r /launcher-temp && \ - chmod 4555 ${N8N_RUNNERS_LAUNCHER_PATH} && \ - addgroup -g 2000 task-runner && \ - adduser -D -u 2000 -g "Task Runner User" -G task-runner task-runner + rm -r /launcher-temp COPY docker-entrypoint.sh / diff --git a/docker/images/n8n/n8n-task-runners.json b/docker/images/n8n/n8n-task-runners.json index 2dd65b67c8..c64d0ecdd0 100644 --- a/docker/images/n8n/n8n-task-runners.json +++ b/docker/images/n8n/n8n-task-runners.json @@ -2,18 +2,28 @@ "task-runners": [ { "runner-type": "javascript", - "workdir": "/home/task-runner", + "workdir": "/home/node", "command": "/usr/local/bin/node", "args": ["/usr/local/lib/node_modules/n8n/node_modules/@n8n/task-runner/dist/start.js"], "allowed-env": [ "PATH", + "GENERIC_TIMEZONE", "N8N_RUNNERS_GRANT_TOKEN", - "N8N_RUNNERS_N8N_URI", + "N8N_RUNNERS_TASK_BROKER_URI", + "N8N_RUNNERS_MAX_PAYLOAD", + "N8N_RUNNERS_MAX_CONCURRENCY", + "N8N_RUNNERS_TASK_TIMEOUT", + "N8N_RUNNERS_HEALTH_CHECK_SERVER_ENABLED", + "N8N_RUNNERS_HEALTH_CHECK_SERVER_HOST", + "N8N_RUNNERS_HEALTH_CHECK_SERVER_PORT", "NODE_FUNCTION_ALLOW_BUILTIN", - "NODE_FUNCTION_ALLOW_EXTERNAL" - ], - "uid": 2000, - "gid": 2000 + "NODE_FUNCTION_ALLOW_EXTERNAL", + "NODE_OPTIONS", + "N8N_SENTRY_DSN", + "N8N_VERSION", + "ENVIRONMENT", + "DEPLOYMENT_NAME" + ] } ] } diff --git a/package.json b/package.json index d25f0a13b2..063accd855 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,12 @@ { "name": "n8n-monorepo", - "version": "1.64.0", + "version": "1.73.0", "private": true, "engines": { "node": ">=20.15", - "pnpm": ">=9.5" + "pnpm": ">=9.15" }, - "packageManager": "pnpm@9.6.0", + "packageManager": "pnpm@9.15.1", "scripts": { "prepare": "node scripts/prepare.mjs", "preinstall": "node scripts/block-npm-install.js", @@ -16,6 +16,7 @@ "build:nodes": "turbo run build:nodes", "typecheck": "turbo typecheck", "dev": "turbo run dev --parallel --env-mode=loose --filter=!n8n-design-system --filter=!@n8n/chat --filter=!@n8n/task-runner", + "dev:be": "turbo run dev --parallel --env-mode=loose --filter=!n8n-design-system --filter=!@n8n/chat --filter=!@n8n/task-runner --filter=!n8n-editor-ui", "dev:ai": "turbo run dev --parallel --env-mode=loose --filter=@n8n/nodes-langchain --filter=n8n --filter=n8n-core", "clean": "turbo run clean --parallel", "reset": "node scripts/ensure-zx.mjs && zx scripts/reset.mjs", @@ -43,7 +44,9 @@ "@biomejs/biome": "^1.9.0", "@n8n_io/eslint-config": "workspace:*", "@types/jest": "^29.5.3", + "@types/node": "*", "@types/supertest": "^6.0.2", + "cross-env": "^7.0.3", "jest": "^29.6.2", "jest-environment-jsdom": "^29.6.2", "jest-expect-message": "^1.1.3", @@ -57,9 +60,9 @@ "run-script-os": "^1.0.7", "supertest": "^7.0.0", "ts-jest": "^29.1.1", - "tsc-alias": "^1.8.7", - "tsc-watch": "^6.0.4", - "turbo": "2.1.2", + "tsc-alias": "^1.8.10", + "tsc-watch": "^6.2.0", + "turbo": "2.3.3", "typescript": "*", "zx": "^8.1.4" }, @@ -76,18 +79,18 @@ "semver": "^7.5.4", "tslib": "^2.6.2", "tsconfig-paths": "^4.2.0", - "typescript": "^5.6.2", - "vue-tsc": "^2.1.6", + "typescript": "^5.7.2", + "vue-tsc": "^2.1.10", "ws": ">=8.17.1" }, "patchedDependencies": { "typedi@0.10.0": "patches/typedi@0.10.0.patch", - "@sentry/cli@2.36.2": "patches/@sentry__cli@2.36.2.patch", "pkce-challenge@3.0.0": "patches/pkce-challenge@3.0.0.patch", "pyodide@0.23.4": "patches/pyodide@0.23.4.patch", "@types/express-serve-static-core@4.17.43": "patches/@types__express-serve-static-core@4.17.43.patch", "@types/ws@8.5.4": "patches/@types__ws@8.5.4.patch", - "@types/uuencode@0.0.3": "patches/@types__uuencode@0.0.3.patch" + "@types/uuencode@0.0.3": "patches/@types__uuencode@0.0.3.patch", + "vue-tsc@2.1.10": "patches/vue-tsc@2.1.10.patch" } } } diff --git a/packages/@n8n/api-types/package.json b/packages/@n8n/api-types/package.json index 0c4440eb6b..299986a5c9 100644 --- a/packages/@n8n/api-types/package.json +++ b/packages/@n8n/api-types/package.json @@ -1,6 +1,6 @@ { "name": "@n8n/api-types", - "version": "0.5.0", + "version": "0.11.0", "scripts": { "clean": "rimraf dist .turbo", "dev": "pnpm watch", @@ -21,11 +21,12 @@ "dist/**/*" ], "devDependencies": { + "@n8n/config": "workspace:*", "n8n-workflow": "workspace:*" }, "dependencies": { "xss": "catalog:", "zod": "catalog:", - "zod-class": "0.0.15" + "zod-class": "0.0.16" } } diff --git a/packages/@n8n/api-types/src/dto/ai/__tests__/ai-apply-suggestion-request.dto.test.ts b/packages/@n8n/api-types/src/dto/ai/__tests__/ai-apply-suggestion-request.dto.test.ts new file mode 100644 index 0000000000..568900e409 --- /dev/null +++ b/packages/@n8n/api-types/src/dto/ai/__tests__/ai-apply-suggestion-request.dto.test.ts @@ -0,0 +1,36 @@ +import { AiApplySuggestionRequestDto } from '../ai-apply-suggestion-request.dto'; + +describe('AiApplySuggestionRequestDto', () => { + it('should validate a valid suggestion application request', () => { + const validRequest = { + sessionId: 'session-123', + suggestionId: 'suggestion-456', + }; + + const result = AiApplySuggestionRequestDto.safeParse(validRequest); + + expect(result.success).toBe(true); + }); + + it('should fail if sessionId is missing', () => { + const invalidRequest = { + suggestionId: 'suggestion-456', + }; + + const result = AiApplySuggestionRequestDto.safeParse(invalidRequest); + + expect(result.success).toBe(false); + expect(result.error?.issues[0].path).toEqual(['sessionId']); + }); + + it('should fail if suggestionId is missing', () => { + const invalidRequest = { + sessionId: 'session-123', + }; + + const result = AiApplySuggestionRequestDto.safeParse(invalidRequest); + + expect(result.success).toBe(false); + expect(result.error?.issues[0].path).toEqual(['suggestionId']); + }); +}); diff --git a/packages/@n8n/api-types/src/dto/ai/__tests__/ai-ask-request.dto.test.ts b/packages/@n8n/api-types/src/dto/ai/__tests__/ai-ask-request.dto.test.ts new file mode 100644 index 0000000000..a87eb5f3a4 --- /dev/null +++ b/packages/@n8n/api-types/src/dto/ai/__tests__/ai-ask-request.dto.test.ts @@ -0,0 +1,252 @@ +import { AiAskRequestDto } from '../ai-ask-request.dto'; + +describe('AiAskRequestDto', () => { + const validRequest = { + question: 'How can I improve this workflow?', + context: { + schema: [ + { + nodeName: 'TestNode', + schema: { + type: 'string', + key: 'testKey', + value: 'testValue', + path: '/test/path', + }, + }, + ], + inputSchema: { + nodeName: 'InputNode', + schema: { + type: 'object', + key: 'inputKey', + value: [ + { + type: 'string', + key: 'nestedKey', + value: 'nestedValue', + path: '/nested/path', + }, + ], + path: '/input/path', + }, + }, + pushRef: 'push-123', + ndvPushRef: 'ndv-push-456', + }, + forNode: 'TestWorkflowNode', + }; + + it('should validate a valid AI ask request', () => { + const result = AiAskRequestDto.safeParse(validRequest); + + expect(result.success).toBe(true); + }); + + it('should fail if question is missing', () => { + const invalidRequest = { + ...validRequest, + question: undefined, + }; + + const result = AiAskRequestDto.safeParse(invalidRequest); + + expect(result.success).toBe(false); + expect(result.error?.issues[0].path).toEqual(['question']); + }); + + it('should fail if context is invalid', () => { + const invalidRequest = { + ...validRequest, + context: { + ...validRequest.context, + schema: [ + { + nodeName: 'TestNode', + schema: { + type: 'invalid-type', // Invalid type + value: 'testValue', + path: '/test/path', + }, + }, + ], + }, + }; + + const result = AiAskRequestDto.safeParse(invalidRequest); + + expect(result.success).toBe(false); + }); + + it('should fail if forNode is missing', () => { + const invalidRequest = { + ...validRequest, + forNode: undefined, + }; + + const result = AiAskRequestDto.safeParse(invalidRequest); + + expect(result.success).toBe(false); + expect(result.error?.issues[0].path).toEqual(['forNode']); + }); + + it('should validate all possible schema types', () => { + const allTypesRequest = { + question: 'Test all possible types', + context: { + schema: [ + { + nodeName: 'AllTypesNode', + schema: { + type: 'object', + key: 'typesRoot', + value: [ + { type: 'string', key: 'stringType', value: 'string', path: '/types/string' }, + { type: 'number', key: 'numberType', value: 'number', path: '/types/number' }, + { type: 'boolean', key: 'booleanType', value: 'boolean', path: '/types/boolean' }, + { type: 'bigint', key: 'bigintType', value: 'bigint', path: '/types/bigint' }, + { type: 'symbol', key: 'symbolType', value: 'symbol', path: '/types/symbol' }, + { type: 'array', key: 'arrayType', value: [], path: '/types/array' }, + { type: 'object', key: 'objectType', value: [], path: '/types/object' }, + { + type: 'function', + key: 'functionType', + value: 'function', + path: '/types/function', + }, + { type: 'null', key: 'nullType', value: 'null', path: '/types/null' }, + { + type: 'undefined', + key: 'undefinedType', + value: 'undefined', + path: '/types/undefined', + }, + ], + path: '/types/root', + }, + }, + ], + inputSchema: { + nodeName: 'InputNode', + schema: { + type: 'object', + key: 'simpleInput', + value: [ + { + type: 'string', + key: 'simpleKey', + value: 'simpleValue', + path: '/simple/path', + }, + ], + path: '/simple/input/path', + }, + }, + pushRef: 'push-types-123', + ndvPushRef: 'ndv-push-types-456', + }, + forNode: 'TypeCheckNode', + }; + + const result = AiAskRequestDto.safeParse(allTypesRequest); + expect(result.success).toBe(true); + }); + + it('should fail with invalid type', () => { + const invalidTypeRequest = { + question: 'Test invalid type', + context: { + schema: [ + { + nodeName: 'InvalidTypeNode', + schema: { + type: 'invalid-type', // This should fail + key: 'invalidKey', + value: 'invalidValue', + path: '/invalid/path', + }, + }, + ], + inputSchema: { + nodeName: 'InputNode', + schema: { + type: 'object', + key: 'simpleInput', + value: [ + { + type: 'string', + key: 'simpleKey', + value: 'simpleValue', + path: '/simple/path', + }, + ], + path: '/simple/input/path', + }, + }, + pushRef: 'push-invalid-123', + ndvPushRef: 'ndv-push-invalid-456', + }, + forNode: 'InvalidTypeNode', + }; + + const result = AiAskRequestDto.safeParse(invalidTypeRequest); + expect(result.success).toBe(false); + }); + + it('should validate multiple schema entries', () => { + const multiSchemaRequest = { + question: 'Multiple schema test', + context: { + schema: [ + { + nodeName: 'FirstNode', + schema: { + type: 'string', + key: 'firstKey', + value: 'firstValue', + path: '/first/path', + }, + }, + { + nodeName: 'SecondNode', + schema: { + type: 'object', + key: 'secondKey', + value: [ + { + type: 'number', + key: 'nestedKey', + value: 'nestedValue', + path: '/second/nested/path', + }, + ], + path: '/second/path', + }, + }, + ], + inputSchema: { + nodeName: 'InputNode', + schema: { + type: 'object', + key: 'simpleInput', + value: [ + { + type: 'string', + key: 'simpleKey', + value: 'simpleValue', + path: '/simple/path', + }, + ], + path: '/simple/input/path', + }, + }, + pushRef: 'push-multi-123', + ndvPushRef: 'ndv-push-multi-456', + }, + forNode: 'MultiSchemaNode', + }; + + const result = AiAskRequestDto.safeParse(multiSchemaRequest); + expect(result.success).toBe(true); + }); +}); diff --git a/packages/@n8n/api-types/src/dto/ai/__tests__/ai-chat-request.dto.test.ts b/packages/@n8n/api-types/src/dto/ai/__tests__/ai-chat-request.dto.test.ts new file mode 100644 index 0000000000..ce1ccffac5 --- /dev/null +++ b/packages/@n8n/api-types/src/dto/ai/__tests__/ai-chat-request.dto.test.ts @@ -0,0 +1,34 @@ +import { AiChatRequestDto } from '../ai-chat-request.dto'; + +describe('AiChatRequestDto', () => { + it('should validate a request with a payload and session ID', () => { + const validRequest = { + payload: { someKey: 'someValue' }, + sessionId: 'session-123', + }; + + const result = AiChatRequestDto.safeParse(validRequest); + + expect(result.success).toBe(true); + }); + + it('should validate a request with only a payload', () => { + const validRequest = { + payload: { complexObject: { nested: 'value' } }, + }; + + const result = AiChatRequestDto.safeParse(validRequest); + + expect(result.success).toBe(true); + }); + + it('should fail if payload is missing', () => { + const invalidRequest = { + sessionId: 'session-123', + }; + + const result = AiChatRequestDto.safeParse(invalidRequest); + + expect(result.success).toBe(false); + }); +}); diff --git a/packages/@n8n/api-types/src/dto/ai/__tests__/ai-free-credits-request.dto.test.ts b/packages/@n8n/api-types/src/dto/ai/__tests__/ai-free-credits-request.dto.test.ts new file mode 100644 index 0000000000..2b61eeaee9 --- /dev/null +++ b/packages/@n8n/api-types/src/dto/ai/__tests__/ai-free-credits-request.dto.test.ts @@ -0,0 +1,32 @@ +import { nanoId } from 'minifaker'; + +import { AiFreeCreditsRequestDto } from '../ai-free-credits-request.dto'; +import 'minifaker/locales/en'; + +describe('AiChatRequestDto', () => { + it('should succeed if projectId is a valid nanoid', () => { + const validRequest = { + projectId: nanoId.nanoid(), + }; + + const result = AiFreeCreditsRequestDto.safeParse(validRequest); + + expect(result.success).toBe(true); + }); + + it('should succeed if no projectId is sent', () => { + const result = AiFreeCreditsRequestDto.safeParse({}); + + expect(result.success).toBe(true); + }); + + it('should fail is projectId invalid value', () => { + const validRequest = { + projectId: '', + }; + + const result = AiFreeCreditsRequestDto.safeParse(validRequest); + + expect(result.success).toBe(false); + }); +}); diff --git a/packages/@n8n/api-types/src/dto/ai/ai-apply-suggestion-request.dto.ts b/packages/@n8n/api-types/src/dto/ai/ai-apply-suggestion-request.dto.ts new file mode 100644 index 0000000000..cc808dfd24 --- /dev/null +++ b/packages/@n8n/api-types/src/dto/ai/ai-apply-suggestion-request.dto.ts @@ -0,0 +1,7 @@ +import { z } from 'zod'; +import { Z } from 'zod-class'; + +export class AiApplySuggestionRequestDto extends Z.class({ + sessionId: z.string(), + suggestionId: z.string(), +}) {} diff --git a/packages/@n8n/api-types/src/dto/ai/ai-ask-request.dto.ts b/packages/@n8n/api-types/src/dto/ai/ai-ask-request.dto.ts new file mode 100644 index 0000000000..9039243e05 --- /dev/null +++ b/packages/@n8n/api-types/src/dto/ai/ai-ask-request.dto.ts @@ -0,0 +1,53 @@ +import type { AiAssistantSDK, SchemaType } from '@n8n_io/ai-assistant-sdk'; +import { z } from 'zod'; +import { Z } from 'zod-class'; + +// Note: This is copied from the sdk, since this type is not exported +type Schema = { + type: SchemaType; + key?: string; + value: string | Schema[]; + path: string; +}; + +// Create a lazy validator to handle the recursive type +const schemaValidator: z.ZodType = z.lazy(() => + z.object({ + type: z.enum([ + 'string', + 'number', + 'boolean', + 'bigint', + 'symbol', + 'array', + 'object', + 'function', + 'null', + 'undefined', + ]), + key: z.string().optional(), + value: z.union([z.string(), z.lazy(() => schemaValidator.array())]), + path: z.string(), + }), +); + +export class AiAskRequestDto + extends Z.class({ + question: z.string(), + context: z.object({ + schema: z.array( + z.object({ + nodeName: z.string(), + schema: schemaValidator, + }), + ), + inputSchema: z.object({ + nodeName: z.string(), + schema: schemaValidator, + }), + pushRef: z.string(), + ndvPushRef: z.string(), + }), + forNode: z.string(), + }) + implements AiAssistantSDK.AskAiRequestPayload {} diff --git a/packages/@n8n/api-types/src/dto/ai/ai-chat-request.dto.ts b/packages/@n8n/api-types/src/dto/ai/ai-chat-request.dto.ts new file mode 100644 index 0000000000..59e7a26aa3 --- /dev/null +++ b/packages/@n8n/api-types/src/dto/ai/ai-chat-request.dto.ts @@ -0,0 +1,10 @@ +import type { AiAssistantSDK } from '@n8n_io/ai-assistant-sdk'; +import { z } from 'zod'; +import { Z } from 'zod-class'; + +export class AiChatRequestDto + extends Z.class({ + payload: z.object({}).passthrough(), // Allow any object shape + sessionId: z.string().optional(), + }) + implements AiAssistantSDK.ChatRequestPayload {} diff --git a/packages/@n8n/api-types/src/dto/ai/ai-free-credits-request.dto.ts b/packages/@n8n/api-types/src/dto/ai/ai-free-credits-request.dto.ts new file mode 100644 index 0000000000..9f9120d417 --- /dev/null +++ b/packages/@n8n/api-types/src/dto/ai/ai-free-credits-request.dto.ts @@ -0,0 +1,6 @@ +import { z } from 'zod'; +import { Z } from 'zod-class'; + +export class AiFreeCreditsRequestDto extends Z.class({ + projectId: z.string().min(1).optional(), +}) {} diff --git a/packages/@n8n/api-types/src/dto/auth/__tests__/login-request.dto.test.ts b/packages/@n8n/api-types/src/dto/auth/__tests__/login-request.dto.test.ts new file mode 100644 index 0000000000..f222f1d93e --- /dev/null +++ b/packages/@n8n/api-types/src/dto/auth/__tests__/login-request.dto.test.ts @@ -0,0 +1,93 @@ +import { LoginRequestDto } from '../login-request.dto'; + +describe('LoginRequestDto', () => { + describe('Valid requests', () => { + test.each([ + { + name: 'complete valid login request', + request: { + email: 'test@example.com', + password: 'securePassword123', + mfaCode: '123456', + }, + }, + { + name: 'login request without optional MFA', + request: { + email: 'test@example.com', + password: 'securePassword123', + }, + }, + { + name: 'login request with both mfaCode and mfaRecoveryCode', + request: { + email: 'test@example.com', + password: 'securePassword123', + mfaCode: '123456', + mfaRecoveryCode: 'recovery-code-123', + }, + }, + { + name: 'login request with only mfaRecoveryCode', + request: { + email: 'test@example.com', + password: 'securePassword123', + mfaRecoveryCode: 'recovery-code-123', + }, + }, + ])('should validate $name', ({ request }) => { + const result = LoginRequestDto.safeParse(request); + expect(result.success).toBe(true); + }); + }); + + describe('Invalid requests', () => { + test.each([ + { + name: 'invalid email', + request: { + email: 'invalid-email', + password: 'securePassword123', + }, + expectedErrorPath: ['email'], + }, + { + name: 'empty password', + request: { + email: 'test@example.com', + password: '', + }, + expectedErrorPath: ['password'], + }, + { + name: 'missing email', + request: { + password: 'securePassword123', + }, + expectedErrorPath: ['email'], + }, + { + name: 'missing password', + request: { + email: 'test@example.com', + }, + expectedErrorPath: ['password'], + }, + { + name: 'whitespace in email and password', + request: { + email: ' test@example.com ', + password: ' securePassword123 ', + }, + expectedErrorPath: ['email'], + }, + ])('should fail validation for $name', ({ request, expectedErrorPath }) => { + const result = LoginRequestDto.safeParse(request); + expect(result.success).toBe(false); + + if (expectedErrorPath) { + expect(result.error?.issues[0].path).toEqual(expectedErrorPath); + } + }); + }); +}); diff --git a/packages/@n8n/api-types/src/dto/auth/__tests__/resolve-signup-token-query.dto.test.ts b/packages/@n8n/api-types/src/dto/auth/__tests__/resolve-signup-token-query.dto.test.ts new file mode 100644 index 0000000000..218fe9107a --- /dev/null +++ b/packages/@n8n/api-types/src/dto/auth/__tests__/resolve-signup-token-query.dto.test.ts @@ -0,0 +1,87 @@ +import { ResolveSignupTokenQueryDto } from '../resolve-signup-token-query.dto'; + +describe('ResolveSignupTokenQueryDto', () => { + const validUuid = '123e4567-e89b-12d3-a456-426614174000'; + + describe('Valid requests', () => { + test.each([ + { + name: 'standard UUID', + request: { + inviterId: validUuid, + inviteeId: validUuid, + }, + }, + ])('should validate $name', ({ request }) => { + const result = ResolveSignupTokenQueryDto.safeParse(request); + expect(result.success).toBe(true); + }); + }); + + describe('Invalid requests', () => { + test.each([ + { + name: 'invalid inviterId UUID', + request: { + inviterId: 'not-a-valid-uuid', + inviteeId: validUuid, + }, + expectedErrorPath: ['inviterId'], + }, + { + name: 'invalid inviteeId UUID', + request: { + inviterId: validUuid, + inviteeId: 'not-a-valid-uuid', + }, + expectedErrorPath: ['inviteeId'], + }, + { + name: 'missing inviterId', + request: { + inviteeId: validUuid, + }, + expectedErrorPath: ['inviterId'], + }, + { + name: 'missing inviteeId', + request: { + inviterId: validUuid, + }, + expectedErrorPath: ['inviteeId'], + }, + { + name: 'UUID with invalid characters', + request: { + inviterId: '123e4567-e89b-12d3-a456-42661417400G', + inviteeId: validUuid, + }, + expectedErrorPath: ['inviterId'], + }, + { + name: 'UUID too long', + request: { + inviterId: '123e4567-e89b-12d3-a456-426614174001234', + inviteeId: validUuid, + }, + expectedErrorPath: ['inviterId'], + }, + { + name: 'UUID too short', + request: { + inviterId: '123e4567-e89b-12d3-a456', + inviteeId: validUuid, + }, + expectedErrorPath: ['inviterId'], + }, + ])('should fail validation for $name', ({ request, expectedErrorPath }) => { + const result = ResolveSignupTokenQueryDto.safeParse(request); + + expect(result.success).toBe(false); + + if (expectedErrorPath) { + expect(result.error?.issues[0].path).toEqual(expectedErrorPath); + } + }); + }); +}); diff --git a/packages/@n8n/api-types/src/dto/auth/login-request.dto.ts b/packages/@n8n/api-types/src/dto/auth/login-request.dto.ts new file mode 100644 index 0000000000..894263992c --- /dev/null +++ b/packages/@n8n/api-types/src/dto/auth/login-request.dto.ts @@ -0,0 +1,9 @@ +import { z } from 'zod'; +import { Z } from 'zod-class'; + +export class LoginRequestDto extends Z.class({ + email: z.string().email(), + password: z.string().min(1), + mfaCode: z.string().optional(), + mfaRecoveryCode: z.string().optional(), +}) {} diff --git a/packages/@n8n/api-types/src/dto/auth/resolve-signup-token-query.dto.ts b/packages/@n8n/api-types/src/dto/auth/resolve-signup-token-query.dto.ts new file mode 100644 index 0000000000..768202ff04 --- /dev/null +++ b/packages/@n8n/api-types/src/dto/auth/resolve-signup-token-query.dto.ts @@ -0,0 +1,7 @@ +import { z } from 'zod'; +import { Z } from 'zod-class'; + +export class ResolveSignupTokenQueryDto extends Z.class({ + inviterId: z.string().uuid(), + inviteeId: z.string().uuid(), +}) {} diff --git a/packages/@n8n/api-types/src/dto/credentials/__tests__/credentials-get-many-request.dto.test.ts b/packages/@n8n/api-types/src/dto/credentials/__tests__/credentials-get-many-request.dto.test.ts new file mode 100644 index 0000000000..0fa074b97d --- /dev/null +++ b/packages/@n8n/api-types/src/dto/credentials/__tests__/credentials-get-many-request.dto.test.ts @@ -0,0 +1,55 @@ +import { CredentialsGetManyRequestQuery } from '../credentials-get-many-request.dto'; + +describe('CredentialsGetManyRequestQuery', () => { + describe('should pass validation', () => { + it('with empty object', () => { + const data = {}; + + const result = CredentialsGetManyRequestQuery.safeParse(data); + + expect(result.success).toBe(true); + }); + + test.each([ + { field: 'includeScopes', value: 'true' }, + { field: 'includeScopes', value: 'false' }, + { field: 'includeData', value: 'true' }, + { field: 'includeData', value: 'false' }, + ])('with $field set to $value', ({ field, value }) => { + const data = { [field]: value }; + + const result = CredentialsGetManyRequestQuery.safeParse(data); + + expect(result.success).toBe(true); + }); + + it('with both parameters set', () => { + const data = { + includeScopes: 'true', + includeData: 'true', + }; + + const result = CredentialsGetManyRequestQuery.safeParse(data); + + expect(result.success).toBe(true); + }); + }); + + describe('should fail validation', () => { + test.each([ + { field: 'includeScopes', value: true }, + { field: 'includeScopes', value: false }, + { field: 'includeScopes', value: 'invalid' }, + { field: 'includeData', value: true }, + { field: 'includeData', value: false }, + { field: 'includeData', value: 'invalid' }, + ])('with invalid value $value for $field', ({ field, value }) => { + const data = { [field]: value }; + + const result = CredentialsGetManyRequestQuery.safeParse(data); + + expect(result.success).toBe(false); + expect(result.error?.issues[0].path[0]).toBe(field); + }); + }); +}); diff --git a/packages/@n8n/api-types/src/dto/credentials/__tests__/credentials-get-one-request.dto.test.ts b/packages/@n8n/api-types/src/dto/credentials/__tests__/credentials-get-one-request.dto.test.ts new file mode 100644 index 0000000000..274b00b759 --- /dev/null +++ b/packages/@n8n/api-types/src/dto/credentials/__tests__/credentials-get-one-request.dto.test.ts @@ -0,0 +1,52 @@ +import { CredentialsGetOneRequestQuery } from '../credentials-get-one-request.dto'; + +describe('CredentialsGetManyRequestQuery', () => { + describe('should pass validation', () => { + it('with empty object', () => { + const data = {}; + + const result = CredentialsGetOneRequestQuery.safeParse(data); + + expect(result.success).toBe(true); + // defaults to false + expect(result.data?.includeData).toBe(false); + }); + + test.each([ + { field: 'includeData', value: 'true' }, + { field: 'includeData', value: 'false' }, + ])('with $field set to $value', ({ field, value }) => { + const data = { [field]: value }; + + const result = CredentialsGetOneRequestQuery.safeParse(data); + + expect(result.success).toBe(true); + }); + + it('with both parameters set', () => { + const data = { + includeScopes: 'true', + includeData: 'true', + }; + + const result = CredentialsGetOneRequestQuery.safeParse(data); + + expect(result.success).toBe(true); + }); + }); + + describe('should fail validation', () => { + test.each([ + { field: 'includeData', value: true }, + { field: 'includeData', value: false }, + { field: 'includeData', value: 'invalid' }, + ])('with invalid value $value for $field', ({ field, value }) => { + const data = { [field]: value }; + + const result = CredentialsGetOneRequestQuery.safeParse(data); + + expect(result.success).toBe(false); + expect(result.error?.issues[0].path[0]).toBe(field); + }); + }); +}); diff --git a/packages/@n8n/api-types/src/dto/credentials/credentials-get-many-request.dto.ts b/packages/@n8n/api-types/src/dto/credentials/credentials-get-many-request.dto.ts new file mode 100644 index 0000000000..47332ca7f9 --- /dev/null +++ b/packages/@n8n/api-types/src/dto/credentials/credentials-get-many-request.dto.ts @@ -0,0 +1,22 @@ +import { Z } from 'zod-class'; + +import { booleanFromString } from '../../schemas/booleanFromString'; + +export class CredentialsGetManyRequestQuery extends Z.class({ + /** + * Adds the `scopes` field to each credential which includes all scopes the + * requesting user has in relation to the credential, e.g. + * ['credential:read', 'credential:update'] + */ + includeScopes: booleanFromString.optional(), + + /** + * Adds the decrypted `data` field to each credential. + * + * It only does this for credentials for which the user has the + * `credential:update` scope. + * + * This switches `includeScopes` to true to be able to check for the scopes + */ + includeData: booleanFromString.optional(), +}) {} diff --git a/packages/@n8n/api-types/src/dto/credentials/credentials-get-one-request.dto.ts b/packages/@n8n/api-types/src/dto/credentials/credentials-get-one-request.dto.ts new file mode 100644 index 0000000000..ad790014e8 --- /dev/null +++ b/packages/@n8n/api-types/src/dto/credentials/credentials-get-one-request.dto.ts @@ -0,0 +1,13 @@ +import { Z } from 'zod-class'; + +import { booleanFromString } from '../../schemas/booleanFromString'; + +export class CredentialsGetOneRequestQuery extends Z.class({ + /** + * Adds the decrypted `data` field to each credential. + * + * It only does this for credentials for which the user has the + * `credential:update` scope. + */ + includeData: booleanFromString.optional().default('false'), +}) {} diff --git a/packages/@n8n/api-types/src/dto/dynamic-node-parameters/__tests__/action-result-request.dto.test.ts b/packages/@n8n/api-types/src/dto/dynamic-node-parameters/__tests__/action-result-request.dto.test.ts new file mode 100644 index 0000000000..eb451e5b09 --- /dev/null +++ b/packages/@n8n/api-types/src/dto/dynamic-node-parameters/__tests__/action-result-request.dto.test.ts @@ -0,0 +1,81 @@ +import { ActionResultRequestDto } from '../action-result-request.dto'; + +describe('ActionResultRequestDto', () => { + const baseValidRequest = { + path: '/test/path', + nodeTypeAndVersion: { name: 'TestNode', version: 1 }, + handler: 'testHandler', + currentNodeParameters: {}, + }; + + describe('Valid requests', () => { + test.each([ + { + name: 'minimal valid request', + request: baseValidRequest, + }, + { + name: 'request with payload', + request: { + ...baseValidRequest, + payload: { key: 'value' }, + }, + }, + { + name: 'request with credentials', + request: { + ...baseValidRequest, + credentials: { testCredential: { id: 'cred1', name: 'Test Cred' } }, + }, + }, + { + name: 'request with current node parameters', + request: { + ...baseValidRequest, + currentNodeParameters: { param1: 'value1' }, + }, + }, + ])('should validate $name', ({ request }) => { + const result = ActionResultRequestDto.safeParse(request); + expect(result.success).toBe(true); + }); + }); + + describe('Invalid requests', () => { + test.each([ + { + name: 'missing path', + request: { + nodeTypeAndVersion: { name: 'TestNode', version: 1 }, + handler: 'testHandler', + }, + expectedErrorPath: ['path'], + }, + { + name: 'missing handler', + request: { + path: '/test/path', + currentNodeParameters: {}, + nodeTypeAndVersion: { name: 'TestNode', version: 1 }, + }, + expectedErrorPath: ['handler'], + }, + { + name: 'invalid node version', + request: { + ...baseValidRequest, + nodeTypeAndVersion: { name: 'TestNode', version: 0 }, + }, + expectedErrorPath: ['nodeTypeAndVersion', 'version'], + }, + ])('should fail validation for $name', ({ request, expectedErrorPath }) => { + const result = ActionResultRequestDto.safeParse(request); + + expect(result.success).toBe(false); + + if (expectedErrorPath) { + expect(result.error?.issues[0].path).toEqual(expectedErrorPath); + } + }); + }); +}); diff --git a/packages/@n8n/api-types/src/dto/dynamic-node-parameters/__tests__/options-request.dto.test.ts b/packages/@n8n/api-types/src/dto/dynamic-node-parameters/__tests__/options-request.dto.test.ts new file mode 100644 index 0000000000..28c5534cc7 --- /dev/null +++ b/packages/@n8n/api-types/src/dto/dynamic-node-parameters/__tests__/options-request.dto.test.ts @@ -0,0 +1,90 @@ +import { OptionsRequestDto } from '../options-request.dto'; + +describe('OptionsRequestDto', () => { + const baseValidRequest = { + path: '/test/path', + nodeTypeAndVersion: { name: 'TestNode', version: 1 }, + currentNodeParameters: {}, + }; + + describe('Valid requests', () => { + test.each([ + { + name: 'minimal valid request', + request: baseValidRequest, + }, + { + name: 'request with method name', + request: { + ...baseValidRequest, + methodName: 'testMethod', + }, + }, + { + name: 'request with load options', + request: { + ...baseValidRequest, + loadOptions: { + routing: { + operations: { someOperation: 'test' }, + output: { someOutput: 'test' }, + request: { someRequest: 'test' }, + }, + }, + }, + }, + { + name: 'request with credentials', + request: { + ...baseValidRequest, + credentials: { testCredential: { id: 'cred1', name: 'Test Cred' } }, + }, + }, + { + name: 'request with current node parameters', + request: { + ...baseValidRequest, + currentNodeParameters: { param1: 'value1' }, + }, + }, + ])('should validate $name', ({ request }) => { + const result = OptionsRequestDto.safeParse(request); + expect(result.success).toBe(true); + }); + }); + + describe('Invalid requests', () => { + test.each([ + { + name: 'missing path', + request: { + nodeTypeAndVersion: { name: 'TestNode', version: 1 }, + }, + expectedErrorPath: ['path'], + }, + { + name: 'missing node type and version', + request: { + path: '/test/path', + }, + expectedErrorPath: ['nodeTypeAndVersion'], + }, + { + name: 'invalid node version', + request: { + ...baseValidRequest, + nodeTypeAndVersion: { name: 'TestNode', version: 0 }, + }, + expectedErrorPath: ['nodeTypeAndVersion', 'version'], + }, + ])('should fail validation for $name', ({ request, expectedErrorPath }) => { + const result = OptionsRequestDto.safeParse(request); + + expect(result.success).toBe(false); + + if (expectedErrorPath) { + expect(result.error?.issues[0].path).toEqual(expectedErrorPath); + } + }); + }); +}); diff --git a/packages/@n8n/api-types/src/dto/dynamic-node-parameters/__tests__/resource-locator-request.dto.test.ts b/packages/@n8n/api-types/src/dto/dynamic-node-parameters/__tests__/resource-locator-request.dto.test.ts new file mode 100644 index 0000000000..d64f31dec2 --- /dev/null +++ b/packages/@n8n/api-types/src/dto/dynamic-node-parameters/__tests__/resource-locator-request.dto.test.ts @@ -0,0 +1,95 @@ +import { ResourceLocatorRequestDto } from '../resource-locator-request.dto'; + +describe('ResourceLocatorRequestDto', () => { + const baseValidRequest = { + path: '/test/path', + nodeTypeAndVersion: { name: 'TestNode', version: 1 }, + methodName: 'testMethod', + currentNodeParameters: {}, + }; + + describe('Valid requests', () => { + test.each([ + { + name: 'minimal valid request', + request: baseValidRequest, + }, + { + name: 'request with filter', + request: { + ...baseValidRequest, + filter: 'testFilter', + }, + }, + { + name: 'request with pagination token', + request: { + ...baseValidRequest, + paginationToken: 'token123', + }, + }, + { + name: 'request with credentials', + request: { + ...baseValidRequest, + credentials: { testCredential: { id: 'cred1', name: 'Test Cred' } }, + }, + }, + { + name: 'request with current node parameters', + request: { + ...baseValidRequest, + currentNodeParameters: { param1: 'value1' }, + }, + }, + { + name: 'request with a semver node version', + request: { + ...baseValidRequest, + nodeTypeAndVersion: { name: 'TestNode', version: 1.1 }, + }, + }, + ])('should validate $name', ({ request }) => { + const result = ResourceLocatorRequestDto.safeParse(request); + expect(result.success).toBe(true); + }); + }); + + describe('Invalid requests', () => { + test.each([ + { + name: 'missing path', + request: { + nodeTypeAndVersion: { name: 'TestNode', version: 1 }, + methodName: 'testMethod', + }, + expectedErrorPath: ['path'], + }, + { + name: 'missing method name', + request: { + path: '/test/path', + nodeTypeAndVersion: { name: 'TestNode', version: 1 }, + currentNodeParameters: {}, + }, + expectedErrorPath: ['methodName'], + }, + { + name: 'invalid node version', + request: { + ...baseValidRequest, + nodeTypeAndVersion: { name: 'TestNode', version: 0 }, + }, + expectedErrorPath: ['nodeTypeAndVersion', 'version'], + }, + ])('should fail validation for $name', ({ request, expectedErrorPath }) => { + const result = ResourceLocatorRequestDto.safeParse(request); + + expect(result.success).toBe(false); + + if (expectedErrorPath) { + expect(result.error?.issues[0].path).toEqual(expectedErrorPath); + } + }); + }); +}); diff --git a/packages/@n8n/api-types/src/dto/dynamic-node-parameters/__tests__/resource-mapper-fields-request.dto.test.ts b/packages/@n8n/api-types/src/dto/dynamic-node-parameters/__tests__/resource-mapper-fields-request.dto.test.ts new file mode 100644 index 0000000000..2370177ab0 --- /dev/null +++ b/packages/@n8n/api-types/src/dto/dynamic-node-parameters/__tests__/resource-mapper-fields-request.dto.test.ts @@ -0,0 +1,74 @@ +import { ResourceMapperFieldsRequestDto } from '../resource-mapper-fields-request.dto'; + +describe('ResourceMapperFieldsRequestDto', () => { + const baseValidRequest = { + path: '/test/path', + nodeTypeAndVersion: { name: 'TestNode', version: 1 }, + methodName: 'testMethod', + currentNodeParameters: {}, + }; + + describe('Valid requests', () => { + test.each([ + { + name: 'minimal valid request', + request: baseValidRequest, + }, + { + name: 'request with credentials', + request: { + ...baseValidRequest, + credentials: { testCredential: { id: 'cred1', name: 'Test Cred' } }, + }, + }, + { + name: 'request with current node parameters', + request: { + ...baseValidRequest, + currentNodeParameters: { param1: 'value1' }, + }, + }, + ])('should validate $name', ({ request }) => { + const result = ResourceMapperFieldsRequestDto.safeParse(request); + expect(result.success).toBe(true); + }); + }); + + describe('Invalid requests', () => { + test.each([ + { + name: 'missing path', + request: { + nodeTypeAndVersion: { name: 'TestNode', version: 1 }, + methodName: 'testMethod', + }, + expectedErrorPath: ['path'], + }, + { + name: 'missing method name', + request: { + path: '/test/path', + nodeTypeAndVersion: { name: 'TestNode', version: 1 }, + currentNodeParameters: {}, + }, + expectedErrorPath: ['methodName'], + }, + { + name: 'invalid node version', + request: { + ...baseValidRequest, + nodeTypeAndVersion: { name: 'TestNode', version: 0 }, + }, + expectedErrorPath: ['nodeTypeAndVersion', 'version'], + }, + ])('should fail validation for $name', ({ request, expectedErrorPath }) => { + const result = ResourceMapperFieldsRequestDto.safeParse(request); + + expect(result.success).toBe(false); + + if (expectedErrorPath) { + expect(result.error?.issues[0].path).toEqual(expectedErrorPath); + } + }); + }); +}); diff --git a/packages/@n8n/api-types/src/dto/dynamic-node-parameters/action-result-request.dto.ts b/packages/@n8n/api-types/src/dto/dynamic-node-parameters/action-result-request.dto.ts new file mode 100644 index 0000000000..d6f867af6d --- /dev/null +++ b/packages/@n8n/api-types/src/dto/dynamic-node-parameters/action-result-request.dto.ts @@ -0,0 +1,11 @@ +import type { IDataObject } from 'n8n-workflow'; +import { z } from 'zod'; + +import { BaseDynamicParametersRequestDto } from './base-dynamic-parameters-request.dto'; + +export class ActionResultRequestDto extends BaseDynamicParametersRequestDto.extend({ + handler: z.string(), + payload: z + .union([z.object({}).catchall(z.any()) satisfies z.ZodType, z.string()]) + .optional(), +}) {} diff --git a/packages/@n8n/api-types/src/dto/dynamic-node-parameters/base-dynamic-parameters-request.dto.ts b/packages/@n8n/api-types/src/dto/dynamic-node-parameters/base-dynamic-parameters-request.dto.ts new file mode 100644 index 0000000000..66b9cd7629 --- /dev/null +++ b/packages/@n8n/api-types/src/dto/dynamic-node-parameters/base-dynamic-parameters-request.dto.ts @@ -0,0 +1,18 @@ +import type { INodeCredentials, INodeParameters, INodeTypeNameVersion } from 'n8n-workflow'; +import { z } from 'zod'; +import { Z } from 'zod-class'; + +import { nodeVersionSchema } from '../../schemas/nodeVersion.schema'; + +export class BaseDynamicParametersRequestDto extends Z.class({ + path: z.string(), + nodeTypeAndVersion: z.object({ + name: z.string(), + version: nodeVersionSchema, + }) satisfies z.ZodType, + currentNodeParameters: z.record(z.string(), z.any()) satisfies z.ZodType, + methodName: z.string().optional(), + credentials: z.record(z.string(), z.any()).optional() satisfies z.ZodType< + INodeCredentials | undefined + >, +}) {} diff --git a/packages/@n8n/api-types/src/dto/dynamic-node-parameters/options-request.dto.ts b/packages/@n8n/api-types/src/dto/dynamic-node-parameters/options-request.dto.ts new file mode 100644 index 0000000000..b9d34ef75d --- /dev/null +++ b/packages/@n8n/api-types/src/dto/dynamic-node-parameters/options-request.dto.ts @@ -0,0 +1,18 @@ +import type { ILoadOptions } from 'n8n-workflow'; +import { z } from 'zod'; + +import { BaseDynamicParametersRequestDto } from './base-dynamic-parameters-request.dto'; + +export class OptionsRequestDto extends BaseDynamicParametersRequestDto.extend({ + loadOptions: z + .object({ + routing: z + .object({ + operations: z.any().optional(), + output: z.any().optional(), + request: z.any().optional(), + }) + .optional(), + }) + .optional() as z.ZodType, +}) {} diff --git a/packages/@n8n/api-types/src/dto/dynamic-node-parameters/resource-locator-request.dto.ts b/packages/@n8n/api-types/src/dto/dynamic-node-parameters/resource-locator-request.dto.ts new file mode 100644 index 0000000000..ac8e8df274 --- /dev/null +++ b/packages/@n8n/api-types/src/dto/dynamic-node-parameters/resource-locator-request.dto.ts @@ -0,0 +1,9 @@ +import { z } from 'zod'; + +import { BaseDynamicParametersRequestDto } from './base-dynamic-parameters-request.dto'; + +export class ResourceLocatorRequestDto extends BaseDynamicParametersRequestDto.extend({ + methodName: z.string(), + filter: z.string().optional(), + paginationToken: z.string().optional(), +}) {} diff --git a/packages/@n8n/api-types/src/dto/dynamic-node-parameters/resource-mapper-fields-request.dto.ts b/packages/@n8n/api-types/src/dto/dynamic-node-parameters/resource-mapper-fields-request.dto.ts new file mode 100644 index 0000000000..3c6d00eb3c --- /dev/null +++ b/packages/@n8n/api-types/src/dto/dynamic-node-parameters/resource-mapper-fields-request.dto.ts @@ -0,0 +1,7 @@ +import { z } from 'zod'; + +import { BaseDynamicParametersRequestDto } from './base-dynamic-parameters-request.dto'; + +export class ResourceMapperFieldsRequestDto extends BaseDynamicParametersRequestDto.extend({ + methodName: z.string(), +}) {} diff --git a/packages/@n8n/api-types/src/dto/index.ts b/packages/@n8n/api-types/src/dto/index.ts index 41a55f050a..6dbfe17361 100644 --- a/packages/@n8n/api-types/src/dto/index.ts +++ b/packages/@n8n/api-types/src/dto/index.ts @@ -1,5 +1,37 @@ +export { AiAskRequestDto } from './ai/ai-ask-request.dto'; +export { AiChatRequestDto } from './ai/ai-chat-request.dto'; +export { AiApplySuggestionRequestDto } from './ai/ai-apply-suggestion-request.dto'; +export { AiFreeCreditsRequestDto } from './ai/ai-free-credits-request.dto'; + +export { LoginRequestDto } from './auth/login-request.dto'; +export { ResolveSignupTokenQueryDto } from './auth/resolve-signup-token-query.dto'; + +export { OptionsRequestDto } from './dynamic-node-parameters/options-request.dto'; +export { ResourceLocatorRequestDto } from './dynamic-node-parameters/resource-locator-request.dto'; +export { ResourceMapperFieldsRequestDto } from './dynamic-node-parameters/resource-mapper-fields-request.dto'; +export { ActionResultRequestDto } from './dynamic-node-parameters/action-result-request.dto'; + +export { InviteUsersRequestDto } from './invitation/invite-users-request.dto'; +export { AcceptInvitationRequestDto } from './invitation/accept-invitation-request.dto'; + +export { OwnerSetupRequestDto } from './owner/owner-setup-request.dto'; +export { DismissBannerRequestDto } from './owner/dismiss-banner-request.dto'; + +export { ForgotPasswordRequestDto } from './password-reset/forgot-password-request.dto'; +export { ResolvePasswordTokenQueryDto } from './password-reset/resolve-password-token-query.dto'; +export { ChangePasswordRequestDto } from './password-reset/change-password-request.dto'; + +export { SamlAcsDto } from './saml/saml-acs.dto'; +export { SamlPreferences } from './saml/saml-preferences.dto'; +export { SamlToggleDto } from './saml/saml-toggle.dto'; + export { PasswordUpdateRequestDto } from './user/password-update-request.dto'; export { RoleChangeRequestDto } from './user/role-change-request.dto'; export { SettingsUpdateRequestDto } from './user/settings-update-request.dto'; export { UserUpdateRequestDto } from './user/user-update-request.dto'; + export { CommunityRegisteredRequestDto } from './license/community-registered-request.dto'; + +export { VariableListRequestDto } from './variables/variables-list-request.dto'; +export { CredentialsGetOneRequestQuery } from './credentials/credentials-get-one-request.dto'; +export { CredentialsGetManyRequestQuery } from './credentials/credentials-get-many-request.dto'; diff --git a/packages/@n8n/api-types/src/dto/invitation/__tests__/accept-invitation-request.dto.test.ts b/packages/@n8n/api-types/src/dto/invitation/__tests__/accept-invitation-request.dto.test.ts new file mode 100644 index 0000000000..f78de8ab6d --- /dev/null +++ b/packages/@n8n/api-types/src/dto/invitation/__tests__/accept-invitation-request.dto.test.ts @@ -0,0 +1,94 @@ +import { AcceptInvitationRequestDto } from '../accept-invitation-request.dto'; + +describe('AcceptInvitationRequestDto', () => { + const validUuid = '123e4567-e89b-12d3-a456-426614174000'; + + describe('Valid requests', () => { + test.each([ + { + name: 'complete valid invitation acceptance', + request: { + inviterId: validUuid, + firstName: 'John', + lastName: 'Doe', + password: 'SecurePassword123', + }, + }, + ])('should validate $name', ({ request }) => { + const result = AcceptInvitationRequestDto.safeParse(request); + expect(result.success).toBe(true); + }); + }); + + describe('Invalid requests', () => { + test.each([ + { + name: 'missing inviterId', + request: { + firstName: 'John', + lastName: 'Doe', + password: 'SecurePassword123', + }, + expectedErrorPath: ['inviterId'], + }, + { + name: 'invalid inviterId', + request: { + inviterId: 'not-a-valid-uuid', + firstName: 'John', + lastName: 'Doe', + password: 'SecurePassword123', + }, + expectedErrorPath: ['inviterId'], + }, + { + name: 'missing first name', + request: { + inviterId: validUuid, + firstName: '', + lastName: 'Doe', + password: 'SecurePassword123', + }, + expectedErrorPath: ['firstName'], + }, + { + name: 'missing last name', + request: { + inviterId: validUuid, + firstName: 'John', + lastName: '', + password: 'SecurePassword123', + }, + expectedErrorPath: ['lastName'], + }, + { + name: 'password too short', + request: { + inviterId: validUuid, + firstName: 'John', + lastName: 'Doe', + password: 'short', + }, + expectedErrorPath: ['password'], + }, + { + name: 'password without number', + request: { + inviterId: validUuid, + firstName: 'John', + lastName: 'Doe', + password: 'NoNumberPassword', + }, + expectedErrorPath: ['password'], + }, + ])('should fail validation for $name', ({ request, expectedErrorPath }) => { + const result = AcceptInvitationRequestDto.safeParse(request); + + expect(result.success).toBe(false); + + if (expectedErrorPath) { + expect(result.error?.issues[0].path).toEqual(expectedErrorPath); + } + }); + }); +}); diff --git a/packages/@n8n/api-types/src/dto/invitation/__tests__/invite-users-request.dto.test.ts b/packages/@n8n/api-types/src/dto/invitation/__tests__/invite-users-request.dto.test.ts new file mode 100644 index 0000000000..f47a138ed5 --- /dev/null +++ b/packages/@n8n/api-types/src/dto/invitation/__tests__/invite-users-request.dto.test.ts @@ -0,0 +1,60 @@ +import { InviteUsersRequestDto } from '../invite-users-request.dto'; + +describe('InviteUsersRequestDto', () => { + describe('Valid requests', () => { + test.each([ + { + name: 'empty array', + request: [], + }, + { + name: 'single user invitation with default role', + request: [{ email: 'user@example.com' }], + }, + { + name: 'multiple user invitations with different roles', + request: [ + { email: 'user1@example.com', role: 'global:member' }, + { email: 'user2@example.com', role: 'global:admin' }, + ], + }, + ])('should validate $name', ({ request }) => { + const result = InviteUsersRequestDto.safeParse(request); + expect(result.success).toBe(true); + }); + + it('should default role to global:member', () => { + const result = InviteUsersRequestDto.safeParse([{ email: 'user@example.com' }]); + expect(result.success).toBe(true); + expect(result.data?.[0].role).toBe('global:member'); + }); + }); + + describe('Invalid requests', () => { + test.each([ + { + name: 'invalid email', + request: [{ email: 'invalid-email' }], + expectedErrorPath: [0, 'email'], + }, + { + name: 'invalid role', + request: [ + { + email: 'user@example.com', + role: 'invalid-role', + }, + ], + expectedErrorPath: [0, 'role'], + }, + ])('should fail validation for $name', ({ request, expectedErrorPath }) => { + const result = InviteUsersRequestDto.safeParse(request); + + expect(result.success).toBe(false); + + if (expectedErrorPath) { + expect(result.error?.issues[0].path).toEqual(expectedErrorPath); + } + }); + }); +}); diff --git a/packages/@n8n/api-types/src/dto/invitation/accept-invitation-request.dto.ts b/packages/@n8n/api-types/src/dto/invitation/accept-invitation-request.dto.ts new file mode 100644 index 0000000000..7c93e708ba --- /dev/null +++ b/packages/@n8n/api-types/src/dto/invitation/accept-invitation-request.dto.ts @@ -0,0 +1,11 @@ +import { z } from 'zod'; +import { Z } from 'zod-class'; + +import { passwordSchema } from '../../schemas/password.schema'; + +export class AcceptInvitationRequestDto extends Z.class({ + inviterId: z.string().uuid(), + firstName: z.string().min(1, 'First name is required'), + lastName: z.string().min(1, 'Last name is required'), + password: passwordSchema, +}) {} diff --git a/packages/@n8n/api-types/src/dto/invitation/invite-users-request.dto.ts b/packages/@n8n/api-types/src/dto/invitation/invite-users-request.dto.ts new file mode 100644 index 0000000000..9693234c64 --- /dev/null +++ b/packages/@n8n/api-types/src/dto/invitation/invite-users-request.dto.ts @@ -0,0 +1,16 @@ +import { z } from 'zod'; + +const roleSchema = z.enum(['global:member', 'global:admin']); + +const invitedUserSchema = z.object({ + email: z.string().email(), + role: roleSchema.default('global:member'), +}); + +const invitationsSchema = z.array(invitedUserSchema); + +export class InviteUsersRequestDto extends Array> { + static safeParse(data: unknown) { + return invitationsSchema.safeParse(data); + } +} diff --git a/packages/@n8n/api-types/src/dto/owner/__tests__/dismiss-banner-request.dto.test.ts b/packages/@n8n/api-types/src/dto/owner/__tests__/dismiss-banner-request.dto.test.ts new file mode 100644 index 0000000000..97371de16a --- /dev/null +++ b/packages/@n8n/api-types/src/dto/owner/__tests__/dismiss-banner-request.dto.test.ts @@ -0,0 +1,64 @@ +import { bannerNameSchema } from '../../../schemas/bannerName.schema'; +import { DismissBannerRequestDto } from '../dismiss-banner-request.dto'; + +describe('DismissBannerRequestDto', () => { + describe('Valid requests', () => { + test.each( + bannerNameSchema.options.map((banner) => ({ + name: `valid banner: ${banner}`, + request: { banner }, + })), + )('should validate $name', ({ request }) => { + const result = DismissBannerRequestDto.safeParse(request); + expect(result.success).toBe(true); + }); + }); + + describe('Invalid requests', () => { + test.each([ + { + name: 'invalid banner string', + request: { + banner: 'not-a-valid-banner', + }, + expectedErrorPath: ['banner'], + }, + { + name: 'non-string banner', + request: { + banner: 123, + }, + expectedErrorPath: ['banner'], + }, + ])('should fail validation for $name', ({ request, expectedErrorPath }) => { + const result = DismissBannerRequestDto.safeParse(request); + + expect(result.success).toBe(false); + + if (expectedErrorPath) { + expect(result.error?.issues[0].path).toEqual(expectedErrorPath); + } + }); + }); + + describe('Optional banner', () => { + test('should validate empty request', () => { + const result = DismissBannerRequestDto.safeParse({}); + expect(result.success).toBe(true); + }); + }); + + describe('Exhaustive banner name check', () => { + test('should have all banner names defined', () => { + const expectedBanners = [ + 'V1', + 'TRIAL_OVER', + 'TRIAL', + 'NON_PRODUCTION_LICENSE', + 'EMAIL_CONFIRMATION', + ]; + + expect(bannerNameSchema.options).toEqual(expectedBanners); + }); + }); +}); diff --git a/packages/@n8n/api-types/src/dto/owner/__tests__/owner-setup-request.dto.test.ts b/packages/@n8n/api-types/src/dto/owner/__tests__/owner-setup-request.dto.test.ts new file mode 100644 index 0000000000..facf808ec3 --- /dev/null +++ b/packages/@n8n/api-types/src/dto/owner/__tests__/owner-setup-request.dto.test.ts @@ -0,0 +1,93 @@ +import { OwnerSetupRequestDto } from '../owner-setup-request.dto'; + +describe('OwnerSetupRequestDto', () => { + describe('Valid requests', () => { + test.each([ + { + name: 'complete valid setup request', + request: { + email: 'owner@example.com', + firstName: 'John', + lastName: 'Doe', + password: 'SecurePassword123', + }, + }, + ])('should validate $name', ({ request }) => { + const result = OwnerSetupRequestDto.safeParse(request); + expect(result.success).toBe(true); + }); + }); + + describe('Invalid requests', () => { + test.each([ + { + name: 'invalid email', + request: { + email: 'invalid-email', + firstName: 'John', + lastName: 'Doe', + password: 'SecurePassword123', + }, + expectedErrorPath: ['email'], + }, + { + name: 'missing first name', + request: { + email: 'owner@example.com', + firstName: '', + lastName: 'Doe', + password: 'SecurePassword123', + }, + expectedErrorPath: ['firstName'], + }, + { + name: 'missing last name', + request: { + email: 'owner@example.com', + firstName: 'John', + lastName: '', + password: 'SecurePassword123', + }, + expectedErrorPath: ['lastName'], + }, + { + name: 'password too short', + request: { + email: 'owner@example.com', + firstName: 'John', + lastName: 'Doe', + password: 'short', + }, + expectedErrorPath: ['password'], + }, + { + name: 'password without number', + request: { + email: 'owner@example.com', + firstName: 'John', + lastName: 'Doe', + password: 'NoNumberPassword', + }, + expectedErrorPath: ['password'], + }, + { + name: 'password without uppercase letter', + request: { + email: 'owner@example.com', + firstName: 'John', + lastName: 'Doe', + password: 'nouppercasepassword123', + }, + expectedErrorPath: ['password'], + }, + ])('should fail validation for $name', ({ request, expectedErrorPath }) => { + const result = OwnerSetupRequestDto.safeParse(request); + + expect(result.success).toBe(false); + + if (expectedErrorPath) { + expect(result.error?.issues[0].path).toEqual(expectedErrorPath); + } + }); + }); +}); diff --git a/packages/@n8n/api-types/src/dto/owner/dismiss-banner-request.dto.ts b/packages/@n8n/api-types/src/dto/owner/dismiss-banner-request.dto.ts new file mode 100644 index 0000000000..1f42381e7a --- /dev/null +++ b/packages/@n8n/api-types/src/dto/owner/dismiss-banner-request.dto.ts @@ -0,0 +1,7 @@ +import { Z } from 'zod-class'; + +import { bannerNameSchema } from '../../schemas/bannerName.schema'; + +export class DismissBannerRequestDto extends Z.class({ + banner: bannerNameSchema.optional(), +}) {} diff --git a/packages/@n8n/api-types/src/dto/owner/owner-setup-request.dto.ts b/packages/@n8n/api-types/src/dto/owner/owner-setup-request.dto.ts new file mode 100644 index 0000000000..ccaa06b18e --- /dev/null +++ b/packages/@n8n/api-types/src/dto/owner/owner-setup-request.dto.ts @@ -0,0 +1,11 @@ +import { z } from 'zod'; +import { Z } from 'zod-class'; + +import { passwordSchema } from '../../schemas/password.schema'; + +export class OwnerSetupRequestDto extends Z.class({ + email: z.string().email(), + firstName: z.string().min(1, 'First name is required'), + lastName: z.string().min(1, 'Last name is required'), + password: passwordSchema, +}) {} diff --git a/packages/@n8n/api-types/src/dto/password-reset/__tests__/change-password-request.dto.test.ts b/packages/@n8n/api-types/src/dto/password-reset/__tests__/change-password-request.dto.test.ts new file mode 100644 index 0000000000..86b230ba5a --- /dev/null +++ b/packages/@n8n/api-types/src/dto/password-reset/__tests__/change-password-request.dto.test.ts @@ -0,0 +1,114 @@ +import { ChangePasswordRequestDto } from '../change-password-request.dto'; + +describe('ChangePasswordRequestDto', () => { + describe('Valid requests', () => { + test.each([ + { + name: 'valid password reset with token', + request: { + token: 'valid-reset-token-with-sufficient-length', + password: 'newSecurePassword123', + }, + }, + { + name: 'valid password reset with MFA code', + request: { + token: 'another-valid-reset-token', + password: 'newSecurePassword123', + mfaCode: '123456', + }, + }, + ])('should validate $name', ({ request }) => { + const result = ChangePasswordRequestDto.safeParse(request); + expect(result.success).toBe(true); + }); + }); + + describe('Invalid requests', () => { + test.each([ + { + name: 'missing token', + request: { password: 'newSecurePassword123' }, + expectedErrorPath: ['token'], + }, + { + name: 'empty token', + request: { token: '', password: 'newSecurePassword123' }, + expectedErrorPath: ['token'], + }, + { + name: 'short token', + request: { token: 'short', password: 'newSecurePassword123' }, + expectedErrorPath: ['token'], + }, + { + name: 'missing password', + request: { token: 'valid-reset-token' }, + expectedErrorPath: ['password'], + }, + { + name: 'password too short', + request: { + token: 'valid-reset-token', + password: 'short', + }, + expectedErrorPath: ['password'], + }, + { + name: 'password too long', + request: { + token: 'valid-reset-token', + password: 'a'.repeat(65), + }, + expectedErrorPath: ['password'], + }, + { + name: 'password without number', + request: { + token: 'valid-reset-token', + password: 'NoNumberPassword', + }, + expectedErrorPath: ['password'], + }, + { + name: 'password without uppercase letter', + request: { + token: 'valid-reset-token', + password: 'nouppercasepassword123', + }, + expectedErrorPath: ['password'], + }, + ])('should fail validation for $name', ({ request, expectedErrorPath }) => { + const result = ChangePasswordRequestDto.safeParse(request); + + expect(result.success).toBe(false); + + if (expectedErrorPath) { + expect(result.error?.issues[0].path).toEqual(expectedErrorPath); + } + }); + + describe('Edge cases', () => { + test('should handle optional MFA code correctly', () => { + const validRequest = { + token: 'valid-reset-token', + password: 'newSecurePassword123', + mfaCode: undefined, + }; + + const result = ChangePasswordRequestDto.safeParse(validRequest); + expect(result.success).toBe(true); + }); + + test('should handle token with special characters', () => { + const validRequest = { + token: 'valid-reset-token-with-special-!@#$%^&*()_+', + password: 'newSecurePassword123', + }; + + const result = ChangePasswordRequestDto.safeParse(validRequest); + expect(result.success).toBe(true); + }); + }); + }); +}); diff --git a/packages/@n8n/api-types/src/dto/password-reset/__tests__/forgot-password-request.dto.test.ts b/packages/@n8n/api-types/src/dto/password-reset/__tests__/forgot-password-request.dto.test.ts new file mode 100644 index 0000000000..891d52fdad --- /dev/null +++ b/packages/@n8n/api-types/src/dto/password-reset/__tests__/forgot-password-request.dto.test.ts @@ -0,0 +1,47 @@ +import { ForgotPasswordRequestDto } from '../forgot-password-request.dto'; + +describe('ForgotPasswordRequestDto', () => { + describe('Valid requests', () => { + test.each([ + { + name: 'valid email', + request: { email: 'test@example.com' }, + }, + { + name: 'email with subdomain', + request: { email: 'user@sub.example.com' }, + }, + ])('should validate $name', ({ request }) => { + const result = ForgotPasswordRequestDto.safeParse(request); + expect(result.success).toBe(true); + }); + }); + + describe('Invalid requests', () => { + test.each([ + { + name: 'invalid email format', + request: { email: 'invalid-email' }, + expectedErrorPath: ['email'], + }, + { + name: 'missing email', + request: {}, + expectedErrorPath: ['email'], + }, + { + name: 'empty email', + request: { email: '' }, + expectedErrorPath: ['email'], + }, + ])('should fail validation for $name', ({ request, expectedErrorPath }) => { + const result = ForgotPasswordRequestDto.safeParse(request); + + expect(result.success).toBe(false); + + if (expectedErrorPath) { + expect(result.error?.issues[0].path).toEqual(expectedErrorPath); + } + }); + }); +}); diff --git a/packages/@n8n/api-types/src/dto/password-reset/__tests__/resolve-password-token-query.dto.test.ts b/packages/@n8n/api-types/src/dto/password-reset/__tests__/resolve-password-token-query.dto.test.ts new file mode 100644 index 0000000000..a2f5881ac8 --- /dev/null +++ b/packages/@n8n/api-types/src/dto/password-reset/__tests__/resolve-password-token-query.dto.test.ts @@ -0,0 +1,42 @@ +import { ResolvePasswordTokenQueryDto } from '../resolve-password-token-query.dto'; + +describe('ResolvePasswordTokenQueryDto', () => { + describe('Valid requests', () => { + test.each([ + { + name: 'valid token', + request: { token: 'valid-reset-token' }, + }, + { + name: 'long token', + request: { token: 'x'.repeat(50) }, + }, + ])('should validate $name', ({ request }) => { + const result = ResolvePasswordTokenQueryDto.safeParse(request); + expect(result.success).toBe(true); + }); + }); + + describe('Invalid requests', () => { + test.each([ + { + name: 'missing token', + request: {}, + expectedErrorPath: ['token'], + }, + { + name: 'empty token', + request: { token: '' }, + expectedErrorPath: ['token'], + }, + ])('should fail validation for $name', ({ request, expectedErrorPath }) => { + const result = ResolvePasswordTokenQueryDto.safeParse(request); + + expect(result.success).toBe(false); + + if (expectedErrorPath) { + expect(result.error?.issues[0].path).toEqual(expectedErrorPath); + } + }); + }); +}); diff --git a/packages/@n8n/api-types/src/dto/password-reset/change-password-request.dto.ts b/packages/@n8n/api-types/src/dto/password-reset/change-password-request.dto.ts new file mode 100644 index 0000000000..33ef47b3f1 --- /dev/null +++ b/packages/@n8n/api-types/src/dto/password-reset/change-password-request.dto.ts @@ -0,0 +1,11 @@ +import { z } from 'zod'; +import { Z } from 'zod-class'; + +import { passwordSchema } from '../../schemas/password.schema'; +import { passwordResetTokenSchema } from '../../schemas/passwordResetToken.schema'; + +export class ChangePasswordRequestDto extends Z.class({ + token: passwordResetTokenSchema, + password: passwordSchema, + mfaCode: z.string().optional(), +}) {} diff --git a/packages/@n8n/api-types/src/dto/password-reset/forgot-password-request.dto.ts b/packages/@n8n/api-types/src/dto/password-reset/forgot-password-request.dto.ts new file mode 100644 index 0000000000..f6ab3cfac5 --- /dev/null +++ b/packages/@n8n/api-types/src/dto/password-reset/forgot-password-request.dto.ts @@ -0,0 +1,6 @@ +import { z } from 'zod'; +import { Z } from 'zod-class'; + +export class ForgotPasswordRequestDto extends Z.class({ + email: z.string().email(), +}) {} diff --git a/packages/@n8n/api-types/src/dto/password-reset/resolve-password-token-query.dto.ts b/packages/@n8n/api-types/src/dto/password-reset/resolve-password-token-query.dto.ts new file mode 100644 index 0000000000..88385df244 --- /dev/null +++ b/packages/@n8n/api-types/src/dto/password-reset/resolve-password-token-query.dto.ts @@ -0,0 +1,7 @@ +import { Z } from 'zod-class'; + +import { passwordResetTokenSchema } from '../../schemas/passwordResetToken.schema'; + +export class ResolvePasswordTokenQueryDto extends Z.class({ + token: passwordResetTokenSchema, +}) {} diff --git a/packages/@n8n/api-types/src/dto/saml/__tests__/saml-preferences.dto.test.ts b/packages/@n8n/api-types/src/dto/saml/__tests__/saml-preferences.dto.test.ts new file mode 100644 index 0000000000..6d11483347 --- /dev/null +++ b/packages/@n8n/api-types/src/dto/saml/__tests__/saml-preferences.dto.test.ts @@ -0,0 +1,155 @@ +import { SamlPreferences } from '../saml-preferences.dto'; + +describe('SamlPreferences', () => { + describe('Valid requests', () => { + test.each([ + { + name: 'valid minimal configuration', + request: { + mapping: { + email: 'user@example.com', + firstName: 'John', + lastName: 'Doe', + userPrincipalName: 'johndoe', + }, + metadata: 'metadata', + metadataUrl: 'https://example.com/metadata', + loginEnabled: true, + loginLabel: 'Login with SAML', + }, + }, + { + name: 'valid full configuration', + request: { + mapping: { + email: 'user@example.com', + firstName: 'John', + lastName: 'Doe', + userPrincipalName: 'johndoe', + }, + metadata: 'metadata', + metadataUrl: 'https://example.com/metadata', + ignoreSSL: true, + loginBinding: 'post', + loginEnabled: true, + loginLabel: 'Login with SAML', + authnRequestsSigned: true, + wantAssertionsSigned: true, + wantMessageSigned: true, + acsBinding: 'redirect', + signatureConfig: { + prefix: 'ds', + location: { + reference: '/samlp:Response/saml:Issuer', + action: 'after', + }, + }, + relayState: 'https://example.com/relay', + }, + }, + ])('should validate $name', ({ request }) => { + const result = SamlPreferences.safeParse(request); + expect(result.success).toBe(true); + }); + }); + + describe('Invalid requests', () => { + test.each([ + { + name: 'invalid loginBinding', + request: { + loginBinding: 'invalid', + }, + expectedErrorPath: ['loginBinding'], + }, + { + name: 'invalid acsBinding', + request: { + acsBinding: 'invalid', + }, + expectedErrorPath: ['acsBinding'], + }, + { + name: 'invalid signatureConfig location action', + request: { + signatureConfig: { + prefix: 'ds', + location: { + reference: '/samlp:Response/saml:Issuer', + action: 'invalid', + }, + }, + }, + expectedErrorPath: ['signatureConfig', 'location', 'action'], + }, + { + name: 'missing signatureConfig location reference', + request: { + signatureConfig: { + prefix: 'ds', + location: { + action: 'after', + }, + }, + }, + expectedErrorPath: ['signatureConfig', 'location', 'reference'], + }, + { + name: 'invalid mapping email', + request: { + mapping: { + email: 123, + firstName: 'John', + lastName: 'Doe', + userPrincipalName: 'johndoe', + }, + }, + expectedErrorPath: ['mapping', 'email'], + }, + ])('should fail validation for $name', ({ request, expectedErrorPath }) => { + const result = SamlPreferences.safeParse(request); + + expect(result.success).toBe(false); + + if (expectedErrorPath) { + expect(result.error?.issues[0].path).toEqual(expectedErrorPath); + } + }); + + describe('Edge cases', () => { + test('should handle optional fields correctly', () => { + const validRequest = { + mapping: undefined, + metadata: undefined, + metadataUrl: undefined, + loginEnabled: undefined, + loginLabel: undefined, + }; + + const result = SamlPreferences.safeParse(validRequest); + expect(result.success).toBe(true); + }); + + test('should handle default values correctly', () => { + const validRequest = {}; + + const result = SamlPreferences.safeParse(validRequest); + expect(result.success).toBe(true); + expect(result.data?.ignoreSSL).toBe(false); + expect(result.data?.loginBinding).toBe('redirect'); + expect(result.data?.authnRequestsSigned).toBe(false); + expect(result.data?.wantAssertionsSigned).toBe(true); + expect(result.data?.wantMessageSigned).toBe(true); + expect(result.data?.acsBinding).toBe('post'); + expect(result.data?.signatureConfig).toEqual({ + prefix: 'ds', + location: { + reference: '/samlp:Response/saml:Issuer', + action: 'after', + }, + }); + expect(result.data?.relayState).toBe(''); + }); + }); + }); +}); diff --git a/packages/@n8n/api-types/src/dto/saml/saml-acs.dto.ts b/packages/@n8n/api-types/src/dto/saml/saml-acs.dto.ts new file mode 100644 index 0000000000..2bfbece7d6 --- /dev/null +++ b/packages/@n8n/api-types/src/dto/saml/saml-acs.dto.ts @@ -0,0 +1,6 @@ +import { z } from 'zod'; +import { Z } from 'zod-class'; + +export class SamlAcsDto extends Z.class({ + RelayState: z.string().optional(), +}) {} diff --git a/packages/@n8n/api-types/src/dto/saml/saml-preferences.dto.ts b/packages/@n8n/api-types/src/dto/saml/saml-preferences.dto.ts new file mode 100644 index 0000000000..e07504c1b3 --- /dev/null +++ b/packages/@n8n/api-types/src/dto/saml/saml-preferences.dto.ts @@ -0,0 +1,50 @@ +import { z } from 'zod'; +import { Z } from 'zod-class'; + +const SamlLoginBindingSchema = z.enum(['redirect', 'post']); + +/** Schema for configuring the signature in SAML requests/responses. */ +const SignatureConfigSchema = z.object({ + prefix: z.string().default('ds'), + location: z.object({ + reference: z.string(), + action: z.enum(['before', 'after', 'prepend', 'append']), + }), +}); + +export class SamlPreferences extends Z.class({ + /** Mapping of SAML attributes to user fields. */ + mapping: z + .object({ + email: z.string(), + firstName: z.string(), + lastName: z.string(), + userPrincipalName: z.string(), + }) + .optional(), + /** SAML metadata in XML format. */ + metadata: z.string().optional(), + metadataUrl: z.string().optional(), + + ignoreSSL: z.boolean().default(false), + loginBinding: SamlLoginBindingSchema.default('redirect'), + /** Whether SAML login is enabled. */ + loginEnabled: z.boolean().optional(), + /** Label for the SAML login button. on the Auth screen */ + loginLabel: z.string().optional(), + + authnRequestsSigned: z.boolean().default(false), + wantAssertionsSigned: z.boolean().default(true), + wantMessageSigned: z.boolean().default(true), + + acsBinding: SamlLoginBindingSchema.default('post'), + signatureConfig: SignatureConfigSchema.default({ + prefix: 'ds', + location: { + reference: '/samlp:Response/saml:Issuer', + action: 'after', + }, + }), + + relayState: z.string().default(''), +}) {} diff --git a/packages/@n8n/api-types/src/dto/saml/saml-toggle.dto.ts b/packages/@n8n/api-types/src/dto/saml/saml-toggle.dto.ts new file mode 100644 index 0000000000..be07933d06 --- /dev/null +++ b/packages/@n8n/api-types/src/dto/saml/saml-toggle.dto.ts @@ -0,0 +1,6 @@ +import { z } from 'zod'; +import { Z } from 'zod-class'; + +export class SamlToggleDto extends Z.class({ + loginEnabled: z.boolean(), +}) {} diff --git a/packages/@n8n/api-types/src/dto/user/settings-update-request.dto.ts b/packages/@n8n/api-types/src/dto/user/settings-update-request.dto.ts index f4c0eb0af3..247b830d91 100644 --- a/packages/@n8n/api-types/src/dto/user/settings-update-request.dto.ts +++ b/packages/@n8n/api-types/src/dto/user/settings-update-request.dto.ts @@ -4,4 +4,5 @@ import { Z } from 'zod-class'; export class SettingsUpdateRequestDto extends Z.class({ userActivated: z.boolean().optional(), allowSSOManualLogin: z.boolean().optional(), + easyAIWorkflowOnboarded: z.boolean().optional(), }) {} diff --git a/packages/@n8n/api-types/src/dto/variables/variables-list-request.dto.ts b/packages/@n8n/api-types/src/dto/variables/variables-list-request.dto.ts new file mode 100644 index 0000000000..804bcb2786 --- /dev/null +++ b/packages/@n8n/api-types/src/dto/variables/variables-list-request.dto.ts @@ -0,0 +1,6 @@ +import { z } from 'zod'; +import { Z } from 'zod-class'; + +export class VariableListRequestDto extends Z.class({ + state: z.literal('empty').optional(), +}) {} diff --git a/packages/@n8n/api-types/src/frontend-settings.ts b/packages/@n8n/api-types/src/frontend-settings.ts index 5084344aeb..3ce856d6ad 100644 --- a/packages/@n8n/api-types/src/frontend-settings.ts +++ b/packages/@n8n/api-types/src/frontend-settings.ts @@ -1,3 +1,4 @@ +import type { FrontendBetaFeatures } from '@n8n/config'; import type { ExpressionEvaluatorType, LogLevel, WorkflowSettings } from 'n8n-workflow'; export interface IVersionNotificationSettings { @@ -26,7 +27,8 @@ export interface IUserManagementSettings { } export interface FrontendSettings { - isDocker?: boolean; + inE2ETests: boolean; + isDocker: boolean; databaseType: 'sqlite' | 'mariadb' | 'mysqldb' | 'postgresdb'; endpointForm: string; endpointFormTest: string; @@ -161,7 +163,11 @@ export interface FrontendSettings { pruneTime: number; licensePruneTime: number; }; - pruning: { + aiCredits: { + enabled: boolean; + credits: number; + }; + pruning?: { isEnabled: boolean; maxAge: number; maxCount: number; @@ -169,4 +175,6 @@ export interface FrontendSettings { security: { blockFileAccessToN8nFiles: boolean; }; + betaFeatures: FrontendBetaFeatures[]; + easyAIWorkflowOnboarded: boolean; } diff --git a/packages/@n8n/api-types/src/index.ts b/packages/@n8n/api-types/src/index.ts index d0067f7fff..a003d54201 100644 --- a/packages/@n8n/api-types/src/index.ts +++ b/packages/@n8n/api-types/src/index.ts @@ -7,3 +7,6 @@ export type * from './user'; export type { Collaborator } from './push/collaboration'; export type { SendWorkerStatusMessage } from './push/worker'; + +export type { BannerName } from './schemas/bannerName.schema'; +export { passwordSchema } from './schemas/password.schema'; diff --git a/packages/@n8n/api-types/src/push/execution.ts b/packages/@n8n/api-types/src/push/execution.ts index 78c0b34a36..320b3dc264 100644 --- a/packages/@n8n/api-types/src/push/execution.ts +++ b/packages/@n8n/api-types/src/push/execution.ts @@ -1,4 +1,4 @@ -import type { IRun, ITaskData, WorkflowExecuteMode } from 'n8n-workflow'; +import type { ExecutionStatus, ITaskData, WorkflowExecuteMode } from 'n8n-workflow'; type ExecutionStarted = { type: 'executionStarted'; @@ -9,6 +9,14 @@ type ExecutionStarted = { workflowId: string; workflowName?: string; retryOf?: string; + flattedRunData: string; + }; +}; + +type ExecutionWaiting = { + type: 'executionWaiting'; + data: { + executionId: string; }; }; @@ -16,8 +24,10 @@ type ExecutionFinished = { type: 'executionFinished'; data: { executionId: string; - data: IRun; - retryOf?: string; + workflowId: string; + status: ExecutionStatus; + /** @deprecated: Please construct execution data in the frontend from the data pushed in previous messages, instead of depending on this additional payload serialization */ + rawData?: string; }; }; @@ -47,6 +57,7 @@ type NodeExecuteAfter = { export type ExecutionPushMessage = | ExecutionStarted + | ExecutionWaiting | ExecutionFinished | ExecutionRecovered | NodeExecuteBefore diff --git a/packages/@n8n/api-types/src/schemas/__tests__/nodeVersion.schema.test.ts b/packages/@n8n/api-types/src/schemas/__tests__/nodeVersion.schema.test.ts new file mode 100644 index 0000000000..098db82096 --- /dev/null +++ b/packages/@n8n/api-types/src/schemas/__tests__/nodeVersion.schema.test.ts @@ -0,0 +1,28 @@ +import { nodeVersionSchema } from '../nodeVersion.schema'; + +describe('nodeVersionSchema', () => { + describe('valid versions', () => { + test.each([ + [1, 'single digit'], + [2, 'single digit'], + [1.0, 'major.minor with zero minor'], + [1.2, 'major.minor'], + [10.5, 'major.minor with double digits'], + ])('should accept %s as a valid version (%s)', (version) => { + const validated = nodeVersionSchema.parse(version); + expect(validated).toBe(version); + }); + }); + + describe('invalid versions', () => { + test.each([ + ['not-a-number', 'non-number input'], + ['1.2.3', 'more than two parts'], + ['1.a', 'non-numeric characters'], + ['1.2.3', 'more than two parts as string'], + ])('should reject %s as an invalid version (%s)', (version) => { + const check = () => nodeVersionSchema.parse(version); + expect(check).toThrowError(); + }); + }); +}); diff --git a/packages/@n8n/api-types/src/schemas/__tests__/paswword.schema.test.ts b/packages/@n8n/api-types/src/schemas/__tests__/paswword.schema.test.ts new file mode 100644 index 0000000000..c3bcd5f4c8 --- /dev/null +++ b/packages/@n8n/api-types/src/schemas/__tests__/paswword.schema.test.ts @@ -0,0 +1,54 @@ +import { passwordSchema } from '../password.schema'; + +describe('passwordSchema', () => { + test('should throw on empty password', () => { + const check = () => passwordSchema.parse(''); + + expect(check).toThrowError('Password must be 8 to 64 characters long'); + }); + + test('should return same password if valid', () => { + const validPassword = 'abcd1234X'; + + const validated = passwordSchema.parse(validPassword); + + expect(validated).toBe(validPassword); + }); + + test('should require at least one uppercase letter', () => { + const invalidPassword = 'abcd1234'; + + const failingCheck = () => passwordSchema.parse(invalidPassword); + + expect(failingCheck).toThrowError('Password must contain at least 1 uppercase letter.'); + }); + + test('should require at least one number', () => { + const validPassword = 'abcd1234X'; + const invalidPassword = 'abcdEFGH'; + + const validated = passwordSchema.parse(validPassword); + + expect(validated).toBe(validPassword); + + const check = () => passwordSchema.parse(invalidPassword); + + expect(check).toThrowError('Password must contain at least 1 number.'); + }); + + test('should require a minimum length of 8 characters', () => { + const invalidPassword = 'a'.repeat(7); + + const check = () => passwordSchema.parse(invalidPassword); + + expect(check).toThrowError('Password must be 8 to 64 characters long.'); + }); + + test('should require a maximum length of 64 characters', () => { + const invalidPassword = 'a'.repeat(65); + + const check = () => passwordSchema.parse(invalidPassword); + + expect(check).toThrowError('Password must be 8 to 64 characters long.'); + }); +}); diff --git a/packages/@n8n/api-types/src/schemas/bannerName.schema.ts b/packages/@n8n/api-types/src/schemas/bannerName.schema.ts new file mode 100644 index 0000000000..445bc31d1a --- /dev/null +++ b/packages/@n8n/api-types/src/schemas/bannerName.schema.ts @@ -0,0 +1,11 @@ +import { z } from 'zod'; + +export const bannerNameSchema = z.enum([ + 'V1', + 'TRIAL_OVER', + 'TRIAL', + 'NON_PRODUCTION_LICENSE', + 'EMAIL_CONFIRMATION', +]); + +export type BannerName = z.infer; diff --git a/packages/@n8n/api-types/src/schemas/booleanFromString.ts b/packages/@n8n/api-types/src/schemas/booleanFromString.ts new file mode 100644 index 0000000000..bcc9e8133c --- /dev/null +++ b/packages/@n8n/api-types/src/schemas/booleanFromString.ts @@ -0,0 +1,3 @@ +import { z } from 'zod'; + +export const booleanFromString = z.enum(['true', 'false']).transform((value) => value === 'true'); diff --git a/packages/@n8n/api-types/src/schemas/nodeVersion.schema.ts b/packages/@n8n/api-types/src/schemas/nodeVersion.schema.ts new file mode 100644 index 0000000000..3edb8cc5fe --- /dev/null +++ b/packages/@n8n/api-types/src/schemas/nodeVersion.schema.ts @@ -0,0 +1,17 @@ +import { z } from 'zod'; + +export const nodeVersionSchema = z + .number() + .min(1) + .refine( + (val) => { + const parts = String(val).split('.'); + return ( + (parts.length === 1 && !isNaN(Number(parts[0]))) || + (parts.length === 2 && !isNaN(Number(parts[0])) && !isNaN(Number(parts[1]))) + ); + }, + { + message: 'Invalid node version. Must be in format: major.minor', + }, + ); diff --git a/packages/@n8n/api-types/src/schemas/password.schema.ts b/packages/@n8n/api-types/src/schemas/password.schema.ts new file mode 100644 index 0000000000..3c60470af7 --- /dev/null +++ b/packages/@n8n/api-types/src/schemas/password.schema.ts @@ -0,0 +1,16 @@ +import { z } from 'zod'; + +// TODO: Delete these from `cli` after all password-validation code starts using this schema +const minLength = 8; +const maxLength = 64; + +export const passwordSchema = z + .string() + .min(minLength, `Password must be ${minLength} to ${maxLength} characters long.`) + .max(maxLength, `Password must be ${minLength} to ${maxLength} characters long.`) + .refine((password) => /\d/.test(password), { + message: 'Password must contain at least 1 number.', + }) + .refine((password) => /[A-Z]/.test(password), { + message: 'Password must contain at least 1 uppercase letter.', + }); diff --git a/packages/@n8n/api-types/src/schemas/passwordResetToken.schema.ts b/packages/@n8n/api-types/src/schemas/passwordResetToken.schema.ts new file mode 100644 index 0000000000..b7c55bb886 --- /dev/null +++ b/packages/@n8n/api-types/src/schemas/passwordResetToken.schema.ts @@ -0,0 +1,3 @@ +import { z } from 'zod'; + +export const passwordResetTokenSchema = z.string().min(10, 'Token too short'); diff --git a/packages/@n8n/benchmark/README.md b/packages/@n8n/benchmark/README.md index b3a0a4d548..af8726d0ea 100644 --- a/packages/@n8n/benchmark/README.md +++ b/packages/@n8n/benchmark/README.md @@ -1,6 +1,6 @@ # n8n benchmarking tool -Tool for executing benchmarks against an n8n instance. The tool consists of these components: +Tool for executing benchmarks against an n8n instance. ## Directory structure @@ -12,6 +12,39 @@ packages/@n8n/benchmark ├── scripts Orchestration scripts ``` +## Benchmarking an existing n8n instance + +The easiest way to run the existing benchmark scenarios is to use the benchmark docker image: + +```sh +docker pull ghcr.io/n8n-io/n8n-benchmark:latest +# Print the help to list all available flags +docker run ghcr.io/n8n-io/n8n-benchmark:latest run --help +# Run all available benchmark scenarios for 1 minute with 5 concurrent requests +docker run ghcr.io/n8n-io/n8n-benchmark:latest run \ + --n8nBaseUrl=https://instance.url \ + --n8nUserEmail=InstanceOwner@email.com \ + --n8nUserPassword=InstanceOwnerPassword \ + --vus=5 \ + --duration=1m \ + --scenarioFilter SingleWebhook +``` + +### Using custom scenarios with the Docker image + +It is also possible to create your own [benchmark scenarios](#benchmark-scenarios) and load them using the `--testScenariosPath` flag: + +```sh +# Assuming your scenarios are located in `./scenarios`, mount them into `/scenarios` in the container +docker run -v ./scenarios:/scenarios ghcr.io/n8n-io/n8n-benchmark:latest run \ + --n8nBaseUrl=https://instance.url \ + --n8nUserEmail=InstanceOwner@email.com \ + --n8nUserPassword=InstanceOwnerPassword \ + --vus=5 \ + --duration=1m \ + --testScenariosPath=/scenarios +``` + ## Running the entire benchmark suite The benchmark suite consists of [benchmark scenarios](#benchmark-scenarios) and different [n8n setups](#n8n-setups). diff --git a/packages/@n8n/benchmark/package.json b/packages/@n8n/benchmark/package.json index 98edd1dabd..abb8514c3d 100644 --- a/packages/@n8n/benchmark/package.json +++ b/packages/@n8n/benchmark/package.json @@ -1,6 +1,6 @@ { "name": "@n8n/n8n-benchmark", - "version": "1.7.0", + "version": "1.8.0", "description": "Cli for running benchmark tests for n8n", "main": "dist/index", "scripts": { @@ -17,7 +17,7 @@ "benchmark-locally": "pnpm benchmark --env local", "provision-cloud-env": "zx scripts/provision-cloud-env.mjs", "destroy-cloud-env": "zx scripts/destroy-cloud-env.mjs", - "watch": "concurrently \"tsc -w -p tsconfig.build.json\" \"tsc-alias -w -p tsconfig.build.json\"" + "watch": "tsc-watch -p tsconfig.build.json --onCompilationComplete \"tsc-alias -p tsconfig.build.json\"" }, "engines": { "node": ">=20.10" @@ -41,10 +41,7 @@ }, "devDependencies": { "@types/convict": "^6.1.1", - "@types/k6": "^0.52.0", - "@types/node": "^20.14.8", - "tsc-alias": "^1.8.7", - "typescript": "^5.5.2" + "@types/k6": "^0.52.0" }, "bin": { "n8n-benchmark": "./bin/n8n-benchmark" diff --git a/packages/@n8n/benchmark/scenarios/binary-data/binary-data.script.js b/packages/@n8n/benchmark/scenarios/binary-data/binary-data.script.js index c10e667cbb..28edfdf9ec 100644 --- a/packages/@n8n/benchmark/scenarios/binary-data/binary-data.script.js +++ b/packages/@n8n/benchmark/scenarios/binary-data/binary-data.script.js @@ -15,6 +15,12 @@ export default function () { const res = http.post(`${apiBaseUrl}/webhook/binary-files-benchmark`, data); + if (res.status !== 200) { + console.error( + `Invalid response. Received status ${res.status}. Body: ${JSON.stringify(res.body)}`, + ); + } + check(res, { 'is status 200': (r) => r.status === 200, 'has correct content type': (r) => diff --git a/packages/@n8n/benchmark/scenarios/http-node/http-node.script.js b/packages/@n8n/benchmark/scenarios/http-node/http-node.script.js index b391982259..4ecee9d1bd 100644 --- a/packages/@n8n/benchmark/scenarios/http-node/http-node.script.js +++ b/packages/@n8n/benchmark/scenarios/http-node/http-node.script.js @@ -6,6 +6,12 @@ const apiBaseUrl = __ENV.API_BASE_URL; export default function () { const res = http.post(`${apiBaseUrl}/webhook/benchmark-http-node`); + if (res.status !== 200) { + console.error( + `Invalid response. Received status ${res.status}. Body: ${JSON.stringify(res.body)}`, + ); + } + check(res, { 'is status 200': (r) => r.status === 200, 'http requests were OK': (r) => { diff --git a/packages/@n8n/benchmark/scenarios/js-code-node/js-code-node.script.js b/packages/@n8n/benchmark/scenarios/js-code-node/js-code-node.script.js index 74cef4f441..b2fd8eb315 100644 --- a/packages/@n8n/benchmark/scenarios/js-code-node/js-code-node.script.js +++ b/packages/@n8n/benchmark/scenarios/js-code-node/js-code-node.script.js @@ -5,6 +5,13 @@ const apiBaseUrl = __ENV.API_BASE_URL; export default function () { const res = http.post(`${apiBaseUrl}/webhook/code-node-benchmark`, {}); + + if (res.status !== 200) { + console.error( + `Invalid response. Received status ${res.status}. Body: ${JSON.stringify(res.body)}`, + ); + } + check(res, { 'is status 200': (r) => r.status === 200, 'has items in response': (r) => { diff --git a/packages/@n8n/benchmark/scenarios/set-node-expressions/set-node-expressions.script.js b/packages/@n8n/benchmark/scenarios/set-node-expressions/set-node-expressions.script.js index 4bea17eb9f..9564fcc53c 100644 --- a/packages/@n8n/benchmark/scenarios/set-node-expressions/set-node-expressions.script.js +++ b/packages/@n8n/benchmark/scenarios/set-node-expressions/set-node-expressions.script.js @@ -5,6 +5,13 @@ const apiBaseUrl = __ENV.API_BASE_URL; export default function () { const res = http.post(`${apiBaseUrl}/webhook/set-expressions-benchmark`, {}); + + if (res.status !== 200) { + console.error( + `Invalid response. Received status ${res.status}. Body: ${JSON.stringify(res.body)}`, + ); + } + check(res, { 'is status 200': (r) => r.status === 200, }); diff --git a/packages/@n8n/benchmark/scenarios/single-webhook/single-webhook.manifest.json b/packages/@n8n/benchmark/scenarios/single-webhook/single-webhook.manifest.json index 9c68908eef..2113c73ec9 100644 --- a/packages/@n8n/benchmark/scenarios/single-webhook/single-webhook.manifest.json +++ b/packages/@n8n/benchmark/scenarios/single-webhook/single-webhook.manifest.json @@ -3,5 +3,5 @@ "name": "SingleWebhook", "description": "A single webhook trigger that responds with a 200 status code", "scenarioData": { "workflowFiles": ["single-webhook.json"] }, - "scriptPath": "single-webhook.script.ts" + "scriptPath": "single-webhook.script.js" } diff --git a/packages/@n8n/benchmark/scenarios/single-webhook/single-webhook.script.ts b/packages/@n8n/benchmark/scenarios/single-webhook/single-webhook.script.js similarity index 63% rename from packages/@n8n/benchmark/scenarios/single-webhook/single-webhook.script.ts rename to packages/@n8n/benchmark/scenarios/single-webhook/single-webhook.script.js index 72e2563cbe..41facc8aeb 100644 --- a/packages/@n8n/benchmark/scenarios/single-webhook/single-webhook.script.ts +++ b/packages/@n8n/benchmark/scenarios/single-webhook/single-webhook.script.js @@ -5,6 +5,13 @@ const apiBaseUrl = __ENV.API_BASE_URL; export default function () { const res = http.get(`${apiBaseUrl}/webhook/single-webhook`); + + if (res.status !== 200) { + console.error( + `Invalid response. Received status ${res.status}. Body: ${JSON.stringify(res.body)}`, + ); + } + check(res, { 'is status 200': (r) => r.status === 200, }); diff --git a/packages/@n8n/benchmark/scripts/n8n-setups/scaling-multi-main/docker-compose.yml b/packages/@n8n/benchmark/scripts/n8n-setups/scaling-multi-main/docker-compose.yml index ca3ad9c23d..c686f581b3 100644 --- a/packages/@n8n/benchmark/scripts/n8n-setups/scaling-multi-main/docker-compose.yml +++ b/packages/@n8n/benchmark/scripts/n8n-setups/scaling-multi-main/docker-compose.yml @@ -176,7 +176,7 @@ services: # Load balancer that acts as an entry point for n8n n8n: - image: nginx:latest + image: nginx:1.27.2 ports: - '5678:80' volumes: diff --git a/packages/@n8n/benchmark/scripts/n8n-setups/scaling-multi-main/nginx.conf b/packages/@n8n/benchmark/scripts/n8n-setups/scaling-multi-main/nginx.conf index 86100f8c50..142da7416e 100644 --- a/packages/@n8n/benchmark/scripts/n8n-setups/scaling-multi-main/nginx.conf +++ b/packages/@n8n/benchmark/scripts/n8n-setups/scaling-multi-main/nginx.conf @@ -3,6 +3,7 @@ events {} http { client_max_body_size 50M; access_log off; + error_log /dev/stderr warn; upstream backend { server n8n_main1:5678; diff --git a/packages/@n8n/benchmark/scripts/run-in-cloud.mjs b/packages/@n8n/benchmark/scripts/run-in-cloud.mjs index c61c0901d4..35e90bdee5 100755 --- a/packages/@n8n/benchmark/scripts/run-in-cloud.mjs +++ b/packages/@n8n/benchmark/scripts/run-in-cloud.mjs @@ -78,12 +78,6 @@ async function runBenchmarksOnVm(config, benchmarkEnv) { const bootstrapScriptPath = path.join(scriptsDir, 'bootstrap.sh'); await sshClient.ssh(`chmod a+x ${bootstrapScriptPath} && ${bootstrapScriptPath}`); - // Benchmarking the VM - const vmBenchmarkScriptPath = path.join(scriptsDir, 'vm-benchmark.sh'); - await sshClient.ssh(`chmod a+x ${vmBenchmarkScriptPath} && ${vmBenchmarkScriptPath}`, { - verbose: true, - }); - // Give some time for the VM to be ready await sleep(1000); diff --git a/packages/@n8n/benchmark/scripts/vm-benchmark.sh b/packages/@n8n/benchmark/scripts/vm-benchmark.sh deleted file mode 100644 index 13b7eb2b1a..0000000000 --- a/packages/@n8n/benchmark/scripts/vm-benchmark.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash - -# Install fio -DEBIAN_FRONTEND=noninteractive sudo apt-get -y install fio > /dev/null - -# Run the disk benchmark -fio --name=rand_rw --ioengine=libaio --rw=randrw --rwmixread=70 --bs=4k --numjobs=4 --size=1G --runtime=30 --directory=/n8n --group_reporting - -# Remove files -sudo rm /n8n/rand_rw.* - -# Uninstall fio -DEBIAN_FRONTEND=noninteractive sudo apt-get -y remove fio > /dev/null diff --git a/packages/@n8n/benchmark/src/commands/run.ts b/packages/@n8n/benchmark/src/commands/run.ts index 164eef0f41..312de91c3c 100644 --- a/packages/@n8n/benchmark/src/commands/run.ts +++ b/packages/@n8n/benchmark/src/commands/run.ts @@ -13,6 +13,10 @@ export default class RunCommand extends Command { static flags = { testScenariosPath, + scenarioFilter: Flags.string({ + char: 'f', + description: 'Filter scenarios by name', + }), scenarioNamePrefix: Flags.string({ description: 'Prefix for the scenario name', default: 'Unnamed', @@ -95,7 +99,7 @@ export default class RunCommand extends Command { flags.scenarioNamePrefix, ); - const allScenarios = scenarioLoader.loadAll(flags.testScenariosPath); + const allScenarios = scenarioLoader.loadAll(flags.testScenariosPath, flags.scenarioFilter); await scenarioRunner.runManyScenarios(allScenarios); } diff --git a/packages/@n8n/benchmark/src/n8n-api-client/n8n-api-client.ts b/packages/@n8n/benchmark/src/n8n-api-client/n8n-api-client.ts index dd81fa9cfb..aa3ce82a96 100644 --- a/packages/@n8n/benchmark/src/n8n-api-client/n8n-api-client.ts +++ b/packages/@n8n/benchmark/src/n8n-api-client/n8n-api-client.ts @@ -2,7 +2,7 @@ import type { AxiosError, AxiosRequestConfig } from 'axios'; import axios from 'axios'; export class N8nApiClient { - constructor(public readonly apiBaseUrl: string) {} + constructor(readonly apiBaseUrl: string) {} async waitForInstanceToBecomeOnline(): Promise { const HEALTH_ENDPOINT = 'healthz'; diff --git a/packages/@n8n/benchmark/src/scenario/scenario-loader.ts b/packages/@n8n/benchmark/src/scenario/scenario-loader.ts index 4f315c1bf7..2da9109e86 100644 --- a/packages/@n8n/benchmark/src/scenario/scenario-loader.ts +++ b/packages/@n8n/benchmark/src/scenario/scenario-loader.ts @@ -8,7 +8,7 @@ export class ScenarioLoader { /** * Loads all scenarios from the given path */ - loadAll(pathToScenarios: string): Scenario[] { + loadAll(pathToScenarios: string, filter?: string): Scenario[] { pathToScenarios = path.resolve(pathToScenarios); const scenarioFolders = fs .readdirSync(pathToScenarios, { withFileTypes: true }) @@ -18,6 +18,9 @@ export class ScenarioLoader { const scenarios: Scenario[] = []; for (const folder of scenarioFolders) { + if (filter && !folder.toLowerCase().includes(filter.toLowerCase())) { + continue; + } const scenarioPath = path.join(pathToScenarios, folder); const manifestFileName = `${folder}.manifest.json`; const scenarioManifestPath = path.join(pathToScenarios, folder, manifestFileName); diff --git a/packages/@n8n/benchmark/src/test-execution/scenario-runner.ts b/packages/@n8n/benchmark/src/test-execution/scenario-runner.ts index 84d1d8b096..def841ccf5 100644 --- a/packages/@n8n/benchmark/src/test-execution/scenario-runner.ts +++ b/packages/@n8n/benchmark/src/test-execution/scenario-runner.ts @@ -1,3 +1,5 @@ +import { sleep } from 'zx'; + import { AuthenticatedN8nApiClient } from '@/n8n-api-client/authenticated-n8n-api-client'; import type { N8nApiClient } from '@/n8n-api-client/n8n-api-client'; import type { ScenarioDataFileLoader } from '@/scenario/scenario-data-loader'; @@ -47,6 +49,10 @@ export class ScenarioRunner { const testData = await this.dataLoader.loadDataForScenario(scenario); await testDataImporter.importTestScenarioData(testData.workflows); + // Wait for 1s before executing the scenario to ensure that the workflows are activated. + // In multi-main mode it can take some time before the workflow becomes active. + await sleep(1000); + console.log('Executing scenario script'); await this.k6Executor.executeTestScenario(scenario, { scenarioRunName, diff --git a/packages/@n8n/chat/package.json b/packages/@n8n/chat/package.json index 5dc5881181..1db1c50f9a 100644 --- a/packages/@n8n/chat/package.json +++ b/packages/@n8n/chat/package.json @@ -1,11 +1,11 @@ { "name": "@n8n/chat", - "version": "0.29.0", + "version": "0.33.0", "scripts": { "dev": "pnpm run storybook", "build": "pnpm build:vite && pnpm build:bundle", - "build:vite": "vite build", - "build:bundle": "INCLUDE_VUE=true vite build", + "build:vite": "cross-env vite build", + "build:bundle": "cross-env INCLUDE_VUE=true vite build", "preview": "vite preview", "test:dev": "vitest", "test": "vitest run", @@ -46,11 +46,12 @@ "devDependencies": { "@iconify-json/mdi": "^1.1.54", "@n8n/storybook": "workspace:*", + "@vitejs/plugin-vue": "catalog:frontend", "@vitest/coverage-v8": "catalog:frontend", "unplugin-icons": "^0.19.0", "vite": "catalog:frontend", "vitest": "catalog:frontend", - "vite-plugin-dts": "^4.2.3", + "vite-plugin-dts": "^4.3.0", "vue-tsc": "catalog:frontend" }, "files": [ diff --git a/packages/@n8n/chat/src/__tests__/setup.ts b/packages/@n8n/chat/src/__tests__/setup.ts index 7b0828bfa8..33e89fb68b 100644 --- a/packages/@n8n/chat/src/__tests__/setup.ts +++ b/packages/@n8n/chat/src/__tests__/setup.ts @@ -1 +1,13 @@ import '@testing-library/jest-dom'; +import '@testing-library/jest-dom'; +import { configure } from '@testing-library/vue'; + +configure({ testIdAttribute: 'data-test-id' }); + +window.ResizeObserver = + window.ResizeObserver || + vi.fn().mockImplementation(() => ({ + disconnect: vi.fn(), + observe: vi.fn(), + unobserve: vi.fn(), + })); diff --git a/packages/@n8n/chat/src/components/ChatFile.vue b/packages/@n8n/chat/src/components/ChatFile.vue index d3c3c2db7d..52b4acf789 100644 --- a/packages/@n8n/chat/src/components/ChatFile.vue +++ b/packages/@n8n/chat/src/components/ChatFile.vue @@ -30,22 +30,23 @@ const TypeIcon = computed(() => { }); function onClick() { - if (props.isRemovable) { - emit('remove', props.file); - } - if (props.isPreviewable) { window.open(URL.createObjectURL(props.file)); } } +function onDelete() { + emit('remove', props.file); +} @@ -80,12 +81,25 @@ function onClick() { .chat-file-preview { background: none; border: none; - display: none; + display: block; cursor: pointer; flex-shrink: 0; +} - .chat-file:hover & { - display: block; +.chat-file-delete { + position: relative; + &:hover { + color: red; + } + + /* Increase hit area for better clickability */ + &:before { + content: ''; + position: absolute; + top: -10px; + right: -10px; + bottom: -10px; + left: -10px; } } diff --git a/packages/@n8n/chat/src/components/Input.vue b/packages/@n8n/chat/src/components/Input.vue index 3e823917e0..1b9e0b9608 100644 --- a/packages/@n8n/chat/src/components/Input.vue +++ b/packages/@n8n/chat/src/components/Input.vue @@ -1,6 +1,6 @@