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-master.yml b/.github/workflows/ci-master.yml index 5e828a7022..b6972e1932 100644 --- a/.github/workflows/ci-master.yml +++ b/.github/workflows/ci-master.yml @@ -47,6 +47,7 @@ jobs: nodeVersion: ${{ matrix.node-version }} cacheKey: ${{ github.sha }}-base:build collectCoverage: ${{ matrix.node-version == '20.x' }} + ignoreTurboCache: ${{ matrix.node-version == '20.x' }} secrets: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 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/docker-base-image.yml b/.github/workflows/docker-base-image.yml index c32160e763..c2b5c9b6e6 100644 --- a/.github/workflows/docker-base-image.yml +++ b/.github/workflows/docker-base-image.yml @@ -20,26 +20,28 @@ jobs: - uses: actions/checkout@v4.1.1 - name: Set up QEMU - uses: docker/setup-qemu-action@v3.0.0 + uses: docker/setup-qemu-action@v3.3.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3.0.0 + uses: docker/setup-buildx-action@v3.8.0 - name: Login to GitHub Container Registry - uses: docker/login-action@v3.0.0 + uses: docker/login-action@v3.3.0 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Login to DockerHub - uses: docker/login-action@v3.0.0 + uses: docker/login-action@v3.3.0 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Build - uses: docker/build-push-action@v5.1.0 + uses: docker/build-push-action@v6.11.0 + env: + DOCKER_BUILD_SUMMARY: false with: context: . file: ./docker/images/n8n-base/Dockerfile diff --git a/.github/workflows/docker-images-benchmark.yml b/.github/workflows/docker-images-benchmark.yml index b0aa6e997d..d4bf2f98a0 100644 --- a/.github/workflows/docker-images-benchmark.yml +++ b/.github/workflows/docker-images-benchmark.yml @@ -19,20 +19,22 @@ jobs: - uses: actions/checkout@v4.1.1 - name: Set up QEMU - uses: docker/setup-qemu-action@v3.0.0 + uses: docker/setup-qemu-action@v3.3.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3.0.0 + uses: docker/setup-buildx-action@v3.8.0 - name: Login to GitHub Container Registry - uses: docker/login-action@v3.0.0 + uses: docker/login-action@v3.3.0 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build - uses: docker/build-push-action@v5.1.0 + uses: docker/build-push-action@v6.11.0 + env: + DOCKER_BUILD_SUMMARY: false with: context: . file: ./packages/@n8n/benchmark/Dockerfile diff --git a/.github/workflows/docker-images-custom.yml b/.github/workflows/docker-images-custom.yml new file mode 100644 index 0000000000..dfc9d77a03 --- /dev/null +++ b/.github/workflows/docker-images-custom.yml @@ -0,0 +1,83 @@ +name: Docker Custom Image CI +run-name: Build ${{ inputs.branch }} - ${{ inputs.user }} + +on: + workflow_dispatch: + inputs: + branch: + description: 'GitHub branch to create image off.' + required: true + tag: + description: 'Name of the docker tag to create.' + required: true + merge-master: + description: 'Merge with master.' + type: boolean + required: true + default: false + user: + description: '' + required: false + default: 'none' + start-url: + description: 'URL to call after workflow is kicked off.' + required: false + default: '' + success-url: + description: 'URL to call after Docker Image got built successfully.' + required: false + default: '' + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Call Start URL - optionally + if: ${{ github.event.inputs.start-url != '' }} + run: curl -v -X POST -d 'url=${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}' ${{github.event.inputs.start-url}} || echo "" + shell: bash + + - name: Checkout + uses: actions/checkout@v4.1.1 + with: + ref: ${{ github.event.inputs.branch }} + + - name: Merge Master - optionally + if: github.event.inputs.merge-master + run: git remote add upstream https://github.com/n8n-io/n8n.git -f; git merge upstream/master --allow-unrelated-histories || echo "" + shell: bash + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3.3.0 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3.8.0 + + - name: Login to GHCR + uses: docker/login-action@v3.3.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push image to GHCR + uses: docker/build-push-action@v6.11.0 + env: + DOCKER_BUILD_SUMMARY: false + with: + context: . + file: ./docker/images/n8n-custom/Dockerfile + build-args: | + N8N_RELEASE_TYPE=development + platforms: linux/amd64 + provenance: false + push: true + cache-from: type=gha + cache-to: type=gha,mode=max + tags: ghcr.io/${{ github.repository_owner }}/n8n:${{ inputs.tag }} + + - name: Call Success URL - optionally + if: ${{ github.event.inputs.success-url != '' }} + run: curl -v ${{github.event.inputs.success-url}} || echo "" + shell: bash diff --git a/.github/workflows/docker-images-nightly.yml b/.github/workflows/docker-images-nightly.yml index b6831b6399..c924f8b2c1 100644 --- a/.github/workflows/docker-images-nightly.yml +++ b/.github/workflows/docker-images-nightly.yml @@ -1,74 +1,42 @@ name: Docker Nightly Image CI -run-name: Build ${{ inputs.branch }} - ${{ inputs.user }} on: schedule: - - cron: '0 1 * * *' + - cron: '0 0 * * *' workflow_dispatch: - inputs: - branch: - description: 'GitHub branch to create image off.' - required: true - default: 'master' - tag: - description: 'Name of the docker tag to create.' - required: true - default: 'nightly' - merge-master: - description: 'Merge with master.' - type: boolean - required: true - default: false - user: - description: '' - required: false - default: 'schedule' - start-url: - description: 'URL to call after workflow is kicked off.' - required: false - default: '' - success-url: - description: 'URL to call after Docker Image got built successfully.' - required: false - default: '' - -env: - N8N_TAG: ${{ inputs.tag || 'nightly' }} jobs: build: runs-on: ubuntu-latest - steps: - - name: Call Start URL - optionally - run: | - [[ "${{github.event.inputs.start-url}}" != "" ]] && curl -v -X POST -d 'url=${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}' ${{github.event.inputs.start-url}} || echo "" - shell: bash - - name: Checkout uses: actions/checkout@v4.1.1 with: - ref: ${{ github.event.inputs.branch || 'master' }} + ref: master - name: Set up QEMU - uses: docker/setup-qemu-action@v3.0.0 + uses: docker/setup-qemu-action@v3.3.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3.0.0 + uses: docker/setup-buildx-action@v3.8.0 + + - name: Login to GHCR + uses: docker/login-action@v3.3.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - name: Login to DockerHub - uses: docker/login-action@v3.0.0 + uses: docker/login-action@v3.3.0 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - - name: Merge Master - optionally - run: | - [[ "${{github.event.inputs.merge-master}}" == "true" ]] && git remote add upstream https://github.com/n8n-io/n8n.git -f; git merge upstream/master --allow-unrelated-histories || echo "" - shell: bash - - - name: Build and push to DockerHub - uses: docker/build-push-action@v5.1.0 + - name: Build and push image to GHCR and DockerHub + uses: docker/build-push-action@v6.11.0 + env: + DOCKER_BUILD_SUMMARY: false with: context: . file: ./docker/images/n8n-custom/Dockerfile @@ -79,24 +47,6 @@ jobs: push: true cache-from: type=gha cache-to: type=gha,mode=max - tags: ${{ secrets.DOCKER_USERNAME }}/n8n:${{ env.N8N_TAG }} - - - name: Login to GitHub Container Registry - if: env.N8N_TAG == 'nightly' - uses: docker/login-action@v3.0.0 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Push image to GHCR - if: env.N8N_TAG == 'nightly' - run: | - docker buildx imagetools create \ - --tag ghcr.io/${{ github.repository_owner }}/n8n:nightly \ + tags: | + ghcr.io/${{ github.repository_owner }}/n8n:nightly ${{ secrets.DOCKER_USERNAME }}/n8n:nightly - - - name: Call Success URL - optionally - run: | - [[ "${{github.event.inputs.success-url}}" != "" ]] && curl -v ${{github.event.inputs.success-url}} || echo "" - shell: bash diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml index c3548a0cda..b95350a577 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 @@ -67,26 +73,28 @@ jobs: fetch-depth: 0 - name: Set up QEMU - uses: docker/setup-qemu-action@v3.0.0 + uses: docker/setup-qemu-action@v3.3.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3.0.0 + uses: docker/setup-buildx-action@v3.8.0 - name: Login to GitHub Container Registry - uses: docker/login-action@v3.0.0 + uses: docker/login-action@v3.3.0 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Login to DockerHub - uses: docker/login-action@v3.0.0 + uses: docker/login-action@v3.3.0 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Build - uses: docker/build-push-action@v5.1.0 + uses: docker/build-push-action@v6.11.0 + env: + DOCKER_BUILD_SUMMARY: false with: context: ./docker/images/n8n build-args: | @@ -119,6 +127,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/release-push-to-channel.yml b/.github/workflows/release-push-to-channel.yml index 3eda6d4ebb..9cb3a99b63 100644 --- a/.github/workflows/release-push-to-channel.yml +++ b/.github/workflows/release-push-to-channel.yml @@ -34,7 +34,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 5 steps: - - uses: docker/login-action@v3.0.0 + - uses: docker/login-action@v3.3.0 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} @@ -46,7 +46,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 5 steps: - - uses: docker/login-action@v3.0.0 + - uses: docker/login-action@v3.3.0 with: registry: ghcr.io username: ${{ github.actor }} diff --git a/.github/workflows/test-workflows.yml b/.github/workflows/test-workflows.yml index 2bb91dd065..2df829f7c6 100644 --- a/.github/workflows/test-workflows.yml +++ b/.github/workflows/test-workflows.yml @@ -4,18 +4,72 @@ 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/')) && + !contains(github.event.pull_request.labels.*.name, 'community') 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 +77,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..62eca74b15 100644 --- a/.github/workflows/units-tests-reusable.yml +++ b/.github/workflows/units-tests-reusable.yml @@ -22,6 +22,10 @@ on: required: false default: false type: boolean + ignoreTurboCache: + required: false + default: false + type: boolean secrets: CODECOV_TOKEN: description: 'Codecov upload token.' @@ -32,6 +36,7 @@ jobs: name: Unit tests runs-on: ubuntu-latest env: + TURBO_FORCE: ${{ inputs.ignoreTurboCache }} COVERAGE_ENABLED: ${{ inputs.collectCoverage }} steps: - uses: actions/checkout@v4.1.1 @@ -49,7 +54,6 @@ jobs: run: pnpm install --frozen-lockfile - name: Setup build cache - if: inputs.collectCoverage != true uses: rharkor/caching-for-turbo@v1.5 - name: Build @@ -74,6 +78,6 @@ jobs: - name: Upload coverage to Codecov if: inputs.collectCoverage - uses: codecov/codecov-action@v4.5.0 + uses: codecov/codecov-action@v5.1.2 with: token: ${{ secrets.CODECOV_TOKEN }} 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 baa7b95b7c..1629f7ec29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,476 @@ +# [1.75.0](https://github.com/n8n-io/n8n/compare/n8n@1.74.0...n8n@1.75.0) (2025-01-15) + + +### Bug Fixes + +* **core:** AugmentObject should check for own propeties correctly ([#12534](https://github.com/n8n-io/n8n/issues/12534)) ([0cdf393](https://github.com/n8n-io/n8n/commit/0cdf39374305e6bbcedb047db7d3756168e6e89e)) +* **core:** Disallow code generation in task runner ([#12522](https://github.com/n8n-io/n8n/issues/12522)) ([35b6180](https://github.com/n8n-io/n8n/commit/35b618098b7d23e272bf77b55c172dbe531c821f)) +* **core:** Fix node exclusion on the frontend types ([#12544](https://github.com/n8n-io/n8n/issues/12544)) ([b2cbed9](https://github.com/n8n-io/n8n/commit/b2cbed9865888f6f3bc528984d4091d86a88f0d6)) +* **core:** Fix orchestration flow with expired license ([#12444](https://github.com/n8n-io/n8n/issues/12444)) ([ecff3b7](https://github.com/n8n-io/n8n/commit/ecff3b732a028d7225bfbed4ffc65dc20c4ed608)) +* **core:** Fix Sentry error reporting on task runners ([#12495](https://github.com/n8n-io/n8n/issues/12495)) ([88c0838](https://github.com/n8n-io/n8n/commit/88c0838dd72f11646bdb3586223d6c16631cccab)) +* **core:** Improve cyclic dependency check in the DI container ([#12600](https://github.com/n8n-io/n8n/issues/12600)) ([c3c4a20](https://github.com/n8n-io/n8n/commit/c3c4a200024fb08afb9380357d1490c6707c5ec3)) +* **core:** Only show personal credentials in the personal space ([#12433](https://github.com/n8n-io/n8n/issues/12433)) ([8a42d55](https://github.com/n8n-io/n8n/commit/8a42d55d91f4a37fff5669d52d52428b3a4ddd44)) +* **core:** Prefix package name in `supportedNodes` on generated types as well ([#12514](https://github.com/n8n-io/n8n/issues/12514)) ([4a1a999](https://github.com/n8n-io/n8n/commit/4a1a9993624c92dd81f5418f9268cb93878069ab)) +* **core:** Prevent prototype pollution in task runner ([#12588](https://github.com/n8n-io/n8n/issues/12588)) ([bdf266c](https://github.com/n8n-io/n8n/commit/bdf266cf55032d05641b20dce8804412dc93b6d5)) +* **core:** Prevent prototype pollution of internal classes in task runner ([#12610](https://github.com/n8n-io/n8n/issues/12610)) ([eceee7f](https://github.com/n8n-io/n8n/commit/eceee7f3f8899d200b1c5720087cc494eec22e6a)) +* **core:** Use timing safe function to compare runner auth tokens ([#12485](https://github.com/n8n-io/n8n/issues/12485)) ([8fab98f](https://github.com/n8n-io/n8n/commit/8fab98f3f1f767d05825d24cbf155d56375fdb3e)) +* **core:** Validate values which are intentionally 0 ([#12382](https://github.com/n8n-io/n8n/issues/12382)) ([562506e](https://github.com/n8n-io/n8n/commit/562506e92aeb26423145801bff80037e5ce2ac46)) +* Don't break oauth credentials when updating them and allow fixing broken oauth credentials by repeating the authorization flow ([#12563](https://github.com/n8n-io/n8n/issues/12563)) ([73897c7](https://github.com/n8n-io/n8n/commit/73897c7662a432834eb6f9d0f9ace8d986c1acb5)) +* **editor:** Don't show toolsUnused notice if run had errors ([#12529](https://github.com/n8n-io/n8n/issues/12529)) ([3ec5b28](https://github.com/n8n-io/n8n/commit/3ec5b2850c47057032e61c2acdbdfc1dcdd931f7)) +* **editor:** Ensure proper "AI Template" URL construction in node creator ([#12566](https://github.com/n8n-io/n8n/issues/12566)) ([13bf69f](https://github.com/n8n-io/n8n/commit/13bf69f75c67bc37a37013e776525768676a4b88)) +* **editor:** Fix NDV resize handle and scrollbar overlapping ([#12509](https://github.com/n8n-io/n8n/issues/12509)) ([c28f302](https://github.com/n8n-io/n8n/commit/c28f302c2f863bd7aa73ad52e5d040f927e33220)) +* **editor:** Fix parameter input validation ([#12532](https://github.com/n8n-io/n8n/issues/12532)) ([6711cbc](https://github.com/n8n-io/n8n/commit/6711cbcc641a2fc70f5c15a7e2dcc640a3f98b66)) +* **editor:** Fix selection rectangle context menu on new canvas ([#12584](https://github.com/n8n-io/n8n/issues/12584)) ([c8e3c53](https://github.com/n8n-io/n8n/commit/c8e3c5399efde93486c1dd5c373cb2c5ff8a0691)) +* **editor:** Fix the `openselectivenodecreator` custom action on new canvas ([#12580](https://github.com/n8n-io/n8n/issues/12580)) ([2110e9a](https://github.com/n8n-io/n8n/commit/2110e9a0513b8c36beb85302e0d38a2658ea5d6e)) +* **editor:** Fix workflow initilisation for test definition routes & add unit tests ([#12507](https://github.com/n8n-io/n8n/issues/12507)) ([2775f61](https://github.com/n8n-io/n8n/commit/2775f617ae5c267c0a1ce7a54d05d4077cdbc0f7)) +* **editor:** Make clicking item in RLC work the first time on small screens ([#12585](https://github.com/n8n-io/n8n/issues/12585)) ([479933f](https://github.com/n8n-io/n8n/commit/479933fbd5c88e783827960e018abb979de8a039)) +* **editor:** Make sure code editors work correctly in fullscreen ([#12597](https://github.com/n8n-io/n8n/issues/12597)) ([aa1f3a7](https://github.com/n8n-io/n8n/commit/aa1f3a7d989883d55df3777775b8d7d336f6e3b7)) +* **editor:** Override selected nodes on single click without Meta/Ctrl key ([#12549](https://github.com/n8n-io/n8n/issues/12549)) ([02c2d5e](https://github.com/n8n-io/n8n/commit/02c2d5e71d15b9292fddd585f47bd8334da468c5)) +* **editor:** Show NDV errors when opening existing nodes with errors ([#12567](https://github.com/n8n-io/n8n/issues/12567)) ([bee7267](https://github.com/n8n-io/n8n/commit/bee7267fe38ab12a79fa4ec0e775f45d98d48aa5)) +* **editor:** Swap Activate/Deactivate texts in FloatingToolbar ([#12526](https://github.com/n8n-io/n8n/issues/12526)) ([44679b4](https://github.com/n8n-io/n8n/commit/44679b42aa1e14bc7069bee47d0a91ca84b1dba4)) +* **editor:** Update filter and feedback for source control ([#12504](https://github.com/n8n-io/n8n/issues/12504)) ([865fc21](https://github.com/n8n-io/n8n/commit/865fc21276727e8d88ccee0355147904b81c4421)) +* **editor:** Update selected node when navigating via flowing nodes ([#12581](https://github.com/n8n-io/n8n/issues/12581)) ([88659d8](https://github.com/n8n-io/n8n/commit/88659d8a2901786c894902e19466f395bcdaab8e)) +* **Google Calendar Node:** Updates and fixes ([#10715](https://github.com/n8n-io/n8n/issues/10715)) ([7227a29](https://github.com/n8n-io/n8n/commit/7227a29845fd178ced4d281597c62e7a03245456)) +* **Spotify Node:** Fix issue with null values breaking the response ([#12080](https://github.com/n8n-io/n8n/issues/12080)) ([a56a462](https://github.com/n8n-io/n8n/commit/a56a46259d257003c813103578260d625b3f17dd)) + + +### Features + +* **editor:** Make node credential select searchable ([#12497](https://github.com/n8n-io/n8n/issues/12497)) ([91277c4](https://github.com/n8n-io/n8n/commit/91277c44f1cf3f334b3b50d47d7dcc79b11c7c63)) +* **editor:** Persist sidebar collapsed status preference ([#12505](https://github.com/n8n-io/n8n/issues/12505)) ([dba7d46](https://github.com/n8n-io/n8n/commit/dba7d46f3ec91d26a597a50dede7b6ca292c728f)) + + + +# [1.74.0](https://github.com/n8n-io/n8n/compare/n8n@1.73.0...n8n@1.74.0) (2025-01-08) + + +### Bug Fixes + +* **core:** Align concurrency and timeout defaults between instance and runner ([#12503](https://github.com/n8n-io/n8n/issues/12503)) ([9953477](https://github.com/n8n-io/n8n/commit/9953477450c28ec2d211e55aadb825dbae2ee4d6)) +* **core:** Allow `index` as top-level item key for Code node ([#12469](https://github.com/n8n-io/n8n/issues/12469)) ([1b91000](https://github.com/n8n-io/n8n/commit/1b9100032fc9f8c33e263c8299e04054105da384)) +* **core:** Don't fail task runner task if logging fails ([#12401](https://github.com/n8n-io/n8n/issues/12401)) ([0860fbe](https://github.com/n8n-io/n8n/commit/0860fbe97108edc21bc01dec3b6ef13e60e728d4)) +* **core:** Ensure tasks timeout even if they don't receive settings ([#12431](https://github.com/n8n-io/n8n/issues/12431)) ([b194026](https://github.com/n8n-io/n8n/commit/b1940268e6110ed3d8949318a5252ac6563d624f)) +* **core:** Fix execution cancellation issues in scaling mode ([#12343](https://github.com/n8n-io/n8n/issues/12343)) ([e26b406](https://github.com/n8n-io/n8n/commit/e26b406665e20761279c4e315d04501350427de5)) +* **core:** Fix manually running a pinned trigger with offloading enabled ([#12491](https://github.com/n8n-io/n8n/issues/12491)) ([be2dcff](https://github.com/n8n-io/n8n/commit/be2dcffc9487973d3e287dd4f6956dbba03757e3)) +* **core:** Fix task runner sending too many offers ([#12415](https://github.com/n8n-io/n8n/issues/12415)) ([4498e35](https://github.com/n8n-io/n8n/commit/4498e3519276020d3eb01752b5ce0d8ecfbf5fa4)) +* **core:** Increase default concurrency and timeout in task runners ([#12496](https://github.com/n8n-io/n8n/issues/12496)) ([4182095](https://github.com/n8n-io/n8n/commit/4182095af1c02832af2523f31e9cb85d9a345e60)) +* **core:** Prevent `__default__` jobs in scaling mode ([#12402](https://github.com/n8n-io/n8n/issues/12402)) ([072664b](https://github.com/n8n-io/n8n/commit/072664b40e06943e0b8ff44287730f2ca569646f)) +* **core:** Register workflows as active only after all of the triggers and pollers setup successfully ([#12244](https://github.com/n8n-io/n8n/issues/12244)) ([f924f2a](https://github.com/n8n-io/n8n/commit/f924f2a6d736e33ab5fc12cbac6cba27340839db)) +* **core:** Return unredacted credentials from `GET credentials/:id` ([#12447](https://github.com/n8n-io/n8n/issues/12447)) ([ecabe34](https://github.com/n8n-io/n8n/commit/ecabe34705bbbba07613ba14760449ef38e1b31f)) +* **core:** Use rate limiter for task runner endpoints ([#12486](https://github.com/n8n-io/n8n/issues/12486)) ([491cb60](https://github.com/n8n-io/n8n/commit/491cb605e3c93d7a261bb0cef0d38f2ddc3affe8)) +* **editor:** Allow zooming when panning keycode is pressed on new canvas ([#12327](https://github.com/n8n-io/n8n/issues/12327)) ([983e87a](https://github.com/n8n-io/n8n/commit/983e87a9b0c83d35354ce4df34096f47173d0ea7)) +* **editor:** Consistent protected environment styling and messaging ([#12374](https://github.com/n8n-io/n8n/issues/12374)) ([6891cef](https://github.com/n8n-io/n8n/commit/6891cefa6d0359f85a596829b6055a13529fb1fb)) +* **editor:** First project button tweaks border and copy ([#12376](https://github.com/n8n-io/n8n/issues/12376)) ([e234756](https://github.com/n8n-io/n8n/commit/e234756457d3c3526531ced4471bf9e69a79fa55)) +* **editor:** Fix Multi option parameter expression when the value is an array ([#12430](https://github.com/n8n-io/n8n/issues/12430)) ([452a7bf](https://github.com/n8n-io/n8n/commit/452a7bfe2c1e786c46a3ed99de007b0cf3f28d15)) +* **editor:** Improve configurable nodes design on new canvas ([#12317](https://github.com/n8n-io/n8n/issues/12317)) ([0ecce10](https://github.com/n8n-io/n8n/commit/0ecce10faf60ae44d11007d45e87766b678d3a84)) +* **editor:** Minor styling improvements in project settings page ([#12405](https://github.com/n8n-io/n8n/issues/12405)) ([09ddce0](https://github.com/n8n-io/n8n/commit/09ddce05800f426d33489ae28c416bb6aab2fd91)) +* **editor:** Never show Pinned Data Callout for Input Panel ([#12446](https://github.com/n8n-io/n8n/issues/12446)) ([1d5c9bd](https://github.com/n8n-io/n8n/commit/1d5c9bd466becf8aa245a1e8d0b799616d18914a)) +* **editor:** Nodes' icon color in dark mode ([#12279](https://github.com/n8n-io/n8n/issues/12279)) ([01b781a](https://github.com/n8n-io/n8n/commit/01b781a10828ca2c4cf32762373ad40904c02d2c)) +* **editor:** Only ignore managed credentials in the HTTP node ([#12417](https://github.com/n8n-io/n8n/issues/12417)) ([6b46657](https://github.com/n8n-io/n8n/commit/6b46657412a1efff35be5083f0ff4c00f9b3e7f9)) +* **editor:** Remove primary highlight color from edge being executed on new canvas ([#12307](https://github.com/n8n-io/n8n/issues/12307)) ([50913de](https://github.com/n8n-io/n8n/commit/50913de2651450e18307a833ada57656d8959493)) +* **editor:** Render empty string instead of [empty] ([#12448](https://github.com/n8n-io/n8n/issues/12448)) ([2c72047](https://github.com/n8n-io/n8n/commit/2c72047d0b260db5a4b1fd0d7448ab19378e908f)) +* **editor:** Show all workflows in the error workflow dropdown in the workflow settings ([#12413](https://github.com/n8n-io/n8n/issues/12413)) ([ccda7f9](https://github.com/n8n-io/n8n/commit/ccda7f9c62e2ba04dbd8a86cfeb5016b56f19c7a)) +* **editor:** Unify disabled parameters background color ([#12306](https://github.com/n8n-io/n8n/issues/12306)) ([8c63599](https://github.com/n8n-io/n8n/commit/8c635993bd65c84707938d9564d54c1ae17f1c1f)) +* **HTTP Request Node:** Fix typo in hint ([#12439](https://github.com/n8n-io/n8n/issues/12439)) ([b6230b6](https://github.com/n8n-io/n8n/commit/b6230b63f2ed8c7531b53c896f8b033c599e156e)) +* **OpenAI Node:** Add quotes to default base URL ([#12312](https://github.com/n8n-io/n8n/issues/12312)) ([2e90eba](https://github.com/n8n-io/n8n/commit/2e90eba47eff81f8b17a305cbc1656f929d622f8)) +* **OpenAI Node:** Update node to account for URL in credentials ([#12356](https://github.com/n8n-io/n8n/issues/12356)) ([f78cceb](https://github.com/n8n-io/n8n/commit/f78ccebe514819dca03f5c220274b94fd6d1c73b)) +* **Postgres Node:** Account for JSON expressions ([#12012](https://github.com/n8n-io/n8n/issues/12012)) ([06b86af](https://github.com/n8n-io/n8n/commit/06b86af7356b3be0af146c49f9720b24157b9e61)) +* **Postgres Node:** Allow passing in arrays to JSON columns for insert ([#12452](https://github.com/n8n-io/n8n/issues/12452)) ([9dd0686](https://github.com/n8n-io/n8n/commit/9dd068632b1542126831baa83cf638ce369b0947)) +* **Postgres Node:** Re-use connection pool across executions ([#12346](https://github.com/n8n-io/n8n/issues/12346)) ([2ca37f5](https://github.com/n8n-io/n8n/commit/2ca37f5f7f7f80c50dbc8c87146b8bff510f01c8)) +* Run workflow if active and single webhook service has pin data ([#12425](https://github.com/n8n-io/n8n/issues/12425)) ([8053a4a](https://github.com/n8n-io/n8n/commit/8053a4a1763d143da80b9e4e00dcef9b716ce6b2)) +* Set correct default for added Resource Mapper boolean fields ([#12344](https://github.com/n8n-io/n8n/issues/12344)) ([b4c77f2](https://github.com/n8n-io/n8n/commit/b4c77f27b66275ddb58138e8d2fe1509265e9652)) +* **Supabase Node:** Allow for filtering on the same field multiple times ([#12429](https://github.com/n8n-io/n8n/issues/12429)) ([d7cc789](https://github.com/n8n-io/n8n/commit/d7cc789d79477aff40ff4eca0175c7578aef338a)) +* **Zep Vector Store Node:** Cloud vector store integration ([#12353](https://github.com/n8n-io/n8n/issues/12353)) ([2433d6b](https://github.com/n8n-io/n8n/commit/2433d6b7d3dede2595dd5b637ca8bbc1103272b3)) + + +### Features + +* (Execute Workflow Node): Inputs for Sub-workflows ([#11830](https://github.com/n8n-io/n8n/issues/11830)) ([#11837](https://github.com/n8n-io/n8n/issues/11837)) ([d411663](https://github.com/n8n-io/n8n/commit/d4116630a638195c7d87e01e2b5c151941636056)) +* Add load options to new tool mode for vector stores ([#12462](https://github.com/n8n-io/n8n/issues/12462)) ([3109de6](https://github.com/n8n-io/n8n/commit/3109de6073b237ee3dcc93afb69345586f3b836d)) +* Add migration to add `managed` column to credentials table ([#12275](https://github.com/n8n-io/n8n/issues/12275)) ([3cb7081](https://github.com/n8n-io/n8n/commit/3cb70814465e8fa504e909ef36b21b79d4b70b28)) +* Allow using Vector Stores directly as Tools ([#12311](https://github.com/n8n-io/n8n/issues/12311)) ([76dded4](https://github.com/n8n-io/n8n/commit/76dded4bea9d26ad84fdbde74d577d244eb4e223)) +* **core:** Add endpoint to create free AI credits ([#12362](https://github.com/n8n-io/n8n/issues/12362)) ([ac4e042](https://github.com/n8n-io/n8n/commit/ac4e0422316a4dcd19151dd7d504e2b3cccbc038)) +* **core:** Add includeData parameter to `GET /credentials` ([#12220](https://github.com/n8n-io/n8n/issues/12220)) ([f56ad8c](https://github.com/n8n-io/n8n/commit/f56ad8cf49f7cf0665035d2e43bb7ff5b8fd75f3)) +* **core:** Comply with `NO_COLOR` in logs ([#12347](https://github.com/n8n-io/n8n/issues/12347)) ([1e60bbc](https://github.com/n8n-io/n8n/commit/1e60bbcf169e8624a97ddde543cdd1d406e5c7ca)) +* **core:** Offload manual executions to workers ([#11284](https://github.com/n8n-io/n8n/issues/11284)) ([9432aa0](https://github.com/n8n-io/n8n/commit/9432aa0b00e74faf4651ac673f18e16b7e56e145)) +* **editor:** Add free AI credits CTA ([#12365](https://github.com/n8n-io/n8n/issues/12365)) ([f873196](https://github.com/n8n-io/n8n/commit/f8731963f6754386f15c8417c0cc32dba87c481a)) +* **editor:** Add support for project icons ([#12349](https://github.com/n8n-io/n8n/issues/12349)) ([9117718](https://github.com/n8n-io/n8n/commit/9117718cc960e2bad5a5db07b10e9e7b561ec5e4)) +* **editor:** Easy AI workflow improvements ([#12400](https://github.com/n8n-io/n8n/issues/12400)) ([8dc691d](https://github.com/n8n-io/n8n/commit/8dc691dc62692f8af143c84032391397adeb790d)) +* **editor:** Make workflows, credentials, executions and new canvas usable on mobile and touch devices ([#12372](https://github.com/n8n-io/n8n/issues/12372)) ([06c9473](https://github.com/n8n-io/n8n/commit/06c94732103687705d71c5a1c5bfa993e3df3427)) +* **editor:** New Code editor based on the TypeScript language service ([#12285](https://github.com/n8n-io/n8n/issues/12285)) ([52ae02a](https://github.com/n8n-io/n8n/commit/52ae02abaa92e5bbfda58843c8eccc845506fa4b)) +* **editor:** Update Sub-Workflow Debugging Copy ([#12483](https://github.com/n8n-io/n8n/issues/12483)) ([04e2928](https://github.com/n8n-io/n8n/commit/04e2928d345f83c202c762e4673cf878b4762f33)) +* **Google Vertex Chat Model Node:** Add an option to specify GCP region ([#12300](https://github.com/n8n-io/n8n/issues/12300)) ([30f9c03](https://github.com/n8n-io/n8n/commit/30f9c033db28112e1f97bb55d41b5bfce265cb51)) +* **HighLevel Node:** Add support for calendar items ([#10820](https://github.com/n8n-io/n8n/issues/10820)) ([6e189fd](https://github.com/n8n-io/n8n/commit/6e189fda776051e09e90b3d86ecd0d1e80dcc0c6)) +* **Microsoft Entra ID Node:** New node ([#11779](https://github.com/n8n-io/n8n/issues/11779)) ([3006ccf](https://github.com/n8n-io/n8n/commit/3006ccf41bb911ba72f087a1479889fbf308c17d)) + + + +# [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) 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..8d01e943d3 100644 --- a/README.md +++ b/README.md @@ -1,104 +1,72 @@ -![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) +Try n8n instantly with [npx](https://docs.n8n.io/hosting/installation/npm/) (requires [Node.js](https://nodejs.org/en/)): -## Documentation +``` +npx n8n +``` -The official n8n documentation can be found on our [documentation website](https://docs.n8n.io) +Or deploy with [Docker](https://docs.n8n.io/hosting/installation/docker/): -Additional information and example workflows on the [n8n.io website](https://n8n.io) +``` +docker volume create n8n_data +docker run -it --rm --name n8n -p 5678:5678 -v n8n_data:/home/node/.n8n docker.n8n.io/n8nio/n8n +``` -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). +Access the editor at http://localhost:5678 -## Usage +## Resources -- :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: - -`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). - -## n8n cloud - -Sign-up for an [n8n cloud](https://www.n8n.io/cloud/) account. - -While n8n cloud and n8n are the same in terms of features, n8n cloud provides certain conveniences such as: - -- 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/codecov.yml b/codecov.yml new file mode 100644 index 0000000000..362ebcb760 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,60 @@ +codecov: + max_report_age: off + require_ci_to_pass: true + +coverage: + status: + patch: false + project: + default: + threshold: 0.5% + +github_checks: + annotations: false + +flags: + tests: + paths: + - "**" + carryforward: true + +component_management: + default_rules: + statuses: + - type: project + target: auto + branches: + - "!master" + individual_components: + - component_id: backend_packages + name: Backend + paths: + - packages/@n8n/api-types/** + - packages/@n8n/config/** + - packages/@n8n/client-oauth2/** + - packages/@n8n/di/** + - packages/@n8n/imap/** + - packages/@n8n/permissions/** + - packages/@n8n/task-runner/** + - packages/workflow/** + - packages/core/** + - packages/cli/** + - component_id: frontend_packages + name: Frontend + paths: + - packages/@n8n/chat/** + - packages/@n8n/codemirror-lang/** + - packages/design-system/** + - packages/editor-ui/** + - component_id: nodes_packages + name: Nodes + paths: + - packages/node-dev/** + - packages/nodes-base/** + - packages/@n8n/json-schema-to-zod/** + - packages/@n8n/nodes-langchain/** + +ignore: + - (?s:.*/[^\/]*\.spec\.ts.*)\Z + - (?s:.*/[^\/]*\.test\.ts.*)\Z + - (?s:.*/[^\/]*e2e[^\/]*\.ts.*)\Z 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..4e6533300a 100644 --- a/cypress/composables/modals/credential-modal.ts +++ b/cypress/composables/modals/credential-modal.ts @@ -2,6 +2,8 @@ * Getters */ +import { clearNotifications } from '../../pages/notifications'; + export function getCredentialConnectionParameterInputs() { return cy.getByTestId('credential-connection-parameter'); } @@ -35,7 +37,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() { @@ -50,5 +57,6 @@ export function setCredentialValues(values: Record, save = true) if (save) { saveCredential(); closeCredentialModal(); + clearNotifications(); } } diff --git a/cypress/composables/ndv.ts b/cypress/composables/ndv.ts index 5b3690e6a6..9ec6f3ba19 100644 --- a/cypress/composables/ndv.ts +++ b/cypress/composables/ndv.ts @@ -2,7 +2,7 @@ * Getters */ -import { getVisibleSelect } from '../utils'; +import { getVisiblePopper, getVisibleSelect } from '../utils/popper'; export function getCredentialSelect(eq = 0) { return cy.getByTestId('node-credentials-select').eq(eq); @@ -36,14 +36,90 @@ export function getOutputPanel() { return cy.getByTestId('output-panel'); } +export function getFixedCollection(collectionName: string) { + return cy.getByTestId(`fixed-collection-${collectionName}`); +} + +export function getResourceLocator(paramName: string) { + return cy.getByTestId(`resource-locator-${paramName}`); +} + +export function getResourceLocatorInput(paramName: string) { + return getResourceLocator(paramName).find('[data-test-id="rlc-input-container"]'); +} + 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'); +} + +export function getNodeOutputHint() { + return cy.getByTestId('ndv-output-run-node-hint'); +} + +export function getWorkflowCards() { + return cy.getByTestId('resources-list-item'); +} + +export function getWorkflowCard(workflowName: string) { + return getWorkflowCards().contains(workflowName).parents('[data-test-id="resources-list-item"]'); +} + +export function getWorkflowCardContent(workflowName: string) { + return getWorkflowCard(workflowName).findChildByTestId('card-content'); +} + +export function getNodeRunInfoStale() { + return cy.getByTestId('node-run-info-stale'); +} + +export function getNodeOutputErrorMessage() { + return getOutputPanel().findChildByTestId('node-error-message'); +} + /** * Actions */ @@ -70,15 +146,111 @@ export function clickExecuteNode() { getExecuteNodeButton().click(); } +export function clickResourceLocatorInput(paramName: string) { + getResourceLocatorInput(paramName).click(); +} + export function setParameterInputByName(name: string, value: string) { getParameterInputByName(name).clear().type(value); } -export function toggleParameterCheckboxInputByName(name: string) { - getParameterInputByName(name).find('input[type="checkbox"]').realClick(); +export function checkParameterCheckboxInputByName(name: string) { + getParameterInputByName(name).find('input[type="checkbox"]').check({ force: true }); +} + +export function uncheckParameterCheckboxInputByName(name: string) { + getParameterInputByName(name).find('input[type="checkbox"]').uncheck({ force: true }); } 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(); +} + +export function addItemToFixedCollection(collectionName: string) { + getFixedCollection(collectionName).getByTestId('fixed-collection-add').click(); +} + +export function typeIntoFixedCollectionItem(collectionName: string, index: number, value: string) { + getFixedCollection(collectionName).within(() => + cy.getByTestId('parameter-input').eq(index).type(value), + ); +} + +export function selectResourceLocatorItem( + resourceLocator: string, + index: number, + expectedText: string, +) { + clickResourceLocatorInput(resourceLocator); + + getVisiblePopper().findChildByTestId('rlc-item').eq(0).should('exist'); + getVisiblePopper() + .findChildByTestId('rlc-item') + .eq(index) + .find('span') + .should('contain.text', expectedText) + .click(); +} + +export function clickWorkflowCardContent(workflowName: string) { + getWorkflowCardContent(workflowName).click(); +} + +export function assertNodeOutputHintExists() { + getNodeOutputHint().should('exist'); +} + +export function assertNodeOutputErrorMessageExists() { + return getNodeOutputErrorMessage().should('exist'); +} + +// Note that this only validates the expectedContent is *included* in the output table +export function assertOutputTableContent(expectedContent: unknown[][]) { + for (const [i, row] of expectedContent.entries()) { + for (const [j, value] of row.entries()) { + // + 1 to skip header + getOutputTbodyCell(1 + i, j).should('have.text', value); + } + } +} + +export function populateMapperFields(fields: ReadonlyArray<[string, string]>) { + for (const [name, value] of fields) { + getParameterInputByName(name).type(value); + + // Click on a parent to dismiss the pop up which hides the field below. + getParameterInputByName(name).parent().parent().parent().click('topLeft'); + } +} + +/** + * 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"], ["myField2", "Number"]] + * @param collectionName - name of the fixedCollection to populate + * @param offset - amount of 'parameter-input's before start, e.g. from a controlling dropdown that makes the fields appear + * @returns + */ +export function populateFixedCollection( + items: readonly T[], + collectionName: string, + offset: number = 0, +) { + if (items.length === 0) return; + const n = items[0].length; + for (const [i, params] of items.entries()) { + addItemToFixedCollection(collectionName); + for (const [j, param] of params.entries()) { + getFixedCollection(collectionName) + .getByTestId('parameter-input') + .eq(offset + i * n + j) + .type(`${param}{downArrow}{enter}`); + } + } +} 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..66782f02cf 100644 --- a/cypress/composables/workflow.ts +++ b/cypress/composables/workflow.ts @@ -1,4 +1,5 @@ import { getManualChatModal } from './modals/chat-modal'; +import { clickGetBackToCanvas, getParameterInputByName } from './ndv'; import { ROUTES } from '../constants'; /** @@ -6,6 +7,7 @@ import { ROUTES } from '../constants'; */ export type EndpointType = + | 'main' | 'ai_chain' | 'ai_document' | 'ai_embedding' @@ -23,8 +25,15 @@ export type EndpointType = */ export function getAddInputEndpointByType(nodeName: string, endpointType: EndpointType) { - return cy.get( - `.add-input-endpoint[data-jtk-scope-${endpointType}][data-endpoint-name="${nodeName}"]`, + return cy.ifCanvasVersion( + () => + cy.get( + `.add-input-endpoint[data-jtk-scope-${endpointType}][data-endpoint-name="${nodeName}"]`, + ), + () => + cy.get( + `[data-test-id="canvas-node-input-handle"][data-connection-type="${endpointType}"][data-node-name="${nodeName}"] [data-test-id="canvas-handle-plus"]`, + ), ); } @@ -45,7 +54,14 @@ export function getNodes() { } export function getNodeByName(name: string) { - return cy.getByTestId('canvas-node').filter(`[data-name="${name}"]`).eq(0); + return cy.ifCanvasVersion( + () => cy.getByTestId('canvas-node').filter(`[data-name="${name}"]`).eq(0), + () => cy.getByTestId('canvas-node').filter(`[data-node-name="${name}"]`).eq(0), + ); +} + +export function getWorkflowHistoryCloseButton() { + return cy.getByTestId('workflow-history-close-button'); } export function disableNode(name: string) { @@ -55,10 +71,18 @@ export function disableNode(name: string) { } export function getConnectionBySourceAndTarget(source: string, target: string) { - return cy - .get('.jtk-connector') - .filter(`[data-source-node="${source}"][data-target-node="${target}"]`) - .eq(0); + return cy.ifCanvasVersion( + () => + cy + .get('.jtk-connector') + .filter(`[data-source-node="${source}"][data-target-node="${target}"]`) + .eq(0), + () => + cy + .getByTestId('edge') + .filter(`[data-source-node-name="${source}"][data-target-node-name="${target}"]`) + .eq(0), + ); } export function getNodeCreatorSearchBar() { @@ -69,6 +93,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 */ @@ -108,7 +151,7 @@ export function navigateToNewWorkflowPage(preventNodeViewUnload = true) { }); } -export function addSupplementalNodeToParent( +function connectNodeToParent( nodeName: string, endpointType: EndpointType, parentNodeName: string, @@ -122,7 +165,28 @@ export function addSupplementalNodeToParent( } else { getNodeCreatorItems().contains(nodeName).click(); } - getConnectionBySourceAndTarget(parentNodeName, nodeName).should('exist'); +} + +export function addSupplementalNodeToParent( + nodeName: string, + endpointType: EndpointType, + parentNodeName: string, + exactMatch = false, +) { + connectNodeToParent(nodeName, endpointType, parentNodeName, exactMatch); + + cy.ifCanvasVersion( + () => { + getConnectionBySourceAndTarget(parentNodeName, nodeName).should('exist'); + }, + () => { + if (endpointType === 'main') { + getConnectionBySourceAndTarget(parentNodeName, nodeName).should('exist'); + } else { + getConnectionBySourceAndTarget(nodeName, parentNodeName).should('exist'); + } + }, + ); } export function addLanguageModelNodeToParent( @@ -141,6 +205,15 @@ export function addToolNodeToParent(nodeName: string, parentNodeName: string) { addSupplementalNodeToParent(nodeName, 'ai_tool', parentNodeName); } +export function addVectorStoreToolToParent(nodeName: string, parentNodeName: string) { + connectNodeToParent(nodeName, 'ai_tool', parentNodeName, false); + getParameterInputByName('mode') + .find('input') + .should('have.value', 'Retrieve Documents (As Tool for AI Agent)'); + clickGetBackToCanvas(); + getConnectionBySourceAndTarget(nodeName, parentNodeName).should('exist'); +} + export function addOutputParserNodeToParent(nodeName: string, parentNodeName: string) { addSupplementalNodeToParent(nodeName, 'ai_outputParser', parentNodeName); } @@ -163,3 +236,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/composables/workflowsPage.ts b/cypress/composables/workflowsPage.ts new file mode 100644 index 0000000000..c7bcf39888 --- /dev/null +++ b/cypress/composables/workflowsPage.ts @@ -0,0 +1,15 @@ +/** + * Getters + */ + +export function getWorkflowsPageUrl() { + return '/home/workflows'; +} + +/** + * Actions + */ + +export function visitWorkflowsPage() { + cy.visit(getWorkflowsPageUrl()); +} 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/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..4ebce02d9d 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 @@ -113,6 +115,8 @@ describe('Data mapping', () => { }); it('maps expressions from json view', () => { + // ADO-3063 - followup to make this viewport global + cy.viewport('macbook-16'); cy.fixture('Test_workflow_3.json').then((data) => { cy.get('body').paste(JSON.stringify(data)); }); @@ -121,17 +125,17 @@ describe('Data mapping', () => { workflowPage.actions.openNode('Set'); ndv.actions.switchInputMode('JSON'); + ndv.getters.inputDataContainer().should('exist'); + ndv.getters .inputDataContainer() - .should('exist') .find('.json-data') .should( 'have.text', '[{"input": [{"count": 0,"with space": "!!","with.dot": "!!","with"quotes": "!!"}]},{"input": [{"count": 1}]}]', - ) - .find('span') - .contains('"count"') - .realMouseDown(); + ); + + ndv.getters.inputDataContainer().find('span').contains('"count"').realMouseDown(); ndv.actions.mapToParameter('value'); ndv.getters.inlineExpressionEditorInput().should('have.text', '{{ $json.input[0].count }}'); @@ -185,7 +189,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 60fbd7c419..ed901107ea 100644 --- a/cypress/e2e/16-form-trigger-node.cy.ts +++ b/cypress/e2e/16-form-trigger-node.cy.ts @@ -44,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(); @@ -56,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(); @@ -94,7 +90,6 @@ describe('n8n Form Trigger', () => { .children() .children() .first() - .clear() .type('Your test form was successfully submitted'); ndv.getters.backToCanvas().click(); diff --git a/cypress/e2e/16-webhook-node.cy.ts b/cypress/e2e/16-webhook-node.cy.ts index 3d6c1049a2..193ada0bcc 100644 --- a/cypress/e2e/16-webhook-node.cy.ts +++ b/cypress/e2e/16-webhook-node.cy.ts @@ -250,7 +250,7 @@ describe('Webhook Trigger node', () => { }); // add credentials workflowPage.getters.nodeCredentialsSelect().click(); - getVisibleSelect().find('li').last().click(); + workflowPage.getters.nodeCredentialsCreateOption().click(); credentialsModal.getters.credentialsEditModal().should('be.visible'); credentialsModal.actions.fillCredentialsForm(); @@ -293,7 +293,7 @@ describe('Webhook Trigger node', () => { }); // add credentials workflowPage.getters.nodeCredentialsSelect().click(); - getVisibleSelect().find('li').last().click(); + workflowPage.getters.nodeCredentialsCreateOption().click(); credentialsModal.getters.credentialsEditModal().should('be.visible'); credentialsModal.actions.fillCredentialsForm(); diff --git a/cypress/e2e/17-sharing.cy.ts b/cypress/e2e/17-sharing.cy.ts index 51b4a674d3..60eb474b07 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,8 +226,7 @@ describe('Sharing', { disableAutoLogin: true }, () => { .filter(':contains("Development")') .should('have.length', 1) .click(); - credentialsModal.getters.saveButton().click(); - credentialsModal.getters.saveButton().should('have.text', 'Saved'); + saveCredential(); credentialsModal.actions.close(); projects.getProjectTabWorkflows().click(); @@ -252,8 +252,7 @@ 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(); - credentialsModal.getters.saveButton().should('have.text', 'Saved'); + saveCredential(); credentialsModal.actions.close(); credentialsPage.getters @@ -298,10 +297,9 @@ describe('Credential Usage in Cross Shared Workflows', () => { workflowsPage.actions.createWorkflowFromCard(); workflowPage.actions.addNodeToCanvas(NOTION_NODE_NAME, true, true); - // Only the credential in this project (+ the 'Create new' option) should - // be in the dropdown + // Only the credential in this project should be in the dropdown workflowPage.getters.nodeCredentialsSelect().click(); - getVisibleSelect().find('li').should('have.length', 2); + getVisibleSelect().find('li').should('have.length', 1); }); it('should only show credentials in their personal project for members', () => { @@ -326,10 +324,9 @@ describe('Credential Usage in Cross Shared Workflows', () => { workflowsPage.actions.createWorkflowFromCard(); workflowPage.actions.addNodeToCanvas(NOTION_NODE_NAME, true, true); - // Only the own credential the shared one (+ the 'Create new' option) - // should be in the dropdown + // Only the own credential the shared one should be in the dropdown workflowPage.getters.nodeCredentialsSelect().click(); - getVisibleSelect().find('li').should('have.length', 3); + getVisibleSelect().find('li').should('have.length', 2); }); it('should only show credentials in their personal project for members if the workflow was shared with them', () => { @@ -353,13 +350,12 @@ 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) - // should be in the dropdown + // Only the own credential the shared one should be in the dropdown workflowPage.getters.nodeCredentialsSelect().click(); - getVisibleSelect().find('li').should('have.length', 2); + getVisibleSelect().find('li').should('have.length', 1); }); it("should show all credentials from all personal projects the workflow's been shared into for the global owner", () => { @@ -398,13 +394,12 @@ 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 - // should show up. + // Only the personal credentials of the workflow owner and the global owner should show up. workflowPage.getters.nodeCredentialsSelect().click(); - getVisibleSelect().find('li').should('have.length', 4); + getVisibleSelect().find('li').should('have.length', 3); }); it('should show all personal credentials if the global owner owns the workflow', () => { @@ -422,6 +417,6 @@ describe('Credential Usage in Cross Shared Workflows', () => { // Show all personal credentials workflowPage.getters.nodeCredentialsSelect().click(); - getVisibleSelect().find('li').should('have.have.length', 2); + getVisibleSelect().find('li').should('have.have.length', 1); }); }); 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/2-credentials.cy.ts b/cypress/e2e/2-credentials.cy.ts index 8ce3bc4080..260d5f63a0 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, @@ -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(); + workflowPage.getters.nodeCredentialsCreateOption().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); @@ -62,7 +79,7 @@ describe('Credentials', () => { workflowPage.getters.canvasNodes().last().click(); cy.get('body').type('{enter}'); workflowPage.getters.nodeCredentialsSelect().click(); - getVisibleSelect().find('li').last().click(); + workflowPage.getters.nodeCredentialsCreateOption().click(); credentialsModal.getters.credentialsEditModal().should('be.visible'); credentialsModal.getters.credentialAuthTypeRadioButtons().should('have.length', 2); credentialsModal.getters.credentialAuthTypeRadioButtons().first().click(); @@ -82,7 +99,7 @@ describe('Credentials', () => { cy.get('body').type('{enter}'); workflowPage.getters.nodeCredentialsSelect().click(); // Add oAuth credentials - getVisibleSelect().find('li').last().click(); + workflowPage.getters.nodeCredentialsCreateOption().click(); credentialsModal.getters.credentialsEditModal().should('be.visible'); credentialsModal.getters.credentialAuthTypeRadioButtons().should('have.length', 2); credentialsModal.getters.credentialAuthTypeRadioButtons().first().click(); @@ -90,14 +107,13 @@ describe('Credentials', () => { cy.get('.el-message-box').find('button').contains('Close').click(); workflowPage.getters.nodeCredentialsSelect().click(); // Add Service account credentials - getVisibleSelect().find('li').last().click(); + workflowPage.getters.nodeCredentialsCreateOption().click(); credentialsModal.getters.credentialsEditModal().should('be.visible'); credentialsModal.getters.credentialAuthTypeRadioButtons().should('have.length', 2); credentialsModal.getters.credentialAuthTypeRadioButtons().last().click(); credentialsModal.actions.fillCredentialsForm(); - // Both (+ the 'Create new' option) should be in the dropdown workflowPage.getters.nodeCredentialsSelect().click(); - getVisibleSelect().find('li').should('have.length.greaterThan', 2); + getVisibleSelect().find('li').should('have.length', 3); }); it('should correctly render required and optional credentials', () => { @@ -113,13 +129,13 @@ describe('Credentials', () => { workflowPage.getters.nodeCredentialsSelect().should('have.length', 2); workflowPage.getters.nodeCredentialsSelect().first().click(); - getVisibleSelect().find('li').contains('Create New Credential').click(); + workflowPage.getters.nodeCredentialsCreateOption().first().click(); // This one should show auth type selector credentialsModal.getters.credentialAuthTypeRadioButtons().should('have.length', 2); cy.get('body').type('{esc}'); workflowPage.getters.nodeCredentialsSelect().last().click(); - getVisibleSelect().find('li').contains('Create New Credential').click(); + workflowPage.getters.nodeCredentialsCreateOption().last().click(); // This one should not show auth type selector credentialsModal.getters.credentialsAuthTypeSelector().should('not.exist'); }); @@ -131,7 +147,7 @@ describe('Credentials', () => { workflowPage.getters.canvasNodes().last().click(); cy.get('body').type('{enter}'); workflowPage.getters.nodeCredentialsSelect().click(); - getVisibleSelect().find('li').last().click(); + workflowPage.getters.nodeCredentialsCreateOption().click(); credentialsModal.getters.credentialsAuthTypeSelector().should('not.exist'); credentialsModal.actions.fillCredentialsForm(); workflowPage.getters @@ -147,7 +163,7 @@ describe('Credentials', () => { workflowPage.getters.canvasNodes().last().click(); cy.get('body').type('{enter}'); workflowPage.getters.nodeCredentialsSelect().click(); - getVisibleSelect().find('li').last().click(); + workflowPage.getters.nodeCredentialsCreateOption().click(); credentialsModal.actions.fillCredentialsForm(); workflowPage.getters .nodeCredentialsSelect() @@ -172,13 +188,13 @@ describe('Credentials', () => { workflowPage.getters.canvasNodes().last().click(); cy.get('body').type('{enter}'); workflowPage.getters.nodeCredentialsSelect().click(); - getVisibleSelect().find('li').last().click(); + workflowPage.getters.nodeCredentialsCreateOption().click(); credentialsModal.actions.fillCredentialsForm(); workflowPage.getters.nodeCredentialsEditButton().click(); credentialsModal.getters.credentialsEditModal().should('be.visible'); credentialsModal.getters.name().click(); credentialsModal.actions.renameCredential(NEW_CREDENTIAL_NAME); - credentialsModal.getters.saveButton().click(); + saveCredential(); credentialsModal.getters.closeButton().click(); workflowPage.getters .nodeCredentialsSelect() @@ -196,7 +212,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() @@ -215,13 +231,13 @@ describe('Credentials', () => { cy.getByTestId('credential-select').click(); cy.contains('Adalo API').click(); workflowPage.getters.nodeCredentialsSelect().click(); - getVisibleSelect().find('li').last().click(); + workflowPage.getters.nodeCredentialsCreateOption().click(); credentialsModal.actions.fillCredentialsForm(); workflowPage.getters.nodeCredentialsEditButton().click(); credentialsModal.getters.credentialsEditModal().should('be.visible'); credentialsModal.getters.name().click(); credentialsModal.actions.renameCredential(NEW_CREDENTIAL_NAME); - credentialsModal.getters.saveButton().click(); + saveCredential(); credentialsModal.getters.closeButton().click(); workflowPage.getters .nodeCredentialsSelect() @@ -229,6 +245,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); @@ -245,7 +295,7 @@ describe('Credentials', () => { workflowPage.getters.nodeCredentialsSelect().should('exist'); workflowPage.getters.nodeCredentialsSelect().click(); - getVisibleSelect().find('li').last().click(); + workflowPage.getters.nodeCredentialsCreateOption().click(); credentialsModal.actions.fillCredentialsForm(); workflowPage.getters .nodeCredentialsSelect() @@ -274,7 +324,7 @@ describe('Credentials', () => { workflowPage.actions.addNodeToCanvas('Slack', true, true, 'Get a channel'); workflowPage.getters.nodeCredentialsSelect().should('exist'); workflowPage.getters.nodeCredentialsSelect().click(); - getVisibleSelect().find('li').last().click(); + workflowPage.getters.nodeCredentialsCreateOption().click(); credentialsModal.getters.credentialAuthTypeRadioButtons().first().click(); nodeDetailsView.getters.copyInput().should('not.exist'); }); @@ -292,7 +342,8 @@ describe('Credentials', () => { credentialsModal.getters.connectionParameter('Internal Integration Secret').type('1234567890'); credentialsModal.actions.setName('My awesome Notion account'); - credentialsModal.getters.saveButton().click({ force: true }); + getCredentialSaveButton().click(); + errorToast().should('have.length', 1); errorToast().should('be.visible'); diff --git a/cypress/e2e/21-community-nodes.cy.ts b/cypress/e2e/21-community-nodes.cy.ts index 17f82ec573..283c08d557 100644 --- a/cypress/e2e/21-community-nodes.cy.ts +++ b/cypress/e2e/21-community-nodes.cy.ts @@ -89,7 +89,7 @@ describe('Community and custom nodes in canvas', () => { workflowPage.actions.addNodeToCanvas('Manual'); workflowPage.actions.addNodeToCanvas('E2E Node with native n8n credential', true, true); workflowPage.getters.nodeCredentialsLabel().click(); - cy.contains('Create New Credential').click(); + workflowPage.getters.nodeCredentialsCreateOption().click(); credentialsModal.getters.editCredentialModal().should('be.visible'); credentialsModal.getters.editCredentialModal().should('contain.text', 'Notion API'); }); @@ -98,7 +98,7 @@ describe('Community and custom nodes in canvas', () => { workflowPage.actions.addNodeToCanvas('Manual'); workflowPage.actions.addNodeToCanvas('E2E Node with custom credential', true, true); workflowPage.getters.nodeCredentialsLabel().click(); - cy.contains('Create New Credential').click(); + workflowPage.getters.nodeCredentialsCreateOption().click(); credentialsModal.getters.editCredentialModal().should('be.visible'); credentialsModal.getters.editCredentialModal().should('contain.text', 'Custom E2E Credential'); }); 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/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-cloud.cy.ts b/cypress/e2e/27-cloud.cy.ts index e9b814597d..bcf9750ecb 100644 --- a/cypress/e2e/27-cloud.cy.ts +++ b/cypress/e2e/27-cloud.cy.ts @@ -1,15 +1,17 @@ import planData from '../fixtures/Plan_data_opt_in_trial.json'; import { - BannerStack, MainSidebar, WorkflowPage, visitPublicApiPage, getPublicApiUpgradeCTA, + WorkflowsPage, } from '../pages'; +const NUMBER_OF_AI_CREDITS = 100; + const mainSidebar = new MainSidebar(); -const bannerStack = new BannerStack(); const workflowPage = new WorkflowPage(); +const workflowsPage = new WorkflowsPage(); describe('Cloud', () => { before(() => { @@ -22,6 +24,10 @@ describe('Cloud', () => { cy.overrideSettings({ deployment: { type: 'cloud' }, n8nMetadata: { userId: '1' }, + aiCredits: { + enabled: true, + credits: NUMBER_OF_AI_CREDITS, + }, }); cy.intercept('GET', '/rest/admin/cloud-plan', planData).as('getPlanData'); cy.intercept('GET', '/rest/cloud/proxy/user/me', {}).as('getCloudUserInfo'); @@ -40,11 +46,11 @@ describe('Cloud', () => { it('should render trial banner for opt-in cloud user', () => { visitWorkflowPage(); - bannerStack.getters.banner().should('be.visible'); + cy.getByTestId('banner-stack').should('be.visible'); mainSidebar.actions.signout(); - bannerStack.getters.banner().should('not.be.visible'); + cy.getByTestId('banner-stack').should('not.be.visible'); }); }); @@ -64,4 +70,66 @@ describe('Cloud', () => { getPublicApiUpgradeCTA().should('be.visible'); }); }); + + describe('Easy AI workflow experiment', () => { + it('should not show option to take you to the easy AI workflow if experiment is control', () => { + window.localStorage.setItem( + 'N8N_EXPERIMENT_OVERRIDES', + JSON.stringify({ '026_easy_ai_workflow': 'control' }), + ); + + cy.visit(workflowsPage.url); + + cy.getByTestId('easy-ai-workflow-card').should('not.exist'); + }); + + it('should show option to take you to the easy AI workflow if experiment is variant', () => { + window.localStorage.setItem( + 'N8N_EXPERIMENT_OVERRIDES', + JSON.stringify({ '026_easy_ai_workflow': 'variant' }), + ); + + cy.visit(workflowsPage.url); + + cy.getByTestId('easy-ai-workflow-card').should('to.exist'); + }); + + it('should show default instructions if free AI credits experiment is control', () => { + window.localStorage.setItem( + 'N8N_EXPERIMENT_OVERRIDES', + JSON.stringify({ '027_free_openai_calls': 'control', '026_easy_ai_workflow': 'variant' }), + ); + + cy.visit(workflowsPage.url); + + cy.getByTestId('easy-ai-workflow-card').click(); + + workflowPage.getters + .stickies() + .eq(0) + .should(($el) => { + expect($el).contains.text('Set up your OpenAI credentials in the OpenAI Model node'); + }); + }); + + it('should show updated instructions if free AI credits experiment is variant', () => { + window.localStorage.setItem( + 'N8N_EXPERIMENT_OVERRIDES', + JSON.stringify({ '027_free_openai_calls': 'variant', '026_easy_ai_workflow': 'variant' }), + ); + + cy.visit(workflowsPage.url); + + cy.getByTestId('easy-ai-workflow-card').click(); + + workflowPage.getters + .stickies() + .eq(0) + .should(($el) => { + expect($el).contains.text( + `Claim your free ${NUMBER_OF_AI_CREDITS} OpenAI calls in the OpenAI model node`, + ); + }); + }); + }); }); 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 b5159951a7..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,7 +131,18 @@ 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.actions.unPinData(); 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-editor-after-route-changes.cy.ts b/cypress/e2e/30-editor-after-route-changes.cy.ts index f0381a32a2..307c4a9537 100644 --- a/cypress/e2e/30-editor-after-route-changes.cy.ts +++ b/cypress/e2e/30-editor-after-route-changes.cy.ts @@ -1,18 +1,14 @@ +import { getWorkflowHistoryCloseButton } from '../composables/workflow'; import { CODE_NODE_NAME, EDIT_FIELDS_SET_NODE_NAME, IF_NODE_NAME, SCHEDULE_TRIGGER_NODE_NAME, } from '../constants'; -import { - WorkflowExecutionsTab, - WorkflowPage as WorkflowPageClass, - WorkflowHistoryPage, -} from '../pages'; +import { WorkflowExecutionsTab, WorkflowPage as WorkflowPageClass } from '../pages'; const workflowPage = new WorkflowPageClass(); const executionsTab = new WorkflowExecutionsTab(); -const workflowHistoryPage = new WorkflowHistoryPage(); const createNewWorkflowAndActivate = () => { workflowPage.actions.visit(); @@ -92,7 +88,7 @@ const switchBetweenEditorAndHistory = () => { cy.wait(['@getVersion']); cy.intercept('GET', '/rest/workflows/*').as('workflowGet'); - workflowHistoryPage.getters.workflowHistoryCloseButton().click(); + getWorkflowHistoryCloseButton().click(); cy.wait(['@workflowGet']); cy.wait(1000); @@ -168,7 +164,7 @@ describe('Editor actions should work', () => { cy.wait(['@getVersion']); cy.intercept('GET', '/rest/workflows/*').as('workflowGet'); - workflowHistoryPage.getters.workflowHistoryCloseButton().click(); + getWorkflowHistoryCloseButton().click(); cy.wait(['@workflowGet']); cy.wait(1000); diff --git a/cypress/e2e/30-langchain.cy.ts b/cypress/e2e/30-langchain.cy.ts index 0deec76e9f..b6f1b56eed 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,7 +26,9 @@ import { clickCreateNewCredential, clickExecuteNode, clickGetBackToCanvas, - toggleParameterCheckboxInputByName, + getRunDataInfoCallout, + getOutputPanelTable, + checkParameterCheckboxInputByName, } from '../composables/ndv'; import { addLanguageModelNodeToParent, @@ -44,6 +45,7 @@ import { openNode, getConnectionBySourceAndTarget, } from '../composables/workflow'; +import { NDV, WorkflowPage } from '../pages'; import { createMockNodeExecutionData, runMockWorkflowExecution } from '../utils'; describe('Langchain Integration', () => { @@ -95,7 +97,7 @@ describe('Langchain Integration', () => { it('should add nodes to all Agent node input types', () => { addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME, true); addNodeToCanvas(AGENT_NODE_NAME, true, true); - toggleParameterCheckboxInputByName('hasOutputParser'); + checkParameterCheckboxInputByName('hasOutputParser'); clickGetBackToCanvas(); addLanguageModelNodeToParent( @@ -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..8d372acac0 100644 --- a/cypress/e2e/34-template-credentials-setup.cy.ts +++ b/cypress/e2e/34-template-credentials-setup.cy.ts @@ -3,6 +3,7 @@ import * as formStep from '../composables/setup-template-form-step'; import { getSetupWorkflowCredentialsButton } from '../composables/setup-workflow-credentials-button'; import TestTemplate1 from '../fixtures/Test_Template_1.json'; import TestTemplate2 from '../fixtures/Test_Template_2.json'; +import { clearNotifications } from '../pages/notifications'; import { clickUseWorkflowButtonByTitle, visitTemplateCollectionPage, @@ -56,10 +57,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 +68,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 @@ -111,16 +112,19 @@ describe('Template credentials setup', () => { templateCredentialsSetupPage.fillInDummyCredentialsForAppWithConfirm('X (Formerly Twitter)'); templateCredentialsSetupPage.fillInDummyCredentialsForApp('Telegram'); + clearNotifications(); + templateCredentialsSetupPage.finishCredentialSetup(); workflowPage.getters.canvasNodes().should('have.length', 3); + cy.grantBrowserPermissions('clipboardReadWrite', 'clipboardSanitizedWrite'); + // Focus the canvas so the copy to clipboard works workflowPage.getters.canvasNodes().eq(0).realClick(); workflowPage.actions.hitSelectAll(); workflowPage.actions.hitCopy(); - cy.grantBrowserPermissions('clipboardReadWrite', 'clipboardSanitizedWrite'); // Check workflow JSON by copying it to clipboard cy.readClipboard().then((workflowJSON) => { const workflow = JSON.parse(workflowJSON); @@ -154,6 +158,8 @@ describe('Template credentials setup', () => { templateCredentialsSetupPage.fillInDummyCredentialsForApp('Email (IMAP)'); templateCredentialsSetupPage.fillInDummyCredentialsForApp('Nextcloud'); + clearNotifications(); + templateCredentialsSetupPage.finishCredentialSetup(); workflowPage.getters.canvasNodes().should('have.length', 3); @@ -176,6 +182,8 @@ describe('Template credentials setup', () => { templateCredentialsSetupPage.visitTemplateCredentialSetupPage(testTemplate.id); templateCredentialsSetupPage.fillInDummyCredentialsForApp('Shopify'); + clearNotifications(); + templateCredentialsSetupPage.finishCredentialSetup(); getSetupWorkflowCredentialsButton().should('be.visible'); @@ -192,6 +200,8 @@ describe('Template credentials setup', () => { templateCredentialsSetupPage.fillInDummyCredentialsForAppWithConfirm('X (Formerly Twitter)'); templateCredentialsSetupPage.fillInDummyCredentialsForApp('Telegram'); + clearNotifications(); + setupCredsModal.closeModalFromContinueButton(); setupCredsModal.getWorkflowCredentialsModal().should('not.exist'); diff --git a/cypress/e2e/35-admin-user-smoke-test.cy.ts b/cypress/e2e/35-admin-user-smoke-test.cy.ts index c8585118e7..6bb31ae1c2 100644 --- a/cypress/e2e/35-admin-user-smoke-test.cy.ts +++ b/cypress/e2e/35-admin-user-smoke-test.cy.ts @@ -1,22 +1,20 @@ -import { SettingsPage } from '../pages/settings'; - -const settingsPage = new SettingsPage(); +const url = '/settings'; describe('Admin user', { disableAutoLogin: true }, () => { it('should see same Settings sub menu items as instance owner', () => { cy.signinAsOwner(); - cy.visit(settingsPage.url); + cy.visit(url); let ownerMenuItems = 0; - settingsPage.getters.menuItems().then(($el) => { + cy.getByTestId('menu-item').then(($el) => { ownerMenuItems = $el.length; }); cy.signout(); cy.signinAsAdmin(); - cy.visit(settingsPage.url); + cy.visit(url); - settingsPage.getters.menuItems().should('have.length', ownerMenuItems); + cy.getByTestId('menu-item').should('have.length', ownerMenuItems); }); }); diff --git a/cypress/e2e/39-projects.cy.ts b/cypress/e2e/39-projects.cy.ts index 138f67838a..197d585256 100644 --- a/cypress/e2e/39-projects.cy.ts +++ b/cypress/e2e/39-projects.cy.ts @@ -15,7 +15,7 @@ import { NDV, MainSidebar, } from '../pages'; -import { clearNotifications } from '../pages/notifications'; +import { clearNotifications, successToast } from '../pages/notifications'; import { getVisibleDropdown, getVisibleModalOverlay, getVisibleSelect } from '../utils'; const workflowsPage = new WorkflowsPage(); @@ -51,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'); @@ -101,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(); @@ -176,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(); @@ -186,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'); @@ -222,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(); @@ -230,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); @@ -367,7 +367,7 @@ describe('Projects', { disableAutoLogin: true }, () => { workflowPage.getters.nodeCredentialsSelect().first().click(); getVisibleSelect() .find('li') - .should('have.length', 2) + .should('have.length', 1) .first() .should('contain.text', 'Notion account project 1'); ndv.getters.backToCanvas().click(); @@ -382,7 +382,7 @@ describe('Projects', { disableAutoLogin: true }, () => { workflowPage.getters.nodeCredentialsSelect().first().click(); getVisibleSelect() .find('li') - .should('have.length', 2) + .should('have.length', 1) .first() .should('contain.text', 'Notion account project 1'); ndv.getters.backToCanvas().click(); @@ -396,7 +396,7 @@ describe('Projects', { disableAutoLogin: true }, () => { workflowPage.getters.nodeCredentialsSelect().first().click(); getVisibleSelect() .find('li') - .should('have.length', 2) + .should('have.length', 1) .first() .should('contain.text', 'Notion account project 2'); ndv.getters.backToCanvas().click(); @@ -407,7 +407,7 @@ describe('Projects', { disableAutoLogin: true }, () => { workflowPage.getters.nodeCredentialsSelect().first().click(); getVisibleSelect() .find('li') - .should('have.length', 2) + .should('have.length', 1) .first() .should('contain.text', 'Notion account project 2'); ndv.getters.backToCanvas().click(); @@ -425,7 +425,7 @@ describe('Projects', { disableAutoLogin: true }, () => { workflowPage.getters.nodeCredentialsSelect().first().click(); getVisibleSelect() .find('li') - .should('have.length', 2) + .should('have.length', 1) .first() .should('contain.text', 'Notion account personal project'); ndv.getters.backToCanvas().click(); @@ -436,7 +436,7 @@ describe('Projects', { disableAutoLogin: true }, () => { workflowPage.getters.nodeCredentialsSelect().first().click(); getVisibleSelect() .find('li') - .should('have.length', 2) + .should('have.length', 1) .first() .should('contain.text', 'Notion account personal project'); }); @@ -498,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() @@ -506,7 +506,7 @@ 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 @@ -524,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() @@ -532,7 +532,7 @@ 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(); @@ -544,7 +544,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() @@ -553,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 @@ -569,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() @@ -578,7 +578,7 @@ 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() @@ -596,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() @@ -604,7 +604,7 @@ 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'); @@ -619,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() @@ -627,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 @@ -641,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() @@ -649,7 +649,7 @@ 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(); @@ -666,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() @@ -674,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(); @@ -697,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(); @@ -723,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() @@ -731,7 +729,7 @@ 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() @@ -749,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(); @@ -785,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') @@ -831,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/4-node-creator.cy.ts b/cypress/e2e/4-node-creator.cy.ts index a2cd5968d1..b70e121fd0 100644 --- a/cypress/e2e/4-node-creator.cy.ts +++ b/cypress/e2e/4-node-creator.cy.ts @@ -1,10 +1,12 @@ +import { clickGetBackToCanvas } from '../composables/ndv'; import { addNodeToCanvas, addRetrieverNodeToParent, addVectorStoreNodeToParent, + addVectorStoreToolToParent, getNodeCreatorItems, } from '../composables/workflow'; -import { IF_NODE_NAME } from '../constants'; +import { AGENT_NODE_NAME, IF_NODE_NAME, MANUAL_CHAT_TRIGGER_NODE_NAME } from '../constants'; import { NodeCreator } from '../pages/features/node-creator'; import { NDV } from '../pages/ndv'; import { WorkflowPage as WorkflowPageClass } from '../pages/workflow'; @@ -74,11 +76,21 @@ describe('Node Creator', () => { nodeCreatorFeature.getters.canvasAddButton().click(); WorkflowPage.actions.addNodeToCanvas('Manual', false); - nodeCreatorFeature.getters.canvasAddButton().should('not.be.visible'); - nodeCreatorFeature.getters.nodeCreator().should('not.exist'); + cy.ifCanvasVersion( + () => { + nodeCreatorFeature.getters.canvasAddButton().should('not.be.visible'); + nodeCreatorFeature.getters.nodeCreator().should('not.exist'); + // TODO: Replace once we have canvas feature utils + cy.get('div').contains('Add first step').should('be.hidden'); + }, + () => { + nodeCreatorFeature.getters.canvasAddButton().should('not.exist'); + nodeCreatorFeature.getters.nodeCreator().should('not.exist'); + // TODO: Replace once we have canvas feature utils + cy.get('div').contains('Add first step').should('not.exist'); + }, + ); - // TODO: Replace once we have canvas feature utils - cy.get('div').contains('Add first step').should('be.hidden'); nodeCreatorFeature.actions.openNodeCreator(); nodeCreatorFeature.getters.nodeCreator().contains('What happens next?').should('be.visible'); @@ -344,7 +356,15 @@ describe('Node Creator', () => { it('should correctly append a No Op node when Loop Over Items node is added (from connection)', () => { WorkflowPage.actions.addNodeToCanvas('Manual'); - cy.get('.plus-endpoint').should('be.visible').click(); + + cy.ifCanvasVersion( + () => { + cy.get('.plus-endpoint').click(); + }, + () => { + cy.getByTestId('canvas-handle-plus').click(); + }, + ); nodeCreatorFeature.getters.searchBar().find('input').type('Loop Over Items'); nodeCreatorFeature.getters.getCreatorItem('Loop Over Items').click(); @@ -515,7 +535,7 @@ describe('Node Creator', () => { const actions = [ 'Get ranked documents from vector store', 'Add documents to vector store', - 'Retrieve documents for AI processing', + 'Retrieve documents for Chain/Tool as Vector Store', ]; nodeCreatorFeature.actions.openNodeCreator(); @@ -529,14 +549,14 @@ describe('Node Creator', () => { vectorStores.each((_i, vectorStore) => { nodeCreatorFeature.getters.getCreatorItem(vectorStore).click(); actions.forEach((action) => { - nodeCreatorFeature.getters.getCreatorItem(action).should('be.visible'); + nodeCreatorFeature.getters.getCreatorItem(action).should('be.visible').realHover(); }); cy.realPress('ArrowLeft'); }); }); }); - it('should add node directly for sub-connection', () => { + it('should add node directly for sub-connection as vector store', () => { addNodeToCanvas('Question and Answer Chain', true); addRetrieverNodeToParent('Vector Store Retriever', 'Question and Answer Chain'); cy.realPress('Escape'); @@ -544,4 +564,12 @@ describe('Node Creator', () => { cy.realPress('Escape'); WorkflowPage.getters.canvasNodes().should('have.length', 4); }); + + it('should add node directly for sub-connection as tool', () => { + addNodeToCanvas(MANUAL_CHAT_TRIGGER_NODE_NAME, true); + addNodeToCanvas(AGENT_NODE_NAME, true, true); + clickGetBackToCanvas(); + + addVectorStoreToolToParent('In-Memory Vector Store', AGENT_NODE_NAME); + }); }); 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..157c656b46 100644 --- a/cypress/e2e/45-ai-assistant.cy.ts +++ b/cypress/e2e/45-ai-assistant.cy.ts @@ -4,13 +4,14 @@ 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 { getVisibleSelect } from '../utils'; +import { NodeCreator } from '../pages/features/node-creator'; const wf = new WorkflowPage(); 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 +223,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 +329,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', () => { @@ -370,7 +433,7 @@ describe('AI Assistant Credential Help', () => { wf.actions.addNodeToCanvas('Slack', true, true, 'Get a channel'); wf.getters.nodeCredentialsSelect().should('exist'); wf.getters.nodeCredentialsSelect().click(); - getVisibleSelect().find('li').last().click(); + wf.getters.nodeCredentialsCreateOption().click(); credentialsModal.getters.credentialAuthTypeRadioButtons().first().click(); ndv.getters.copyInput().should('not.exist'); credentialsModal.getters.oauthConnectButton().should('have.length', 1); @@ -403,7 +466,7 @@ describe('AI Assistant Credential Help', () => { wf.actions.addNodeToCanvas('Microsoft Outlook', true, true, 'Get a calendar'); wf.getters.nodeCredentialsSelect().should('exist'); wf.getters.nodeCredentialsSelect().click(); - getVisibleSelect().find('li').last().click(); + wf.getters.nodeCredentialsCreateOption().click(); ndv.getters.copyInput().should('not.exist'); credentialsModal.getters.oauthConnectButton().should('have.length', 1); credentialsModal.getters.credentialInputs().should('have.length', 1); @@ -493,6 +556,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..38de780490 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('contain.text', 'Create a'); // Due to some inconsistency we're sometimes in a project and sometimes not, this covers both cases + + 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..725b6b32c4 --- /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', 'View 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', 'View 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', 'View 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', + 'View 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..aababf4cb6 --- /dev/null +++ b/cypress/e2e/48-subworkflow-inputs.cy.ts @@ -0,0 +1,242 @@ +import { + addItemToFixedCollection, + assertNodeOutputHintExists, + clickExecuteNode, + clickGetBackToCanvas, + getExecuteNodeButton, + getOutputTableHeaders, + getParameterInputByName, + populateFixedCollection, + selectResourceLocatorItem, + typeIntoFixedCollectionItem, + clickWorkflowCardContent, + assertOutputTableContent, + populateMapperFields, + getNodeRunInfoStale, + assertNodeOutputErrorMessageExists, + checkParameterCheckboxInputByName, + uncheckParameterCheckboxInputByName, +} from '../composables/ndv'; +import { + clickExecuteWorkflowButton, + clickZoomToFit, + navigateToNewWorkflowPage, + openNode, + pasteWorkflow, + saveWorkflowOnButtonClick, +} from '../composables/workflow'; +import { visitWorkflowsPage } from '../composables/workflowsPage'; +import SUB_WORKFLOW_INPUTS from '../fixtures/Test_Subworkflow-Inputs.json'; +import { errorToast, successToast } from '../pages/notifications'; +import { getVisiblePopper } from '../utils'; + +const DEFAULT_WORKFLOW_NAME = 'My workflow'; +const DEFAULT_SUBWORKFLOW_NAME_1 = 'My Sub-Workflow 1'; +const DEFAULT_SUBWORKFLOW_NAME_2 = 'My Sub-Workflow 2'; + +const EXAMPLE_FIELDS = [ + ['aNumber', 'Number'], + ['aString', 'String'], + ['aArray', 'Array'], + ['aObject', 'Object'], + ['aAny', 'Allow Any Type'], + // bool last because it's a switch instead of a normal inputField so we'll skip it for some cases + ['aBool', 'Boolean'], +] as const; + +type TypeField = 'Allow Any Type' | 'String' | 'Number' | 'Boolean' | 'Array' | 'Object'; + +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); + }); + }); + selectResourceLocatorItem('workflowId', 0, 'Create a'); + // ************************** + // NAVIGATE TO CHILD WORKFLOW + // ************************** + + openNode('Workflow Input Trigger'); + }); + + it('works with type-checked values', () => { + populateFixedCollection(EXAMPLE_FIELDS, 'workflowInputs', 1); + + validateAndReturnToParent( + DEFAULT_SUBWORKFLOW_NAME_1, + 1, + EXAMPLE_FIELDS.map((f) => f[0]), + ); + + const values = [ + '-1', // number fields don't support `=` switch to expression, so let's test the Fixed case with it + ...EXAMPLE_FIELDS.slice(1).map((x) => `={{}{{} $json.a${x[0]}`), // the `}}` at the end are added automatically + ]; + + // this matches with the pinned data provided in the fixture + populateMapperFields(values.map((x, i) => [EXAMPLE_FIELDS[i][0], x])); + + clickExecuteNode(); + + const expected = [ + ['-1', 'A String', '0:11:true2:3', 'aKey:-1', '[empty object]', 'false'], + ['-1', 'Another String', '[empty array]', 'aDifferentKey:-1', '[empty array]', 'false'], + ]; + assertOutputTableContent(expected); + + // Test the type-checking options + populateMapperFields([['aString', '{selectAll}{backspace}{{}{{} 5']]); + + getNodeRunInfoStale().should('exist'); + clickExecuteNode(); + + assertNodeOutputErrorMessageExists(); + + // attemptToConvertTypes enabled + checkParameterCheckboxInputByName('attemptToConvertTypes'); + + getNodeRunInfoStale().should('exist'); + clickExecuteNode(); + + const expected2 = [ + ['-1', '5', '0:11:true2:3', 'aKey:-1', '[empty object]', 'false'], + ['-1', '5', '[empty array]', 'aDifferentKey:-1', '[empty array]', 'false'], + ]; + + assertOutputTableContent(expected2); + + // disabled again + uncheckParameterCheckboxInputByName('attemptToConvertTypes'); + + getNodeRunInfoStale().should('exist'); + clickExecuteNode(); + + assertNodeOutputErrorMessageExists(); + }); + + it('works with Fields input source, and can then be changed to JSON input source', () => { + assertNodeOutputHintExists(); + + populateFixedCollection(EXAMPLE_FIELDS, 'workflowInputs', 1); + + validateAndReturnToParent( + DEFAULT_SUBWORKFLOW_NAME_1, + 1, + EXAMPLE_FIELDS.map((f) => f[0]), + ); + + cy.window().then((win) => { + cy.stub(win, 'open').callsFake((url) => { + cy.visit(url); + }); + }); + selectResourceLocatorItem('workflowId', 0, 'Create a'); + + openNode('Workflow Input Trigger'); + + getParameterInputByName('inputSource').click(); + + getVisiblePopper() + .getByTestId('parameter-input') + .eq(0) + .type('Using JSON Example{downArrow}{enter}'); + + const exampleJson = + '{{}' + EXAMPLE_FIELDS.map((x) => `"${x[0]}": ${makeExample(x[1])}`).join(',') + '}'; + getParameterInputByName('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? + clickExecuteNode(); + + validateAndReturnToParent( + DEFAULT_SUBWORKFLOW_NAME_2, + 2, + EXAMPLE_FIELDS.map((f) => f[0]), + ); + + assertOutputTableContent([ + ['[null]', '[null]', '[null]', '[null]', '[null]', 'false'], + ['[null]', '[null]', '[null]', '[null]', '[null]', 'false'], + ]); + + clickExecuteNode(); + }); + + it('should show node issue when no fields are defined in manual mode', () => { + getExecuteNodeButton().should('be.disabled'); + clickGetBackToCanvas(); + // Executing the workflow should show an error toast + clickExecuteWorkflowButton(); + errorToast().should('contain', 'The workflow has issues'); + openNode('Workflow Input Trigger'); + // Add a field to the workflowInputs fixedCollection + addItemToFixedCollection('workflowInputs'); + typeIntoFixedCollectionItem('workflowInputs', 0, 'test'); + // Executing the workflow should not show error now + clickGetBackToCanvas(); + clickExecuteWorkflowButton(); + successToast().should('contain', 'Workflow executed successfully'); + }); +}); + +// 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 the outputPanel matches our changes +function validateAndReturnToParent(targetChild: string, offset: number, fields: string[]) { + clickExecuteNode(); + + // + 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(); + + visitWorkflowsPage(); + + clickWorkflowCardContent(DEFAULT_WORKFLOW_NAME); + + 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 + selectResourceLocatorItem('workflowId', offset, targetChild); + + clickExecuteNode(); + + getOutputTableHeaders().should('have.length', fields.length + 1); + for (const [i, name] of fields.entries()) { + getOutputTableHeaders().eq(i).should('have.text', name); + } +} + +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'; + } +} diff --git a/cypress/e2e/5-ndv.cy.ts b/cypress/e2e/5-ndv.cy.ts index f2ccccb6ab..d9b7ecf2d1 100644 --- a/cypress/e2e/5-ndv.cy.ts +++ b/cypress/e2e/5-ndv.cy.ts @@ -1,6 +1,10 @@ import { setCredentialValues } from '../composables/modals/credential-modal'; -import { clickCreateNewCredential } from '../composables/ndv'; -import { MANUAL_TRIGGER_NODE_DISPLAY_NAME, NOTION_NODE_NAME } from '../constants'; +import { clickCreateNewCredential, setParameterSelectByContent } from '../composables/ndv'; +import { + EDIT_FIELDS_SET_NODE_NAME, + MANUAL_TRIGGER_NODE_DISPLAY_NAME, + NOTION_NODE_NAME, +} from '../constants'; import { NDV, WorkflowPage } from '../pages'; import { NodeCreator } from '../pages/features/node-creator'; @@ -65,26 +69,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 +95,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); @@ -125,7 +110,10 @@ describe('NDV', () => { ndv.actions.execute(); ndv.getters .nodeRunErrorMessage() - .should('have.text', 'Info for expression missing from previous node'); + .should( + 'have.text', + "Using the item method doesn't work with pinned data in this scenario. Please unpin 'Break pairedItem chain' and try again.", + ); ndv.getters .nodeRunErrorDescription() .should( @@ -204,7 +192,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() @@ -213,9 +201,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)', @@ -245,8 +235,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); }); }); @@ -376,15 +366,71 @@ describe('NDV', () => { ndv.getters.nodeExecuteButton().should('be.visible'); }); - it('should allow editing code in fullscreen in the Code node', () => { + it('should allow editing code in fullscreen in the code editors', () => { + // Code (JavaScript) workflowPage.actions.addInitialNodeToCanvas('Code', { keepNdvOpen: true }); ndv.actions.openCodeEditorFullscreen(); ndv.getters.codeEditorFullscreen().type('{selectall}').type('{backspace}').type('foo()'); ndv.getters.codeEditorFullscreen().should('contain.text', 'foo()'); - cy.wait(200); + cy.wait(200); // allow change to emit before closing modal ndv.getters.codeEditorDialog().find('.el-dialog__close').click(); ndv.getters.parameterInput('jsCode').get('.cm-content').should('contain.text', 'foo()'); + ndv.actions.close(); + + // SQL + workflowPage.actions.addNodeToCanvas('Postgres', true, true, 'Execute a SQL query'); + ndv.actions.openCodeEditorFullscreen(); + + ndv.getters + .codeEditorFullscreen() + .type('{selectall}') + .type('{backspace}') + .type('SELECT * FROM workflows'); + ndv.getters.codeEditorFullscreen().should('contain.text', 'SELECT * FROM workflows'); + cy.wait(200); + ndv.getters.codeEditorDialog().find('.el-dialog__close').click(); + ndv.getters + .parameterInput('query') + .get('.cm-content') + .should('contain.text', 'SELECT * FROM workflows'); + ndv.actions.close(); + + // HTML + workflowPage.actions.addNodeToCanvas('HTML', true, true, 'Generate HTML template'); + ndv.actions.openCodeEditorFullscreen(); + + ndv.getters + .codeEditorFullscreen() + .type('{selectall}') + .type('{backspace}') + .type('
Hello World'); + ndv.getters.codeEditorFullscreen().should('contain.text', '
Hello World
'); + cy.wait(200); + + ndv.getters.codeEditorDialog().find('.el-dialog__close').click(); + ndv.getters + .parameterInput('html') + .get('.cm-content') + .should('contain.text', '
Hello World
'); + ndv.actions.close(); + + // JSON + workflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME, true, true); + setParameterSelectByContent('mode', 'JSON'); + ndv.actions.openCodeEditorFullscreen(); + ndv.getters + .codeEditorFullscreen() + .type('{selectall}') + .type('{backspace}') + .type('{ "key": "value" }', { parseSpecialCharSequences: false }); + ndv.getters.codeEditorFullscreen().should('contain.text', '{ "key": "value" }'); + cy.wait(200); + ndv.getters.codeEditorDialog().find('.el-dialog__close').click(); + ndv.getters + .parameterInput('jsonOutput') + .get('.cm-content') + .should('contain.text', '{ "key": "value" }'); }); it('should not retrieve remote options when a parameter value changes', () => { @@ -407,8 +453,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'); @@ -419,6 +475,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() @@ -426,10 +483,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 }); @@ -453,8 +508,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'); @@ -465,6 +529,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() @@ -492,6 +557,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() @@ -717,6 +783,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 = { @@ -734,6 +801,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'); @@ -795,4 +863,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 5bc7d05ee2..674d91af18 100644 --- a/cypress/e2e/6-code-node.cy.ts +++ b/cypress/e2e/6-code-node.cy.ts @@ -57,7 +57,7 @@ for (const item of $input.all()) { return `); - getParameter().get('.cm-lint-marker-error').should('have.length', 6); + getParameter().get('.cm-lintRange-error').should('have.length', 6); getParameter().contains('itemMatching').realHover(); cy.get('.cm-tooltip-lint').should( 'have.text', @@ -81,7 +81,7 @@ $input.item() return [] `); - getParameter().get('.cm-lint-marker-error').should('have.length', 5); + getParameter().get('.cm-lintRange-error').should('have.length', 5); getParameter().contains('all').realHover(); cy.get('.cm-tooltip-lint').should( 'have.text', diff --git a/cypress/e2e/7-workflow-actions.cy.ts b/cypress/e2e/7-workflow-actions.cy.ts index 8571b174d9..f0f3ae019a 100644 --- a/cypress/e2e/7-workflow-actions.cy.ts +++ b/cypress/e2e/7-workflow-actions.cy.ts @@ -171,9 +171,16 @@ describe('Workflow Actions', () => { cy.get('#node-creator').should('not.exist'); WorkflowPage.actions.hitSelectAll(); - cy.get('.jtk-drag-selected').should('have.length', 2); WorkflowPage.actions.hitCopy(); successToast().should('exist'); + // Both nodes should be copied + cy.window() + .its('navigator.clipboard') + .then((clip) => clip.readText()) + .then((text) => { + const copiedWorkflow = JSON.parse(text); + expect(copiedWorkflow.nodes).to.have.length(2); + }); }); it('should paste nodes (both current and old node versions)', () => { @@ -345,7 +352,15 @@ describe('Workflow Actions', () => { WorkflowPage.actions.hitDeleteAllNodes(); WorkflowPage.getters.canvasNodes().should('have.length', 0); // Button should be disabled - WorkflowPage.getters.executeWorkflowButton().should('be.disabled'); + cy.ifCanvasVersion( + () => { + WorkflowPage.getters.executeWorkflowButton().should('be.disabled'); + }, + () => { + // In new canvas, button does not exist when there are no nodes + WorkflowPage.getters.executeWorkflowButton().should('not.exist'); + }, + ); // Keyboard shortcut should not work WorkflowPage.actions.hitExecuteWorkflow(); successToast().should('not.exist'); 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_Subworkflow-Inputs.json b/cypress/fixtures/Test_Subworkflow-Inputs.json new file mode 100644 index 0000000000..ee2b34513e --- /dev/null +++ b/cypress/fixtures/Test_Subworkflow-Inputs.json @@ -0,0 +1,69 @@ +{ + "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": [], + "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..6725c46bc6 100644 --- a/cypress/package.json +++ b/cypress/package.json @@ -6,12 +6,13 @@ "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 .", "lint": "eslint . --quiet", "lintfix": "eslint . --fix", - "develop": "cd ..; pnpm dev", + "develop": "cd ..; pnpm dev:e2e:server", "start": "cd ..; pnpm start" }, "devDependencies": { @@ -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/bannerStack.ts b/cypress/pages/bannerStack.ts deleted file mode 100644 index c4936891ae..0000000000 --- a/cypress/pages/bannerStack.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { BasePage } from './base'; - -export class BannerStack extends BasePage { - getters = { - banner: () => cy.getByTestId('banner-stack'), - }; - - actions = {}; -} diff --git a/cypress/pages/base.ts b/cypress/pages/base.ts index abd7a210a8..dbcb65e5bd 100644 --- a/cypress/pages/base.ts +++ b/cypress/pages/base.ts @@ -1,5 +1,13 @@ import type { IE2ETestPage } from '../types'; +/** + * @deprecated Use functional composables from @composables instead. + * If a composable doesn't exist for your use case, please create a new one in: + * cypress/composables + * + * This class-based approach is being phased out in favor of more modular functional composables. + * Each getter and action in this class should be moved to individual composable functions. + */ export class BasePage implements IE2ETestPage { getters = {}; diff --git a/cypress/pages/credentials.ts b/cypress/pages/credentials.ts index 9b20b48ec4..b7b68504f9 100644 --- a/cypress/pages/credentials.ts +++ b/cypress/pages/credentials.ts @@ -1,11 +1,23 @@ import { BasePage } from './base'; +/** + * @deprecated Use functional composables from @composables instead. + * If a composable doesn't exist for your use case, please create a new one in: + * cypress/composables + * + * This class-based approach is being phased out in favor of more modular functional composables. + * Each getter and action in this class should be moved to individual composable functions. + */ export class CredentialsPage extends BasePage { url = '/home/credentials'; 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/features/ai-assistant.ts b/cypress/pages/features/ai-assistant.ts index 6ff48851f3..f2e747817b 100644 --- a/cypress/pages/features/ai-assistant.ts +++ b/cypress/pages/features/ai-assistant.ts @@ -8,6 +8,14 @@ const AI_ASSISTANT_FEATURE = { disabledFor: 'control', }; +/** + * @deprecated Use functional composables from @composables instead. + * If a composable doesn't exist for your use case, please create a new one in: + * cypress/composables + * + * This class-based approach is being phased out in favor of more modular functional composables. + * Each getter and action in this class should be moved to individual composable functions. + */ export class AIAssistant extends BasePage { url = '/workflows/new'; diff --git a/cypress/pages/features/node-creator.ts b/cypress/pages/features/node-creator.ts index a0d3995160..38c932468f 100644 --- a/cypress/pages/features/node-creator.ts +++ b/cypress/pages/features/node-creator.ts @@ -1,5 +1,13 @@ import { BasePage } from '../base'; +/** + * @deprecated Use functional composables from @composables instead. + * If a composable doesn't exist for your use case, please create a new one in: + * cypress/composables + * + * This class-based approach is being phased out in favor of more modular functional composables. + * Each getter and action in this class should be moved to individual composable functions. + */ export class NodeCreator extends BasePage { url = '/workflow/new'; diff --git a/cypress/pages/index.ts b/cypress/pages/index.ts index 39c9be3b56..93dd165621 100644 --- a/cypress/pages/index.ts +++ b/cypress/pages/index.ts @@ -7,9 +7,7 @@ export * from './settings-users'; export * from './settings-log-streaming'; export * from './sidebar'; export * from './ndv'; -export * from './bannerStack'; export * from './workflow-executions-tab'; export * from './signin'; -export * from './workflow-history'; export * from './workerView'; export * from './settings-public-api'; diff --git a/cypress/pages/mfa-login.ts b/cypress/pages/mfa-login.ts index 66fc197e3f..884be7d75d 100644 --- a/cypress/pages/mfa-login.ts +++ b/cypress/pages/mfa-login.ts @@ -3,23 +3,31 @@ import { SigninPage } from './signin'; import { WorkflowsPage } from './workflows'; import { N8N_AUTH_COOKIE } from '../constants'; +/** + * @deprecated Use functional composables from @composables instead. + * If a composable doesn't exist for your use case, please create a new one in: + * cypress/composables + * + * This class-based approach is being phased out in favor of more modular functional composables. + * Each getter and action in this class should be moved to individual composable functions. + */ export class MfaLoginPage extends BasePage { url = '/mfa'; 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 +38,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 +51,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 +69,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/change-password-modal.ts b/cypress/pages/modals/change-password-modal.ts index 3e9ebc8697..28c4d01d86 100644 --- a/cypress/pages/modals/change-password-modal.ts +++ b/cypress/pages/modals/change-password-modal.ts @@ -1,5 +1,13 @@ import { BasePage } from './../base'; +/** + * @deprecated Use functional composables from @composables instead. + * If a composable doesn't exist for your use case, please create a new one in: + * cypress/composables + * + * This class-based approach is being phased out in favor of more modular functional composables. + * Each getter and action in this class should be moved to individual composable functions. + */ export class ChangePasswordModal extends BasePage { getters = { modalContainer: () => cy.getByTestId('changePassword-modal').last(), diff --git a/cypress/pages/modals/credentials-modal.ts b/cypress/pages/modals/credentials-modal.ts index cd3ded63f8..592a396161 100644 --- a/cypress/pages/modals/credentials-modal.ts +++ b/cypress/pages/modals/credentials-modal.ts @@ -1,6 +1,15 @@ +import { getCredentialSaveButton, saveCredential } from '../../composables/modals/credential-modal'; import { getVisibleSelect } from '../../utils'; import { BasePage } from '../base'; +/** + * @deprecated Use functional composables from @composables instead. + * If a composable doesn't exist for your use case, please create a new one in: + * cypress/composables + * + * This class-based approach is being phased out in favor of more modular functional composables. + * Each getter and action in this class should be moved to individual composable functions. + */ export class CredentialsModal extends BasePage { getters = { newCredentialModal: () => cy.getByTestId('selectCredential-modal', { timeout: 5000 }), @@ -13,8 +22,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 +48,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(); @@ -62,10 +69,11 @@ export class CredentialsModal extends BasePage { this.getters .credentialInputs() .find('input[type=text], input[type=password]') + .filter(':not([readonly])') .each(($el) => { cy.wrap($el).type('test'); }); - this.getters.saveButton().click(); + saveCredential(); if (closeModal) { this.getters.closeButton().click(); } diff --git a/cypress/pages/modals/message-box.ts b/cypress/pages/modals/message-box.ts index a40c2d1a88..42f83d9a15 100644 --- a/cypress/pages/modals/message-box.ts +++ b/cypress/pages/modals/message-box.ts @@ -1,5 +1,13 @@ import { BasePage } from '../base'; +/** + * @deprecated Use functional composables from @composables instead. + * If a composable doesn't exist for your use case, please create a new one in: + * cypress/composables + * + * This class-based approach is being phased out in favor of more modular functional composables. + * Each getter and action in this class should be moved to individual composable functions. + */ export class MessageBox extends BasePage { getters = { modal: () => cy.get('.el-message-box', { withinSubject: null }), diff --git a/cypress/pages/modals/mfa-setup-modal.ts b/cypress/pages/modals/mfa-setup-modal.ts index d127731be2..baa37f5f3a 100644 --- a/cypress/pages/modals/mfa-setup-modal.ts +++ b/cypress/pages/modals/mfa-setup-modal.ts @@ -1,5 +1,13 @@ import { BasePage } from './../base'; +/** + * @deprecated Use functional composables from @composables instead. + * If a composable doesn't exist for your use case, please create a new one in: + * cypress/composables + * + * This class-based approach is being phased out in favor of more modular functional composables. + * Each getter and action in this class should be moved to individual composable functions. + */ export class MfaSetupModal extends BasePage { getters = { modalContainer: () => cy.getByTestId('changePassword-modal').last(), diff --git a/cypress/pages/modals/workflow-sharing-modal.ts b/cypress/pages/modals/workflow-sharing-modal.ts index 02e183fc81..176cd84a7b 100644 --- a/cypress/pages/modals/workflow-sharing-modal.ts +++ b/cypress/pages/modals/workflow-sharing-modal.ts @@ -1,5 +1,13 @@ import { BasePage } from '../base'; +/** + * @deprecated Use functional composables from @composables instead. + * If a composable doesn't exist for your use case, please create a new one in: + * cypress/composables + * + * This class-based approach is being phased out in favor of more modular functional composables. + * Each getter and action in this class should be moved to individual composable functions. + */ export class WorkflowSharingModal extends BasePage { getters = { modal: () => cy.getByTestId('workflowShare-modal', { timeout: 5000 }), diff --git a/cypress/pages/ndv.ts b/cypress/pages/ndv.ts index 4504552e26..91ece23122 100644 --- a/cypress/pages/ndv.ts +++ b/cypress/pages/ndv.ts @@ -1,6 +1,14 @@ import { BasePage } from './base'; import { getVisiblePopper, getVisibleSelect } from '../utils'; +/** + * @deprecated Use functional composables from @composables instead. + * If a composable doesn't exist for your use case, please create a new one in: + * cypress/composables + * + * This class-based approach is being phased out in favor of more modular functional composables. + * Each getter and action in this class should be moved to individual composable functions. + */ export class NDV extends BasePage { getters = { container: () => cy.getByTestId('ndv'), @@ -227,9 +235,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(); @@ -323,6 +328,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/notifications.ts b/cypress/pages/notifications.ts index 162c536007..af2a51e369 100644 --- a/cypress/pages/notifications.ts +++ b/cypress/pages/notifications.ts @@ -13,5 +13,10 @@ export const infoToast = () => cy.get('.el-notification:has(.el-notification--in * Actions */ export const clearNotifications = () => { - successToast().find('.el-notification__closeBtn').click({ multiple: true }); + const buttons = successToast().find('.el-notification__closeBtn'); + buttons.then(($buttons) => { + if ($buttons.length) { + buttons.click({ multiple: true }); + } + }); }; diff --git a/cypress/pages/settings-log-streaming.ts b/cypress/pages/settings-log-streaming.ts index 9063b8dc41..959b18bce5 100644 --- a/cypress/pages/settings-log-streaming.ts +++ b/cypress/pages/settings-log-streaming.ts @@ -1,6 +1,14 @@ import { BasePage } from './base'; import { getVisibleSelect } from '../utils'; +/** + * @deprecated Use functional composables from @composables instead. + * If a composable doesn't exist for your use case, please create a new one in: + * cypress/composables + * + * This class-based approach is being phased out in favor of more modular functional composables. + * Each getter and action in this class should be moved to individual composable functions. + */ export class SettingsLogStreamingPage extends BasePage { url = '/settings/log-streaming'; diff --git a/cypress/pages/settings-personal.ts b/cypress/pages/settings-personal.ts index 4574f95691..49c7de1283 100644 --- a/cypress/pages/settings-personal.ts +++ b/cypress/pages/settings-personal.ts @@ -7,6 +7,14 @@ import { MfaSetupModal } from './modals/mfa-setup-modal'; const changePasswordModal = new ChangePasswordModal(); const mfaSetupModal = new MfaSetupModal(); +/** + * @deprecated Use functional composables from @composables instead. + * If a composable doesn't exist for your use case, please create a new one in: + * cypress/composables + * + * This class-based approach is being phased out in favor of more modular functional composables. + * Each getter and action in this class should be moved to individual composable functions. + */ export class PersonalSettingsPage extends BasePage { url = '/settings/personal'; @@ -22,6 +30,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 +93,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/settings-usage.ts b/cypress/pages/settings-usage.ts deleted file mode 100644 index 85300fe05f..0000000000 --- a/cypress/pages/settings-usage.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { BasePage } from './base'; - -export class SettingsUsagePage extends BasePage { - url = '/settings/usage'; - - getters = {}; - - actions = {}; -} diff --git a/cypress/pages/settings-users.ts b/cypress/pages/settings-users.ts index 1eaebc911a..f442377fad 100644 --- a/cypress/pages/settings-users.ts +++ b/cypress/pages/settings-users.ts @@ -9,6 +9,14 @@ const workflowsPage = new WorkflowsPage(); const mainSidebar = new MainSidebar(); const settingsSidebar = new SettingsSidebar(); +/** + * @deprecated Use functional composables from @composables instead. + * If a composable doesn't exist for your use case, please create a new one in: + * cypress/composables + * + * This class-based approach is being phased out in favor of more modular functional composables. + * Each getter and action in this class should be moved to individual composable functions. + */ export class SettingsUsersPage extends BasePage { url = '/settings/users'; diff --git a/cypress/pages/settings.ts b/cypress/pages/settings.ts deleted file mode 100644 index 74c3b0fe76..0000000000 --- a/cypress/pages/settings.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { BasePage } from './base'; - -export class SettingsPage extends BasePage { - url = '/settings'; - - getters = { - menuItems: () => cy.getByTestId('menu-item'), - }; - - actions = {}; -} diff --git a/cypress/pages/sidebar/main-sidebar.ts b/cypress/pages/sidebar/main-sidebar.ts index 4266b93688..7824a6bebb 100644 --- a/cypress/pages/sidebar/main-sidebar.ts +++ b/cypress/pages/sidebar/main-sidebar.ts @@ -1,6 +1,14 @@ import { BasePage } from '../base'; import { WorkflowsPage } from '../workflows'; +/** + * @deprecated Use functional composables from @composables instead. + * If a composable doesn't exist for your use case, please create a new one in: + * cypress/composables + * + * This class-based approach is being phased out in favor of more modular functional composables. + * Each getter and action in this class should be moved to individual composable functions. + */ export class MainSidebar extends BasePage { getters = { menuItem: (id: string) => cy.getByTestId('menu-item').get('#' + id), diff --git a/cypress/pages/sidebar/settings-sidebar.ts b/cypress/pages/sidebar/settings-sidebar.ts index 17d43b65e7..17bf5d10b7 100644 --- a/cypress/pages/sidebar/settings-sidebar.ts +++ b/cypress/pages/sidebar/settings-sidebar.ts @@ -1,5 +1,13 @@ import { BasePage } from '../base'; +/** + * @deprecated Use functional composables from @composables instead. + * If a composable doesn't exist for your use case, please create a new one in: + * cypress/composables + * + * This class-based approach is being phased out in favor of more modular functional composables. + * Each getter and action in this class should be moved to individual composable functions. + */ export class SettingsSidebar extends BasePage { getters = { menuItem: (id: string) => cy.getByTestId('menu-item').get('#' + id), diff --git a/cypress/pages/signin.ts b/cypress/pages/signin.ts index a97fe4888e..bc0d7196a3 100644 --- a/cypress/pages/signin.ts +++ b/cypress/pages/signin.ts @@ -2,6 +2,14 @@ import { BasePage } from './base'; import { WorkflowsPage } from './workflows'; import { N8N_AUTH_COOKIE } from '../constants'; +/** + * @deprecated Use functional composables from @composables instead. + * If a composable doesn't exist for your use case, please create a new one in: + * cypress/composables + * + * This class-based approach is being phased out in favor of more modular functional composables. + * Each getter and action in this class should be moved to individual composable functions. + */ export class SigninPage extends BasePage { url = '/signin'; diff --git a/cypress/pages/templates.ts b/cypress/pages/templates.ts index a17da87ba2..8d8fdc15c5 100644 --- a/cypress/pages/templates.ts +++ b/cypress/pages/templates.ts @@ -1,5 +1,13 @@ import { BasePage } from './base'; +/** + * @deprecated Use functional composables from @composables instead. + * If a composable doesn't exist for your use case, please create a new one in: + * cypress/composables + * + * This class-based approach is being phased out in favor of more modular functional composables. + * Each getter and action in this class should be moved to individual composable functions. + */ export class TemplatesPage extends BasePage { url = '/templates'; diff --git a/cypress/pages/variables.ts b/cypress/pages/variables.ts index 6ac9a939b2..5fb0a64d9a 100644 --- a/cypress/pages/variables.ts +++ b/cypress/pages/variables.ts @@ -2,6 +2,14 @@ import { BasePage } from './base'; import Chainable = Cypress.Chainable; +/** + * @deprecated Use functional composables from @composables instead. + * If a composable doesn't exist for your use case, please create a new one in: + * cypress/composables + * + * This class-based approach is being phased out in favor of more modular functional composables. + * Each getter and action in this class should be moved to individual composable functions. + */ export class VariablesPage extends BasePage { url = '/variables'; diff --git a/cypress/pages/workerView.ts b/cypress/pages/workerView.ts index f442468c52..ff56ab1ec5 100644 --- a/cypress/pages/workerView.ts +++ b/cypress/pages/workerView.ts @@ -1,5 +1,13 @@ import { BasePage } from './base'; +/** + * @deprecated Use functional composables from @composables instead. + * If a composable doesn't exist for your use case, please create a new one in: + * cypress/composables + * + * This class-based approach is being phased out in favor of more modular functional composables. + * Each getter and action in this class should be moved to individual composable functions. + */ export class WorkerViewPage extends BasePage { url = '/settings/workers'; diff --git a/cypress/pages/workflow-executions-tab.ts b/cypress/pages/workflow-executions-tab.ts index 5e8c36c055..da91e99a98 100644 --- a/cypress/pages/workflow-executions-tab.ts +++ b/cypress/pages/workflow-executions-tab.ts @@ -3,6 +3,14 @@ import { WorkflowPage } from './workflow'; const workflowPage = new WorkflowPage(); +/** + * @deprecated Use functional composables from @composables instead. + * If a composable doesn't exist for your use case, please create a new one in: + * cypress/composables + * + * This class-based approach is being phased out in favor of more modular functional composables. + * Each getter and action in this class should be moved to individual composable functions. + */ export class WorkflowExecutionsTab extends BasePage { getters = { executionsTabButton: () => cy.getByTestId('radio-button-executions'), @@ -30,6 +38,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-history.ts b/cypress/pages/workflow-history.ts deleted file mode 100644 index 1b9d7328b1..0000000000 --- a/cypress/pages/workflow-history.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { BasePage } from './base'; - -export class WorkflowHistoryPage extends BasePage { - getters = { - workflowHistoryCloseButton: () => cy.getByTestId('workflow-history-close-button'), - }; -} diff --git a/cypress/pages/workflow.ts b/cypress/pages/workflow.ts index cd1e7d9462..69a1becc02 100644 --- a/cypress/pages/workflow.ts +++ b/cypress/pages/workflow.ts @@ -1,10 +1,20 @@ 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, isCanvasV2 } from '../utils/workflowUtils'; const nodeCreator = new NodeCreator(); + +/** + * @deprecated Use functional composables from @composables instead. + * If a composable doesn't exist for your use case, please create a new one in: + * cypress/composables + * + * This class-based approach is being phased out in favor of more modular functional composables. + * Each getter and action in this class should be moved to individual composable functions. + */ export class WorkflowPage extends BasePage { url = '/workflow/new'; @@ -17,7 +27,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'), @@ -30,7 +41,11 @@ export class WorkflowPage extends BasePage { canvasNodes: () => cy.ifCanvasVersion( () => cy.getByTestId('canvas-node'), - () => cy.getByTestId('canvas-node').not('[data-node-type="n8n-nodes-internal.addNodes"]'), + () => + 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})`), @@ -43,13 +58,13 @@ export class WorkflowPage extends BasePage { 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}"]`; + return `[data-test-id="canvas-node-input-handle"][data-node-name="${nodeName}"][data-index="${index}"]`; } if (type === 'output') { - return `[data-test-id="canvas-node-output-handle"][data-node-name="${nodeName}"][data-handle-index="${index}"]`; + return `[data-test-id="canvas-node-output-handle"][data-node-name="${nodeName}"][data-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-test-id="canvas-node-output-handle"][data-node-name="${nodeName}"][data-index="${index}"] [data-test-id="canvas-handle-plus"]`; } } return `[data-endpoint-name='${nodeName}'][data-endpoint-type='${type}'][data-input-index='${index}']`; @@ -66,7 +81,7 @@ export class WorkflowPage extends BasePage { () => cy .get( - `[data-test-id="canvas-node-output-handle"][data-node-name="${nodeName}"] [data-test-id="canvas-handle-plus"] .clickable`, + `[data-test-id="canvas-node-output-handle"][data-node-name="${nodeName}"] [data-test-id="canvas-handle-plus"]`, ) .eq(index), ); @@ -88,14 +103,14 @@ export class WorkflowPage extends BasePage { nodeConnections: () => cy.ifCanvasVersion( () => cy.get('.jtk-connector'), - () => cy.getByTestId('edge-label-wrapper'), + () => cy.getByTestId('edge-label'), ), zoomToFitButton: () => cy.getByTestId('zoom-to-fit'), nodeEndpoints: () => cy.get('.jtk-endpoint-connected'), disabledNodes: () => cy.ifCanvasVersion( () => cy.get('.node-box.disabled'), - () => cy.get('[data-test-id="canvas-trigger-node"][class*="disabled"]'), + () => cy.get('[data-test-id*="node"][class*="disabled"]'), ), selectedNodes: () => cy.ifCanvasVersion( @@ -174,7 +189,7 @@ export class WorkflowPage extends BasePage { ), () => cy.get( - `[data-test-id="edge-label-wrapper"][data-source-node-name="${sourceNodeName}"][data-target-node-name="${targetNodeName}"]`, + `[data-test-id="edge-label"][data-source-node-name="${sourceNodeName}"][data-target-node-name="${targetNodeName}"]`, ), ), getConnectionActionsBetweenNodes: (sourceNodeName: string, targetNodeName: string) => @@ -185,7 +200,7 @@ export class WorkflowPage extends BasePage { ), () => cy.get( - `[data-test-id="edge-label-wrapper"][data-source-node-name="${sourceNodeName}"][data-target-node-name="${targetNodeName}"] [data-test-id="canvas-edge-toolbar"]`, + `[data-test-id="edge-label"][data-source-node-name="${sourceNodeName}"][data-target-node-name="${targetNodeName}"] [data-test-id="canvas-edge-toolbar"]`, ), ), addStickyButton: () => cy.getByTestId('add-sticky-button'), @@ -271,14 +286,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 }); @@ -295,8 +310,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: () => { @@ -323,7 +338,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: () => { @@ -331,8 +346,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(); diff --git a/cypress/pages/workflows.ts b/cypress/pages/workflows.ts index 5829ecb863..7441bfa256 100644 --- a/cypress/pages/workflows.ts +++ b/cypress/pages/workflows.ts @@ -1,5 +1,13 @@ import { BasePage } from './base'; +/** + * @deprecated Use functional composables from @composables instead. + * If a composable doesn't exist for your use case, please create a new one in: + * cypress/composables + * + * This class-based approach is being phased out in favor of more modular functional composables. + * Each getter and action in this class should be moved to individual composable functions. + */ export class WorkflowsPage extends BasePage { url = '/home/workflows'; @@ -7,7 +15,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 +27,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 6cad68b34f..c414c9fea9 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -75,8 +75,13 @@ 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.switcher.discovered', 'true'); // @TODO Remove this once the switcher is removed + 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'); }); }); }); @@ -177,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/e2e.ts b/cypress/support/e2e.ts index 4261cb4b63..0fe782499d 100644 --- a/cypress/support/e2e.ts +++ b/cypress/support/e2e.ts @@ -20,11 +20,6 @@ beforeEach(() => { win.localStorage.setItem('N8N_THEME', 'light'); win.localStorage.setItem('N8N_AUTOCOMPLETE_ONBOARDED', 'true'); win.localStorage.setItem('N8N_MAPPING_ONBOARDED', 'true'); - - const nodeViewVersion = Cypress.env('NODE_VIEW_VERSION'); - if (nodeViewVersion) { - win.localStorage.setItem('NodeView.version', nodeViewVersion); - } }); cy.intercept('GET', '/rest/settings', (req) => { diff --git a/cypress/support/index.ts b/cypress/support/index.ts index 2fd1faeb22..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, @@ -59,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/docker/images/n8n-custom/Dockerfile b/docker/images/n8n-custom/Dockerfile index 78eedaa2c3..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_MODE=internal_launcher \ - 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 8a94d0c9ec..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_MODE=internal_launcher \ - 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/README.md b/docker/images/n8n/README.md index f7b45f9467..73dbb7557b 100644 --- a/docker/images/n8n/README.md +++ b/docker/images/n8n/README.md @@ -73,7 +73,7 @@ docker run -it --rm \ -p 5678:5678 \ -v ~/.n8n:/home/node/.n8n \ docker.n8n.io/n8nio/n8n \ - n8n start --tunnel + start --tunnel ``` ## Persist data diff --git a/docker/images/n8n/n8n-task-runners.json b/docker/images/n8n/n8n-task-runners.json index 56a48b2d09..a26053e5e2 100644 --- a/docker/images/n8n/n8n-task-runners.json +++ b/docker/images/n8n/n8n-task-runners.json @@ -2,19 +2,32 @@ "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"], + "args": [ + "--disallow-code-generation-from-strings", + "--disable-proto=delete", + "/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 09c576a8c3..3e05dba4c0 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,12 @@ { "name": "n8n-monorepo", - "version": "1.65.0", + "version": "1.75.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,7 +16,13 @@ "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", + "dev:fe": "run-p start \"dev:fe:editor --filter=n8n-design-system\"", + "dev:fe:editor": "turbo run dev --parallel --env-mode=loose --filter=n8n-editor-ui", + "dev:e2e": "cd cypress && pnpm run test:e2e:dev", + "dev:e2e:v2": "cd cypress && pnpm run test:e2e:dev:v2", + "dev:e2e:server": "run-p start dev:fe:editor", "clean": "turbo run clean --parallel", "reset": "node scripts/ensure-zx.mjs && zx scripts/reset.mjs", "format": "turbo run format && node scripts/format.mjs", @@ -45,6 +51,7 @@ "@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", @@ -53,6 +60,7 @@ "lefthook": "^1.7.15", "nock": "^13.3.2", "nodemon": "^3.0.1", + "npm-run-all2": "^7.0.2", "p-limit": "^3.1.0", "rimraf": "^5.0.1", "run-script-os": "^1.0.7", @@ -60,7 +68,7 @@ "ts-jest": "^29.1.1", "tsc-alias": "^1.8.10", "tsc-watch": "^6.2.0", - "turbo": "2.1.2", + "turbo": "2.3.3", "typescript": "*", "zx": "^8.1.4" }, @@ -77,19 +85,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", + "bull@4.12.1": "patches/bull@4.12.1.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", - "@langchain/core@0.3.3": "patches/@langchain__core@0.3.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..1255320c5a 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.13.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..9be09c02f3 100644 --- a/packages/@n8n/api-types/src/dto/index.ts +++ b/packages/@n8n/api-types/src/dto/index.ts @@ -1,5 +1,49 @@ +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 { CreateProjectDto } from './project/create-project.dto'; +export { UpdateProjectDto } from './project/update-project.dto'; +export { DeleteProjectDto } from './project/delete-project.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 { PullWorkFolderRequestDto } from './source-control/pull-work-folder-request.dto'; +export { PushWorkFolderRequestDto } from './source-control/push-work-folder-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'; + +export { ImportWorkflowFromUrlDto } from './workflows/import-workflow-from-url.dto'; + +export { CreateOrUpdateTagRequestDto } from './tag/create-or-update-tag-request.dto'; +export { RetrieveTagQueryDto } from './tag/retrieve-tag-query.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/project/__tests__/create-project.dto.test.ts b/packages/@n8n/api-types/src/dto/project/__tests__/create-project.dto.test.ts new file mode 100644 index 0000000000..42eb553030 --- /dev/null +++ b/packages/@n8n/api-types/src/dto/project/__tests__/create-project.dto.test.ts @@ -0,0 +1,75 @@ +import { CreateProjectDto } from '../create-project.dto'; + +describe('CreateProjectDto', () => { + describe('Valid requests', () => { + test.each([ + { + name: 'with just the name', + request: { + name: 'My Awesome Project', + }, + }, + { + name: 'with name and emoji icon', + request: { + name: 'My Awesome Project', + icon: { + type: 'emoji', + value: '🚀', + }, + }, + }, + { + name: 'with name and regular icon', + request: { + name: 'My Awesome Project', + icon: { + type: 'icon', + value: 'blah', + }, + }, + }, + ])('should validate $name', ({ request }) => { + const result = CreateProjectDto.safeParse(request); + expect(result.success).toBe(true); + }); + }); + + describe('Invalid requests', () => { + test.each([ + { + name: 'missing name', + request: { icon: { type: 'emoji', value: '🚀' } }, + expectedErrorPath: ['name'], + }, + { + name: 'empty name', + request: { name: '', icon: { type: 'emoji', value: '🚀' } }, + expectedErrorPath: ['name'], + }, + { + name: 'name too long', + request: { name: 'a'.repeat(256), icon: { type: 'emoji', value: '🚀' } }, + expectedErrorPath: ['name'], + }, + { + name: 'invalid icon type', + request: { name: 'My Awesome Project', icon: { type: 'invalid', value: '🚀' } }, + expectedErrorPath: ['icon', 'type'], + }, + { + name: 'invalid icon value', + request: { name: 'My Awesome Project', icon: { type: 'emoji', value: '' } }, + expectedErrorPath: ['icon', 'value'], + }, + ])('should fail validation for $name', ({ request, expectedErrorPath }) => { + const result = CreateProjectDto.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/project/__tests__/update-project.dto.test.ts b/packages/@n8n/api-types/src/dto/project/__tests__/update-project.dto.test.ts new file mode 100644 index 0000000000..86a8630026 --- /dev/null +++ b/packages/@n8n/api-types/src/dto/project/__tests__/update-project.dto.test.ts @@ -0,0 +1,121 @@ +import { UpdateProjectDto } from '../update-project.dto'; + +describe('UpdateProjectDto', () => { + describe('Valid requests', () => { + test.each([ + { + name: 'with just the name', + request: { + name: 'My Updated Project', + }, + }, + { + name: 'with name and emoji icon', + request: { + name: 'My Updated Project', + icon: { + type: 'emoji', + value: '🚀', + }, + }, + }, + { + name: 'with name and regular icon', + request: { + name: 'My Updated Project', + icon: { + type: 'icon', + value: 'blah', + }, + }, + }, + { + name: 'with relations', + request: { + relations: [ + { + userId: 'user-123', + role: 'project:admin', + }, + ], + }, + }, + { + name: 'with all fields', + request: { + name: 'My Updated Project', + icon: { + type: 'emoji', + value: '🚀', + }, + relations: [ + { + userId: 'user-123', + role: 'project:admin', + }, + ], + }, + }, + ])('should validate $name', ({ request }) => { + const result = UpdateProjectDto.safeParse(request); + expect(result.success).toBe(true); + }); + }); + + describe('Invalid requests', () => { + test.each([ + { + name: 'invalid name type', + request: { name: 123 }, + expectedErrorPath: ['name'], + }, + { + name: 'name too long', + request: { name: 'a'.repeat(256) }, + expectedErrorPath: ['name'], + }, + { + name: 'invalid icon type', + request: { icon: { type: 'invalid', value: '🚀' } }, + expectedErrorPath: ['icon', 'type'], + }, + { + name: 'invalid icon value', + request: { icon: { type: 'emoji', value: '' } }, + expectedErrorPath: ['icon', 'value'], + }, + { + name: 'invalid relations userId', + request: { + relations: [ + { + userId: 123, + role: 'project:admin', + }, + ], + }, + expectedErrorPath: ['relations', 0, 'userId'], + }, + { + name: 'invalid relations role', + request: { + relations: [ + { + userId: 'user-123', + role: 'invalid-role', + }, + ], + }, + expectedErrorPath: ['relations', 0, 'role'], + }, + ])('should fail validation for $name', ({ request, expectedErrorPath }) => { + const result = UpdateProjectDto.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/project/create-project.dto.ts b/packages/@n8n/api-types/src/dto/project/create-project.dto.ts new file mode 100644 index 0000000000..cf748f5e13 --- /dev/null +++ b/packages/@n8n/api-types/src/dto/project/create-project.dto.ts @@ -0,0 +1,8 @@ +import { Z } from 'zod-class'; + +import { projectIconSchema, projectNameSchema } from '../../schemas/project.schema'; + +export class CreateProjectDto extends Z.class({ + name: projectNameSchema, + icon: projectIconSchema.optional(), +}) {} diff --git a/packages/@n8n/api-types/src/dto/project/delete-project.dto.ts b/packages/@n8n/api-types/src/dto/project/delete-project.dto.ts new file mode 100644 index 0000000000..cc8d8c2679 --- /dev/null +++ b/packages/@n8n/api-types/src/dto/project/delete-project.dto.ts @@ -0,0 +1,6 @@ +import { z } from 'zod'; +import { Z } from 'zod-class'; + +export class DeleteProjectDto extends Z.class({ + transferId: z.string().optional(), +}) {} diff --git a/packages/@n8n/api-types/src/dto/project/update-project.dto.ts b/packages/@n8n/api-types/src/dto/project/update-project.dto.ts new file mode 100644 index 0000000000..b167ed88d3 --- /dev/null +++ b/packages/@n8n/api-types/src/dto/project/update-project.dto.ts @@ -0,0 +1,14 @@ +import { z } from 'zod'; +import { Z } from 'zod-class'; + +import { + projectIconSchema, + projectNameSchema, + projectRelationSchema, +} from '../../schemas/project.schema'; + +export class UpdateProjectDto extends Z.class({ + name: projectNameSchema.optional(), + icon: projectIconSchema.optional(), + relations: z.array(projectRelationSchema).optional(), +}) {} 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/source-control/__tests__/pull-work-folder-request.dto.test.ts b/packages/@n8n/api-types/src/dto/source-control/__tests__/pull-work-folder-request.dto.test.ts new file mode 100644 index 0000000000..0e47a223b9 --- /dev/null +++ b/packages/@n8n/api-types/src/dto/source-control/__tests__/pull-work-folder-request.dto.test.ts @@ -0,0 +1,38 @@ +import { PullWorkFolderRequestDto } from '../pull-work-folder-request.dto'; + +describe('PullWorkFolderRequestDto', () => { + describe('Valid requests', () => { + test.each([ + { + name: 'with force', + request: { force: true }, + }, + { + name: 'without force', + request: {}, + }, + ])('should validate $name', ({ request }) => { + const result = PullWorkFolderRequestDto.safeParse(request); + expect(result.success).toBe(true); + }); + }); + + describe('Invalid requests', () => { + test.each([ + { + name: 'invalid force type', + request: { + force: 'true', // Should be boolean + }, + expectedErrorPath: ['force'], + }, + ])('should fail validation for $name', ({ request, expectedErrorPath }) => { + const result = PullWorkFolderRequestDto.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/source-control/__tests__/push-work-folder-request.dto.test.ts b/packages/@n8n/api-types/src/dto/source-control/__tests__/push-work-folder-request.dto.test.ts new file mode 100644 index 0000000000..b3bbd984e1 --- /dev/null +++ b/packages/@n8n/api-types/src/dto/source-control/__tests__/push-work-folder-request.dto.test.ts @@ -0,0 +1,112 @@ +import { PushWorkFolderRequestDto } from '../push-work-folder-request.dto'; + +describe('PushWorkFolderRequestDto', () => { + describe('Valid requests', () => { + test.each([ + { + name: 'complete valid push request with all fields', + request: { + force: true, + fileNames: [ + { + file: 'file1.json', + id: '1', + name: 'File 1', + type: 'workflow', + status: 'modified', + location: 'local', + conflict: false, + updatedAt: '2023-10-01T12:00:00Z', + pushed: true, + }, + ], + message: 'Initial commit', + }, + }, + { + name: 'push request with only required fields', + request: { + fileNames: [ + { + file: 'file2.json', + id: '2', + name: 'File 2', + type: 'credential', + status: 'new', + location: 'remote', + conflict: true, + updatedAt: '2023-10-02T12:00:00Z', + }, + ], + }, + }, + ])('should validate $name', ({ request }) => { + const result = PushWorkFolderRequestDto.safeParse(request); + expect(result.success).toBe(true); + }); + }); + + describe('Invalid requests', () => { + test.each([ + { + name: 'missing required fileNames field', + request: { + force: true, + message: 'Initial commit', + }, + expectedErrorPath: ['fileNames'], + }, + { + name: 'invalid fileNames type', + request: { + fileNames: 'not-an-array', // Should be an array + }, + expectedErrorPath: ['fileNames'], + }, + { + name: 'invalid fileNames array element', + request: { + fileNames: [ + { + file: 'file4.json', + id: '4', + name: 'File 4', + type: 'invalid-type', // Invalid type + status: 'modified', + location: 'local', + conflict: false, + updatedAt: '2023-10-04T12:00:00Z', + }, + ], + }, + expectedErrorPath: ['fileNames', 0, 'type'], + }, + { + name: 'invalid force type', + request: { + force: 'true', // Should be boolean + fileNames: [ + { + file: 'file5.json', + id: '5', + name: 'File 5', + type: 'workflow', + status: 'modified', + location: 'local', + conflict: false, + updatedAt: '2023-10-05T12:00:00Z', + }, + ], + }, + expectedErrorPath: ['force'], + }, + ])('should fail validation for $name', ({ request, expectedErrorPath }) => { + const result = PushWorkFolderRequestDto.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/source-control/pull-work-folder-request.dto.ts b/packages/@n8n/api-types/src/dto/source-control/pull-work-folder-request.dto.ts new file mode 100644 index 0000000000..2773e0bb59 --- /dev/null +++ b/packages/@n8n/api-types/src/dto/source-control/pull-work-folder-request.dto.ts @@ -0,0 +1,6 @@ +import { z } from 'zod'; +import { Z } from 'zod-class'; + +export class PullWorkFolderRequestDto extends Z.class({ + force: z.boolean().optional(), +}) {} diff --git a/packages/@n8n/api-types/src/dto/source-control/push-work-folder-request.dto.ts b/packages/@n8n/api-types/src/dto/source-control/push-work-folder-request.dto.ts new file mode 100644 index 0000000000..be305ee7b3 --- /dev/null +++ b/packages/@n8n/api-types/src/dto/source-control/push-work-folder-request.dto.ts @@ -0,0 +1,10 @@ +import { z } from 'zod'; +import { Z } from 'zod-class'; + +import { SourceControlledFileSchema } from '../../schemas/source-controlled-file.schema'; + +export class PushWorkFolderRequestDto extends Z.class({ + force: z.boolean().optional(), + commitMessage: z.string().optional(), + fileNames: z.array(SourceControlledFileSchema), +}) {} diff --git a/packages/@n8n/api-types/src/dto/tag/__tests__/create-or-update-tag-request.dto.test.ts b/packages/@n8n/api-types/src/dto/tag/__tests__/create-or-update-tag-request.dto.test.ts new file mode 100644 index 0000000000..0af3ec6b5c --- /dev/null +++ b/packages/@n8n/api-types/src/dto/tag/__tests__/create-or-update-tag-request.dto.test.ts @@ -0,0 +1,37 @@ +import { CreateOrUpdateTagRequestDto } from '../create-or-update-tag-request.dto'; + +describe('CreateOrUpdateTagRequestDto', () => { + describe('Valid requests', () => { + test.each([ + { + name: 'valid name', + request: { + name: 'tag-name', + }, + }, + ])('should validate $name', ({ request }) => { + const result = CreateOrUpdateTagRequestDto.safeParse(request); + expect(result.success).toBe(true); + }); + }); + + describe('Invalid requests', () => { + test.each([ + { + name: 'empty tag name', + request: { + name: '', + }, + expectedErrorPath: ['name'], + }, + ])('should fail validation for $name', ({ request, expectedErrorPath }) => { + const result = CreateOrUpdateTagRequestDto.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/tag/__tests__/retrieve-tag-query.dto.test.ts b/packages/@n8n/api-types/src/dto/tag/__tests__/retrieve-tag-query.dto.test.ts new file mode 100644 index 0000000000..902bc62764 --- /dev/null +++ b/packages/@n8n/api-types/src/dto/tag/__tests__/retrieve-tag-query.dto.test.ts @@ -0,0 +1,64 @@ +import { RetrieveTagQueryDto } from '../retrieve-tag-query.dto'; + +describe('RetrieveTagQueryDto', () => { + describe('Valid requests', () => { + test.each([ + { + name: 'with "true"', + request: { + withUsageCount: 'true', + }, + }, + { + name: 'with "false"', + request: { + withUsageCount: 'false', + }, + }, + ])('should pass validation for withUsageCount $name', ({ request }) => { + const result = RetrieveTagQueryDto.safeParse(request); + expect(result.success).toBe(true); + }); + }); + + describe('Invalid requests', () => { + test.each([ + { + name: 'with number', + request: { + withUsageCount: 1, + }, + expectedErrorPath: ['withUsageCount'], + }, + { + name: 'with boolean (true) ', + request: { + withUsageCount: true, + }, + expectedErrorPath: ['withUsageCount'], + }, + { + name: 'with boolean (false)', + request: { + withUsageCount: false, + }, + expectedErrorPath: ['withUsageCount'], + }, + { + name: 'with invalid string', + request: { + withUsageCount: 'invalid', + }, + expectedErrorPath: ['withUsageCount'], + }, + ])('should fail validation for withUsageCount $name', ({ request, expectedErrorPath }) => { + const result = RetrieveTagQueryDto.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/tag/create-or-update-tag-request.dto.ts b/packages/@n8n/api-types/src/dto/tag/create-or-update-tag-request.dto.ts new file mode 100644 index 0000000000..f916e1665f --- /dev/null +++ b/packages/@n8n/api-types/src/dto/tag/create-or-update-tag-request.dto.ts @@ -0,0 +1,6 @@ +import { z } from 'zod'; +import { Z } from 'zod-class'; + +export class CreateOrUpdateTagRequestDto extends Z.class({ + name: z.string().trim().min(1), +}) {} diff --git a/packages/@n8n/api-types/src/dto/tag/retrieve-tag-query.dto.ts b/packages/@n8n/api-types/src/dto/tag/retrieve-tag-query.dto.ts new file mode 100644 index 0000000000..b7b53a0d6f --- /dev/null +++ b/packages/@n8n/api-types/src/dto/tag/retrieve-tag-query.dto.ts @@ -0,0 +1,7 @@ +import { Z } from 'zod-class'; + +import { booleanFromString } from '../../schemas/booleanFromString'; + +export class RetrieveTagQueryDto extends Z.class({ + withUsageCount: booleanFromString.optional().default('false'), +}) {} 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/dto/workflows/__tests__/import-workflow-from-url.dto.test.ts b/packages/@n8n/api-types/src/dto/workflows/__tests__/import-workflow-from-url.dto.test.ts new file mode 100644 index 0000000000..3c17e873e0 --- /dev/null +++ b/packages/@n8n/api-types/src/dto/workflows/__tests__/import-workflow-from-url.dto.test.ts @@ -0,0 +1,63 @@ +import { ImportWorkflowFromUrlDto } from '../import-workflow-from-url.dto'; + +describe('ImportWorkflowFromUrlDto', () => { + describe('Valid requests', () => { + test('should validate $name', () => { + const result = ImportWorkflowFromUrlDto.safeParse({ + url: 'https://example.com/workflow.json', + }); + expect(result.success).toBe(true); + }); + }); + + describe('Invalid requests', () => { + test.each([ + { + name: 'invalid URL (not ending with .json)', + url: 'https://example.com/workflow', + expectedErrorPath: ['url'], + }, + { + name: 'invalid URL (missing protocol)', + url: 'example.com/workflow.json', + expectedErrorPath: ['url'], + }, + { + name: 'invalid URL (not a URL)', + url: 'not-a-url', + expectedErrorPath: ['url'], + }, + { + name: 'missing URL', + url: undefined, + expectedErrorPath: ['url'], + }, + { + name: 'null URL', + url: null, + expectedErrorPath: ['url'], + }, + { + name: 'invalid URL (ends with .json but not a valid URL)', + url: 'not-a-url.json', + expectedErrorPath: ['url'], + }, + { + name: 'valid URL with query parameters', + url: 'https://example.com/workflow.json?param=value', + }, + { + name: 'valid URL with fragments', + url: 'https://example.com/workflow.json#section', + }, + ])('should fail validation for $name', ({ url, expectedErrorPath }) => { + const result = ImportWorkflowFromUrlDto.safeParse({ url }); + + expect(result.success).toBe(false); + + if (expectedErrorPath) { + expect(result.error?.issues[0].path).toEqual(expectedErrorPath); + } + }); + }); +}); diff --git a/packages/@n8n/api-types/src/dto/workflows/import-workflow-from-url.dto.ts b/packages/@n8n/api-types/src/dto/workflows/import-workflow-from-url.dto.ts new file mode 100644 index 0000000000..310e620fde --- /dev/null +++ b/packages/@n8n/api-types/src/dto/workflows/import-workflow-from-url.dto.ts @@ -0,0 +1,6 @@ +import { z } from 'zod'; +import { Z } from 'zod-class'; + +export class ImportWorkflowFromUrlDto extends Z.class({ + url: z.string().url().endsWith('.json'), +}) {} 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..5620689af0 100644 --- a/packages/@n8n/api-types/src/index.ts +++ b/packages/@n8n/api-types/src/index.ts @@ -7,3 +7,20 @@ 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'; + +export type { + ProjectType, + ProjectIcon, + ProjectRole, + ProjectRelation, +} from './schemas/project.schema'; + +export { + type SourceControlledFile, + SOURCE_CONTROL_FILE_LOCATION, + SOURCE_CONTROL_FILE_STATUS, + SOURCE_CONTROL_FILE_TYPE, +} from './schemas/source-controlled-file.schema'; diff --git a/packages/@n8n/api-types/src/push/execution.ts b/packages/@n8n/api-types/src/push/execution.ts index 78c0b34a36..b87bb67d0f 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; }; }; @@ -42,11 +52,23 @@ type NodeExecuteAfter = { executionId: string; nodeName: string; data: ITaskData; + + /** + * When a worker relays updates about a manual execution to main, if the + * payload size is above a limit, we send only a placeholder to the client. + * Later we fetch the entire execution data and fill in any placeholders. + * + * When sending a placheolder, we also send the number of output items, so + * the client knows ahead of time how many items are there, to prevent the + * items count from jumping up when the execution finishes. + */ + itemCount?: number; }; }; 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/__tests__/project.schema.test.ts b/packages/@n8n/api-types/src/schemas/__tests__/project.schema.test.ts new file mode 100644 index 0000000000..9a1cf47414 --- /dev/null +++ b/packages/@n8n/api-types/src/schemas/__tests__/project.schema.test.ts @@ -0,0 +1,105 @@ +import { + projectNameSchema, + projectTypeSchema, + projectIconSchema, + projectRoleSchema, + projectRelationSchema, +} from '../project.schema'; + +describe('project.schema', () => { + describe('projectNameSchema', () => { + test.each([ + { name: 'valid name', value: 'My Project', expected: true }, + { name: 'empty name', value: '', expected: false }, + { name: 'name too long', value: 'a'.repeat(256), expected: false }, + ])('should validate $name', ({ value, expected }) => { + const result = projectNameSchema.safeParse(value); + expect(result.success).toBe(expected); + }); + }); + + describe('projectTypeSchema', () => { + test.each([ + { name: 'valid type: personal', value: 'personal', expected: true }, + { name: 'valid type: team', value: 'team', expected: true }, + { name: 'invalid type', value: 'invalid', expected: false }, + ])('should validate $name', ({ value, expected }) => { + const result = projectTypeSchema.safeParse(value); + expect(result.success).toBe(expected); + }); + }); + + describe('projectIconSchema', () => { + test.each([ + { + name: 'valid emoji icon', + value: { type: 'emoji', value: '🚀' }, + expected: true, + }, + { + name: 'valid icon', + value: { type: 'icon', value: 'blah' }, + expected: true, + }, + { + name: 'invalid icon type', + value: { type: 'invalid', value: '🚀' }, + expected: false, + }, + { + name: 'empty icon value', + value: { type: 'emoji', value: '' }, + expected: false, + }, + ])('should validate $name', ({ value, expected }) => { + const result = projectIconSchema.safeParse(value); + expect(result.success).toBe(expected); + }); + }); + + describe('projectRoleSchema', () => { + test.each([ + { name: 'valid role: project:personalOwner', value: 'project:personalOwner', expected: true }, + { name: 'valid role: project:admin', value: 'project:admin', expected: true }, + { name: 'valid role: project:editor', value: 'project:editor', expected: true }, + { name: 'valid role: project:viewer', value: 'project:viewer', expected: true }, + { name: 'invalid role', value: 'invalid-role', expected: false }, + ])('should validate $name', ({ value, expected }) => { + const result = projectRoleSchema.safeParse(value); + expect(result.success).toBe(expected); + }); + }); + + describe('projectRelationSchema', () => { + test.each([ + { + name: 'valid relation', + value: { userId: 'user-123', role: 'project:admin' }, + expected: true, + }, + { + name: 'invalid userId type', + value: { userId: 123, role: 'project:admin' }, + expected: false, + }, + { + name: 'invalid role', + value: { userId: 'user-123', role: 'invalid-role' }, + expected: false, + }, + { + name: 'missing userId', + value: { role: 'project:admin' }, + expected: false, + }, + { + name: 'missing role', + value: { userId: 'user-123' }, + expected: false, + }, + ])('should validate $name', ({ value, expected }) => { + const result = projectRelationSchema.safeParse(value); + expect(result.success).toBe(expected); + }); + }); +}); 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/api-types/src/schemas/project.schema.ts b/packages/@n8n/api-types/src/schemas/project.schema.ts new file mode 100644 index 0000000000..11c6cc2b37 --- /dev/null +++ b/packages/@n8n/api-types/src/schemas/project.schema.ts @@ -0,0 +1,26 @@ +import { z } from 'zod'; + +export const projectNameSchema = z.string().min(1).max(255); + +export const projectTypeSchema = z.enum(['personal', 'team']); +export type ProjectType = z.infer; + +export const projectIconSchema = z.object({ + type: z.enum(['emoji', 'icon']), + value: z.string().min(1), +}); +export type ProjectIcon = z.infer; + +export const projectRoleSchema = z.enum([ + 'project:personalOwner', // personalOwner is only used for personal projects + 'project:admin', + 'project:editor', + 'project:viewer', +]); +export type ProjectRole = z.infer; + +export const projectRelationSchema = z.object({ + userId: z.string(), + role: projectRoleSchema, +}); +export type ProjectRelation = z.infer; diff --git a/packages/@n8n/api-types/src/schemas/source-controlled-file.schema.ts b/packages/@n8n/api-types/src/schemas/source-controlled-file.schema.ts new file mode 100644 index 0000000000..31f2db8f92 --- /dev/null +++ b/packages/@n8n/api-types/src/schemas/source-controlled-file.schema.ts @@ -0,0 +1,34 @@ +import { z } from 'zod'; + +const FileTypeSchema = z.enum(['credential', 'workflow', 'tags', 'variables', 'file']); +export const SOURCE_CONTROL_FILE_TYPE = FileTypeSchema.Values; + +const FileStatusSchema = z.enum([ + 'new', + 'modified', + 'deleted', + 'created', + 'renamed', + 'conflicted', + 'ignored', + 'staged', + 'unknown', +]); +export const SOURCE_CONTROL_FILE_STATUS = FileStatusSchema.Values; + +const FileLocationSchema = z.enum(['local', 'remote']); +export const SOURCE_CONTROL_FILE_LOCATION = FileLocationSchema.Values; + +export const SourceControlledFileSchema = z.object({ + file: z.string(), + id: z.string(), + name: z.string(), + type: FileTypeSchema, + status: FileStatusSchema, + location: FileLocationSchema, + conflict: z.boolean(), + updatedAt: z.string(), + pushed: z.boolean().optional(), +}); + +export type SourceControlledFile = z.infer; diff --git a/packages/@n8n/benchmark/package.json b/packages/@n8n/benchmark/package.json index abb8514c3d..f42bd7c508 100644 --- a/packages/@n8n/benchmark/package.json +++ b/packages/@n8n/benchmark/package.json @@ -1,6 +1,6 @@ { "name": "@n8n/n8n-benchmark", - "version": "1.8.0", + "version": "1.9.0", "description": "Cli for running benchmark tests for n8n", "main": "dist/index", "scripts": { diff --git a/packages/@n8n/benchmark/scripts/n8n-setups/postgres/docker-compose.yml b/packages/@n8n/benchmark/scripts/n8n-setups/postgres/docker-compose.yml index 3cc08227c1..5241971325 100644 --- a/packages/@n8n/benchmark/scripts/n8n-setups/postgres/docker-compose.yml +++ b/packages/@n8n/benchmark/scripts/n8n-setups/postgres/docker-compose.yml @@ -32,6 +32,9 @@ services: - DB_TYPE=postgresdb - DB_POSTGRESDB_HOST=postgres - DB_POSTGRESDB_PASSWORD=password + # Task Runner config + - N8N_RUNNERS_ENABLED=true + - N8N_RUNNERS_MODE=internal ports: - 5678:5678 volumes: 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 c686f581b3..723c10f00e 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 @@ -50,6 +50,9 @@ services: - DB_TYPE=postgresdb - DB_POSTGRESDB_HOST=postgres - DB_POSTGRESDB_PASSWORD=password + # Task Runner config + - N8N_RUNNERS_ENABLED=true + - N8N_RUNNERS_MODE=internal command: worker volumes: - ${RUN_DIR}/n8n-worker1:/n8n @@ -82,6 +85,9 @@ services: - DB_TYPE=postgresdb - DB_POSTGRESDB_HOST=postgres - DB_POSTGRESDB_PASSWORD=password + # Task Runner config + - N8N_RUNNERS_ENABLED=true + - N8N_RUNNERS_MODE=internal command: worker volumes: - ${RUN_DIR}/n8n-worker2:/n8n @@ -117,6 +123,9 @@ services: - DB_TYPE=postgresdb - DB_POSTGRESDB_HOST=postgres - DB_POSTGRESDB_PASSWORD=password + # Task Runner config + - N8N_RUNNERS_ENABLED=true + - N8N_RUNNERS_MODE=internal volumes: - ${RUN_DIR}/n8n-main2:/n8n depends_on: @@ -154,7 +163,9 @@ services: - DB_TYPE=postgresdb - DB_POSTGRESDB_HOST=postgres - DB_POSTGRESDB_PASSWORD=password - + # Task Runner config + - N8N_RUNNERS_ENABLED=true + - N8N_RUNNERS_MODE=internal volumes: - ${RUN_DIR}/n8n-main1:/n8n depends_on: diff --git a/packages/@n8n/benchmark/scripts/n8n-setups/scaling-single-main/docker-compose.yml b/packages/@n8n/benchmark/scripts/n8n-setups/scaling-single-main/docker-compose.yml index fe9e3a26c0..ecefddf5b0 100644 --- a/packages/@n8n/benchmark/scripts/n8n-setups/scaling-single-main/docker-compose.yml +++ b/packages/@n8n/benchmark/scripts/n8n-setups/scaling-single-main/docker-compose.yml @@ -48,6 +48,9 @@ services: - DB_TYPE=postgresdb - DB_POSTGRESDB_HOST=postgres - DB_POSTGRESDB_PASSWORD=password + # Task Runner config + - N8N_RUNNERS_ENABLED=true + - N8N_RUNNERS_MODE=internal command: worker volumes: - ${RUN_DIR}/n8n-worker1:/n8n @@ -78,6 +81,9 @@ services: - DB_TYPE=postgresdb - DB_POSTGRESDB_HOST=postgres - DB_POSTGRESDB_PASSWORD=password + # Task Runner config + - N8N_RUNNERS_ENABLED=true + - N8N_RUNNERS_MODE=internal command: worker volumes: - ${RUN_DIR}/n8n-worker2:/n8n @@ -109,6 +115,9 @@ services: - DB_TYPE=postgresdb - DB_POSTGRESDB_HOST=postgres - DB_POSTGRESDB_PASSWORD=password + # Task Runner config + - N8N_RUNNERS_ENABLED=true + - N8N_RUNNERS_MODE=internal ports: - 5678:5678 volumes: diff --git a/packages/@n8n/benchmark/scripts/n8n-setups/sqlite/docker-compose.yml b/packages/@n8n/benchmark/scripts/n8n-setups/sqlite/docker-compose.yml index 20ec0067fa..37e1424cfd 100644 --- a/packages/@n8n/benchmark/scripts/n8n-setups/sqlite/docker-compose.yml +++ b/packages/@n8n/benchmark/scripts/n8n-setups/sqlite/docker-compose.yml @@ -14,6 +14,9 @@ services: - N8N_USER_FOLDER=/n8n - DB_SQLITE_POOL_SIZE=3 - DB_SQLITE_ENABLE_WAL=true + # Task Runner config + - N8N_RUNNERS_ENABLED=true + - N8N_RUNNERS_MODE=internal ports: - 5678:5678 volumes: 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/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 @@