diff --git a/.github/workflows/ci-pull-requests.yml b/.github/workflows/ci-pull-requests.yml index 5895e8d82b9e3..e6d4cdabd741c 100644 --- a/.github/workflows/ci-pull-requests.yml +++ b/.github/workflows/ci-pull-requests.yml @@ -112,12 +112,13 @@ jobs: name: E2E [Electron/Node 16] uses: ./.github/workflows/e2e-reusable.yml with: - branch: ${{ github.event.pull_request.head.ref }} + branch: ${{ github.event.pull_request.base.ref }} user: ${{ github.event.inputs.user || 'PR User' }} spec: ${{ github.event.inputs.spec || 'e2e/0-smoke.cy.ts' }} - run-env: base:16.18.1 + run-env: browsers:node16.18.0-chrome107-ff106-edge record: false parallel: false + pr_number: ${{ github.event.number }} containers: '[1]' secrets: CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} diff --git a/.github/workflows/e2e-reusable.yml b/.github/workflows/e2e-reusable.yml index c982d84e1fd1b..45b842044ba8f 100644 --- a/.github/workflows/e2e-reusable.yml +++ b/.github/workflows/e2e-reusable.yml @@ -7,7 +7,6 @@ on: description: 'GitHub branch to test.' required: false type: string - default: 'master' user: description: 'User who kicked this off.' required: false @@ -43,10 +42,19 @@ on: required: false default: '[1, 2, 3, 4, 5, 6, 7, 8]' type: string + pr_number: + description: 'PR number to run tests for.' + required: false + type: number secrets: CYPRESS_RECORD_KEY: description: 'Cypress record key.' required: true + outputs: + tests_passed: + description: 'True if all E2E tests passed, otherwise false' + value: ${{ jobs.check_testing_matrix.outputs.all_tests_passed }} + jobs: # single job that generates and outputs a common id @@ -74,6 +82,12 @@ jobs: repository: n8n-io/n8n ref: ${{ inputs.branch }} + - name: Checkout PR + if: ${{ inputs.pr_number }} + run: | + git fetch origin pull/${{ inputs.pr_number }}/head + git checkout FETCH_HEAD + - name: Setup pnpm uses: pnpm/action-setup@v2.2.4 with: @@ -109,13 +123,21 @@ jobs: strategy: fail-fast: false matrix: - containers: ${{ fromJSON(inputs.containers) }} + # If spec is not e2e/* then we run only one container to prevent + # running the same tests multiple times + containers: ${{ fromJSON( inputs.spec == 'e2e/*' && inputs.containers || '[1]' ) }} steps: - uses: actions/checkout@v3 with: repository: n8n-io/n8n ref: ${{ inputs.branch }} + - name: Checkout PR + if: ${{ inputs.pr_number }} + run: | + git fetch origin pull/${{ inputs.pr_number }}/head + git checkout FETCH_HEAD + - name: Setup pnpm uses: pnpm/action-setup@v2.2.4 @@ -135,9 +157,9 @@ jobs: install: false start: pnpm start wait-on: 'http://localhost:5678' - wait-on-timeout: 120 # + wait-on-timeout: 120 record: ${{ inputs.record }} - parallel: ${{ inputs.parallel }} + parallel: ${{ fromJSON( inputs.spec == 'e2e/*' && inputs.parallel || false ) }} # We have to provide custom ci-build-id key to make sure that this workflow could be run multiple times # in the same parent workflow ci-build-id: ${{ needs.prepare.outputs.uuid }} @@ -148,3 +170,22 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} E2E_TESTS: true COMMIT_INFO_MESSAGE: š³ ${{ inputs.branch }} š„ļø ${{ inputs.run-env }} š¤ ${{ inputs.user }} šļø ${{ inputs.spec }} + + # Check if all tests passed and set the output variable + check_testing_matrix: + runs-on: ubuntu-latest + needs: [testing] + outputs: + all_tests_passed: ${{ steps.all_tests_passed.outputs.result }} + steps: + - name: Check all tests passed + id: all_tests_passed + run: | + success=true + for status in ${{ needs.testing.result }}; do + if [ $status != "success" ]; then + success=false + break + fi + done + echo "::set-output name=result::$success" diff --git a/.github/workflows/e2e-tests-pr.yml b/.github/workflows/e2e-tests-pr.yml index e73f02aa804a8..648383750d1ab 100644 --- a/.github/workflows/e2e-tests-pr.yml +++ b/.github/workflows/e2e-tests-pr.yml @@ -26,7 +26,7 @@ jobs: if: always() steps: - name: E2E success comment - if: ${{!contains(github.event.pull_request.labels.*.name, 'community') || needs.run-e2e-tests.result == 'success' }} + if: ${{!contains(github.event.pull_request.labels.*.name, 'community') && needs.run-e2e-tests.outputs.tests_passed == 'true' }} uses: peter-evans/create-or-update-comment@v3 with: issue-number: ${{ github.event.pull_request.number }} diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml index 162f31c8c0532..25fb683adcf22 100644 --- a/.github/workflows/release-publish.yml +++ b/.github/workflows/release-publish.yml @@ -16,7 +16,7 @@ jobs: permissions: contents: write - timeout-minutes: 10 + timeout-minutes: 60 steps: - name: Checkout @@ -41,12 +41,19 @@ jobs: run: | echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc pnpm publish -r --publish-branch ${{github.event.pull_request.base.ref}} --access public --tag rc + npm dist-tag rm n8n rc - - name: Create Release + - name: Create a Release on GitHub uses: ncipollo/release-action@v1 with: commit: ${{github.event.pull_request.base.ref}} tag: 'n8n@${{env.RELEASE}}' + prerelease: true + makeLatest: false + + - name: Trigger a release note + continue-on-error: true + run: curl -u docsWorkflows:${{ secrets.N8N_WEBHOOK_DOCS_PASSWORD }} --request GET 'https://internal.users.n8n.cloud/webhook/trigger-release-note' --header 'Content-Type:application/json' --data '{"version":"${{env.RELEASE}}"}' - name: Merge Release into 'master' run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index 1853cad3e7e2f..38c765cb5e6fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,154 @@ +# [0.227.0](https://github.com/n8n-io/n8n/compare/n8n@0.226.0...n8n@0.227.0) (2023-05-03) + + +### Bug Fixes + +* **AWS S3 Node:** Fix File upload, and add node tests ([#6153](https://github.com/n8n-io/n8n/issues/6153)) ([deb4c04](https://github.com/n8n-io/n8n/commit/deb4c04f346f8e5985b5f6c3f3a3e929fde13e5b)) +* **Compression Node:** Fix issue with decompression failing with uppercase extensions ([#6098](https://github.com/n8n-io/n8n/issues/6098)) ([aa59329](https://github.com/n8n-io/n8n/commit/aa593298365eabd6eb1dda9fe3f06e7eae7c5ea9)) +* **core:** Account for nodes with renamable content ([#6109](https://github.com/n8n-io/n8n/issues/6109)) ([c99f158](https://github.com/n8n-io/n8n/commit/c99f158120b3c1ffca1718be337afc73d6ec9e65)) +* **core:** Assign Unknown Error only if message or description not present in error ([8aedc03](https://github.com/n8n-io/n8n/commit/8aedc03ddad3f83ffd2569be5b61710f27d2f672)) +* **core:** Avoid using `Object.keys` on Buffer and other non-plain objects ([#6131](https://github.com/n8n-io/n8n/issues/6131)) ([a3aba83](https://github.com/n8n-io/n8n/commit/a3aba835a15a8a32acc1e1ff0b972007df2b2b34)) +* **core:** Better error message in Webhook node when using the POST method ([a0dd17e](https://github.com/n8n-io/n8n/commit/a0dd17e1151e668b95dc57367a0b100d00913ea3)) +* **core:** Better errors for common status codes fix ([700cc39](https://github.com/n8n-io/n8n/commit/700cc39cbc7da3c70513ff586dc97319456308ae)) +* **core:** Fix `hasOwnProperty` on augmented objects ([#6124](https://github.com/n8n-io/n8n/issues/6124)) ([206b6b9](https://github.com/n8n-io/n8n/commit/206b6b90b860ceaab58b9bdd5ff20ffc741c13fa)) +* **core:** Fix bug running addUserActivatedColumn migration on MariaDB ([#6157](https://github.com/n8n-io/n8n/issues/6157)) ([570790e](https://github.com/n8n-io/n8n/commit/570790ed0c9521e09b6414bc1da2c596f17ff072)) +* **core:** Fix canceled execution status ([#6142](https://github.com/n8n-io/n8n/issues/6142)) ([839a56a](https://github.com/n8n-io/n8n/commit/839a56a682674baf44d5beececdbe677d18c0d89)) +* **core:** Improve saml endpoints and audit events ([#6107](https://github.com/n8n-io/n8n/issues/6107)) ([c0b1cdd](https://github.com/n8n-io/n8n/commit/c0b1cddc91fe199377c301f02f230827f231ba73)) +* **core:** Remove SAML config metadataUrl if XML metadata is set directly ([#6143](https://github.com/n8n-io/n8n/issues/6143)) ([25fe14b](https://github.com/n8n-io/n8n/commit/25fe14be56482477c00a360914730b25c9028443)) +* **core:** Skip auth for controllers/routes that don't use the `Authorized` decorator, or use `Authorized('none')` ([#6106](https://github.com/n8n-io/n8n/issues/6106)) ([59aee22](https://github.com/n8n-io/n8n/commit/59aee2270bdc0c8360aa534237b7f6015d382346)) +* Correctly allow sharees to test credential when opening the modal ([#6111](https://github.com/n8n-io/n8n/issues/6111)) ([2e73f4a](https://github.com/n8n-io/n8n/commit/2e73f4abd04ba7ab929b0fce57bf12651a0a2e49)) +* **Date & Time Node:** Numbers conversions fix ([14f7114](https://github.com/n8n-io/n8n/commit/14f71146e21026721fc9d5883bb9d73d38afcf8c)) +* **editor:** Change execution list tab loader design ([#6120](https://github.com/n8n-io/n8n/issues/6120)) ([188ef04](https://github.com/n8n-io/n8n/commit/188ef042cd58b9194dadef4cc68deb3510688c26)) +* **editor:** Disable changing of email and pw when SAML login enabled ([#6104](https://github.com/n8n-io/n8n/issues/6104)) ([3e9ecd9](https://github.com/n8n-io/n8n/commit/3e9ecd939742df8d8ced9179aaa26b081139befa)) +* **editor:** Fix `Show details` summary ([#6113](https://github.com/n8n-io/n8n/issues/6113)) ([90a62cc](https://github.com/n8n-io/n8n/commit/90a62ccfb5b4a959d72336d284ad4ac3b17af582)) +* **editor:** Fix copy selection behavior ([#6112](https://github.com/n8n-io/n8n/issues/6112)) ([1607aeb](https://github.com/n8n-io/n8n/commit/1607aeb9f94700793d58604ea4f89c5555d43981)) +* **editor:** Fix cropped off completions docstrings ([#6129](https://github.com/n8n-io/n8n/issues/6129)) ([85e8145](https://github.com/n8n-io/n8n/commit/85e8145439f89e76fe5fe3a659430c03738d6e2b)) +* **editor:** Fix focus jumping when using chrome autofill ([#6140](https://github.com/n8n-io/n8n/issues/6140)) ([c63181b](https://github.com/n8n-io/n8n/commit/c63181b3171040c3dd3051c2a1358aea0af6bae0)) +* **editor:** Fix missing `Stop Listening` button ([#6125](https://github.com/n8n-io/n8n/issues/6125)) ([20a72bb](https://github.com/n8n-io/n8n/commit/20a72bb28b981e9c8d12dd6398d843b39d80daac)) +* **editor:** Fix quote handling on dollar-sign variable completions ([#6128](https://github.com/n8n-io/n8n/issues/6128)) ([51f5990](https://github.com/n8n-io/n8n/commit/51f59905591fa492017fc3ced46601eeca5fb057)) +* **editor:** Fix sidebar button styling ([#6138](https://github.com/n8n-io/n8n/issues/6138)) ([a72a511](https://github.com/n8n-io/n8n/commit/a72a5112f34a0d8ab248f687c74b758c8db6729c)) +* **editor:** Fix unique names for node duplication ([#6134](https://github.com/n8n-io/n8n/issues/6134)) ([71ae6c6](https://github.com/n8n-io/n8n/commit/71ae6c66ef32ba86edf0bb9cdb9f24a6d40ee80c)) +* **editor:** Fix unscrollable node settings ([#6133](https://github.com/n8n-io/n8n/issues/6133)) ([c8ff368](https://github.com/n8n-io/n8n/commit/c8ff368fc7be58e7c42746f7e7a4c5f6a4149d3e)) +* **editor:** Loading state for executions tab ([#6100](https://github.com/n8n-io/n8n/issues/6100)) ([4cbb05b](https://github.com/n8n-io/n8n/commit/4cbb05b0017ffd77eca51fc5b9c5c4868515a60d)) +* **editor:** Remove pagination from binary data output ([#6093](https://github.com/n8n-io/n8n/issues/6093)) ([c6e665a](https://github.com/n8n-io/n8n/commit/c6e665a975958c433d7991c057a3e4be644daff1)) +* **editor:** Restrict `[empty]` in parameter input hint to zero-length string ([#6003](https://github.com/n8n-io/n8n/issues/6003)) ([8862e1e](https://github.com/n8n-io/n8n/commit/8862e1e7df0be62ab3746b70e613ffd2ab26bc4a)) +* **editor:** Show error in RLC if credentials are not set ([#6108](https://github.com/n8n-io/n8n/issues/6108)) ([2c240a0](https://github.com/n8n-io/n8n/commit/2c240a0e4ecd9157dca612d98a8a7c68a65a9909)) +* **HTTP Request Node:** Add description for 'Specify Body' option ([#6114](https://github.com/n8n-io/n8n/issues/6114)) ([af097ae](https://github.com/n8n-io/n8n/commit/af097ae22c7e87918ada2527c6a2fe62cb8f318a)) +* **HTTP Request Node:** Always lowercase headers ([983e6e1](https://github.com/n8n-io/n8n/commit/983e6e124eb9557eec55c5f2e2b834a926243955)) +* **Mattermost Node:** Fix base url trailing slash error ([#6097](https://github.com/n8n-io/n8n/issues/6097)) ([25a386d](https://github.com/n8n-io/n8n/commit/25a386dd70df516090e622d921a79456fc7d16e3)) +* **Merge Node:** Do not error if expected key is missing ([d219af7](https://github.com/n8n-io/n8n/commit/d219af75cf37c603c34b1ca5851cafd4a490889c)) +* Prevent displaying an endless timer in the execution list for finished executions ([#6137](https://github.com/n8n-io/n8n/issues/6137)) ([701105e](https://github.com/n8n-io/n8n/commit/701105edcf5284f276fe146d8267e1a5560ab186)) +* Prevent invocations of 'GET /rest/license' from returning an error when ephemeral licenses are used ([#6154](https://github.com/n8n-io/n8n/issues/6154)) ([a3d26ef](https://github.com/n8n-io/n8n/commit/a3d26eff79013642865fa59078732526850b96a6)) +* **Slack Node:** Restore ability to send text in addition of blocks or attachments ([8669f95](https://github.com/n8n-io/n8n/commit/8669f95736797da4f3efd33468cdeac5d28667b0)) + + +### Features + +* **core:** Add notice to alert users a new version is available ([cb497fb](https://github.com/n8n-io/n8n/commit/cb497fbbecdba670d5121fa2c6eaf7c66d8a8a38)) +* **editor:** Add support for `loadOptionsDependsOn` to RLC ([#6101](https://github.com/n8n-io/n8n/issues/6101)) ([b17d5f9](https://github.com/n8n-io/n8n/commit/b17d5f9aa086bf408e8450244460ada57de0d7c3)) +* **editor:** Add version controls settings (WIP) ([#6036](https://github.com/n8n-io/n8n/issues/6036)) ([0c9ce3a](https://github.com/n8n-io/n8n/commit/0c9ce3a2ec9487b4eb9130651927e91dcd0f85af)) +* **Item Lists Node:** Split out items work on objects as well as arrays ([c65ac03](https://github.com/n8n-io/n8n/commit/c65ac0336821868c289adc55abab40017b1856da)) +* **Microsoft Excel 365 Node:** Overhaul ([5364a2d](https://github.com/n8n-io/n8n/commit/5364a2dff32e05147b8e9dd392038eb36791e5dc)) + + + +## [0.226.2](https://github.com/n8n-io/n8n/compare/n8n@0.226.1...n8n@0.226.2) (2023-05-03) + + +### Bug Fixes + +* **core:** Fix bug running addUserActivatedColumn migration on MariaDB ([#6157](https://github.com/n8n-io/n8n/issues/6157)) ([aa8e96d](https://github.com/n8n-io/n8n/commit/aa8e96dd6b19f105a957da71a5c4d7ab5caecc01)) + + + +## [0.226.1](https://github.com/n8n-io/n8n/compare/n8n@0.226.0...n8n@0.226.1) (2023-05-02) + + +### Bug Fixes + +* **Compression Node:** Fix issue with decompression failing with uppercase extensions ([#6098](https://github.com/n8n-io/n8n/issues/6098)) ([7136500](https://github.com/n8n-io/n8n/commit/71365002daa71c5fa5e68a5bb373ee200a05b7b9)) +* **core:** Account for nodes with renamable content ([#6109](https://github.com/n8n-io/n8n/issues/6109)) ([b561d46](https://github.com/n8n-io/n8n/commit/b561d463265831f3cd370ec99982847f2bddca41)) +* **core:** Fix `hasOwnProperty` on augmented objects ([#6124](https://github.com/n8n-io/n8n/issues/6124)) ([2f015c0](https://github.com/n8n-io/n8n/commit/2f015c0f153785384b009e71bf4994e18b5d06b8)) +* **core:** Fix canceled execution status ([#6142](https://github.com/n8n-io/n8n/issues/6142)) ([1796101](https://github.com/n8n-io/n8n/commit/1796101fed03d04906f2341a0488b60b7b5bf71c)) +* **core:** Skip auth for controllers/routes that don't use the `Authorized` decorator, or use `Authorized('none')` ([#6106](https://github.com/n8n-io/n8n/issues/6106)) ([9d44991](https://github.com/n8n-io/n8n/commit/9d44991b2a8c9384e2bf32204ac47a4ecb0131be)) +* Correctly allow sharees to test credential when opening the modal ([#6111](https://github.com/n8n-io/n8n/issues/6111)) ([240bb47](https://github.com/n8n-io/n8n/commit/240bb47e8e2cd99c56b8837079073bbf2bf5f687)) +* **Date & Time Node:** Numbers conversions fix ([e11e7cd](https://github.com/n8n-io/n8n/commit/e11e7cd603a145c83c36f6a6deb821a56ccabd6f)) +* **editor:** Change execution list tab loader design ([#6120](https://github.com/n8n-io/n8n/issues/6120)) ([ffc033f](https://github.com/n8n-io/n8n/commit/ffc033ff8ff391be09da8dbd62f8eb06a94f6cb0)) +* **editor:** Fix `Show details` summary ([#6113](https://github.com/n8n-io/n8n/issues/6113)) ([e12bafb](https://github.com/n8n-io/n8n/commit/e12bafb9473393dd9e139d0e0a4a21241b417645)) +* **editor:** Fix copy selection behavior ([#6112](https://github.com/n8n-io/n8n/issues/6112)) ([0efd94a](https://github.com/n8n-io/n8n/commit/0efd94a875fe9e3cac5da3421effcf6c8f6eaae8)) +* **editor:** Fix cropped off completions docstrings ([#6129](https://github.com/n8n-io/n8n/issues/6129)) ([06594cc](https://github.com/n8n-io/n8n/commit/06594cc36f1a0d4069e556d6dbb4e268a78301c6)) +* **editor:** Fix missing `Stop Listening` button ([#6125](https://github.com/n8n-io/n8n/issues/6125)) ([dcbd2d2](https://github.com/n8n-io/n8n/commit/dcbd2d2bc1c59ca8bbe6a802a2a8f482b3d20d74)) +* **editor:** Fix quote handling on dollar-sign variable completions ([#6128](https://github.com/n8n-io/n8n/issues/6128)) ([c23ad35](https://github.com/n8n-io/n8n/commit/c23ad3502d0a8c4bcbd60cce2fc13a91981fb119)) +* **editor:** Fix sidebar button styling ([#6138](https://github.com/n8n-io/n8n/issues/6138)) ([d3f4bc1](https://github.com/n8n-io/n8n/commit/d3f4bc1859104f11d2cf38ef3f3405d62c88bc6d)) +* **editor:** Fix unique names for node duplication ([#6134](https://github.com/n8n-io/n8n/issues/6134)) ([48a4068](https://github.com/n8n-io/n8n/commit/48a4068d7ec1ddf5d5a4d10620dce1c89cb8fa34)) +* **editor:** Fix unscrollable node settings ([#6133](https://github.com/n8n-io/n8n/issues/6133)) ([f762f16](https://github.com/n8n-io/n8n/commit/f762f16afb7b9ea54e29950c06133a292dc87b3f)) +* **editor:** Loading state for executions tab ([#6100](https://github.com/n8n-io/n8n/issues/6100)) ([2e12c50](https://github.com/n8n-io/n8n/commit/2e12c50477014c57fa665ba36f48ff658cf7ee94)) +* **editor:** Remove pagination from binary data output ([#6093](https://github.com/n8n-io/n8n/issues/6093)) ([7b7d9de](https://github.com/n8n-io/n8n/commit/7b7d9de7586905b2740541e7f0289bc78f8f2ad7)) +* **editor:** Show error in RLC if credentials are not set ([#6108](https://github.com/n8n-io/n8n/issues/6108)) ([5bf3400](https://github.com/n8n-io/n8n/commit/5bf3400ca7ffa04bb51214bc539435b847614cbb)) +* **HTTP Request Node:** Add description for 'Specify Body' option ([#6114](https://github.com/n8n-io/n8n/issues/6114)) ([69b6ba8](https://github.com/n8n-io/n8n/commit/69b6ba85202c5a28052a56c041b880bfa9fcf0a0)) +* **HTTP Request Node:** Always lowercase headers ([31c56a1](https://github.com/n8n-io/n8n/commit/31c56a12f273d5569ed3ac4d72cf74c90cc24b31)) +* **Mattermost Node:** Fix base url trailing slash error ([#6097](https://github.com/n8n-io/n8n/issues/6097)) ([788fda1](https://github.com/n8n-io/n8n/commit/788fda1b7dbf29d7a7e614a959ac3630b2a6559f)) +* **Merge Node:** Do not error if expected key is missing ([8b59564](https://github.com/n8n-io/n8n/commit/8b59564776ebaf2c54b9e2dae5b77109895da883)) +* Prevent displaying an endless timer in the execution list for finished executions ([#6137](https://github.com/n8n-io/n8n/issues/6137)) ([2672896](https://github.com/n8n-io/n8n/commit/2672896c8e2de7d1f92899a392a3f2b3f60aaef3)) +* **Slack Node:** Restore ability to send text in addition of blocks or attachments ([625d672](https://github.com/n8n-io/n8n/commit/625d6729b4158fbc811941ce45819f32372e6265)) + + + +# [0.226.0](https://github.com/n8n-io/n8n/compare/n8n@0.225.0...n8n@0.226.0) (2023-04-26) + + +### Bug Fixes + +* **Code Node:** Update vm2 to address CVE-2023-30547 ([#6039](https://github.com/n8n-io/n8n/issues/6039)) ([8268f23](https://github.com/n8n-io/n8n/commit/8268f235abf5277480c215ea953fc3db1c275c95)) +* **core:** Improve domain and url matching for extractDomain and extractUrl ([#6010](https://github.com/n8n-io/n8n/issues/6010)) ([33fb732](https://github.com/n8n-io/n8n/commit/33fb73217dca68244c93296f1a4be96cc83e4480)) +* **core:** Serialize dates and regexps when reading from augmented objects ([#6086](https://github.com/n8n-io/n8n/issues/6086)) ([a4eb46a](https://github.com/n8n-io/n8n/commit/a4eb46acc178533e3c63fedcf0d884a5b66bae28)) +* **core:** Skip license activation when instance was already activated ([#6064](https://github.com/n8n-io/n8n/issues/6064)) ([eaf7090](https://github.com/n8n-io/n8n/commit/eaf70909197ed511efe9add956eb5e4f78b27e20)) +* **editor:** Clean up demo and template callouts from workflows page ([#6023](https://github.com/n8n-io/n8n/issues/6023)) ([4ee5083](https://github.com/n8n-io/n8n/commit/4ee508385ab5ac379925b315006a2d9389183751)) +* **editor:** Fix memory leak in Node Detail View by correctly unsubscribing from event buses ([#6021](https://github.com/n8n-io/n8n/issues/6021)) ([0970ec0](https://github.com/n8n-io/n8n/commit/0970ec066d8f80082f49f0b0f8987b95392102bf)) +* **editor:** Fix typo in SSO upgrade link ([#6031](https://github.com/n8n-io/n8n/issues/6031)) ([9b59f1d](https://github.com/n8n-io/n8n/commit/9b59f1df9c358d3677b4cbc3e80a73af03b7981d)) +* **editor:** Resolve expressions for grandparent nodes ([#5859](https://github.com/n8n-io/n8n/issues/5859)) ([a19d444](https://github.com/n8n-io/n8n/commit/a19d4447ac38e40d1fd1da83beb6c20fb7b2d0ed)) +* **editor:** SettingsSidebar should disconnect from push when navigating away ([#6025](https://github.com/n8n-io/n8n/issues/6025)) ([41660d9](https://github.com/n8n-io/n8n/commit/41660d9e281432b87d875c98992bfaf54d25b37f)) +* **editor:** Update LDAP and Log streaming paywalls ([#6069](https://github.com/n8n-io/n8n/issues/6069)) ([8a3b3e5](https://github.com/n8n-io/n8n/commit/8a3b3e53e1ac0a2a1864b42b24ab46f25253a9d3)) +* **editor:** Update SSO upgrade link ([#6016](https://github.com/n8n-io/n8n/issues/6016)) ([953198e](https://github.com/n8n-io/n8n/commit/953198e092a44029805ff85e4607355444ea8b2c)) +* **Notion Node:** Update credential test to not require user permissions ([#6022](https://github.com/n8n-io/n8n/issues/6022)) ([a68330f](https://github.com/n8n-io/n8n/commit/a68330ff66744551270f40399a6fd7fe330e6f27)) + + +### Features + +* **core:** Add license:info command ([#6047](https://github.com/n8n-io/n8n/issues/6047)) ([ab12d3e](https://github.com/n8n-io/n8n/commit/ab12d3e3278745b290de82c16f358841b20850b4)) +* **core:** Add SSH key generation ([#6006](https://github.com/n8n-io/n8n/issues/6006)) ([71ed1f4](https://github.com/n8n-io/n8n/commit/71ed1f410c5a80f35ecaf913a5522b7788998695)) +* **core:** Add support for digestAuth to httpRequest and declarative style ([#5676](https://github.com/n8n-io/n8n/issues/5676)) ([62f993c](https://github.com/n8n-io/n8n/commit/62f993c84f6ecf2f4d0431d505ba18a0253bd244)) +* **core:** Manage version control settings ([#6079](https://github.com/n8n-io/n8n/issues/6079)) ([f3b4701](https://github.com/n8n-io/n8n/commit/f3b470186360dc3c3a3df599f0a9740183e86696)) +* **core:** Upgrade google-timezones-json to use the correct timezone for Sao Paulo ([#6042](https://github.com/n8n-io/n8n/issues/6042)) ([b8cb5d7](https://github.com/n8n-io/n8n/commit/b8cb5d7f0b11fb138d5c4714bcc9e1d9b6366d76)), closes [#2647](https://github.com/n8n-io/n8n/issues/2647) +* **editor:** Add disable template experiment ([#5963](https://github.com/n8n-io/n8n/issues/5963)) ([a74284b](https://github.com/n8n-io/n8n/commit/a74284bac387338e870dc81ac33748af55521274)) +* **editor:** Add SQL editor support ([#5517](https://github.com/n8n-io/n8n/issues/5517)) ([70aaf24](https://github.com/n8n-io/n8n/commit/70aaf2478461d9ceea98bc91dc935493fd6dbe24)) +* **editor:** Enhance Node Creator actions view ([#5954](https://github.com/n8n-io/n8n/issues/5954)) ([390841b](https://github.com/n8n-io/n8n/commit/390841bbf0fdd4d536101593711a6658ea2784e4)) +* **editor:** Version control (WIP) ([#6013](https://github.com/n8n-io/n8n/issues/6013)) ([0e0a064](https://github.com/n8n-io/n8n/commit/0e0a064fa7ae54a8a6b695bd4bb19da71334fea0)) +* **editor:** Version control paywall (WIP) ([#6030](https://github.com/n8n-io/n8n/issues/6030)) ([ef79b03](https://github.com/n8n-io/n8n/commit/ef79b03f38460a20658c62fd35dbcaf6d266582f)) +* **Google BigQuery Node:** Node improvements ([#4877](https://github.com/n8n-io/n8n/issues/4877)) ([9817a15](https://github.com/n8n-io/n8n/commit/9817a15da4c80425fb77273ed7c9acbe020f0f48)) + + + +## [0.225.2](https://github.com/n8n-io/n8n/compare/n8n@0.225.1...n8n@0.225.2) (2023-04-25) + + +### Bug Fixes + +* **core:** Upgrade google-timezones-json to use the correct timezone for Sao Paulo ([#6042](https://github.com/n8n-io/n8n/issues/6042)) ([f93fd5a](https://github.com/n8n-io/n8n/commit/f93fd5aba2583e1c62efe8dcb423559413cf9c6a)), closes [#2647](https://github.com/n8n-io/n8n/issues/2647) +* **Code Node:** Update vm2 to address CVE-2023-30547 ([#6039](https://github.com/n8n-io/n8n/issues/6039)) ([f1ca4e2](https://github.com/n8n-io/n8n/commit/f1ca4e232865a0ee6ac178b11515de115b0bfd09)) + + + +# [0.224.4](https://github.com/n8n-io/n8n/compare/n8n@0.224.2...n8n@0.224.4) (2023-04-24) + + +### Bug Fixes + +* **core:** Upgrade google-timezones-json to use the correct timezone for Sao Paulo ([#6042](https://github.com/n8n-io/n8n/issues/6042)) ([c23a592](https://github.com/n8n-io/n8n/commit/c23a5923f8df787758f756c529518c9e8b93b96e)), closes [#2647](https://github.com/n8n-io/n8n/issues/2647) +* **Code Node:** Update vm2 to address CVE-2023-30547 ([#6039](https://github.com/n8n-io/n8n/issues/6039)) ([6cd15bd](https://github.com/n8n-io/n8n/commit/6cd15bd889b6eec3b050cfe3c3b190707a09e898)) + + + ## [0.225.1](https://github.com/n8n-io/n8n/compare/n8n@0.225.0...n8n@0.225.1) (2023-04-20) diff --git a/cypress.config.js b/cypress.config.js index d1451fcd06fc2..b6cea71083e09 100644 --- a/cypress.config.js +++ b/cypress.config.js @@ -11,7 +11,7 @@ module.exports = defineConfig({ }, defaultCommandTimeout: 10000, requestTimeout: 12000, - numTestsKeptInMemory: 0, + numTestsKeptInMemory: 2, experimentalMemoryManagement: true, e2e: { baseUrl: BASE_URL, @@ -35,8 +35,13 @@ module.exports = defineConfig({ return null } }, - 'enable-feature': (feature) => - fetch(BASE_URL + `/e2e/enable-feature/${feature}`, { method: 'POST' }), + 'set-feature': ({ feature, enabled }) => { + return fetch(BASE_URL + `/e2e/feature/${feature}`, { + method: 'PATCH', + body: JSON.stringify({ enabled }), + headers: { 'Content-Type': 'application/json' } + }) + }, }); }, }, diff --git a/cypress/e2e/11-inline-expression-editor.cy.ts b/cypress/e2e/11-inline-expression-editor.cy.ts index de5594a4f429a..88fea311d92cb 100644 --- a/cypress/e2e/11-inline-expression-editor.cy.ts +++ b/cypress/e2e/11-inline-expression-editor.cy.ts @@ -69,6 +69,6 @@ describe('Inline expression editor', () => { WorkflowPage.getters.inlineExpressionEditorInput().clear(); WorkflowPage.getters.inlineExpressionEditorInput().type('{{'); WorkflowPage.getters.inlineExpressionEditorInput().type('$parameter["operation"]'); - WorkflowPage.getters.inlineExpressionEditorOutput().contains(/^getAll$/); + WorkflowPage.getters.inlineExpressionEditorOutput().contains(/^get$/); }); }); diff --git a/cypress/e2e/13-pinning.cy.ts b/cypress/e2e/13-pinning.cy.ts index 6f1c328fd9c6b..c278231017f9d 100644 --- a/cypress/e2e/13-pinning.cy.ts +++ b/cypress/e2e/13-pinning.cy.ts @@ -1,6 +1,6 @@ import { HTTP_REQUEST_NODE_NAME, - MANUAL_TRIGGER_NODE_DISPLAY_NAME, + MANUAL_TRIGGER_NODE_NAME, PIPEDRIVE_NODE_NAME, SET_NODE_NAME, } from '../constants'; @@ -75,7 +75,7 @@ describe('Data pinning', () => { }); it('Should be able to reference paired items in a node located before pinned data', () => { - workflowPage.actions.addInitialNodeToCanvas(MANUAL_TRIGGER_NODE_DISPLAY_NAME); + workflowPage.actions.addInitialNodeToCanvas(MANUAL_TRIGGER_NODE_NAME); workflowPage.actions.addNodeToCanvas(HTTP_REQUEST_NODE_NAME, true, true); ndv.actions.setPinnedData([{ http: 123 }]); ndv.actions.close(); diff --git a/cypress/e2e/14-mapping.cy.ts b/cypress/e2e/14-mapping.cy.ts index 69487f699ea06..afab75190e641 100644 --- a/cypress/e2e/14-mapping.cy.ts +++ b/cypress/e2e/14-mapping.cy.ts @@ -205,7 +205,7 @@ describe('Data mapping', () => { 'have.text', `{{ $node['${SCHEDULE_TRIGGER_NODE_NAME}'].json.input[0].count }} {{ $node['${SCHEDULE_TRIGGER_NODE_NAME}'].json.input }}`, ); - ndv.getters.parameterExpressionPreview('value').should('not.exist'); + ndv.getters.parameterExpressionPreview('value').should('have.text', ' '); ndv.actions.selectInputNode('Set'); diff --git a/cypress/e2e/17-sharing.cy.ts b/cypress/e2e/17-sharing.cy.ts index 1a8b0a3d007ba..ea61b5a67eb14 100644 --- a/cypress/e2e/17-sharing.cy.ts +++ b/cypress/e2e/17-sharing.cy.ts @@ -164,4 +164,12 @@ describe('Sharing', () => { workflowsPage.getters.workflowCard('Workflow W2').click(); workflowPage.actions.executeWorkflow(); }); + + it('should automatically test C2 when opened by U2 sharee', () => { + cy.signin(users[0]); + + cy.visit(credentialsPage.url); + credentialsPage.getters.credentialCard('Credential C2').click(); + credentialsModal.getters.testSuccessTag().should('be.visible'); + }); }); diff --git a/cypress/e2e/22-user-activation-modal.cy.ts b/cypress/e2e/22-user-activation-modal.cy.ts index 6b8f23be2fcf5..eb598abff60c6 100644 --- a/cypress/e2e/22-user-activation-modal.cy.ts +++ b/cypress/e2e/22-user-activation-modal.cy.ts @@ -57,6 +57,7 @@ describe('User activation survey', () => { cy.wait(['@getWorkflows', '@getCredentials', '@getActive']); userActivationSurveyModal.getters.modalContainer().should('be.visible'); userActivationSurveyModal.getters.feedbackInput().should('be.visible'); + userActivationSurveyModal.getters.skipButton().should('be.visible'); userActivationSurveyModal.getters.feedbackInput().type('testing'); userActivationSurveyModal.getters.feedbackInput().should('have.value', 'testing'); userActivationSurveyModal.getters.sendFeedbackButton().click(); diff --git a/cypress/e2e/23-variables.cy.ts b/cypress/e2e/23-variables.cy.ts index 8dc16bb8e967d..ce78f8fbe3db5 100644 --- a/cypress/e2e/23-variables.cy.ts +++ b/cypress/e2e/23-variables.cy.ts @@ -16,6 +16,7 @@ describe('Variables', () => { }); it('should show the unlicensed action box when the feature is disabled', () => { + cy.disableFeature('feat:variables'); cy.signin({ email, password }); cy.visit(variablesPage.url); @@ -30,7 +31,10 @@ describe('Variables', () => { beforeEach(() => { cy.signin({ email, password }); + cy.intercept('GET', '/rest/variables').as('loadVariables'); + cy.visit(variablesPage.url); + cy.wait(['@loadVariables', '@loadSettings']); }); it('should show the licensed action box when the feature is enabled', () => { diff --git a/cypress/e2e/24-ndv-paired-item.cy.ts b/cypress/e2e/24-ndv-paired-item.cy.ts new file mode 100644 index 0000000000000..05f5dd8581a71 --- /dev/null +++ b/cypress/e2e/24-ndv-paired-item.cy.ts @@ -0,0 +1,305 @@ +import { WorkflowPage, NDV } from '../pages'; +import { v4 as uuid } from 'uuid'; + +const workflowPage = new WorkflowPage(); +const ndv = new NDV(); + +describe('NDV', () => { + before(() => { + cy.resetAll(); + cy.skipSetup(); + + }); + beforeEach(() => { + workflowPage.actions.visit(); + workflowPage.actions.renameWorkflow(uuid()); + workflowPage.actions.saveWorkflowOnButtonClick(); + }); + + it('maps paired input and output items', () => { + cy.fixture('Test_workflow_5.json').then((data) => { + cy.get('body').paste(JSON.stringify(data)); + }); + workflowPage.actions.zoomToFit(); + + workflowPage.actions.executeWorkflow(); + + workflowPage.actions.openNode('Item Lists'); + + ndv.getters.inputPanel().contains('6 items').should('exist'); + ndv.getters.outputPanel().contains('6 items').should('exist'); + + ndv.actions.switchInputMode('Table'); + ndv.actions.switchOutputMode('Table'); + + // input to output + ndv.getters.inputTableRow(1) + .should('exist') + .invoke('attr', 'data-test-id') + .should('equal', 'hovering-item'); + + ndv.getters.inputTableRow(1) + .realHover(); + ndv.getters.outputTableRow(4) + .invoke('attr', 'data-test-id') + .should('equal', 'hovering-item'); + + ndv.getters.inputTableRow(2) + .realHover(); + ndv.getters.outputTableRow(2) + .invoke('attr', 'data-test-id') + .should('equal', 'hovering-item'); + + ndv.getters.inputTableRow(3) + .realHover(); + ndv.getters.outputTableRow(6) + .invoke('attr', 'data-test-id') + .should('equal', 'hovering-item'); + + // output to input + ndv.getters.outputTableRow(1) + .realHover(); + ndv.getters.inputTableRow(4) + .invoke('attr', 'data-test-id') + .should('equal', 'hovering-item'); + + ndv.getters.outputTableRow(4) + .realHover(); + ndv.getters.inputTableRow(1) + .invoke('attr', 'data-test-id') + .should('equal', 'hovering-item'); + + ndv.getters.outputTableRow(2) + .realHover(); + ndv.getters.inputTableRow(2) + .invoke('attr', 'data-test-id') + .should('equal', 'hovering-item'); + + ndv.getters.outputTableRow(6) + .realHover(); + ndv.getters.inputTableRow(3) + .invoke('attr', 'data-test-id') + .should('equal', 'hovering-item'); + + ndv.getters.outputTableRow(1) + .realHover(); + ndv.getters.inputTableRow(4) + .invoke('attr', 'data-test-id') + .should('equal', 'hovering-item'); + }); + + it('maps paired input and output items based on selected input node', () => { + cy.fixture('Test_workflow_5.json').then((data) => { + cy.get('body').paste(JSON.stringify(data)); + }); + workflowPage.actions.zoomToFit(); + workflowPage.actions.executeWorkflow(); + workflowPage.actions.openNode('Set2'); + + ndv.getters.inputPanel().contains('6 items').should('exist'); + ndv.getters.outputRunSelector() + .should('exist') + .should('include.text', '2 of 2 (6 items)'); + + ndv.actions.switchInputMode('Table'); + ndv.actions.switchOutputMode('Table'); + + ndv.getters.backToCanvas().realHover(); // 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.inputTableRow(1) + .should('have.text', '1000') + .invoke('attr', 'data-test-id') + .should('equal', 'hovering-item'); + ndv.getters.inputTableRow(1).realHover(); + cy.wait(50); + ndv.getters.outputHoveringItem().should('have.text', '1000'); + ndv.getters.parameterExpressionPreview('value').should('include.text', '1000'); + + ndv.actions.selectInputNode('Item Lists'); + ndv.actions.changeOutputRunSelector('1 of 2 (6 items)'); + ndv.getters.backToCanvas().realHover(); // reset to default hover + + ndv.getters.inputTableRow(1) + .should('have.text', '1111') + .invoke('attr', 'data-test-id') + .should('equal', 'hovering-item'); + ndv.getters.inputTableRow(1).realHover(); + cy.wait(50); + ndv.getters.outputHoveringItem().should('have.text', '1111'); + ndv.getters.parameterExpressionPreview('value').should('include.text', '1111'); + }); + + it('maps paired input and output items based on selected run', () => { + cy.fixture('Test_workflow_5.json').then((data) => { + cy.get('body').paste(JSON.stringify(data)); + }); + workflowPage.actions.zoomToFit(); + workflowPage.actions.executeWorkflow(); + workflowPage.actions.openNode('Set3'); + + ndv.getters.inputRunSelector() + .should('exist') + .find('input') + .should('include.value', '2 of 2 (6 items)'); + ndv.getters.outputRunSelector() + .should('exist') + .find('input') + .should('include.value', '2 of 2 (6 items)'); + + ndv.actions.switchInputMode('Table'); + ndv.actions.switchOutputMode('Table'); + + ndv.actions.changeOutputRunSelector('1 of 2 (6 items)'); + ndv.getters.inputRunSelector().find('input') + .should('include.value', '1 of 2 (6 items)'); + ndv.getters.outputRunSelector().find('input') + .should('include.value', '1 of 2 (6 items)'); + + ndv.getters.inputTableRow(1) + .should('have.text', '1111') + .invoke('attr', 'data-test-id') + .should('equal', 'hovering-item'); + ndv.getters.outputTableRow(1) + .should('have.text', '1111') + .realHover(); + + ndv.getters.outputTableRow(3) + .should('have.text', '4444') + .realHover(); + ndv.getters.inputTableRow(3) + .should('have.text', '4444') + .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') + .realHover(); + ndv.getters.outputTableRow(1) + .should('have.text', '1000') + .invoke('attr', 'data-test-id') + .should('equal', 'hovering-item'); + + ndv.getters.outputTableRow(3) + .should('have.text', '2000') + .realHover(); + ndv.getters.inputTableRow(3) + .should('have.text', '2000') + .invoke('attr', 'data-test-id') + .should('equal', 'hovering-item'); + }); + + it('resolves expression with default item when input node is not parent, while still pairing items', () => { + cy.fixture('Test_workflow_5.json').then((data) => { + cy.get('body').paste(JSON.stringify(data)); + }); + workflowPage.actions.zoomToFit(); + workflowPage.actions.executeWorkflow(); + workflowPage.actions.openNode('Set2'); + + ndv.getters.inputPanel().contains('6 items').should('exist'); + ndv.getters.outputRunSelector() + .should('exist') + .should('include.text', '2 of 2 (6 items)'); + + ndv.actions.switchInputMode('Table'); + ndv.actions.switchOutputMode('Table'); + + ndv.getters.backToCanvas().realHover(); // reset to default hover + ndv.getters.inputTableRow(1) + .should('have.text', '1111') + .invoke('attr', 'data-test-id') + .should('equal', 'hovering-item'); + ndv.getters.inputTableRow(1).realHover(); + ndv.getters.outputHoveringItem().should('not.exist'); + ndv.getters.parameterExpressionPreview('value').should('include.text', '1111'); + + ndv.actions.selectInputNode('Code1'); + ndv.getters.inputTableRow(1).realHover(); + ndv.getters.inputTableRow(1) + .should('have.text', '1000') + .invoke('attr', 'data-test-id') + .should('equal', 'hovering-item'); + ndv.getters.outputTableRow(1) + .should('have.text', '1000'); + ndv.getters.parameterExpressionPreview('value').should('include.text', '1000'); + + ndv.actions.selectInputNode('Code'); + + ndv.getters.inputTableRow(1).realHover(); + ndv.getters.inputTableRow(1) + .should('have.text', '6666') + .invoke('attr', 'data-test-id') + .should('equal', 'hovering-item'); + ndv.getters.outputHoveringItem().should('not.exist'); + ndv.getters.parameterExpressionPreview('value').should('include.text', '1000'); + + ndv.actions.selectInputNode('When clicking'); + + ndv.getters.inputTableRow(1).realHover(); + ndv.getters.inputTableRow(1).should('have.text', "This is an item, but it's empty.").realHover(); + ndv.getters.outputHoveringItem().should('have.length', 6); + ndv.getters.parameterExpressionPreview('value').should('include.text', '1000'); + }); + + it('can pair items between input and output across branches and runs', () => { + cy.fixture('Test_workflow_5.json').then((data) => { + cy.get('body').paste(JSON.stringify(data)); + }); + workflowPage.actions.zoomToFit(); + workflowPage.actions.executeWorkflow(); + workflowPage.actions.openNode('IF'); + + ndv.actions.switchInputMode('Table'); + ndv.actions.switchOutputMode('Table'); + + ndv.actions.switchOutputBranch('False Branch (2 items)'); + ndv.getters.outputTableRow(1) + .should('have.text', '8888') + .realHover(); + ndv.getters.inputTableRow(5) + .should('have.text', '8888') + .invoke('attr', 'data-test-id') + .should('equal', 'hovering-item'); + + ndv.getters.outputTableRow(2) + .should('have.text', '9999') + .realHover(); + ndv.getters.inputTableRow(6) + .should('have.text', '9999') + .invoke('attr', 'data-test-id') + .should('equal', 'hovering-item'); + + ndv.actions.close(); + workflowPage.actions.openNode('Set5'); + ndv.getters.outputTableRow(1) + .should('have.text', '8888') + .realHover(); + ndv.getters.inputHoveringItem().should('not.exist'); + + ndv.getters.inputTableRow(1) + .should('have.text', '1111') + .realHover(); + ndv.getters.outputHoveringItem().should('not.exist'); + + ndv.actions.switchIntputBranch('False Branch'); + ndv.getters.inputTableRow(1) + .should('have.text', '8888') + .realHover(); + ndv.getters.outputHoveringItem().should('have.text', '8888'); + + ndv.actions.changeOutputRunSelector('1 of 2 (4 items)') + ndv.getters.outputTableRow(1) + .should('have.text', '1111') + .realHover(); + // todo there's a bug here need to fix ADO-534 + // ndv.getters.outputHoveringItem().should('not.exist'); + }); +}); diff --git a/cypress/e2e/25-stickies.cy.ts b/cypress/e2e/25-stickies.cy.ts new file mode 100644 index 0000000000000..0746fddc0326a --- /dev/null +++ b/cypress/e2e/25-stickies.cy.ts @@ -0,0 +1,262 @@ +import { WorkflowPage as WorkflowPageClass } from '../pages/workflow'; + +const workflowPage = new WorkflowPageClass(); + +function checkStickiesStyle( top: number, left: number, height: number, width: number, zIndex?: number) { + workflowPage.getters.stickies().should(($el) => { + expect($el).to.have.css('top', `${top}px`); + expect($el).to.have.css('left', `${left}px`); + expect($el).to.have.css('height', `${height}px`); + expect($el).to.have.css('width', `${width}px`); + if (zIndex) { + expect($el).to.have.css('z-index', `${zIndex}`); + } + }); +} + +describe('Canvas Actions', () => { + beforeEach(() => { + cy.resetAll(); + cy.skipSetup(); + workflowPage.actions.visit(); + + cy.window().then( + (win) => { + // @ts-ignore + win.preventNodeViewBeforeUnload = true; + }, + ); + }); + + + it('adds sticky to canvas with default text and position', () => { + workflowPage.getters.addStickyButton().should('not.be.visible'); + + addDefaultSticky() + workflowPage.getters.stickies().eq(0) + .should('have.text', 'Iām a note\nDouble click to edit me. Guide\n') + .find('a').contains('Guide').should('have.attr', 'href'); + }); + + it('drags sticky around to top left corner', () => { + // used to caliberate move sticky function + addDefaultSticky(); + moveSticky({ top: 0, left: 0 }); + }); + + it('drags sticky around and position/size are saved correctly', () => { + addDefaultSticky(); + moveSticky({ top: 500, left: 500 }); + + workflowPage.actions.saveWorkflowOnButtonClick(); + cy.wait('@createWorkflow'); + + cy.reload(); + cy.waitForLoad(); + + stickyShouldBePositionedCorrectly({ top: 500, left: 500 }); + }); + + it('deletes sticky', () => { + workflowPage.actions.addSticky(); + workflowPage.getters.stickies().should('have.length', 1) + + workflowPage.actions.deleteSticky(); + + workflowPage.getters.stickies().should('have.length', 0) + }); + + it('edits sticky and updates content as markdown', () => { + workflowPage.actions.addSticky(); + + workflowPage.getters.stickies() + .should('have.text', 'Iām a note\nDouble click to edit me. Guide\n') + + workflowPage.getters.stickies().dblclick(); + workflowPage.actions.editSticky('# hello world \n ## text text'); + workflowPage.getters.stickies().find('h1').should('have.text', 'hello world'); + workflowPage.getters.stickies().find('h2').should('have.text', 'text text'); + }); + + it('expands/shrinks sticky from the right edge', () => { + addDefaultSticky(); + + moveSticky({ top: 200, left: 200 }); + + dragRightEdge({ left: 200, top: 200, height: 160, width: 240 }, 100); + dragRightEdge({ left: 200, top: 200, height: 160, width: 240 }, -50); + }); + + it('expands/shrinks sticky from the left edge', () => { + addDefaultSticky(); + + moveSticky({ left: 600, top: 200 }); + cy.drag('[data-test-id="sticky"] [data-dir="left"]', [100, 100]); + checkStickiesStyle(140, 510, 160, 150); + + cy.drag('[data-test-id="sticky"] [data-dir="left"]', [-50, -50]); + checkStickiesStyle(140, 466, 160, 194); + }); + + it('expands/shrinks sticky from the top edge', () => { + workflowPage.actions.addSticky(); + cy.drag('[data-test-id="sticky"]', [100, 100]); // move away from canvas button + checkStickiesStyle(360, 620, 160, 240); + + cy.drag('[data-test-id="sticky"] [data-dir="top"]', [100, 100]); + checkStickiesStyle(440, 620, 80, 240); + + cy.drag('[data-test-id="sticky"] [data-dir="top"]', [-50, -50]); + checkStickiesStyle(384, 620, 136, 240); + }); + + it('expands/shrinks sticky from the bottom edge', () => { + workflowPage.actions.addSticky(); + cy.drag('[data-test-id="sticky"]', [100, 100]); // move away from canvas button + checkStickiesStyle(360, 620, 160, 240); + + cy.drag('[data-test-id="sticky"] [data-dir="bottom"]', [100, 100]); + checkStickiesStyle(360, 620, 254, 240); + + cy.drag('[data-test-id="sticky"] [data-dir="bottom"]', [-50, -50]); + checkStickiesStyle(360, 620, 198, 240); + }); + + it('expands/shrinks sticky from the bottom right edge', () => { + workflowPage.actions.addSticky(); + cy.drag('[data-test-id="sticky"]', [-100, -100]); // move away from canvas button + checkStickiesStyle(160, 420, 160, 240); + + cy.drag('[data-test-id="sticky"] [data-dir="bottomRight"]', [100, 100]); + checkStickiesStyle(160, 420, 254, 346); + + cy.drag('[data-test-id="sticky"] [data-dir="bottomRight"]', [-50, -50]); + checkStickiesStyle(160, 420, 198, 302); + }); + + it('expands/shrinks sticky from the top right edge', () => { + addDefaultSticky(); + + cy.drag('[data-test-id="sticky"] [data-dir="topRight"]', [100, 100]); + checkStickiesStyle(420, 400, 80, 346); + + cy.drag('[data-test-id="sticky"] [data-dir="topRight"]', [-50, -50]); + checkStickiesStyle(364, 400, 136, 302); + }); + + it('expands/shrinks sticky from the top left edge, and reach min height/width', () => { + addDefaultSticky(); + + cy.drag('[data-test-id="sticky"] [data-dir="topLeft"]', [100, 100]); + checkStickiesStyle(420, 490, 80, 150); + + cy.drag('[data-test-id="sticky"] [data-dir="topLeft"]', [-150, -150]); + checkStickiesStyle(264, 346, 236, 294); + }); + + it('sets sticky behind node', () => { + workflowPage.actions.addInitialNodeToCanvas('Manual Trigger'); + addDefaultSticky(); + + cy.drag('[data-test-id="sticky"] [data-dir="topLeft"]', [-150, -150]); + checkStickiesStyle(184, 256, 316, 384, -121); + + workflowPage.getters.canvasNodes().eq(0) + .should(($el) => { + expect($el).to.have.css('z-index', 'auto'); + }); + + workflowPage.actions.addSticky(); + workflowPage.getters.stickies().eq(0) + .should(($el) => { + expect($el).to.have.css('z-index', '-121'); + }); + workflowPage.getters.stickies().eq(1) + .should(($el) => { + expect($el).to.have.css('z-index', '-38'); + }); + + cy.drag('[data-test-id="sticky"] [data-dir="topLeft"]', [-200, -200], { index: 1 }); + workflowPage.getters.stickies().eq(0) + .should(($el) => { + expect($el).to.have.css('z-index', '-121'); + }); + + workflowPage.getters.stickies().eq(1) + .should(($el) => { + expect($el).to.have.css('z-index', '-158'); + }); + + }); +}); + +type Position = { + top: number; + left: number; +}; + +type BoundingBox = { + height: number; + width: number; + top: number; + left: number; +} + +function dragRightEdge(curr: BoundingBox, move: number) { + workflowPage.getters.stickies().first().then(($el) => { + const { left, top, height, width } = curr; + cy.drag(`[data-test-id="sticky"] [data-dir="right"]`, [left + width + move, 0], { abs: true }); + stickyShouldBePositionedCorrectly({ top, left }); + stickyShouldHaveCorrectSize([height, width * 1.5 + move]); + }); +} + +function shouldHaveOneSticky() { + workflowPage.getters.stickies().should('have.length', 1); +} + +function shouldBeInDefaultLocation() { + workflowPage.getters.stickies().eq(0).should(($el) => { + expect($el).to.have.css('height', '160px'); + expect($el).to.have.css('width', '240px'); + }) +} + +function shouldHaveDefaultSize() { + workflowPage.getters.stickies().should(($el) => { + expect($el).to.have.css('height', '160px'); + expect($el).to.have.css('width', '240px'); + }) +} + +function addDefaultSticky() { + workflowPage.actions.addSticky(); + shouldHaveOneSticky(); + shouldHaveDefaultSize(); + shouldBeInDefaultLocation(); +} + +function stickyShouldBePositionedCorrectly(position: Position) { + const yOffset = -60; + const xOffset = -180; + workflowPage.getters.stickies() + .should(($el) => { + expect($el).to.have.css('top', `${yOffset + position.top}px`); + expect($el).to.have.css('left', `${xOffset + position.left}px`); + }); +} + +function stickyShouldHaveCorrectSize(size: [number, number]) { + const yOffset = 0; + const xOffset = 0; + workflowPage.getters.stickies() + .should(($el) => { + expect($el).to.have.css('height', `${yOffset + size[0]}px`); + expect($el).to.have.css('width', `${xOffset + size[1]}px`); + }); +} + +function moveSticky(target: Position) { + cy.drag('[data-test-id="sticky"]', [target.left, target.top], { abs: true }); + stickyShouldBePositionedCorrectly(target); +} diff --git a/cypress/e2e/26-resource-locator.cy.ts b/cypress/e2e/26-resource-locator.cy.ts new file mode 100644 index 0000000000000..e0ba34d70aa20 --- /dev/null +++ b/cypress/e2e/26-resource-locator.cy.ts @@ -0,0 +1,58 @@ +import { WorkflowPage, NDV, CredentialsModal } from '../pages'; + +const workflowPage = new WorkflowPage(); +const ndv = new NDV(); +const credentialsModal = new CredentialsModal(); + +const NO_CREDENTIALS_MESSAGE = 'Please add your credential'; +const INVALID_CREDENTIALS_MESSAGE = 'Please check your credential'; + +describe('Resource Locator', () => { + before(() => { + cy.resetAll(); + cy.skipSetup(); + }); + + beforeEach(() => { + workflowPage.actions.visit(); + }); + + it('should render both RLC components in google sheets', () => { + workflowPage.actions.addInitialNodeToCanvas('Manual'); + workflowPage.actions.addNodeToCanvas('Google Sheets', true, true); + ndv.getters.resourceLocator('documentId').should('be.visible'); + ndv.getters.resourceLocator('sheetName').should('be.visible'); + }); + + it('should show appropriate error when credentials are not set', () => { + workflowPage.actions.addInitialNodeToCanvas('Manual'); + workflowPage.actions.addNodeToCanvas('Google Sheets', true, true); + ndv.getters.resourceLocator('documentId').should('be.visible'); + ndv.getters.resourceLocatorInput('documentId').click(); + ndv.getters.resourceLocatorErrorMessage().should('contain', NO_CREDENTIALS_MESSAGE); + }); + + it('should show appropriate error when credentials are not valid', () => { + workflowPage.actions.addInitialNodeToCanvas('Manual'); + workflowPage.actions.addNodeToCanvas('Google Sheets', true, true); + workflowPage.getters.nodeCredentialsSelect().click(); + // Add oAuth credentials + workflowPage.getters.nodeCredentialsSelect().find('li').last().click(); + credentialsModal.getters.credentialsEditModal().should('be.visible'); + credentialsModal.getters.credentialAuthTypeRadioButtons().should('have.length', 2); + credentialsModal.getters.credentialAuthTypeRadioButtons().first().click(); + credentialsModal.actions.fillCredentialsForm(); + cy.get('.el-message-box').find('button').contains('Close').click(); + ndv.getters.resourceLocatorInput('documentId').click(); + ndv.getters.resourceLocatorErrorMessage().should('contain', INVALID_CREDENTIALS_MESSAGE); + }); + + it('should reset resource locator when dependent field is changed', () => { + workflowPage.actions.addInitialNodeToCanvas('Manual'); + workflowPage.actions.addNodeToCanvas('Google Sheets', true, true); + ndv.actions.setRLCValue('documentId', '123'); + ndv.actions.setRLCValue('sheetName', '123'); + ndv.actions.setRLCValue('documentId', '321'); + ndv.getters.resourceLocatorInput('sheetName').should('have.value', ''); + }); +}); diff --git a/cypress/e2e/3-default-owner.cy.ts b/cypress/e2e/3-default-owner.cy.ts index 1871dd9c6c950..6aba65180c5b4 100644 --- a/cypress/e2e/3-default-owner.cy.ts +++ b/cypress/e2e/3-default-owner.cy.ts @@ -84,11 +84,12 @@ describe('Default owner', () => { }); it('should be able to setup instance and migrate workflows and credentials', () => { - cy.setup({ email, firstName, lastName, password }); + cy.setup({ email, firstName, lastName, password }, true); messageBox.getters.content().should('contain.text', '1 existing workflow and 1 credential'); messageBox.actions.confirm(); + cy.wait('@setupRequest'); cy.url().should('include', settingsUsersPage.url); settingsSidebar.actions.back(); diff --git a/cypress/e2e/4-node-creator.cy.ts b/cypress/e2e/4-node-creator.cy.ts index 0057afd93c839..0ad0306cb6b93 100644 --- a/cypress/e2e/4-node-creator.cy.ts +++ b/cypress/e2e/4-node-creator.cy.ts @@ -58,8 +58,8 @@ describe('Node Creator', () => { nodeCreatorFeature.getters.getCreatorItem('On app event').click(); nodeCreatorFeature.getters.searchBar().find('input').clear().type('edit image'); - nodeCreatorFeature.getters.getCreatorItem('Results in other categories (1)').should('exist'); - nodeCreatorFeature.getters.creatorItem().should('have.length', 2); + nodeCreatorFeature.getters.getCategoryItem('Results in other categories').should('exist'); + nodeCreatorFeature.getters.creatorItem().should('have.length', 1); nodeCreatorFeature.getters.getCreatorItem('Edit Image').should('exist'); nodeCreatorFeature.getters.searchBar().find('input').clear().type('edit image123123'); nodeCreatorFeature.getters.creatorItem().should('have.length', 0); @@ -101,7 +101,7 @@ describe('Node Creator', () => { nodeCreatorFeature.getters.activeSubcategory().should('have.text', 'FTP'); nodeCreatorFeature.getters.searchBar().find('input').clear().type('file'); // Navigate to rename action which should be the 4th item - nodeCreatorFeature.getters.searchBar().find('input').type('{downarrow} {downarrow} {downarrow} {rightarrow}'); + nodeCreatorFeature.getters.searchBar().find('input').type('{uparrow}{uparrow}{rightarrow}'); NDVModal.getters.parameterInput('operation').should('contain.text', 'Rename'); }) @@ -127,9 +127,107 @@ describe('Node Creator', () => { }) nodeCreatorFeature.getters.searchBar().find('input').clear().type(doubleActionNode); nodeCreatorFeature.getters.getCreatorItem(doubleActionNode).click(); - nodeCreatorFeature.getters.creatorItem().should('have.length', 2); + nodeCreatorFeature.getters.creatorItem().should('have.length', 4); }) + it('should have "Actions" section collapsed when opening actions view from Trigger root view', () => { + nodeCreatorFeature.actions.openNodeCreator(); + nodeCreatorFeature.getters.searchBar().find('input').clear().type('ActiveCampaign'); + nodeCreatorFeature.getters.getCreatorItem('ActiveCampaign').click(); + nodeCreatorFeature.getters.getCategoryItem('Actions').should('exist'); + nodeCreatorFeature.getters.getCategoryItem('Triggers').should('exist'); + + nodeCreatorFeature.getters.getCategoryItem('Triggers').parent().should('not.have.attr', 'data-category-collapsed'); + nodeCreatorFeature.getters.getCategoryItem('Actions').parent().should('have.attr', 'data-category-collapsed', 'true'); + nodeCreatorFeature.getters.getCategoryItem('Actions').click() + nodeCreatorFeature.getters.getCategoryItem('Actions').parent().should('not.have.attr', 'data-category-collapsed'); + }); + + it('should have "Triggers" section collapsed when opening actions view from Regular root view', () => { + nodeCreatorFeature.actions.openNodeCreator(); + nodeCreatorFeature.getters.getCreatorItem('Manually').click(); + + nodeCreatorFeature.actions.openNodeCreator(); + nodeCreatorFeature.getters.searchBar().find('input').clear().type('n8n'); + nodeCreatorFeature.getters.getCreatorItem('n8n').click(); + + nodeCreatorFeature.getters.getCategoryItem('Actions').parent().should('not.have.attr', 'data-category-collapsed'); + nodeCreatorFeature.getters.getCategoryItem('Actions').click() + nodeCreatorFeature.getters.getCategoryItem('Actions').parent().should('have.attr', 'data-category-collapsed'); + nodeCreatorFeature.getters.getCategoryItem('Triggers').parent().should('have.attr', 'data-category-collapsed'); + nodeCreatorFeature.getters.getCategoryItem('Triggers').click() + nodeCreatorFeature.getters.getCategoryItem('Triggers').parent().should('not.have.attr', 'data-category-collapsed'); + }); + + it('should show callout and two suggested nodes if node has no trigger actions', () => { + nodeCreatorFeature.actions.openNodeCreator(); + nodeCreatorFeature.getters.searchBar().find('input').clear().type('Customer Datastore (n8n training)'); + nodeCreatorFeature.getters.getCreatorItem('Customer Datastore (n8n training)').click(); + + cy.getByTestId('actions-panel-no-triggers-callout').should('be.visible'); + nodeCreatorFeature.getters.getCreatorItem('On a Schedule').should('be.visible'); + nodeCreatorFeature.getters.getCreatorItem('On a Webhook call').should('be.visible'); + }); + + it('should show intro callout if user has not made a production execution', () => { + nodeCreatorFeature.actions.openNodeCreator(); + nodeCreatorFeature.getters.searchBar().find('input').clear().type('Customer Datastore (n8n training)'); + nodeCreatorFeature.getters.getCreatorItem('Customer Datastore (n8n training)').click(); + + cy.getByTestId('actions-panel-activation-callout').should('be.visible'); + nodeCreatorFeature.getters.activeSubcategory().find('button').click(); + nodeCreatorFeature.getters.searchBar().find('input').clear() + + nodeCreatorFeature.getters.getCreatorItem('On a schedule').click(); + + // Setup 1s interval execution + cy.getByTestId('parameter-input-field').click(); + cy.getByTestId('parameter-input-field') + .find('.el-select-dropdown') + .find('.option-headline') + .contains('Seconds') + .click(); + cy.getByTestId('parameter-input-secondsInterval').clear().type('1'); + + NDVModal.actions.close(); + + nodeCreatorFeature.actions.openNodeCreator(); + nodeCreatorFeature.getters.searchBar().find('input').clear().type('Customer Datastore (n8n training)'); + nodeCreatorFeature.getters.getCreatorItem('Customer Datastore (n8n training)').click(); + nodeCreatorFeature.getters.getCreatorItem('Get All People').click(); + NDVModal.actions.close(); + + WorkflowPage.actions.saveWorkflowOnButtonClick(); + WorkflowPage.actions.activateWorkflow(); + WorkflowPage.getters.activatorSwitch().should('have.class', 'is-checked'); + + // Wait for schedule 1s execution to mark user as having made a production execution + cy.wait(1500); + cy.reload() + + // Action callout should not be visible after user has made a production execution + nodeCreatorFeature.actions.openNodeCreator(); + nodeCreatorFeature.getters.searchBar().find('input').clear().type('Customer Datastore (n8n training)'); + nodeCreatorFeature.getters.getCreatorItem('Customer Datastore (n8n training)').click(); + + cy.getByTestId('actions-panel-activation-callout').should('not.exist'); + }); + + it('should show Trigger and Actions sections during search', () => { + nodeCreatorFeature.actions.openNodeCreator(); + + nodeCreatorFeature.getters.searchBar().find('input').clear().type('Customer Datastore (n8n training)'); + nodeCreatorFeature.getters.getCreatorItem('Customer Datastore (n8n training)').click(); + + nodeCreatorFeature.getters.searchBar().find('input').clear().type('Non existent action name'); + + nodeCreatorFeature.getters.getCategoryItem('Triggers').should('be.visible'); + nodeCreatorFeature.getters.getCategoryItem('Actions').should('be.visible'); + cy.getByTestId('actions-panel-no-triggers-callout').should('be.visible'); + nodeCreatorFeature.getters.getCreatorItem('On a Schedule').should('be.visible'); + nodeCreatorFeature.getters.getCreatorItem('On a Webhook call').should('be.visible'); + }); + describe('should correctly append manual trigger for regular actions', () => { // For these sources, manual node should be added const sourcesWithAppend = [ @@ -152,6 +250,7 @@ describe('Node Creator', () => { source.handler() nodeCreatorFeature.getters.searchBar().find('input').clear().type('n8n'); nodeCreatorFeature.getters.getCreatorItem('n8n').click(); + nodeCreatorFeature.getters.getCategoryItem('Actions').click(); nodeCreatorFeature.getters.getCreatorItem('Create a credential').click(); NDVModal.actions.close(); WorkflowPage.getters.canvasNodes().should('have.length', 2); @@ -162,12 +261,14 @@ describe('Node Creator', () => { nodeCreatorFeature.getters.canvasAddButton().click(); nodeCreatorFeature.getters.searchBar().find('input').clear().type('n8n'); nodeCreatorFeature.getters.getCreatorItem('n8n').click(); + nodeCreatorFeature.getters.getCategoryItem('Actions').click(); nodeCreatorFeature.getters.getCreatorItem('Create a credential').click(); NDVModal.actions.close(); WorkflowPage.actions.deleteNode('When clicking "Execute Workflow"') WorkflowPage.getters.canvasNodePlusEndpointByName('n8n').click() nodeCreatorFeature.getters.searchBar().find('input').clear().type('n8n'); nodeCreatorFeature.getters.getCreatorItem('n8n').click(); + nodeCreatorFeature.getters.getCategoryItem('Actions').click(); nodeCreatorFeature.getters.getCreatorItem('Create a credential').click(); NDVModal.actions.close(); WorkflowPage.getters.canvasNodes().should('have.length', 2); diff --git a/cypress/e2e/5-ndv.cy.ts b/cypress/e2e/5-ndv.cy.ts index ea4533725cc10..749fb4434e506 100644 --- a/cypress/e2e/5-ndv.cy.ts +++ b/cypress/e2e/5-ndv.cy.ts @@ -15,6 +15,7 @@ describe('NDV', () => { workflowPage.actions.renameWorkflow(uuid()); workflowPage.actions.saveWorkflowOnButtonClick(); }); + it('should show up when double clicked on a node and close when Back to canvas clicked', () => { workflowPage.actions.addInitialNodeToCanvas('Manual'); workflowPage.getters.canvasNodes().first().dblclick(); @@ -162,5 +163,68 @@ 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); }) - }) + }); + + it('can link and unlink run selectors between input and output', () => { + cy.createFixtureWorkflow('Test_workflow_5.json', 'Test'); + workflowPage.actions.zoomToFit(); + workflowPage.actions.executeWorkflow(); + workflowPage.actions.openNode('Set3'); + + ndv.getters.inputRunSelector() + .should('exist') + .find('input') + .should('include.value', '2 of 2 (6 items)'); + ndv.getters.outputRunSelector() + .should('exist') + .find('input') + .should('include.value', '2 of 2 (6 items)'); + + ndv.actions.switchInputMode('Table'); + ndv.actions.switchOutputMode('Table'); + + ndv.actions.changeOutputRunSelector('1 of 2 (6 items)'); + ndv.getters.inputRunSelector() + .find('input') + .should('include.value', '1 of 2 (6 items)'); + ndv.getters.inputTbodyCell(1, 0).should('have.text', '1111'); + ndv.getters.outputTbodyCell(1, 0).should('have.text', '1111'); + + ndv.getters.inputTbodyCell(1, 0).click(); // remove tooltip + ndv.actions.changeInputRunSelector('2 of 2 (6 items)'); + ndv.getters.outputRunSelector() + .find('input') + .should('include.value', '2 of 2 (6 items)'); + + // unlink + ndv.actions.toggleOutputRunLinking(); + ndv.getters.inputTbodyCell(1, 0).click(); // remove tooltip + ndv.actions.changeOutputRunSelector('1 of 2 (6 items)'); + ndv.getters.inputRunSelector() + .should('exist') + .find('input') + .should('include.value', '2 of 2 (6 items)'); + + // link again + ndv.actions.toggleOutputRunLinking(); + ndv.getters.inputTbodyCell(1, 0).click(); // remove tooltip + ndv.getters.inputRunSelector() + .find('input') + .should('include.value', '1 of 2 (6 items)'); + + // unlink again + ndv.actions.toggleInputRunLinking(); + ndv.getters.inputTbodyCell(1, 0).click(); // remove tooltip + ndv.actions.changeInputRunSelector('2 of 2 (6 items)'); + ndv.getters.outputRunSelector() + .find('input') + .should('include.value', '1 of 2 (6 items)'); + + // link again + ndv.actions.toggleInputRunLinking(); + ndv.getters.inputTbodyCell(1, 0).click(); // remove tooltip + ndv.getters.outputRunSelector() + .find('input') + .should('include.value', '2 of 2 (6 items)'); + }); }); diff --git a/cypress/e2e/9-expression-editor-modal.cy.ts b/cypress/e2e/9-expression-editor-modal.cy.ts index 957c0505f5505..dd4e01128b134 100644 --- a/cypress/e2e/9-expression-editor-modal.cy.ts +++ b/cypress/e2e/9-expression-editor-modal.cy.ts @@ -60,6 +60,6 @@ describe('Expression editor modal', () => { it('should resolve $parameter[]', () => { WorkflowPage.getters.expressionModalInput().clear(); WorkflowPage.getters.expressionModalInput().type('{{ $parameter["operation"]'); - WorkflowPage.getters.expressionModalOutput().contains(/^getAll$/); + WorkflowPage.getters.expressionModalOutput().contains(/^get$/); }); }); diff --git a/cypress/fixtures/Test_workflow_5.json b/cypress/fixtures/Test_workflow_5.json new file mode 100644 index 0000000000000..6b87fc33b70a1 --- /dev/null +++ b/cypress/fixtures/Test_workflow_5.json @@ -0,0 +1,292 @@ +{ + "meta": { + "instanceId": "8147b3a74cd161276e0f3bfc17369a724afab0d377593fada8be82d34c0c6a95" + }, + "nodes": [ + { + "parameters": { + "jsCode": "return [\n {\n id: 6666\n },\n {\n id: 3333\n },\n {\n id: 9999\n },\n {\n id: 1111\n },\n {\n id: 4444\n },\n {\n id: 8888\n },\n]" + }, + "id": "5f023c7c-67ca-47a0-8a90-8227fcf29b9c", + "name": "Code", + "type": "n8n-nodes-base.code", + "typeVersion": 1, + "position": [ + -520, + 580 + ] + }, + { + "parameters": { + "values": { + "string": [ + { + "name": "id", + "value": "={{ $json.id }}" + } + ] + }, + "options": {} + }, + "id": "bd454282-9dd7-465f-9b9a-654a0c8532ec", + "name": "Set2", + "type": "n8n-nodes-base.set", + "typeVersion": 2, + "position": [ + -40, + 780 + ] + }, + { + "parameters": {}, + "id": "ef63cdc5-50bc-4525-9873-7e7f7589a60e", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [ + -740, + 580 + ] + }, + { + "parameters": { + "operation": "sort", + "sortFieldsUi": { + "sortField": [ + { + "fieldName": "id" + } + ] + }, + "options": {} + }, + "id": "555a150c-d735-4331-b628-c1f1cfed2da1", + "name": "Item Lists", + "type": "n8n-nodes-base.itemLists", + "typeVersion": 2, + "position": [ + -280, + 580 + ] + }, + { + "parameters": { + "values": { + "string": [ + { + "name": "id", + "value": "={{ $json.id }}" + } + ] + }, + "options": {} + }, + "id": "02372cb6-aac8-45c3-8600-f699901289ac", + "name": "Set", + "type": "n8n-nodes-base.set", + "typeVersion": 2, + "position": [ + -60, + 580 + ] + }, + { + "parameters": { + "options": {} + }, + "id": "00d73944-218c-4896-af68-3f2855a922d1", + "name": "Set1", + "type": "n8n-nodes-base.set", + "typeVersion": 2, + "position": [ + -280, + 780 + ] + }, + { + "parameters": { + "conditions": { + "number": [ + { + "value1": "={{ $json.id }}", + "operation": "smallerEqual", + "value2": 6666 + } + ] + } + }, + "id": "211a7bef-32d1-4928-9cef-3a45f2e61379", + "name": "IF", + "type": "n8n-nodes-base.if", + "typeVersion": 1, + "position": [ + 160, + 580 + ] + }, + { + "parameters": { + "options": {} + }, + "id": "dcbd4745-832f-43d8-8a3c-dd80e8ca2777", + "name": "Set3", + "type": "n8n-nodes-base.set", + "typeVersion": 2, + "position": [ + 140, + 780 + ] + }, + { + "parameters": { + "jsCode": "return [\n {\n id: 1000\n },\n {\n id: 300\n },\n {\n id: 2000\n },\n {\n id: 100\n },\n {\n id: 400\n },\n {\n id: 1300\n },\n]" + }, + "id": "ec9c8f16-f3c8-4054-a6e9-4f1ebcdebb71", + "name": "Code1", + "type": "n8n-nodes-base.code", + "typeVersion": 1, + "position": [ + -520, + 780 + ] + }, + { + "parameters": { + "options": {} + }, + "id": "42e89478-a53a-4d10-b20c-1dc5d5f953d5", + "name": "Set4", + "type": "n8n-nodes-base.set", + "typeVersion": 2, + "position": [ + 460, + 460 + ] + }, + { + "parameters": { + "options": {} + }, + "id": "5085eb1c-0345-4b9d-856a-2955279f2c5d", + "name": "Set5", + "type": "n8n-nodes-base.set", + "typeVersion": 2, + "position": [ + 460, + 660 + ] + } + ], + "connections": { + "Code": { + "main": [ + [ + { + "node": "Item Lists", + "type": "main", + "index": 0 + } + ] + ] + }, + "Set2": { + "main": [ + [ + { + "node": "Set3", + "type": "main", + "index": 0 + } + ] + ] + }, + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Code", + "type": "main", + "index": 0 + }, + { + "node": "Code1", + "type": "main", + "index": 0 + } + ] + ] + }, + "Item Lists": { + "main": [ + [ + { + "node": "Set", + "type": "main", + "index": 0 + }, + { + "node": "Set2", + "type": "main", + "index": 0 + } + ] + ] + }, + "Set": { + "main": [ + [ + { + "node": "IF", + "type": "main", + "index": 0 + } + ] + ] + }, + "Set1": { + "main": [ + [ + { + "node": "Set2", + "type": "main", + "index": 0 + } + ] + ] + }, + "IF": { + "main": [ + [ + { + "node": "Set4", + "type": "main", + "index": 0 + }, + { + "node": "Set5", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Set5", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code1": { + "main": [ + [ + { + "node": "Set1", + "type": "main", + "index": 0 + } + ] + ] + } + } +} \ No newline at end of file diff --git a/cypress/pages/features/node-creator.ts b/cypress/pages/features/node-creator.ts index 8ebe6db702d6e..6686de25ff1fe 100644 --- a/cypress/pages/features/node-creator.ts +++ b/cypress/pages/features/node-creator.ts @@ -7,6 +7,7 @@ export class NodeCreator extends BasePage { plusButton: () => cy.getByTestId('node-creator-plus-button'), canvasAddButton: () => cy.getByTestId('canvas-add-button'), searchBar: () => cy.getByTestId('search-bar'), + getCategoryItem: (label: string) => cy.get(`[data-keyboard-nav-id="${label}"]`), getCreatorItem: (label: string) => this.getters.creatorItem().contains(label).parents('[data-test-id="item-iterator-item"]'), getNthCreatorItem: (n: number) => this.getters.creatorItem().eq(n), @@ -15,10 +16,11 @@ export class NodeCreator extends BasePage { selectedTab: () => this.getters.nodeCreatorTabs().find('.is-active'), categorizedItems: () => cy.getByTestId('categorized-items'), creatorItem: () => cy.getByTestId('item-iterator-item'), + categoryItem: () => cy.getByTestId('node-creator-category-item'), communityNodeTooltip: () => cy.getByTestId('node-item-community-tooltip'), - noResults: () => cy.getByTestId('categorized-no-results'), + noResults: () => cy.getByTestId('node-creator-no-results'), nodeItemName: () => cy.getByTestId('node-creator-item-name'), - activeSubcategory: () => cy.getByTestId('categorized-items-subcategory'), + activeSubcategory: () => cy.getByTestId('nodes-list-header'), expandedCategories: () => this.getters.creatorItem().find('>div').filter('.active').invoke('text'), }; diff --git a/cypress/pages/modals/credentials-modal.ts b/cypress/pages/modals/credentials-modal.ts index 2f5fd298aa99d..99d712e7e3f87 100644 --- a/cypress/pages/modals/credentials-modal.ts +++ b/cypress/pages/modals/credentials-modal.ts @@ -10,9 +10,7 @@ export class CredentialsModal extends BasePage { newCredentialTypeButton: () => cy.getByTestId('new-credential-type-button'), connectionParameters: () => cy.getByTestId('credential-connection-parameter'), connectionParameter: (fieldName: string) => - this.getters - .connectionParameters() - .find(`:contains('${fieldName}') .n8n-input input`), + this.getters.connectionParameters().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 @@ -27,6 +25,7 @@ export class CredentialsModal extends BasePage { menu: () => this.getters.editCredentialModal().get('.menu-container'), menuItem: (name: string) => this.getters.menu().get('.n8n-menu-item').contains(name), usersSelect: () => cy.getByTestId('credential-sharing-modal-users-select'), + testSuccessTag: () => cy.getByTestId('credentials-config-container-test-success'), }; actions = { addUser: (email: string) => { diff --git a/cypress/pages/modals/user-activation-survey-modal.ts b/cypress/pages/modals/user-activation-survey-modal.ts index d47f987887aeb..cc8f139fb614c 100644 --- a/cypress/pages/modals/user-activation-survey-modal.ts +++ b/cypress/pages/modals/user-activation-survey-modal.ts @@ -5,5 +5,6 @@ export class UserActivationSurveyModal extends BasePage { modalContainer: () => cy.getByTestId('userActivationSurvey-modal').last(), feedbackInput: () => cy.getByTestId('activation-feedback-input').find('textarea'), sendFeedbackButton: () => cy.getByTestId('send-activation-feedback-button'), + skipButton: () => cy.getByTestId('skip-button'), }; } diff --git a/cypress/pages/ndv.ts b/cypress/pages/ndv.ts index 7ce0c811fee97..f23f9a0548a61 100644 --- a/cypress/pages/ndv.ts +++ b/cypress/pages/ndv.ts @@ -18,7 +18,7 @@ export class NDV extends BasePage { outputDisplayMode: () => this.getters.outputPanel().findChildByTestId('ndv-run-data-display-mode').first(), pinDataButton: () => cy.getByTestId('ndv-pin-data'), editPinnedDataButton: () => cy.getByTestId('ndv-edit-pinned-data'), - pinnedDataEditor: () => this.getters.outputPanel().find('.monaco-editor[role=code]'), + pinnedDataEditor: () => this.getters.outputPanel().find('.cm-editor .cm-scroller'), runDataPaneHeader: () => cy.getByTestId('run-data-pane-header'), savePinnedDataButton: () => this.getters.runDataPaneHeader().find('button').filter(':visible').contains('Save'), outputTableRows: () => this.getters.outputDataContainer().find('table tr'), @@ -45,6 +45,17 @@ export class NDV extends BasePage { executePrevious: () => cy.getByTestId('execute-previous-node'), httpRequestNotice: () => cy.getByTestId('node-parameters-http-notice'), nthParam: (n: number) => cy.getByTestId('node-parameters').find('.parameter-item').eq(n), + inputRunSelector: () => this.getters.inputPanel().findChildByTestId('run-selector'), + outputRunSelector: () => this.getters.outputPanel().findChildByTestId('run-selector'), + outputHoveringItem: () => this.getters.outputPanel().findChildByTestId('hovering-item'), + inputHoveringItem: () => this.getters.inputPanel().findChildByTestId('hovering-item'), + outputBranches: () => this.getters.outputPanel().findChildByTestId('branches'), + inputBranches: () => this.getters.inputPanel().findChildByTestId('branches'), + resourceLocator: (paramName: string) => cy.getByTestId(`resource-locator-${paramName}`), + resourceLocatorInput: (paramName: string) => this.getters.resourceLocator(paramName).find('[data-test-id="rlc-input-container"]'), + resourceLocatorDropdown: (paramName: string) => this.getters.resourceLocator(paramName).find('[data-test-id="resource-locator-dropdown"]'), + resourceLocatorErrorMessage: () => cy.getByTestId('rlc-error-container'), + resourceLocatorModeSelector: (paramName: string) => this.getters.resourceLocator(paramName).find('[data-test-id="rlc-mode-selector"]'), }; actions = { @@ -71,8 +82,7 @@ export class NDV extends BasePage { this.getters.editPinnedDataButton().click(); this.getters.pinnedDataEditor().click(); - this.getters.pinnedDataEditor().type(`{selectall}{backspace}`); - this.getters.pinnedDataEditor().type(JSON.stringify(data).replace(new RegExp('{', 'g'), '{{}')); + this.getters.pinnedDataEditor().type(`{selectall}{backspace}${JSON.stringify(data).replace(new RegExp('{', 'g'), '{{}')}`); this.actions.savePinnedData(); }, @@ -119,5 +129,34 @@ export class NDV extends BasePage { this.actions.editPinnedData(); this.actions.savePinnedData(); }, + changeInputRunSelector: (runName: string) => { + this.getters.inputRunSelector().click(); + cy.get('.el-select-dropdown:visible .el-select-dropdown__item') + .contains(runName) + .click(); + }, + changeOutputRunSelector: (runName: string) => { + this.getters.outputRunSelector().click(); + cy.get('.el-select-dropdown:visible .el-select-dropdown__item') + .contains(runName) + .click(); + }, + toggleOutputRunLinking: () => { + this.getters.outputRunSelector().find('button').click(); + }, + toggleInputRunLinking: () => { + this.getters.inputRunSelector().find('button').click(); + }, + switchOutputBranch: (name: string) => { + this.getters.outputBranches().get('span').contains(name).click(); + }, + switchIntputBranch: (name: string) => { + this.getters.inputBranches().get('span').contains(name).click(); + }, + setRLCValue: (paramName: string, value: string) => { + this.getters.resourceLocatorModeSelector(paramName).click(); + this.getters.resourceLocatorModeSelector(paramName).find('li').last().click(); + this.getters.resourceLocatorInput(paramName).type(value); + } }; } diff --git a/cypress/pages/workflow.ts b/cypress/pages/workflow.ts index 4ad4b753a9161..8b6d195105248 100644 --- a/cypress/pages/workflow.ts +++ b/cypress/pages/workflow.ts @@ -106,6 +106,8 @@ export class WorkflowPage extends BasePage { cy.get( `.connection-actions[data-source-node="${sourceNodeName}"][data-target-node="${targetNodeName}"]`, ), + addStickyButton: () => cy.getByTestId('add-sticky-button'), + stickies: () => cy.getByTestId('sticky'), editorTabButton: () => cy.getByTestId('radio-button-workflow'), }; actions = { @@ -138,7 +140,8 @@ export class WorkflowPage extends BasePage { if(action) { cy.contains(action).click() } else { - cy.getByTestId('item-iterator-item').eq(1).click() + // Select the first action + cy.get('[data-keyboard-nav-type="action"]').eq(0).click() } } }) @@ -167,11 +170,13 @@ export class WorkflowPage extends BasePage { this.getters.shareButton().click(); }, saveWorkflowOnButtonClick: () => { + cy.intercept('POST', '/rest/workflows').as('createWorkflow'); this.getters.saveButton().should('contain', 'Save'); this.getters.saveButton().click(); this.getters.saveButton().should('contain', 'Saved'); }, saveWorkflowUsingKeyboardShortcut: () => { + cy.intercept('POST', '/rest/workflows').as('createWorkflow'); cy.get('body').type('{meta}', { release: false }).type('s'); }, deleteNode: (name: string) => { @@ -257,6 +262,24 @@ export class WorkflowPage extends BasePage { .first() .click({ force: true }); }, + addSticky: () => { + this.getters.nodeCreatorPlusButton().realHover(); + this.getters.addStickyButton().click(); + }, + deleteSticky: () => { + this.getters.stickies().eq(0) + .realHover() + .find('[data-test-id="delete-sticky"]') + .click(); + }, + editSticky: (content: string) => { + this.getters.stickies() + .dblclick() + .find('textarea') + .clear() + .type(content) + .type('{esc}'); + }, turnOnManualExecutionSaving: () => { this.getters.workflowMenu().click(); this.getters.workflowMenuItemSettings().click(); diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index e096dc0aaa7cf..d0e9ddbaa0347 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -58,8 +58,8 @@ Cypress.Commands.add('waitForLoad', (waitForIntercepts = true) => { // These aliases are set-up before each test in cypress/support/e2e.ts // we can't set them up here because at this point it would be too late // and the requests would already have been made - if(waitForIntercepts) { - cy.wait(['@loadSettings', '@loadLogin']) + if (waitForIntercepts) { + cy.wait(['@loadSettings', '@loadLogin']); } cy.getByTestId('node-view-loader', { timeout: 20000 }).should('not.exist'); cy.get('.el-loading-mask', { timeout: 20000 }).should('not.exist'); @@ -105,18 +105,22 @@ Cypress.Commands.add('signup', ({ firstName, lastName, password, url }) => { signupPage.getters.form().within(() => { cy.url().then((url) => { + cy.intercept('/rest/users/*').as('userSignup') signupPage.getters.firstName().type(firstName); signupPage.getters.lastName().type(lastName); signupPage.getters.password().type(password); signupPage.getters.submit().click(); + cy.wait('@userSignup'); }); }); }); -Cypress.Commands.add('setup', ({ email, firstName, lastName, password }) => { +Cypress.Commands.add('setup', ({ email, firstName, lastName, password }, skipIntercept = false) => { const signupPage = new SignupPage(); + cy.intercept('GET', signupPage.url).as('setupPage'); cy.visit(signupPage.url); + cy.wait('@setupPage'); signupPage.getters.form().within(() => { cy.url().then((url) => { @@ -125,7 +129,13 @@ Cypress.Commands.add('setup', ({ email, firstName, lastName, password }) => { signupPage.getters.firstName().type(firstName); signupPage.getters.lastName().type(lastName); signupPage.getters.password().type(password); + + cy.intercept('POST', '/rest/owner/setup').as('setupRequest'); signupPage.getters.submit().click(); + + if(!skipIntercept) { + cy.wait('@setupRequest'); + } } else { cy.log('User already signed up'); } @@ -168,7 +178,9 @@ Cypress.Commands.add('skipSetup', () => { const workflowPage = new WorkflowPage(); const Confirmation = new MessageBox(); + cy.intercept('GET', signupPage.url).as('setupPage'); cy.visit(signupPage.url); + cy.wait('@setupPage'); signupPage.getters.form().within(() => { cy.url().then((url) => { @@ -199,7 +211,11 @@ Cypress.Commands.add('setupOwner', (payload) => { }); Cypress.Commands.add('enableFeature', (feature) => { - cy.task('enable-feature', feature); + cy.task('set-feature', { feature, enabled: true }); +}); + +Cypress.Commands.add('disableFeature', (feature) => { + cy.task('set-feature', { feature, enabled: false }); }); Cypress.Commands.add('grantBrowserPermissions', (...permissions: string[]) => { @@ -232,18 +248,19 @@ Cypress.Commands.add('paste', { prevSubject: true }, (selector, pastePayload) => }); }); -Cypress.Commands.add('drag', (selector, pos) => { +Cypress.Commands.add('drag', (selector, pos, options) => { + const index = options?.index || 0; const [xDiff, yDiff] = pos; - const element = cy.get(selector); + const element = cy.get(selector).eq(index); element.should('exist'); - const originalLocation = Cypress.$(selector)[0].getBoundingClientRect(); + const originalLocation = Cypress.$(selector)[index].getBoundingClientRect(); element.trigger('mousedown'); element.trigger('mousemove', { which: 1, - pageX: originalLocation.right + xDiff, - pageY: originalLocation.top + yDiff, + pageX: options?.abs ? xDiff : originalLocation.right + xDiff, + pageY: options?.abs ? yDiff : originalLocation.top + yDiff, force: true, }); element.trigger('mouseup', { force: true }); diff --git a/cypress/support/index.ts b/cypress/support/index.ts index b42db7e7171a8..7b1b15db2b962 100644 --- a/cypress/support/index.ts +++ b/cypress/support/index.ts @@ -36,18 +36,19 @@ declare global { signin(payload: SigninPayload): void; signout(): void; signup(payload: SignupPayload): void; - setup(payload: SetupPayload): void; + setup(payload: SetupPayload, skipIntercept?: boolean): void; setupOwner(payload: SetupPayload): void; inviteUsers(payload: InviteUsersPayload): void; interceptREST(method: string, url: string): Chainable<Interception>; skipSetup(): void; resetAll(): void; enableFeature(feature: string): void; + disableFeature(feature: string): void; waitForLoad(waitForIntercepts?: boolean): void; grantBrowserPermissions(...permissions: string[]): void; readClipboard(): Chainable<string>; paste(pastePayload: string): void; - drag(selector: string, target: [number, number]): void; + drag(selector: string, target: [number, number], options?: {abs?: true, index?: number}): void; draganddrop(draggableSelector: string, droppableSelector: string): void; } } diff --git a/package.json b/package.json index df49c446bd138..9d47f412cbb2b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "n8n", - "version": "0.225.0", + "version": "0.227.0", "private": true, "homepage": "https://n8n.io", "engines": { @@ -79,6 +79,7 @@ "tslib": "^2.5.0", "ts-node": "^10.9.1", "typescript": "^5.0.3", + "xlsx": "https://cdn.sheetjs.com/xlsx-0.19.3/xlsx-0.19.3.tgz", "xml2js": "^0.5.0", "cpy@8>globby": "^11.1.0", "qqjs>globby": "^11.1.0" @@ -86,7 +87,8 @@ "patchedDependencies": { "element-ui@2.15.12": "patches/element-ui@2.15.12.patch", "typedi@0.10.0": "patches/typedi@0.10.0.patch", - "@sentry/cli@2.17.0": "patches/@sentry__cli@2.17.0.patch" + "@sentry/cli@2.17.0": "patches/@sentry__cli@2.17.0.patch", + "@typescript-eslint/eslint-plugin@5.59.0": "patches/@typescript-eslint__eslint-plugin@5.59.0.patch" } } } diff --git a/packages/@n8n_io/eslint-config/base.js b/packages/@n8n_io/eslint-config/base.js index 2f673ece0b39a..0958755fc19be 100644 --- a/packages/@n8n_io/eslint-config/base.js +++ b/packages/@n8n_io/eslint-config/base.js @@ -6,9 +6,7 @@ const config = (module.exports = { 'node_modules/**', 'dist/**', // TODO: remove these - 'test/**', - '.eslintrc.js', - 'jest.config.js', + '*.js', ], plugins: [ @@ -35,6 +33,12 @@ const config = (module.exports = { * https://github.com/ivov/eslint-plugin-n8n-local-rules */ 'eslint-plugin-n8n-local-rules', + + /** https://github.com/sweepline/eslint-plugin-unused-imports */ + 'unused-imports', + + /** https://github.com/sindresorhus/eslint-plugin-unicorn */ + 'eslint-plugin-unicorn', ], extends: [ @@ -194,6 +198,11 @@ const config = (module.exports = { */ '@typescript-eslint/consistent-type-assertions': 'error', + /** + * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/consistent-type-imports.md + */ + '@typescript-eslint/consistent-type-imports': 'error', + /** * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/member-delimiter-style.md */ @@ -397,6 +406,18 @@ const config = (module.exports = { }, ], + /** + * https://www.typescriptlang.org/docs/handbook/enums.html#const-enums + */ + 'no-restricted-syntax': [ + 'error', + { + selector: 'TSEnumDeclaration:not([const=true])', + message: + 'Do not declare raw enums as it leads to runtime overhead. Use const enum instead. See https://www.typescriptlang.org/docs/handbook/enums.html#const-enums', + }, + ], + // ---------------------------------- // import // ---------------------------------- @@ -405,6 +426,21 @@ const config = (module.exports = { * https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/prefer-default-export.md */ 'import/prefer-default-export': 'off', + + // ---------------------------------- + // no-unused-imports + // ---------------------------------- + + /** + * https://github.com/sweepline/eslint-plugin-unused-imports/blob/master/docs/rules/no-unused-imports.md + */ + 'unused-imports/no-unused-imports': process.env.NODE_ENV === 'development' ? 'warn' : 'error', + + /** https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-unnecessary-await.md */ + 'unicorn/no-unnecessary-await': 'error', + + /** https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-useless-promise-resolve-reject.md */ + 'unicorn/no-useless-promise-resolve-reject': 'error', }, overrides: [ @@ -414,6 +450,43 @@ const config = (module.exports = { '@typescript-eslint/no-unused-vars': 'off', }, }, + { + files: ['test/**/*.ts'], + // TODO: Remove these + rules: { + '@typescript-eslint/await-thenable': 'off', + '@typescript-eslint/ban-ts-comment': 'off', + '@typescript-eslint/naming-convention': 'off', + '@typescript-eslint/no-duplicate-imports': 'off', + '@typescript-eslint/no-empty-function': 'off', + '@typescript-eslint/no-floating-promises': 'off', + '@typescript-eslint/no-loop-func': 'off', + '@typescript-eslint/no-non-null-assertion': 'off', + '@typescript-eslint/no-shadow': 'off', + '@typescript-eslint/no-throw-literal': 'off', + '@typescript-eslint/no-unsafe-argument': 'off', + '@typescript-eslint/no-unsafe-assignment': 'off', + '@typescript-eslint/no-unsafe-call': 'off', + '@typescript-eslint/no-unsafe-member-access': 'off', + '@typescript-eslint/no-unsafe-return': 'off', + '@typescript-eslint/no-unused-expressions': 'off', + '@typescript-eslint/no-unused-vars': 'off', + '@typescript-eslint/no-use-before-define': 'off', + '@typescript-eslint/no-var-requires': 'off', + '@typescript-eslint/prefer-nullish-coalescing': 'off', + '@typescript-eslint/prefer-optional-chain': 'off', + '@typescript-eslint/restrict-plus-operands': 'off', + '@typescript-eslint/restrict-template-expressions': 'off', + '@typescript-eslint/unbound-method': 'off', + 'id-denylist': 'off', + 'import/no-cycle': 'off', + 'import/no-default-export': 'off', + 'import/no-extraneous-dependencies': 'off', + 'n8n-local-rules/no-uncaught-json-parse': 'off', + 'prefer-const': 'off', + 'prefer-spread': 'off', + }, + }, ], }); diff --git a/packages/@n8n_io/eslint-config/package.json b/packages/@n8n_io/eslint-config/package.json index 0e2c058c49393..34e0abc03ae35 100644 --- a/packages/@n8n_io/eslint-config/package.json +++ b/packages/@n8n_io/eslint-config/package.json @@ -3,18 +3,20 @@ "private": true, "version": "0.0.1", "devDependencies": { - "@types/eslint": "~8.4", - "@typescript-eslint/eslint-plugin": "~5.45", - "@typescript-eslint/parser": "~5.45", + "@types/eslint": "~8.37", + "@typescript-eslint/eslint-plugin": "~5.59", + "@typescript-eslint/parser": "~5.59", "@vue/eslint-config-typescript": "~8.0", - "eslint": "~8.28", + "eslint": "~8.39", "eslint-config-airbnb-typescript": "~17.0", - "eslint-config-prettier": "~8.5", + "eslint-config-prettier": "~8.8", "eslint-import-resolver-typescript": "~3.5", "eslint-plugin-diff": "~2.0", - "eslint-plugin-import": "~2.26", + "eslint-plugin-import": "~2.27", "eslint-plugin-n8n-local-rules": "~1.0", "eslint-plugin-prettier": "~4.2", + "eslint-plugin-unicorn": "~46.0", + "eslint-plugin-unused-imports": "~2.0", "eslint-plugin-vue": "~7.17" }, "scripts": { diff --git a/packages/cli/.eslintrc.js b/packages/cli/.eslintrc.js index 3da2d993a3ca1..2a24787f80e76 100644 --- a/packages/cli/.eslintrc.js +++ b/packages/cli/.eslintrc.js @@ -16,8 +16,6 @@ module.exports = { ], rules: { - '@typescript-eslint/consistent-type-imports': 'error', - // TODO: Remove this 'import/no-cycle': 'warn', 'import/order': 'off', diff --git a/packages/cli/BREAKING-CHANGES.md b/packages/cli/BREAKING-CHANGES.md index 47bb59f918d9e..f260be6fa2fb5 100644 --- a/packages/cli/BREAKING-CHANGES.md +++ b/packages/cli/BREAKING-CHANGES.md @@ -2,6 +2,17 @@ This list shows all the versions which include breaking changes and how to upgrade. +## 0.226.0 + +### What changed? + +The `extractDomain` and `isDomain` are now also matching localhost, domains without protocol and domains with query parameters. +The `extractUrl` and `isUrl` are additionally also matching localhost and domains with query parameters. + +### When is action necessary? + +If you're using the `extractDomain` or `isDomain` functions and expect them to not match localhost, domains without protocol and domains with query parameters. + ## 0.223.0 ### What changed? diff --git a/packages/cli/package.json b/packages/cli/package.json index d9378431a05e4..39112605e2851 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "n8n", - "version": "0.225.0", + "version": "0.227.0", "description": "n8n Workflow Automation Tool", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", @@ -116,7 +116,7 @@ "tsconfig-paths": "^4.1.2" }, "dependencies": { - "@n8n_io/license-sdk": "~2.1.0", + "@n8n_io/license-sdk": "~2.3.0", "@oclif/command": "^1.8.16", "@oclif/core": "^1.16.4", "@oclif/errors": "^1.3.6", diff --git a/packages/cli/src/AbstractServer.ts b/packages/cli/src/AbstractServer.ts index cde2ff3fdd029..55eae9376a520 100644 --- a/packages/cli/src/AbstractServer.ts +++ b/packages/cli/src/AbstractServer.ts @@ -55,6 +55,8 @@ export abstract class AbstractServer { protected endpointWebhookWaiting: string; + protected instanceId = ''; + abstract configure(): Promise<void>; constructor() { diff --git a/packages/cli/src/ActiveWorkflowRunner.ts b/packages/cli/src/ActiveWorkflowRunner.ts index 2cd770291e7bd..ca175b303b715 100644 --- a/packages/cli/src/ActiveWorkflowRunner.ts +++ b/packages/cli/src/ActiveWorkflowRunner.ts @@ -66,6 +66,7 @@ import { ExternalHooks } from '@/ExternalHooks'; import { whereClause } from './UserManagement/UserManagementHelper'; import { WorkflowsService } from './workflows/workflows.services'; import { START_NODES } from './constants'; +import { webhookNotFoundErrorMessage } from './utils'; const WEBHOOK_PROD_UNREGISTERED_HINT = "The workflow must be active for a production URL to run successfully. You can activate the workflow using the toggle in the top-right of the editor. Note that unlike test URL calls, production URL calls aren't shown on the canvas (only in the executions list)"; @@ -221,7 +222,7 @@ export class ActiveWorkflowRunner { if (dynamicWebhooks === undefined || dynamicWebhooks.length === 0) { // The requested webhook is not registered throw new ResponseHelper.NotFoundError( - `The requested webhook "${httpMethod} ${path}" is not registered.`, + webhookNotFoundErrorMessage(path, httpMethod), WEBHOOK_PROD_UNREGISTERED_HINT, ); } @@ -247,7 +248,7 @@ export class ActiveWorkflowRunner { }); if (webhook === null) { throw new ResponseHelper.NotFoundError( - `The requested webhook "${httpMethod} ${path}" is not registered.`, + webhookNotFoundErrorMessage(path, httpMethod), WEBHOOK_PROD_UNREGISTERED_HINT, ); } diff --git a/packages/cli/src/CredentialsHelper.ts b/packages/cli/src/CredentialsHelper.ts index 2851638360576..cfc86bfd263af 100644 --- a/packages/cli/src/CredentialsHelper.ts +++ b/packages/cli/src/CredentialsHelper.ts @@ -542,10 +542,10 @@ export class CredentialsHelper extends ICredentialsHelper { ): Promise<INodeCredentialTestResult> { const credentialTestFunction = this.getCredentialTestFunction(credentialType); if (credentialTestFunction === undefined) { - return Promise.resolve({ + return { status: 'Error', message: 'No testing function found for this credential.', - }); + }; } if (credentialsDecrypted.data) { diff --git a/packages/cli/src/CurlConverterHelper.ts b/packages/cli/src/CurlConverterHelper.ts index 3035ada4c4f46..06da059030549 100644 --- a/packages/cli/src/CurlConverterHelper.ts +++ b/packages/cli/src/CurlConverterHelper.ts @@ -80,7 +80,7 @@ type HttpNodeHeaders = Pick<HttpNodeParameters, 'sendHeaders' | 'headerParameter type HttpNodeQueries = Pick<HttpNodeParameters, 'sendQuery' | 'queryParameters'>; -enum ContentTypes { +const enum ContentTypes { applicationJson = 'application/json', applicationFormUrlEncoded = 'application/x-www-form-urlencoded', applicationMultipart = 'multipart/form-data', diff --git a/packages/cli/src/Interfaces.ts b/packages/cli/src/Interfaces.ts index ddbfce8abfa84..374cd9b9e9a1b 100644 --- a/packages/cli/src/Interfaces.ts +++ b/packages/cli/src/Interfaces.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/naming-convention */ import type { Application } from 'express'; import type { ExecutionError, @@ -13,7 +12,6 @@ import type { IRunData, IRunExecutionData, ITaskData, - ITelemetrySettings, ITelemetryTrackProperties, IWorkflowBase, CredentialLoadingDetails, @@ -23,8 +21,6 @@ import type { ExecutionStatus, IExecutionsSummary, FeatureFlags, - WorkflowSettings, - AuthenticationMethod, } from 'n8n-workflow'; import type { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner'; @@ -85,6 +81,7 @@ export interface ICredentialsOverwrite { [key: string]: ICredentialDataDecryptedObject; } +/* eslint-disable @typescript-eslint/naming-convention */ export interface IDatabaseCollections { AuthIdentity: AuthIdentityRepository; AuthProviderSyncHistory: AuthProviderSyncHistoryRepository; @@ -106,6 +103,7 @@ export interface IDatabaseCollections { WorkflowStatistics: WorkflowStatisticsRepository; WorkflowTagMapping: WorkflowTagMappingRepository; } +/* eslint-enable @typescript-eslint/naming-convention */ // ---------------------------------- // tags @@ -339,6 +337,7 @@ export interface IDiagnosticInfo { n8n_multi_user_allowed: boolean; smtp_set_up: boolean; ldap_allowed: boolean; + saml_enabled: boolean; } export interface ITelemetryUserDeletionData { @@ -469,90 +468,6 @@ export interface IVersionNotificationSettings { infoUrl: string; } -export interface IN8nUISettings { - endpointWebhook: string; - endpointWebhookTest: string; - saveDataErrorExecution: WorkflowSettings.SaveDataExecution; - saveDataSuccessExecution: WorkflowSettings.SaveDataExecution; - saveManualExecutions: boolean; - executionTimeout: number; - maxExecutionTimeout: number; - workflowCallerPolicyDefaultOption: WorkflowSettings.CallerPolicy; - oauthCallbackUrls: { - oauth1: string; - oauth2: string; - }; - timezone: string; - urlBaseWebhook: string; - urlBaseEditor: string; - versionCli: string; - n8nMetadata?: { - [key: string]: string | number | undefined; - }; - versionNotifications: IVersionNotificationSettings; - instanceId: string; - telemetry: ITelemetrySettings; - posthog: { - enabled: boolean; - apiHost: string; - apiKey: string; - autocapture: boolean; - disableSessionRecording: boolean; - debug: boolean; - }; - personalizationSurveyEnabled: boolean; - userActivationSurveyEnabled: boolean; - defaultLocale: string; - userManagement: IUserManagementSettings; - sso: { - saml: { - loginLabel: string; - loginEnabled: boolean; - }; - ldap: { - loginLabel: string; - loginEnabled: boolean; - }; - }; - publicApi: IPublicApiSettings; - workflowTagsDisabled: boolean; - logLevel: 'info' | 'debug' | 'warn' | 'error' | 'verbose' | 'silent'; - hiringBannerEnabled: boolean; - templates: { - enabled: boolean; - host: string; - }; - onboardingCallPromptEnabled: boolean; - missingPackages?: boolean; - executionMode: 'regular' | 'queue'; - pushBackend: 'sse' | 'websocket'; - communityNodesEnabled: boolean; - deployment: { - type: string; - }; - isNpmAvailable: boolean; - allowedModules: { - builtIn?: string; - external?: string; - }; - enterprise: { - sharing: boolean; - ldap: boolean; - saml: boolean; - logStreaming: boolean; - advancedExecutionFilters: boolean; - variables: boolean; - versionControl: boolean; - }; - hideUsagePage: boolean; - license: { - environment: 'production' | 'staging'; - }; - variables: { - limit: number; - }; -} - export interface IPersonalizationSurveyAnswers { email: string | null; codingSkill: string | null; @@ -570,23 +485,9 @@ export interface IUserSettings { userActivated?: boolean; } -export interface IUserManagementSettings { - enabled: boolean; - showSetupOnFirstLoad?: boolean; - smtpSetup: boolean; - authenticationMethod: AuthenticationMethod; -} export interface IActiveDirectorySettings { enabled: boolean; } -export interface IPublicApiSettings { - enabled: boolean; - latestVersion: number; - path: string; - swaggerUi: { - enabled: boolean; - }; -} export interface IPackageVersions { cli: string; @@ -890,3 +791,5 @@ export interface N8nApp { externalHooks: IExternalHooksClass; activeWorkflowRunner: ActiveWorkflowRunner; } + +export type UserSettings = Pick<User, 'id' | 'settings'>; diff --git a/packages/cli/src/InternalHooks.ts b/packages/cli/src/InternalHooks.ts index 6a1ab61b64715..55793c0528d77 100644 --- a/packages/cli/src/InternalHooks.ts +++ b/packages/cli/src/InternalHooks.ts @@ -5,6 +5,7 @@ import { Service } from 'typedi'; import { snakeCase } from 'change-case'; import { BinaryDataManager } from 'n8n-core'; import type { + AuthenticationMethod, ExecutionStatus, INodesGraphResult, IRun, @@ -80,6 +81,7 @@ export class InternalHooks implements IInternalHooksClass { n8n_multi_user_allowed: diagnosticInfo.n8n_multi_user_allowed, smtp_set_up: diagnosticInfo.smtp_set_up, ldap_allowed: diagnosticInfo.ldap_allowed, + saml_enabled: diagnosticInfo.saml_enabled, }; return Promise.all([ @@ -271,12 +273,12 @@ export class InternalHooks implements IInternalHooksClass { runData?: IRun, userId?: string, ): Promise<void> { - const promises = [Promise.resolve()]; - if (!workflow.id) { - return Promise.resolve(); + return; } + const promises = []; + const properties: IExecutionTrackProperties = { workflow_id: workflow.id, is_manual: false, @@ -288,6 +290,10 @@ export class InternalHooks implements IInternalHooksClass { properties.user_id = userId; } + if (runData?.data.resultData.error?.message?.includes('canceled')) { + runData.status = 'canceled'; + } + properties.success = !!runData?.finished; let executionStatus: ExecutionStatus; @@ -295,6 +301,8 @@ export class InternalHooks implements IInternalHooksClass { executionStatus = 'crashed'; } else if (runData?.status === 'waiting' || runData?.data?.waitTill) { executionStatus = 'waiting'; + } else if (runData?.status === 'canceled') { + executionStatus = 'canceled'; } else { executionStatus = properties.success ? 'success' : 'failed'; } @@ -732,6 +740,38 @@ export class InternalHooks implements IInternalHooksClass { ]); } + async onUserLoginSuccess(userLoginData: { + user: User; + authenticationMethod: AuthenticationMethod; + }): Promise<void> { + void Promise.all([ + eventBus.sendAuditEvent({ + eventName: 'n8n.audit.user.login.success', + payload: { + authenticationMethod: userLoginData.authenticationMethod, + ...userToPayload(userLoginData.user), + }, + }), + ]); + } + + async onUserLoginFailed(userLoginData: { + user: string; + authenticationMethod: AuthenticationMethod; + reason?: string; + }): Promise<void> { + void Promise.all([ + eventBus.sendAuditEvent({ + eventName: 'n8n.audit.user.login.failed', + payload: { + authenticationMethod: userLoginData.authenticationMethod, + user: userLoginData.user, + reason: userLoginData.reason, + }, + }), + ]); + } + /** * Credentials */ diff --git a/packages/cli/src/Ldap/LdapService.ee.ts b/packages/cli/src/Ldap/LdapService.ee.ts index bc5432cbea6ef..63394a4145fe4 100644 --- a/packages/cli/src/Ldap/LdapService.ee.ts +++ b/packages/cli/src/Ldap/LdapService.ee.ts @@ -82,7 +82,7 @@ export class LdapService { await this.client.unbind(); return searchEntries; } - return Promise.resolve([]); + return []; } /** diff --git a/packages/cli/src/License.ts b/packages/cli/src/License.ts index d1f2015187496..c00e1b1835691 100644 --- a/packages/cli/src/License.ts +++ b/packages/cli/src/License.ts @@ -12,34 +12,6 @@ import { } from './constants'; import { Service } from 'typedi'; -async function loadCertStr(): Promise<TLicenseBlock> { - // if we have an ephemeral license, we don't want to load it from the database - const ephemeralLicense = config.get('license.cert'); - if (ephemeralLicense) { - return ephemeralLicense; - } - const databaseSettings = await Db.collections.Settings.findOne({ - where: { - key: SETTINGS_LICENSE_CERT_KEY, - }, - }); - - return databaseSettings?.value ?? ''; -} - -async function saveCertStr(value: TLicenseBlock): Promise<void> { - // if we have an ephemeral license, we don't want to save it to the database - if (config.get('license.cert')) return; - await Db.collections.Settings.upsert( - { - key: SETTINGS_LICENSE_CERT_KEY, - value, - loadOnStartup: false, - }, - ['key'], - ); -} - @Service() export class License { private logger: ILogger; @@ -67,8 +39,8 @@ export class License { autoRenewEnabled, autoRenewOffset, logger: this.logger, - loadCertStr, - saveCertStr, + loadCertStr: async () => this.loadCertStr(), + saveCertStr: async (value: TLicenseBlock) => this.saveCertStr(value), deviceFingerprint: () => instanceId, }); @@ -80,6 +52,34 @@ export class License { } } + async loadCertStr(): Promise<TLicenseBlock> { + // if we have an ephemeral license, we don't want to load it from the database + const ephemeralLicense = config.get('license.cert'); + if (ephemeralLicense) { + return ephemeralLicense; + } + const databaseSettings = await Db.collections.Settings.findOne({ + where: { + key: SETTINGS_LICENSE_CERT_KEY, + }, + }); + + return databaseSettings?.value ?? ''; + } + + async saveCertStr(value: TLicenseBlock): Promise<void> { + // if we have an ephemeral license, we don't want to save it to the database + if (config.get('license.cert')) return; + await Db.collections.Settings.upsert( + { + key: SETTINGS_LICENSE_CERT_KEY, + value, + loadOnStartup: false, + }, + ['key'], + ); + } + async activate(activationKey: string): Promise<void> { if (!this.manager) { return; @@ -168,8 +168,7 @@ export class License { } return entitlements.find( - (entitlement) => - (entitlement.productMetadata.terms as unknown as { isMainPlan: boolean }).isMainPlan, + (entitlement) => (entitlement.productMetadata?.terms as { isMainPlan?: boolean })?.isMainPlan, ); } @@ -185,4 +184,12 @@ export class License { getPlanName(): string { return (this.getFeatureValue('planName') ?? 'Community') as string; } + + getInfo(): string { + if (!this.manager) { + return 'n/a'; + } + + return this.manager.toString(); + } } diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index bdce01787f3bd..ddaf2a6f9093a 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -49,6 +49,7 @@ import type { ICredentialTypes, ExecutionStatus, IExecutionsSummary, + IN8nUISettings, } from 'n8n-workflow'; import { LoggerProxy, jsonParse } from 'n8n-workflow'; @@ -112,7 +113,6 @@ import type { IDiagnosticInfo, IExecutionFlattedDb, IExecutionsStopData, - IN8nUISettings, } from '@/Interfaces'; import { ActiveExecutions } from '@/ActiveExecutions'; import { @@ -156,10 +156,14 @@ import { import { getSamlLoginLabel, isSamlLoginEnabled, isSamlLicensed } from './sso/saml/samlHelpers'; import { SamlController } from './sso/saml/routes/saml.controller.ee'; import { SamlService } from './sso/saml/saml.service.ee'; -import { variablesController } from './environments/variables.controller'; +import { variablesController } from './environments/variables/variables.controller'; import { LdapManager } from './Ldap/LdapManager.ee'; -import { getVariablesLimit, isVariablesEnabled } from '@/environments/enviromentHelpers'; -import { getCurrentAuthenticationMethod } from './sso/ssoHelpers'; +import { getVariablesLimit, isVariablesEnabled } from '@/environments/variables/enviromentHelpers'; +import { + getCurrentAuthenticationMethod, + isLdapCurrentAuthenticationMethod, + isSamlCurrentAuthenticationMethod, +} from './sso/ssoHelpers'; import { isVersionControlLicensed } from '@/environments/versionControl/versionControlHelper'; import { VersionControlService } from '@/environments/versionControl/versionControl.service.ee'; import { VersionControlController } from '@/environments/versionControl/versionControl.controller.ee'; @@ -313,8 +317,8 @@ class Server extends AbstractServer { }, isNpmAvailable: false, allowedModules: { - builtIn: process.env.NODE_FUNCTION_ALLOW_BUILTIN, - external: process.env.NODE_FUNCTION_ALLOW_EXTERNAL, + builtIn: process.env.NODE_FUNCTION_ALLOW_BUILTIN?.split(',') ?? undefined, + external: process.env.NODE_FUNCTION_ALLOW_EXTERNAL?.split(',') ?? undefined, }, enterprise: { sharing: false, @@ -444,13 +448,15 @@ class Server extends AbstractServer { async configure(): Promise<void> { configureMetrics(this.app); + this.instanceId = await UserSettings.getInstanceId(); + this.frontendSettings.isNpmAvailable = await exec('npm --version') .then(() => true) .catch(() => false); this.frontendSettings.versionCli = N8N_VERSION; - this.frontendSettings.instanceId = await UserSettings.getInstanceId(); + this.frontendSettings.instanceId = this.instanceId; await this.externalHooks.run('frontend.settings', [this.frontendSettings]); @@ -1417,7 +1423,8 @@ export async function start(): Promise<void> { binaryDataMode: binaryDataConfig.mode, n8n_multi_user_allowed: isUserManagementEnabled(), smtp_set_up: config.getEnv('userManagement.emails.mode') === 'smtp', - ldap_allowed: isLdapEnabled(), + ldap_allowed: isLdapCurrentAuthenticationMethod(), + saml_enabled: isSamlCurrentAuthenticationMethod(), }; // Set up event handling diff --git a/packages/cli/src/TestWebhooks.ts b/packages/cli/src/TestWebhooks.ts index e3b7b4fb035c2..d509a1c030e3f 100644 --- a/packages/cli/src/TestWebhooks.ts +++ b/packages/cli/src/TestWebhooks.ts @@ -18,6 +18,7 @@ import type { IResponseCallbackData, IWorkflowDb } from '@/Interfaces'; import { Push } from '@/push'; import * as ResponseHelper from '@/ResponseHelper'; import * as WebhookHelpers from '@/WebhookHelpers'; +import { webhookNotFoundErrorMessage } from './utils'; const WEBHOOK_TEST_UNREGISTERED_HINT = "Click the 'Execute workflow' button on the canvas, then try again. (In test mode, the webhook only works for one call after you click this button)"; @@ -69,8 +70,9 @@ export class TestWebhooks { webhookData = activeWebhooks.get(httpMethod, pathElements.join('/'), webhookId); if (webhookData === undefined) { // The requested webhook is not registered + const methods = await this.getWebhookMethods(path); throw new ResponseHelper.NotFoundError( - `The requested webhook "${httpMethod} ${path}" is not registered.`, + webhookNotFoundErrorMessage(path, httpMethod, methods), WEBHOOK_TEST_UNREGISTERED_HINT, ); } @@ -95,8 +97,9 @@ export class TestWebhooks { // TODO: Clean that duplication up one day and improve code generally if (testWebhookData[webhookKey] === undefined) { // The requested webhook is not registered + const methods = await this.getWebhookMethods(path); throw new ResponseHelper.NotFoundError( - `The requested webhook "${httpMethod} ${path}" is not registered.`, + webhookNotFoundErrorMessage(path, httpMethod, methods), WEBHOOK_TEST_UNREGISTERED_HINT, ); } @@ -160,7 +163,7 @@ export class TestWebhooks { if (!webhookMethods.length) { // The requested webhook is not registered throw new ResponseHelper.NotFoundError( - `The requested webhook "${path}" is not registered.`, + webhookNotFoundErrorMessage(path), WEBHOOK_TEST_UNREGISTERED_HINT, ); } diff --git a/packages/cli/src/UserManagement/UserManagementHelper.ts b/packages/cli/src/UserManagement/UserManagementHelper.ts index 3e705efbd648c..e299544840759 100644 --- a/packages/cli/src/UserManagement/UserManagementHelper.ts +++ b/packages/cli/src/UserManagement/UserManagementHelper.ts @@ -1,7 +1,6 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import { In } from 'typeorm'; -import type express from 'express'; import { compare, genSaltSync, hash } from 'bcryptjs'; import { Container } from 'typedi'; @@ -12,7 +11,6 @@ import type { User } from '@db/entities/User'; import { MAX_PASSWORD_LENGTH, MIN_PASSWORD_LENGTH } from '@db/entities/User'; import type { Role } from '@db/entities/Role'; import { RoleRepository } from '@db/repositories'; -import type { AuthenticatedRequest } from '@/requests'; import config from '@/config'; import { getWebhookBaseUrl } from '@/WebhookHelpers'; import { License } from '@/License'; @@ -196,30 +194,6 @@ export async function getUserById(userId: string): Promise<User> { return user; } -/** - * Check if a URL contains an auth-excluded endpoint. - */ -export function isAuthExcluded(url: string, ignoredEndpoints: Readonly<string[]>): boolean { - return !!ignoredEndpoints - .filter(Boolean) // skip empty paths - .find((ignoredEndpoint) => url.startsWith(`/${ignoredEndpoint}`)); -} - -/** - * Check if the endpoint is `POST /users/:id`. - */ -export function isPostUsersId(req: express.Request, restEndpoint: string): boolean { - return ( - req.method === 'POST' && - new RegExp(`/${restEndpoint}/users/[\\w\\d-]*`).test(req.url) && - !req.url.includes('reinvite') - ); -} - -export function isAuthenticatedRequest(request: express.Request): request is AuthenticatedRequest { - return request.user !== undefined; -} - // ---------------------------------- // hashing // ---------------------------------- diff --git a/packages/cli/src/WorkflowExecuteAdditionalData.ts b/packages/cli/src/WorkflowExecuteAdditionalData.ts index f958d3a49508d..7f6f26f64b51b 100644 --- a/packages/cli/src/WorkflowExecuteAdditionalData.ts +++ b/packages/cli/src/WorkflowExecuteAdditionalData.ts @@ -583,9 +583,12 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks { } const workflowHasCrashed = fullRunData.status === 'crashed'; - const workflowDidSucceed = !fullRunData.data.resultData.error && !workflowHasCrashed; + const workflowWasCanceled = fullRunData.status === 'canceled'; + const workflowDidSucceed = + !fullRunData.data.resultData.error && !workflowHasCrashed && !workflowWasCanceled; let workflowStatusFinal: ExecutionStatus = workflowDidSucceed ? 'success' : 'failed'; if (workflowHasCrashed) workflowStatusFinal = 'crashed'; + if (workflowWasCanceled) workflowStatusFinal = 'canceled'; if ( (workflowDidSucceed && saveDataSuccessExecution === 'none') || @@ -755,9 +758,12 @@ function hookFunctionsSaveWorker(): IWorkflowExecuteHooks { } const workflowHasCrashed = fullRunData.status === 'crashed'; - const workflowDidSucceed = !fullRunData.data.resultData.error && !workflowHasCrashed; + const workflowWasCanceled = fullRunData.status === 'canceled'; + const workflowDidSucceed = + !fullRunData.data.resultData.error && !workflowHasCrashed && !workflowWasCanceled; let workflowStatusFinal: ExecutionStatus = workflowDidSucceed ? 'success' : 'failed'; if (workflowHasCrashed) workflowStatusFinal = 'crashed'; + if (workflowWasCanceled) workflowStatusFinal = 'canceled'; if (!workflowDidSucceed) { executeErrorWorkflow( diff --git a/packages/cli/src/WorkflowRunner.ts b/packages/cli/src/WorkflowRunner.ts index 8e4268502302f..c785a2fa22160 100644 --- a/packages/cli/src/WorkflowRunner.ts +++ b/packages/cli/src/WorkflowRunner.ts @@ -21,7 +21,6 @@ import type { IRun, WorkflowExecuteMode, WorkflowHooks, - WorkflowSettings, } from 'n8n-workflow'; import { ErrorReporterProxy as ErrorReporter, diff --git a/packages/cli/src/WorkflowRunnerProcess.ts b/packages/cli/src/WorkflowRunnerProcess.ts index 3b61ed11fab82..0e4bb08137e13 100644 --- a/packages/cli/src/WorkflowRunnerProcess.ts +++ b/packages/cli/src/WorkflowRunnerProcess.ts @@ -472,6 +472,8 @@ process.on('message', async (message: IProcessMessage) => { ? new WorkflowOperationError('Workflow execution timed out!') : new WorkflowOperationError('Workflow-Execution has been canceled!'); + runData.status = message.type === 'timeout' ? 'failed' : 'canceled'; + // If there is any data send it to parent process, if execution timedout add the error await workflowRunner.workflowExecute.processSuccessExecution( workflowRunner.startedAt, diff --git a/packages/cli/src/api/e2e.api.ts b/packages/cli/src/api/e2e.api.ts index 73adae880dbf9..32b6425f1f721 100644 --- a/packages/cli/src/api/e2e.api.ts +++ b/packages/cli/src/api/e2e.api.ts @@ -54,14 +54,20 @@ const tablesToTruncate = [ 'installed_nodes', 'user', 'role', + 'variables', ]; const truncateAll = async () => { const connection = Db.getConnection(); + for (const table of tablesToTruncate) { - await connection.query( - `DELETE FROM ${table}; DELETE FROM sqlite_sequence WHERE name=${table};`, - ); + try { + await connection.query( + `DELETE FROM ${table}; DELETE FROM sqlite_sequence WHERE name=${table};`, + ); + } catch (error) { + console.warn('Dropping Table for E2E Reset error: ', error); + } } }; @@ -139,8 +145,14 @@ e2eController.post('/db/setup-owner', bodyParser.json(), async (req, res) => { res.writeHead(204).end(); }); -e2eController.post('/enable-feature/:feature', async (req: Request<{ feature: Feature }>, res) => { - const { feature } = req.params; - enabledFeatures[feature] = true; - res.writeHead(204).end(); -}); +e2eController.patch( + '/feature/:feature', + bodyParser.json(), + async (req: Request<{ feature: Feature }>, res) => { + const { feature } = req.params; + const { enabled } = req.body; + + enabledFeatures[feature] = enabled === undefined || enabled === true; + res.writeHead(204).end(); + }, +); diff --git a/packages/cli/src/commands/license/info.ts b/packages/cli/src/commands/license/info.ts new file mode 100644 index 0000000000000..f4ebd406dc1e9 --- /dev/null +++ b/packages/cli/src/commands/license/info.ts @@ -0,0 +1,22 @@ +import { License } from '@/License'; +import { Container } from 'typedi'; +import { BaseCommand } from '../BaseCommand'; + +export class LicenseInfoCommand extends BaseCommand { + static description = 'Print license information'; + + static examples = ['$ n8n license:info']; + + async run() { + const license = Container.get(License); + await license.init(this.instanceId); + + this.logger.info('Printing license information:\n' + license.getInfo()); + } + + async catch(error: Error) { + this.logger.error('\nGOT ERROR'); + this.logger.info('===================================='); + this.logger.error(error.message); + } +} diff --git a/packages/cli/src/commands/start.ts b/packages/cli/src/commands/start.ts index 359c79d584d54..358d8091eb692 100644 --- a/packages/cli/src/commands/start.ts +++ b/packages/cli/src/commands/start.ts @@ -189,8 +189,16 @@ export class Start extends BaseCommand { await license.init(this.instanceId); const activationKey = config.getEnv('license.activationKey'); + if (activationKey) { + const hasCert = (await license.loadCertStr()).length > 0; + + if (hasCert) { + return LoggerProxy.debug('Skipping license activation'); + } + try { + LoggerProxy.debug('Attempting license activation'); await license.activate(activationKey); } catch (e) { LoggerProxy.error('Could not activate license', e as Error); diff --git a/packages/cli/src/config/index.ts b/packages/cli/src/config/index.ts index 4b87524611196..6f20ba3fb9e0f 100644 --- a/packages/cli/src/config/index.ts +++ b/packages/cli/src/config/index.ts @@ -16,6 +16,7 @@ if (inE2ETests) { N8N_PUBLIC_API_DISABLED: 'true', EXTERNAL_FRONTEND_HOOKS_URLS: '', N8N_PERSONALIZATION_ENABLED: 'false', + NODE_FUNCTION_ALLOW_EXTERNAL: 'node-fetch', }; } else if (inTest) { process.env.N8N_PUBLIC_API_DISABLED = 'true'; diff --git a/packages/cli/src/config/schema.ts b/packages/cli/src/config/schema.ts index 729df98ed72f1..e570109f11240 100644 --- a/packages/cli/src/config/schema.ts +++ b/packages/cli/src/config/schema.ts @@ -1130,7 +1130,7 @@ export const schema = { format: Boolean, default: true, env: 'N8N_LICENSE_AUTO_RENEW_ENABLED', - doc: 'Whether autorenew for licenses is enabled.', + doc: 'Whether auto renewal for licenses is enabled.', }, autoRenewOffset: { format: Number, diff --git a/packages/cli/src/constants.ts b/packages/cli/src/constants.ts index 913f37381fea2..6d6ebab38589e 100644 --- a/packages/cli/src/constants.ts +++ b/packages/cli/src/constants.ts @@ -68,7 +68,7 @@ export const WORKFLOW_REACTIVATE_MAX_TIMEOUT = 24 * 60 * 60 * 1000; // 1 day export const SETTINGS_LICENSE_CERT_KEY = 'license.cert'; -export enum LICENSE_FEATURES { +export const enum LICENSE_FEATURES { SHARING = 'feat:sharing', LDAP = 'feat:ldap', SAML = 'feat:saml', @@ -78,7 +78,7 @@ export enum LICENSE_FEATURES { VERSION_CONTROL = 'feat:versionControl', } -export enum LICENSE_QUOTAS { +export const enum LICENSE_QUOTAS { TRIGGER_LIMIT = 'quota:activeWorkflows', VARIABLES_LIMIT = 'quota:maxVariables', } diff --git a/packages/cli/src/controllers/auth.controller.ts b/packages/cli/src/controllers/auth.controller.ts index 068522ce8700d..3492fe7c145df 100644 --- a/packages/cli/src/controllers/auth.controller.ts +++ b/packages/cli/src/controllers/auth.controller.ts @@ -1,5 +1,5 @@ import validator from 'validator'; -import { Get, Post, RestController } from '@/decorators'; +import { Authorized, Get, Post, RestController } from '@/decorators'; import { AuthError, BadRequestError, InternalServerError } from '@/ResponseHelper'; import { sanitizeUser, withFeatureFlags } from '@/UserManagement/UserManagementHelper'; import { issueCookie, resolveJwt } from '@/auth/jwt'; @@ -19,10 +19,13 @@ import type { import { handleEmailLogin, handleLdapLogin } from '@/auth'; import type { PostHogClient } from '@/posthog'; import { + getCurrentAuthenticationMethod, isLdapCurrentAuthenticationMethod, isSamlCurrentAuthenticationMethod, } from '@/sso/ssoHelpers'; import type { UserRepository } from '@db/repositories'; +import { InternalHooks } from '../InternalHooks'; +import Container from 'typedi'; @RestController() export class AuthController { @@ -58,7 +61,6 @@ export class AuthController { /** * Log in a user. - * Authless endpoint. */ @Post('/login') async login(req: LoginRequest, res: Response): Promise<PublicUser | undefined> { @@ -68,12 +70,15 @@ export class AuthController { let user: User | undefined; + let usedAuthenticationMethod = getCurrentAuthenticationMethod(); + if (isSamlCurrentAuthenticationMethod()) { // attempt to fetch user data with the credentials, but don't log in yet const preliminaryUser = await handleEmailLogin(email, password); // if the user is an owner, continue with the login if (preliminaryUser?.globalRole?.name === 'owner') { user = preliminaryUser; + usedAuthenticationMethod = 'email'; } else { throw new AuthError('SAML is enabled, please log in with SAML'); } @@ -84,9 +89,17 @@ export class AuthController { } if (user) { await issueCookie(res, user); + void Container.get(InternalHooks).onUserLoginSuccess({ + user, + authenticationMethod: usedAuthenticationMethod, + }); return withFeatureFlags(this.postHog, sanitizeUser(user)); } - + void Container.get(InternalHooks).onUserLoginFailed({ + user: email, + authenticationMethod: usedAuthenticationMethod, + reason: 'wrong credentials', + }); throw new AuthError('Wrong username or password. Do you have caps lock on?'); } @@ -135,7 +148,6 @@ export class AuthController { /** * Validate invite token to enable invitee to set up their account. - * Authless endpoint. */ @Get('/resolve-signup-token') async resolveSignupToken(req: UserRequest.ResolveSignUp) { @@ -196,8 +208,8 @@ export class AuthController { /** * Log out a user. - * Authless endpoint. */ + @Authorized() @Post('/logout') logout(req: Request, res: Response) { res.clearCookie(AUTH_COOKIE_NAME); diff --git a/packages/cli/src/controllers/ldap.controller.ts b/packages/cli/src/controllers/ldap.controller.ts index 619a70db286b2..b354ef660021c 100644 --- a/packages/cli/src/controllers/ldap.controller.ts +++ b/packages/cli/src/controllers/ldap.controller.ts @@ -1,5 +1,5 @@ import pick from 'lodash.pick'; -import { Get, Post, Put, RestController } from '@/decorators'; +import { Authorized, Get, Post, Put, RestController } from '@/decorators'; import { getLdapConfig, getLdapSynchronizations, updateLdapConfig } from '@/Ldap/helpers'; import { LdapService } from '@/Ldap/LdapService.ee'; import { LdapSync } from '@/Ldap/LdapSync.ee'; @@ -8,6 +8,7 @@ import { BadRequestError } from '@/ResponseHelper'; import { NON_SENSIBLE_LDAP_CONFIG_PROPERTIES } from '@/Ldap/constants'; import { InternalHooks } from '@/InternalHooks'; +@Authorized(['global', 'owner']) @RestController('/ldap') export class LdapController { constructor( diff --git a/packages/cli/src/controllers/me.controller.ts b/packages/cli/src/controllers/me.controller.ts index 3cf1ba409b262..0ff4cd78f0a5c 100644 --- a/packages/cli/src/controllers/me.controller.ts +++ b/packages/cli/src/controllers/me.controller.ts @@ -1,6 +1,6 @@ import validator from 'validator'; import { plainToInstance } from 'class-transformer'; -import { Delete, Get, Patch, Post, RestController } from '@/decorators'; +import { Authorized, Delete, Get, Patch, Post, RestController } from '@/decorators'; import { compareHash, hashPassword, @@ -30,6 +30,7 @@ import { randomBytes } from 'crypto'; import { isSamlLicensedAndEnabled } from '../sso/saml/samlHelpers'; import { UserService } from '@/user/user.service'; +@Authorized() @RestController('/me') export class MeController { private readonly logger: ILogger; diff --git a/packages/cli/src/controllers/nodeTypes.controller.ts b/packages/cli/src/controllers/nodeTypes.controller.ts index 5f8af5a909f22..63002c7c122a9 100644 --- a/packages/cli/src/controllers/nodeTypes.controller.ts +++ b/packages/cli/src/controllers/nodeTypes.controller.ts @@ -2,11 +2,12 @@ import { readFile } from 'fs/promises'; import get from 'lodash.get'; import { Request } from 'express'; import type { INodeTypeDescription, INodeTypeNameVersion } from 'n8n-workflow'; -import { Post, RestController } from '@/decorators'; +import { Authorized, Post, RestController } from '@/decorators'; import { getNodeTranslationPath } from '@/TranslationHelpers'; import type { Config } from '@/config'; import type { NodeTypes } from '@/NodeTypes'; +@Authorized() @RestController('/node-types') export class NodeTypesController { private readonly config: Config; diff --git a/packages/cli/src/controllers/nodes.controller.ts b/packages/cli/src/controllers/nodes.controller.ts index e82bd3e5895b8..9d872c8693d56 100644 --- a/packages/cli/src/controllers/nodes.controller.ts +++ b/packages/cli/src/controllers/nodes.controller.ts @@ -4,7 +4,7 @@ import { STARTER_TEMPLATE_NAME, UNKNOWN_FAILURE_REASON, } from '@/constants'; -import { Delete, Get, Middleware, Patch, Post, RestController } from '@/decorators'; +import { Authorized, Delete, Get, Middleware, Patch, Post, RestController } from '@/decorators'; import { NodeRequest } from '@/requests'; import { BadRequestError, InternalServerError } from '@/ResponseHelper'; import { @@ -30,10 +30,10 @@ import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; import { InternalHooks } from '@/InternalHooks'; import { Push } from '@/push'; import { Config } from '@/config'; -import { isAuthenticatedRequest } from '@/UserManagement/UserManagementHelper'; const { PACKAGE_NOT_INSTALLED, PACKAGE_NAME_NOT_PROVIDED } = RESPONSE_ERROR_MESSAGES; +@Authorized(['global', 'owner']) @RestController('/nodes') export class NodesController { constructor( @@ -43,14 +43,6 @@ export class NodesController { private internalHooks: InternalHooks, ) {} - // TODO: move this into a new decorator `@Authorized` - @Middleware() - checkIfOwner(req: Request, res: Response, next: NextFunction) { - if (!isAuthenticatedRequest(req) || req.user.globalRole.name !== 'owner') - res.status(403).json({ status: 'error', message: 'Unauthorized' }); - else next(); - } - // TODO: move this into a new decorator `@IfConfig('executions.mode', 'queue')` @Middleware() checkIfCommunityNodesEnabled(req: Request, res: Response, next: NextFunction) { diff --git a/packages/cli/src/controllers/owner.controller.ts b/packages/cli/src/controllers/owner.controller.ts index d83a1fecee17d..ae8745a9afab7 100644 --- a/packages/cli/src/controllers/owner.controller.ts +++ b/packages/cli/src/controllers/owner.controller.ts @@ -1,6 +1,6 @@ import validator from 'validator'; import { validateEntity } from '@/GenericHelpers'; -import { Get, Post, RestController } from '@/decorators'; +import { Authorized, Get, Post, RestController } from '@/decorators'; import { BadRequestError } from '@/ResponseHelper'; import { hashPassword, @@ -20,6 +20,7 @@ import type { WorkflowRepository, } from '@db/repositories'; +@Authorized(['global', 'owner']) @RestController('/owner') export class OwnerController { private readonly config: Config; diff --git a/packages/cli/src/controllers/passwordReset.controller.ts b/packages/cli/src/controllers/passwordReset.controller.ts index d78424f6d7303..64e46582066bd 100644 --- a/packages/cli/src/controllers/passwordReset.controller.ts +++ b/packages/cli/src/controllers/passwordReset.controller.ts @@ -65,7 +65,6 @@ export class PasswordResetController { /** * Send a password reset email. - * Authless endpoint. */ @Post('/forgot-password') async forgotPassword(req: PasswordResetRequest.Email) { @@ -171,7 +170,6 @@ export class PasswordResetController { /** * Verify password reset token and user ID. - * Authless endpoint. */ @Get('/resolve-password-token') async resolvePasswordToken(req: PasswordResetRequest.Credentials) { @@ -213,7 +211,6 @@ export class PasswordResetController { /** * Verify password reset token and user ID and update password. - * Authless endpoint. */ @Post('/change-password') async changePassword(req: PasswordResetRequest.NewPassword, res: Response) { diff --git a/packages/cli/src/controllers/tags.controller.ts b/packages/cli/src/controllers/tags.controller.ts index d8ccb4a28ca9c..7b68d95aafb06 100644 --- a/packages/cli/src/controllers/tags.controller.ts +++ b/packages/cli/src/controllers/tags.controller.ts @@ -1,13 +1,14 @@ import { Request, Response, NextFunction } from 'express'; import type { Config } from '@/config'; -import { Delete, Get, Middleware, Patch, Post, RestController } from '@/decorators'; +import { Authorized, Delete, Get, Middleware, Patch, Post, RestController } from '@/decorators'; import type { IDatabaseCollections, IExternalHooksClass, ITagWithCountDb } from '@/Interfaces'; import { TagEntity } from '@db/entities/TagEntity'; import type { TagRepository } from '@db/repositories'; import { validateEntity } from '@/GenericHelpers'; -import { BadRequestError, UnauthorizedError } from '@/ResponseHelper'; +import { BadRequestError } from '@/ResponseHelper'; import { TagsRequest } from '@/requests'; +@Authorized() @RestController('/tags') export class TagsController { private config: Config; @@ -91,15 +92,9 @@ export class TagsController { return tag; } + @Authorized(['global', 'owner']) @Delete('/:id(\\d+)') async deleteTag(req: TagsRequest.Delete) { - const isInstanceOwnerSetUp = this.config.getEnv('userManagement.isInstanceOwnerSetUp'); - if (isInstanceOwnerSetUp && req.user.globalRole.name !== 'owner') { - throw new UnauthorizedError( - 'You are not allowed to perform this action', - 'Only owners can remove tags', - ); - } const { id } = req.params; await this.externalHooks.run('tag.beforeDelete', [id]); diff --git a/packages/cli/src/controllers/translation.controller.ts b/packages/cli/src/controllers/translation.controller.ts index 8240a376a7e64..6d36d650d2e2c 100644 --- a/packages/cli/src/controllers/translation.controller.ts +++ b/packages/cli/src/controllers/translation.controller.ts @@ -2,7 +2,7 @@ import type { Request } from 'express'; import { ICredentialTypes } from 'n8n-workflow'; import { join } from 'path'; import { access } from 'fs/promises'; -import { Get, RestController } from '@/decorators'; +import { Authorized, Get, RestController } from '@/decorators'; import { BadRequestError, InternalServerError } from '@/ResponseHelper'; import { Config } from '@/config'; import { NODES_BASE_DIR } from '@/constants'; @@ -14,6 +14,7 @@ export declare namespace TranslationRequest { export type Credential = Request<{}, {}, {}, { credentialType: string }>; } +@Authorized() @RestController('/') export class TranslationController { constructor(private config: Config, private credentialTypes: ICredentialTypes) {} diff --git a/packages/cli/src/controllers/users.controller.ts b/packages/cli/src/controllers/users.controller.ts index fc9faf06fd9e3..c2ae07d344d1d 100644 --- a/packages/cli/src/controllers/users.controller.ts +++ b/packages/cli/src/controllers/users.controller.ts @@ -5,7 +5,7 @@ import { ErrorReporterProxy as ErrorReporter } from 'n8n-workflow'; import { User } from '@db/entities/User'; import { SharedCredentials } from '@db/entities/SharedCredentials'; import { SharedWorkflow } from '@db/entities/SharedWorkflow'; -import { Delete, Get, Post, RestController } from '@/decorators'; +import { Authorized, NoAuthRequired, Delete, Get, Post, RestController } from '@/decorators'; import { addInviteLinkToUser, generateUserInviteUrl, @@ -41,6 +41,7 @@ import type { UserRepository, } from '@db/repositories'; +@Authorized(['global', 'owner']) @RestController('/users') export class UsersController { private config: Config; @@ -282,6 +283,7 @@ export class UsersController { /** * Fill out user shell with first name, last name, and password. */ + @NoAuthRequired() @Post('/:id') async updateUser(req: UserRequest.Update, res: Response) { const { id: inviteeId } = req.params; @@ -343,6 +345,7 @@ export class UsersController { return withFeatureFlags(this.postHog, sanitizeUser(updatedUser)); } + @Authorized('any') @Get('/') async listUsers(req: UserRequest.List) { const users = await this.userRepository.find({ relations: ['globalRole', 'authIdentities'] }); diff --git a/packages/cli/src/databases/entities/WorkflowStatistics.ts b/packages/cli/src/databases/entities/WorkflowStatistics.ts index 1d3de6316d832..5181bb257c54a 100644 --- a/packages/cli/src/databases/entities/WorkflowStatistics.ts +++ b/packages/cli/src/databases/entities/WorkflowStatistics.ts @@ -3,7 +3,7 @@ import { idStringifier } from '../utils/transformers'; import { datetimeColumnType } from './AbstractEntity'; import { WorkflowEntity } from './WorkflowEntity'; -export enum StatisticsNames { +export const enum StatisticsNames { productionSuccess = 'production_success', productionError = 'production_error', manualSuccess = 'manual_success', diff --git a/packages/cli/src/databases/migrations/mysqldb/1681134145996-AddUserActivatedProperty.ts b/packages/cli/src/databases/migrations/mysqldb/1681134145996-AddUserActivatedProperty.ts new file mode 100644 index 0000000000000..b08c384c29e14 --- /dev/null +++ b/packages/cli/src/databases/migrations/mysqldb/1681134145996-AddUserActivatedProperty.ts @@ -0,0 +1,63 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; +import { getTablePrefix, logMigrationEnd, logMigrationStart } from '@db/utils/migrationHelpers'; +import type { UserSettings } from '@/Interfaces'; + +export class AddUserActivatedProperty1681134145996 implements MigrationInterface { + name = 'AddUserActivatedProperty1681134145996'; + + async up(queryRunner: QueryRunner): Promise<void> { + logMigrationStart(this.name); + + const tablePrefix = getTablePrefix(); + + const activatedUsers: UserSettings[] = await queryRunner.query( + `SELECT DISTINCT sw.userId AS id, + JSON_SET(COALESCE(u.settings, '{}'), '$.userActivated', true) AS settings + FROM ${tablePrefix}workflow_statistics AS ws + JOIN ${tablePrefix}shared_workflow as sw + ON ws.workflowId = sw.workflowId + JOIN ${tablePrefix}role AS r + ON r.id = sw.roleId + JOIN ${tablePrefix}user AS u + ON u.id = sw.userId + WHERE ws.name = 'production_success' + AND r.name = 'owner' + AND r.scope = 'workflow'`, + ); + + const updatedUsers = activatedUsers.map((user) => { + /* + MariaDB returns settings as a string and MySQL as a JSON + */ + const userSettings = + typeof user.settings === 'string' ? user.settings : JSON.stringify(user.settings); + queryRunner.query( + `UPDATE ${tablePrefix}user SET settings = '${userSettings}' WHERE id = '${user.id}' `, + ); + }); + + await Promise.all(updatedUsers); + + if (!activatedUsers.length) { + await queryRunner.query( + `UPDATE ${tablePrefix}user SET settings = JSON_SET(COALESCE(settings, '{}'), '$.userActivated', false)`, + ); + } else { + const activatedUserIds = activatedUsers.map((user) => `'${user.id}'`).join(','); + + await queryRunner.query( + `UPDATE ${tablePrefix}user SET settings = JSON_SET(COALESCE(settings, '{}'), '$.userActivated', false) WHERE id NOT IN (${activatedUserIds})`, + ); + } + + logMigrationEnd(this.name); + } + + async down(queryRunner: QueryRunner): Promise<void> { + const tablePrefix = getTablePrefix(); + await queryRunner.query( + `UPDATE ${tablePrefix}user SET settings = JSON_REMOVE(settings, '$.userActivated')`, + ); + await queryRunner.query(`UPDATE ${tablePrefix}user SET settings = NULL WHERE settings = '{}'`); + } +} diff --git a/packages/cli/src/databases/migrations/mysqldb/index.ts b/packages/cli/src/databases/migrations/mysqldb/index.ts index 6d97fbceb14cb..3f53f8b24af55 100644 --- a/packages/cli/src/databases/migrations/mysqldb/index.ts +++ b/packages/cli/src/databases/migrations/mysqldb/index.ts @@ -36,6 +36,7 @@ import { MigrateExecutionStatus1676996103000 } from './1676996103000-MigrateExec import { UpdateRunningExecutionStatus1677236788851 } from './1677236788851-UpdateRunningExecutionStatus'; import { CreateExecutionMetadataTable1679416281779 } from './1679416281779-CreateExecutionMetadataTable'; import { CreateVariables1677501636753 } from './1677501636753-CreateVariables'; +import { AddUserActivatedProperty1681134145996 } from './1681134145996-AddUserActivatedProperty'; export const mysqlMigrations = [ InitialMigration1588157391238, @@ -76,4 +77,5 @@ export const mysqlMigrations = [ UpdateRunningExecutionStatus1677236788851, CreateExecutionMetadataTable1679416281779, CreateVariables1677501636753, + AddUserActivatedProperty1681134145996, ]; diff --git a/packages/cli/src/databases/migrations/postgresdb/1681134145996-AddUserActivatedProperty.ts b/packages/cli/src/databases/migrations/postgresdb/1681134145996-AddUserActivatedProperty.ts new file mode 100644 index 0000000000000..bf4293d4ddaae --- /dev/null +++ b/packages/cli/src/databases/migrations/postgresdb/1681134145996-AddUserActivatedProperty.ts @@ -0,0 +1,62 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; +import { getTablePrefix, logMigrationEnd, logMigrationStart } from '@db/utils/migrationHelpers'; +import type { UserSettings } from '@/Interfaces'; + +export class AddUserActivatedProperty1681134145996 implements MigrationInterface { + name = 'AddUserActivatedProperty1681134145996'; + + async up(queryRunner: QueryRunner): Promise<void> { + logMigrationStart(this.name); + + const tablePrefix = getTablePrefix(); + + const activatedUsers: UserSettings[] = await queryRunner.query( + `SELECT DISTINCT sw."userId" AS id, + JSONB_SET(COALESCE(u.settings::jsonb, '{}'), '{userActivated}', 'true', true) as settings + FROM ${tablePrefix}workflow_statistics ws + JOIN ${tablePrefix}shared_workflow sw + ON ws."workflowId" = sw."workflowId" + JOIN ${tablePrefix}role r + ON r.id = sw."roleId" + JOIN "${tablePrefix}user" u + ON u.id = sw."userId" + WHERE ws.name = 'production_success' + AND r.name = 'owner' + AND r.scope = 'workflow'`, + ); + + const updatedUsers = activatedUsers.map((user) => + queryRunner.query( + `UPDATE "${tablePrefix}user" SET settings = '${JSON.stringify( + user.settings, + )}' WHERE id = '${user.id}' `, + ), + ); + + await Promise.all(updatedUsers); + + if (!activatedUsers.length) { + await queryRunner.query( + `UPDATE "${tablePrefix}user" SET settings = JSONB_SET(COALESCE(settings::jsonb, '{}'), '{userActivated}', 'false', true)`, + ); + } else { + const activatedUserIds = activatedUsers.map((user) => `'${user.id}'`).join(','); + + await queryRunner.query( + `UPDATE "${tablePrefix}user" SET settings = JSONB_SET(COALESCE(settings::jsonb, '{}'), '{userActivated}', 'false', true) WHERE id NOT IN (${activatedUserIds})`, + ); + } + + logMigrationEnd(this.name); + } + + async down(queryRunner: QueryRunner): Promise<void> { + const tablePrefix = getTablePrefix(); + await queryRunner.query( + `UPDATE "${tablePrefix}user" SET settings = settings::jsonb - 'userActivated'`, + ); + await queryRunner.query( + `UPDATE "${tablePrefix}user" SET settings = NULL WHERE settings::jsonb = '{}'::jsonb`, + ); + } +} diff --git a/packages/cli/src/databases/migrations/postgresdb/index.ts b/packages/cli/src/databases/migrations/postgresdb/index.ts index 175ff14848473..231518df99249 100644 --- a/packages/cli/src/databases/migrations/postgresdb/index.ts +++ b/packages/cli/src/databases/migrations/postgresdb/index.ts @@ -34,6 +34,7 @@ import { MigrateExecutionStatus1676996103000 } from './1676996103000-MigrateExec import { UpdateRunningExecutionStatus1677236854063 } from './1677236854063-UpdateRunningExecutionStatus'; import { CreateExecutionMetadataTable1679416281778 } from './1679416281778-CreateExecutionMetadataTable'; import { CreateVariables1677501636754 } from './1677501636754-CreateVariables'; +import { AddUserActivatedProperty1681134145996 } from './1681134145996-AddUserActivatedProperty'; export const postgresMigrations = [ InitialMigration1587669153312, @@ -72,4 +73,5 @@ export const postgresMigrations = [ UpdateRunningExecutionStatus1677236854063, CreateExecutionMetadataTable1679416281778, CreateVariables1677501636754, + AddUserActivatedProperty1681134145996, ]; diff --git a/packages/cli/src/databases/migrations/sqlite/1681134145996-AddUserActivatedProperty.ts b/packages/cli/src/databases/migrations/sqlite/1681134145996-AddUserActivatedProperty.ts new file mode 100644 index 0000000000000..335c4b29a8e0f --- /dev/null +++ b/packages/cli/src/databases/migrations/sqlite/1681134145996-AddUserActivatedProperty.ts @@ -0,0 +1,57 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; +import { getTablePrefix, logMigrationEnd, logMigrationStart } from '@db/utils/migrationHelpers'; +import type { UserSettings } from '@/Interfaces'; + +export class AddUserActivatedProperty1681134145996 implements MigrationInterface { + name = 'AddUserActivatedProperty1681134145996'; + + async up(queryRunner: QueryRunner): Promise<void> { + logMigrationStart(this.name); + + const tablePrefix = getTablePrefix(); + + const activatedUsers: UserSettings[] = await queryRunner.query( + `SELECT DISTINCT sw.userId AS id, + JSON_SET(COALESCE(u.settings, '{}'), '$.userActivated', JSON('true')) AS settings + FROM ${tablePrefix}workflow_statistics AS ws + JOIN ${tablePrefix}shared_workflow AS sw + ON ws.workflowId = sw.workflowId + JOIN ${tablePrefix}role AS r + ON r.id = sw.roleId + JOIN ${tablePrefix}user AS u + ON u.id = sw.userId + WHERE ws.name = 'production_success' + AND r.name = 'owner' + AND r.scope = "workflow"`, + ); + + const updatedUsers = activatedUsers.map((user) => + queryRunner.query( + `UPDATE ${tablePrefix}user SET settings = '${user.settings}' WHERE id = '${user.id}' `, + ), + ); + + await Promise.all(updatedUsers); + + if (!activatedUsers.length) { + await queryRunner.query( + `UPDATE ${tablePrefix}user SET settings = JSON_SET(COALESCE(settings, '{}'), '$.userActivated', JSON('false'))`, + ); + } else { + const activatedUserIds = activatedUsers.map((user) => `'${user.id}'`).join(','); + await queryRunner.query( + `UPDATE ${tablePrefix}user SET settings = JSON_SET(COALESCE(settings, '{}'), '$.userActivated', JSON('false')) WHERE id NOT IN (${activatedUserIds})`, + ); + } + + logMigrationEnd(this.name); + } + + async down(queryRunner: QueryRunner): Promise<void> { + const tablePrefix = getTablePrefix(); + await queryRunner.query( + `UPDATE ${tablePrefix}user SET settings = JSON_REMOVE(settings, '$.userActivated')`, + ); + await queryRunner.query(`UPDATE ${tablePrefix}user SET settings = NULL WHERE settings = '{}'`); + } +} diff --git a/packages/cli/src/databases/migrations/sqlite/index.ts b/packages/cli/src/databases/migrations/sqlite/index.ts index b7e324823fb86..e78aad2bc69ac 100644 --- a/packages/cli/src/databases/migrations/sqlite/index.ts +++ b/packages/cli/src/databases/migrations/sqlite/index.ts @@ -33,6 +33,7 @@ import { MigrateExecutionStatus1676996103000 } from './1676996103000-MigrateExec import { UpdateRunningExecutionStatus1677237073720 } from './1677237073720-UpdateRunningExecutionStatus'; import { CreateExecutionMetadataTable1679416281777 } from './1679416281777-CreateExecutionMetadataTable'; import { CreateVariables1677501636752 } from './1677501636752-CreateVariables'; +import { AddUserActivatedProperty1681134145996 } from './1681134145996-AddUserActivatedProperty'; const sqliteMigrations = [ InitialMigration1588102412422, @@ -70,6 +71,7 @@ const sqliteMigrations = [ UpdateRunningExecutionStatus1677237073720, CreateVariables1677501636752, CreateExecutionMetadataTable1679416281777, + AddUserActivatedProperty1681134145996, ]; export { sqliteMigrations }; diff --git a/packages/cli/src/decorators/Authorized.ts b/packages/cli/src/decorators/Authorized.ts new file mode 100644 index 0000000000000..880938ae5ce72 --- /dev/null +++ b/packages/cli/src/decorators/Authorized.ts @@ -0,0 +1,16 @@ +/* eslint-disable @typescript-eslint/ban-types */ +/* eslint-disable @typescript-eslint/naming-convention */ +import { CONTROLLER_AUTH_ROLES } from './constants'; +import type { AuthRoleMetadata } from './types'; + +export function Authorized(authRole: AuthRoleMetadata[string] = 'any'): Function { + return function (target: Function | Object, handlerName?: string) { + const controllerClass = handlerName ? target.constructor : target; + const authRoles = (Reflect.getMetadata(CONTROLLER_AUTH_ROLES, controllerClass) ?? + {}) as AuthRoleMetadata; + authRoles[handlerName ?? '*'] = authRole; + Reflect.defineMetadata(CONTROLLER_AUTH_ROLES, authRoles, controllerClass); + }; +} + +export const NoAuthRequired = () => Authorized('none'); diff --git a/packages/cli/src/decorators/constants.ts b/packages/cli/src/decorators/constants.ts index 6bff0e4a6cb90..5a5efc938db6c 100644 --- a/packages/cli/src/decorators/constants.ts +++ b/packages/cli/src/decorators/constants.ts @@ -1,3 +1,4 @@ export const CONTROLLER_ROUTES = 'CONTROLLER_ROUTES'; export const CONTROLLER_BASE_PATH = 'CONTROLLER_BASE_PATH'; export const CONTROLLER_MIDDLEWARES = 'CONTROLLER_MIDDLEWARES'; +export const CONTROLLER_AUTH_ROLES = 'CONTROLLER_AUTH_ROLES'; diff --git a/packages/cli/src/decorators/index.ts b/packages/cli/src/decorators/index.ts index 71b82b5b69c30..0e683d410cbc9 100644 --- a/packages/cli/src/decorators/index.ts +++ b/packages/cli/src/decorators/index.ts @@ -1,3 +1,4 @@ +export { Authorized, NoAuthRequired } from './Authorized'; export { RestController } from './RestController'; export { Get, Post, Put, Patch, Delete } from './Route'; export { Middleware } from './Middleware'; diff --git a/packages/cli/src/decorators/registerController.ts b/packages/cli/src/decorators/registerController.ts index e20293ae21c71..54ff6bbbbcfd6 100644 --- a/packages/cli/src/decorators/registerController.ts +++ b/packages/cli/src/decorators/registerController.ts @@ -1,10 +1,41 @@ /* eslint-disable @typescript-eslint/naming-convention */ import { Router } from 'express'; +import type { Application, Request, Response, RequestHandler } from 'express'; import type { Config } from '@/config'; -import { CONTROLLER_BASE_PATH, CONTROLLER_MIDDLEWARES, CONTROLLER_ROUTES } from './constants'; +import type { AuthenticatedRequest } from '@/requests'; import { send } from '@/ResponseHelper'; // TODO: move `ResponseHelper.send` to this file -import type { Application, Request, Response, RequestHandler } from 'express'; -import type { Controller, MiddlewareMetadata, RouteMetadata } from './types'; +import { + CONTROLLER_AUTH_ROLES, + CONTROLLER_BASE_PATH, + CONTROLLER_MIDDLEWARES, + CONTROLLER_ROUTES, +} from './constants'; +import type { + AuthRole, + AuthRoleMetadata, + Controller, + MiddlewareMetadata, + RouteMetadata, +} from './types'; + +export const createAuthMiddleware = + (authRole: AuthRole): RequestHandler => + ({ user }: AuthenticatedRequest, res, next) => { + if (authRole === 'none') return next(); + + if (!user) return res.status(401).json({ status: 'error', message: 'Unauthorized' }); + + const { globalRole } = user; + if (authRole === 'any' || (globalRole.scope === authRole[0] && globalRole.name === authRole[1])) + return next(); + + res.status(403).json({ status: 'error', message: 'Unauthorized' }); + }; + +const authFreeRoutes: string[] = []; + +export const canSkipAuth = (method: string, path: string): boolean => + authFreeRoutes.includes(`${method.toLowerCase()} ${path}`); export const registerController = (app: Application, config: Config, controller: object) => { const controllerClass = controller.constructor; @@ -14,11 +45,16 @@ export const registerController = (app: Application, config: Config, controller: if (!controllerBasePath) throw new Error(`${controllerClass.name} is missing the RestController decorator`); + const authRoles = Reflect.getMetadata(CONTROLLER_AUTH_ROLES, controllerClass) as + | AuthRoleMetadata + | undefined; const routes = Reflect.getMetadata(CONTROLLER_ROUTES, controllerClass) as RouteMetadata[]; if (routes.length > 0) { const router = Router({ mergeParams: true }); const restBasePath = config.getEnv('endpoints.rest'); - const prefix = `/${[restBasePath, controllerBasePath].join('/')}`.replace(/\/+/g, '/'); + const prefix = `/${[restBasePath, controllerBasePath].join('/')}` + .replace(/\/+/g, '/') + .replace(/\/$/, ''); const controllerMiddlewares = ( (Reflect.getMetadata(CONTROLLER_MIDDLEWARES, controllerClass) ?? []) as MiddlewareMetadata[] @@ -28,14 +64,17 @@ export const registerController = (app: Application, config: Config, controller: ); routes.forEach(({ method, path, middlewares: routeMiddlewares, handlerName }) => { + const authRole = authRoles && (authRoles[handlerName] ?? authRoles['*']); router[method]( path, + ...(authRole ? [createAuthMiddleware(authRole)] : []), ...controllerMiddlewares, ...routeMiddlewares, send(async (req: Request, res: Response) => (controller as Controller)[handlerName](req, res), ), ); + if (!authRole || authRole === 'none') authFreeRoutes.push(`${method} ${prefix}${path}`); }); app.use(prefix, router); diff --git a/packages/cli/src/decorators/types.ts b/packages/cli/src/decorators/types.ts index 250abf0d27c95..d118e6e6c9cbd 100644 --- a/packages/cli/src/decorators/types.ts +++ b/packages/cli/src/decorators/types.ts @@ -1,7 +1,11 @@ import type { Request, Response, RequestHandler } from 'express'; +import type { RoleNames, RoleScopes } from '@db/entities/Role'; export type Method = 'get' | 'post' | 'put' | 'patch' | 'delete'; +export type AuthRole = [RoleScopes, RoleNames] | 'any' | 'none'; +export type AuthRoleMetadata = Record<string, AuthRole>; + export interface MiddlewareMetadata { handlerName: string; } diff --git a/packages/cli/src/environment/versionControl/versionControlHelper.ee.ts b/packages/cli/src/environment/versionControl/versionControlHelper.ee.ts deleted file mode 100644 index b3857120db144..0000000000000 --- a/packages/cli/src/environment/versionControl/versionControlHelper.ee.ts +++ /dev/null @@ -1,18 +0,0 @@ -import Container from 'typedi'; -import { License } from '../../License'; -import { generateKeyPairSync } from 'crypto'; - -export function isVersionControlEnabled() { - const license = Container.get(License); - return license.isVersionControlLicensed(); -} - -export async function generateSshKeyPair() { - const keyPair = generateKeyPairSync('ed25519', { - privateKeyEncoding: { format: 'pem', type: 'pkcs8' }, - publicKeyEncoding: { format: 'pem', type: 'spki' }, - }); - - console.log(keyPair.privateKey); - console.log(keyPair.publicKey); -} diff --git a/packages/cli/src/environments/enviromentHelpers.ts b/packages/cli/src/environments/variables/enviromentHelpers.ts similarity index 100% rename from packages/cli/src/environments/enviromentHelpers.ts rename to packages/cli/src/environments/variables/enviromentHelpers.ts diff --git a/packages/cli/src/environments/variables.controller.ee.ts b/packages/cli/src/environments/variables/variables.controller.ee.ts similarity index 100% rename from packages/cli/src/environments/variables.controller.ee.ts rename to packages/cli/src/environments/variables/variables.controller.ee.ts diff --git a/packages/cli/src/environments/variables.controller.ts b/packages/cli/src/environments/variables/variables.controller.ts similarity index 100% rename from packages/cli/src/environments/variables.controller.ts rename to packages/cli/src/environments/variables/variables.controller.ts diff --git a/packages/cli/src/environments/variables.service.ee.ts b/packages/cli/src/environments/variables/variables.service.ee.ts similarity index 100% rename from packages/cli/src/environments/variables.service.ee.ts rename to packages/cli/src/environments/variables/variables.service.ee.ts diff --git a/packages/cli/src/environments/variables.service.ts b/packages/cli/src/environments/variables/variables.service.ts similarity index 100% rename from packages/cli/src/environments/variables.service.ts rename to packages/cli/src/environments/variables/variables.service.ts diff --git a/packages/cli/src/environments/versionControl/middleware/versionControlEnabledMiddleware.ts b/packages/cli/src/environments/versionControl/middleware/versionControlEnabledMiddleware.ts index 55a5e2122e127..5ed3a1293b443 100644 --- a/packages/cli/src/environments/versionControl/middleware/versionControlEnabledMiddleware.ts +++ b/packages/cli/src/environments/versionControl/middleware/versionControlEnabledMiddleware.ts @@ -1,22 +1,9 @@ import type { RequestHandler } from 'express'; -import type { AuthenticatedRequest } from '@/requests'; import { isVersionControlLicensed, isVersionControlLicensedAndEnabled, } from '../versionControlHelper'; -export const versionControlLicensedOwnerMiddleware: RequestHandler = ( - req: AuthenticatedRequest, - res, - next, -) => { - if (isVersionControlLicensed() && req.user?.globalRole.name === 'owner') { - next(); - } else { - res.status(401).json({ status: 'error', message: 'Unauthorized' }); - } -}; - export const versionControlLicensedAndEnabledMiddleware: RequestHandler = (req, res, next) => { if (isVersionControlLicensedAndEnabled()) { next(); diff --git a/packages/cli/src/environments/versionControl/types/requests.ts b/packages/cli/src/environments/versionControl/types/requests.ts new file mode 100644 index 0000000000000..0782873ba56ae --- /dev/null +++ b/packages/cli/src/environments/versionControl/types/requests.ts @@ -0,0 +1,6 @@ +import type { AuthenticatedRequest } from '@/requests'; +import type { VersionControlPreferences } from './versionControlPreferences'; + +export declare namespace VersionControlRequest { + type UpdatePreferences = AuthenticatedRequest<{}, {}, Partial<VersionControlPreferences>, {}>; +} diff --git a/packages/cli/src/environments/versionControl/types/versionControlPreferences.ts b/packages/cli/src/environments/versionControl/types/versionControlPreferences.ts index ff2becc5d48fb..9481270ec5394 100644 --- a/packages/cli/src/environments/versionControl/types/versionControlPreferences.ts +++ b/packages/cli/src/environments/versionControl/types/versionControlPreferences.ts @@ -1,9 +1,36 @@ -import { IsString } from 'class-validator'; +import { IsBoolean, IsEmail, IsHexColor, IsOptional, IsString } from 'class-validator'; export class VersionControlPreferences { + constructor(preferences: Partial<VersionControlPreferences> | undefined = undefined) { + if (preferences) Object.assign(this, preferences); + } + + @IsBoolean() + connected: boolean; + + @IsString() + repositoryUrl: string; + + @IsString() + authorName: string; + + @IsEmail() + authorEmail: string; + + @IsString() + branchName: string; + + @IsBoolean() + branchReadOnly: boolean; + + @IsHexColor() + branchColor: string; + + @IsOptional() @IsString() - privateKey: string; + readonly privateKey?: string; + @IsOptional() @IsString() - publicKey: string; + readonly publicKey?: string; } diff --git a/packages/cli/src/environments/versionControl/versionControl.controller.ee.ts b/packages/cli/src/environments/versionControl/versionControl.controller.ee.ts index fd3d38a6eab4a..5bbac46f0bcb6 100644 --- a/packages/cli/src/environments/versionControl/versionControl.controller.ee.ts +++ b/packages/cli/src/environments/versionControl/versionControl.controller.ee.ts @@ -1,21 +1,36 @@ -import { Get, RestController } from '../../decorators'; -import { - versionControlLicensedMiddleware, - versionControlLicensedOwnerMiddleware, -} from './middleware/versionControlEnabledMiddleware'; +import { Authorized, Get, Post, RestController } from '@/decorators'; +import { versionControlLicensedMiddleware } from './middleware/versionControlEnabledMiddleware'; import { VersionControlService } from './versionControl.service.ee'; +import { VersionControlRequest } from './types/requests'; +import type { VersionControlPreferences } from './types/versionControlPreferences'; @RestController('/versionControl') export class VersionControlController { constructor(private versionControlService: VersionControlService) {} + @Authorized('any') @Get('/preferences', { middlewares: [versionControlLicensedMiddleware] }) - async getPreferences() { + async getPreferences(): Promise<VersionControlPreferences> { + // returns the settings with the privateKey property redacted return this.versionControlService.versionControlPreferences; } + @Authorized(['global', 'owner']) + @Post('/preferences', { middlewares: [versionControlLicensedMiddleware] }) + async setPreferences(req: VersionControlRequest.UpdatePreferences) { + const sanitizedPreferences: Partial<VersionControlPreferences> = { + ...req.body, + privateKey: undefined, + publicKey: undefined, + }; + await this.versionControlService.validateVersionControlPreferences(sanitizedPreferences); + return this.versionControlService.setPreferences(sanitizedPreferences); + } + //TODO: temporary function to generate key and save new pair - @Get('/generateKeyPair', { middlewares: [versionControlLicensedOwnerMiddleware] }) + // REMOVE THIS FUNCTION AFTER TESTING + @Authorized(['global', 'owner']) + @Get('/generateKeyPair', { middlewares: [versionControlLicensedMiddleware] }) async generateKeyPair() { return this.versionControlService.generateAndSaveKeyPair(); } diff --git a/packages/cli/src/environments/versionControl/versionControl.service.ee.ts b/packages/cli/src/environments/versionControl/versionControl.service.ee.ts index 3096b5531ddd6..d531935f00152 100644 --- a/packages/cli/src/environments/versionControl/versionControl.service.ee.ts +++ b/packages/cli/src/environments/versionControl/versionControl.service.ee.ts @@ -3,7 +3,9 @@ import { generateSshKeyPair } from './versionControlHelper'; import { VersionControlPreferences } from './types/versionControlPreferences'; import { VERSION_CONTROL_PREFERENCES_DB_KEY } from './constants'; import * as Db from '@/Db'; -import { jsonParse } from 'n8n-workflow'; +import { jsonParse, LoggerProxy } from 'n8n-workflow'; +import type { ValidationError } from 'class-validator'; +import { validate } from 'class-validator'; @Service() export class VersionControlService { @@ -16,55 +18,91 @@ export class VersionControlService { public get versionControlPreferences(): VersionControlPreferences { return { ...this._versionControlPreferences, - privateKey: '', + privateKey: '(redacted)', }; } - async generateAndSaveKeyPair(keyType: 'ed25519' | 'rsa' = 'ed25519') { - const keyPair = generateSshKeyPair(keyType); + public set versionControlPreferences(preferences: Partial<VersionControlPreferences>) { + this._versionControlPreferences = { + connected: preferences.connected ?? this._versionControlPreferences.connected, + authorEmail: preferences.authorEmail ?? this._versionControlPreferences.authorEmail, + authorName: preferences.authorName ?? this._versionControlPreferences.authorName, + branchName: preferences.branchName ?? this._versionControlPreferences.branchName, + branchColor: preferences.branchColor ?? this._versionControlPreferences.branchColor, + branchReadOnly: preferences.branchReadOnly ?? this._versionControlPreferences.branchReadOnly, + privateKey: preferences.privateKey ?? this._versionControlPreferences.privateKey, + publicKey: preferences.publicKey ?? this._versionControlPreferences.publicKey, + repositoryUrl: preferences.repositoryUrl ?? this._versionControlPreferences.repositoryUrl, + }; + } + + async generateAndSaveKeyPair() { + const keyPair = generateSshKeyPair('ed25519'); if (keyPair.publicKey && keyPair.privateKey) { - this.setPreferences({ ...keyPair }); - await this.saveSamlPreferencesToDb(); + await this.setPreferences({ ...keyPair }); + } else { + LoggerProxy.error('Failed to generate key pair'); } return keyPair; } - setPreferences(prefs: Partial<VersionControlPreferences>) { - this._versionControlPreferences = { - ...this._versionControlPreferences, - ...prefs, - }; + async validateVersionControlPreferences( + preferences: Partial<VersionControlPreferences>, + ): Promise<ValidationError[]> { + const preferencesObject = new VersionControlPreferences(preferences); + const validationResult = await validate(preferencesObject, { + forbidUnknownValues: false, + skipMissingProperties: true, + stopAtFirstError: false, + validationError: { target: false }, + }); + if (validationResult.length > 0) { + throw new Error(`Invalid version control preferences: ${JSON.stringify(validationResult)}`); + } + // TODO: if repositoryUrl is changed, check if it is valid + // TODO: if branchName is changed, check if it is valid + return validationResult; + } + + async setPreferences( + preferences: Partial<VersionControlPreferences>, + saveToDb = true, + ): Promise<VersionControlPreferences> { + this.versionControlPreferences = preferences; + if (saveToDb) { + const settingsValue = JSON.stringify(this._versionControlPreferences); + try { + await Db.collections.Settings.save({ + key: VERSION_CONTROL_PREFERENCES_DB_KEY, + value: settingsValue, + loadOnStartup: true, + }); + } catch (error) { + throw new Error(`Failed to save version control preferences: ${(error as Error).message}`); + } + } + return this.versionControlPreferences; } async loadFromDbAndApplyVersionControlPreferences(): Promise< VersionControlPreferences | undefined > { - const loadedPrefs = await Db.collections.Settings.findOne({ + const loadedPreferences = await Db.collections.Settings.findOne({ where: { key: VERSION_CONTROL_PREFERENCES_DB_KEY }, }); - if (loadedPrefs) { + if (loadedPreferences) { try { - const prefs = jsonParse<VersionControlPreferences>(loadedPrefs.value); - if (prefs) { - this.setPreferences(prefs); - return prefs; + const preferences = jsonParse<VersionControlPreferences>(loadedPreferences.value); + if (preferences) { + await this.setPreferences(preferences, false); + return preferences; } - } catch {} + } catch (error) { + LoggerProxy.warn( + `Could not parse Version Control settings from database: ${(error as Error).message}`, + ); + } } return; } - - async saveSamlPreferencesToDb(): Promise<VersionControlPreferences | undefined> { - const settingsValue = JSON.stringify(this._versionControlPreferences); - const result = await Db.collections.Settings.save({ - key: VERSION_CONTROL_PREFERENCES_DB_KEY, - value: settingsValue, - loadOnStartup: true, - }); - if (result) - try { - return jsonParse<VersionControlPreferences>(result.value); - } catch {} - return; - } } diff --git a/packages/cli/src/environments/versionControl/versionControlHelper.ts b/packages/cli/src/environments/versionControl/versionControlHelper.ts index 4aa9a383c503a..9810977f91983 100644 --- a/packages/cli/src/environments/versionControl/versionControlHelper.ts +++ b/packages/cli/src/environments/versionControl/versionControlHelper.ts @@ -49,5 +49,8 @@ export function generateSshKeyPair(keyType: 'ed25519' | 'rsa' = 'ed25519') { keyPair.publicKey = keyPublic.toString('ssh'); const keyPrivate = sshpk.parsePrivateKey(generatedKeyPair.privateKey, 'pem'); keyPair.privateKey = keyPrivate.toString('ssh-private'); - return keyPair; + return { + privateKey: keyPair.privateKey, + publicKey: keyPair.publicKey, + }; } diff --git a/packages/cli/src/eventbus/EventMessageClasses/index.ts b/packages/cli/src/eventbus/EventMessageClasses/index.ts index 4b4fe47f8d822..28da7c5eccaa4 100644 --- a/packages/cli/src/eventbus/EventMessageClasses/index.ts +++ b/packages/cli/src/eventbus/EventMessageClasses/index.ts @@ -11,6 +11,8 @@ export const eventNamesWorkflow = [ ] as const; export const eventNamesNode = ['n8n.node.started', 'n8n.node.finished'] as const; export const eventNamesAudit = [ + 'n8n.audit.user.login.success', + 'n8n.audit.user.login.failed', 'n8n.audit.user.signedup', 'n8n.audit.user.updated', 'n8n.audit.user.deleted', diff --git a/packages/cli/src/eventbus/eventBus.controller.ts b/packages/cli/src/eventbus/eventBus.controller.ts index bc18472d4c910..7c9ce21ccc5ee 100644 --- a/packages/cli/src/eventbus/eventBus.controller.ts +++ b/packages/cli/src/eventbus/eventBus.controller.ts @@ -28,15 +28,13 @@ import type { IRunExecutionData, } from 'n8n-workflow'; import { MessageEventBusDestinationTypeNames, EventMessageTypeNames } from 'n8n-workflow'; -import type { User } from '@db/entities/User'; -import * as ResponseHelper from '@/ResponseHelper'; import type { EventMessageNodeOptions } from './EventMessageClasses/EventMessageNode'; import { EventMessageNode } from './EventMessageClasses/EventMessageNode'; import { recoverExecutionDataFromEventLogMessages } from './MessageEventBus/recoverEvents'; -import { RestController, Get, Post, Delete } from '@/decorators'; +import { RestController, Get, Post, Delete, Authorized } from '@/decorators'; import type { MessageEventBusDestination } from './MessageEventBusDestination/MessageEventBusDestination.ee'; -import { isOwnerMiddleware } from '../middlewares/isOwner'; import type { DeleteResult } from 'typeorm'; +import { AuthenticatedRequest } from '@/requests'; // ---------------------------------------- // TypeGuards @@ -74,12 +72,14 @@ const isMessageEventBusDestinationOptions = ( // Controller // ---------------------------------------- +@Authorized() @RestController('/eventbus') export class EventBusController { // ---------------------------------------- // Events // ---------------------------------------- - @Get('/event', { middlewares: [isOwnerMiddleware] }) + @Authorized(['global', 'owner']) + @Get('/event') async getEvents( req: express.Request, ): Promise<EventMessageTypes[] | Record<string, EventMessageTypes[]>> { @@ -132,7 +132,8 @@ export class EventBusController { return; } - @Post('/event', { middlewares: [isOwnerMiddleware] }) + @Authorized(['global', 'owner']) + @Post('/event') async postEvent(req: express.Request): Promise<EventMessageTypes | undefined> { let msg: EventMessageTypes | undefined; if (isEventMessageOptions(req.body)) { @@ -172,12 +173,9 @@ export class EventBusController { } } - @Post('/destination', { middlewares: [isOwnerMiddleware] }) - async postDestination(req: express.Request): Promise<any> { - if (!req.user || (req.user as User).globalRole.name !== 'owner') { - throw new ResponseHelper.UnauthorizedError('Invalid request'); - } - + @Authorized(['global', 'owner']) + @Post('/destination') + async postDestination(req: AuthenticatedRequest): Promise<any> { let result: MessageEventBusDestination | undefined; if (isMessageEventBusDestinationOptions(req.body)) { switch (req.body.__type) { @@ -228,11 +226,9 @@ export class EventBusController { return false; } - @Delete('/destination', { middlewares: [isOwnerMiddleware] }) - async deleteDestination(req: express.Request): Promise<DeleteResult | undefined> { - if (!req.user || (req.user as User).globalRole.name !== 'owner') { - throw new ResponseHelper.UnauthorizedError('Invalid request'); - } + @Authorized(['global', 'owner']) + @Delete('/destination') + async deleteDestination(req: AuthenticatedRequest): Promise<DeleteResult | undefined> { if (isWithIdString(req.query)) { return eventBus.removeDestination(req.query.id); } else { diff --git a/packages/cli/src/events/WorkflowStatistics.ts b/packages/cli/src/events/WorkflowStatistics.ts index 5567ded3b8afd..8089e57398ca1 100644 --- a/packages/cli/src/events/WorkflowStatistics.ts +++ b/packages/cli/src/events/WorkflowStatistics.ts @@ -9,7 +9,7 @@ import { InternalHooks } from '@/InternalHooks'; import config from '@/config'; import { UserService } from '@/user/user.service'; -enum StatisticsUpsertResult { +const enum StatisticsUpsertResult { insert = 'insert', update = 'update', failed = 'failed', @@ -114,7 +114,7 @@ export async function workflowExecutionCompleted( workflow_id: workflowId, }; - if (!owner.settings?.firstSuccessfulWorkflowId) { + if (!owner.settings?.userActivated) { await UserService.updateUserSettings(owner.id, { firstSuccessfulWorkflowId: workflowId, userActivated: true, diff --git a/packages/cli/src/license/license.controller.ts b/packages/cli/src/license/license.controller.ts index 75d8252411826..7fc24df18846c 100644 --- a/packages/cli/src/license/license.controller.ts +++ b/packages/cli/src/license/license.controller.ts @@ -75,24 +75,29 @@ licenseController.post( } catch (e) { const error = e as Error & { errorId?: string }; + let message = 'Failed to activate license'; + + //override specific error messages (to map License Server vocabulary to n8n terms) switch (error.errorId ?? 'UNSPECIFIED') { case 'SCHEMA_VALIDATION': - error.message = 'Activation key is in the wrong format'; + message = 'Activation key is in the wrong format'; break; case 'RESERVATION_EXHAUSTED': - error.message = + message = 'Activation key has been used too many times. Please contact sales@n8n.io if you would like to extend it'; break; case 'RESERVATION_EXPIRED': - error.message = 'Activation key has expired'; + message = 'Activation key has expired'; break; case 'NOT_FOUND': case 'RESERVATION_CONFLICT': - error.message = 'Activation key not found'; + message = 'Activation key not found'; break; + default: + getLogger().error(message, { stack: error.stack ?? 'n/a' }); } - throw new ResponseHelper.BadRequestError((e as Error).message); + throw new ResponseHelper.BadRequestError(message); } // Return the read data, plus the management JWT @@ -115,10 +120,12 @@ licenseController.post( try { await license.renew(); } catch (e) { + const error = e as Error & { errorId?: string }; + // not awaiting so as not to make the endpoint hang void Container.get(InternalHooks).onLicenseRenewAttempt({ success: false }); - if (e instanceof Error) { - throw new ResponseHelper.BadRequestError(e.message); + if (error instanceof Error) { + throw new ResponseHelper.BadRequestError(error.message); } } diff --git a/packages/cli/src/middlewares/auth.ts b/packages/cli/src/middlewares/auth.ts index bd0f580c8a73c..e28272766b916 100644 --- a/packages/cli/src/middlewares/auth.ts +++ b/packages/cli/src/middlewares/auth.ts @@ -10,14 +10,9 @@ import type { AuthenticatedRequest } from '@/requests'; import config from '@/config'; import { AUTH_COOKIE_NAME, EDITOR_UI_DIST_DIR } from '@/constants'; import { issueCookie, resolveJwtContent } from '@/auth/jwt'; -import { - isAuthenticatedRequest, - isAuthExcluded, - isPostUsersId, - isUserManagementEnabled, -} from '@/UserManagement/UserManagementHelper'; -import { SamlUrls } from '@/sso/saml/constants'; +import { isUserManagementEnabled } from '@/UserManagement/UserManagementHelper'; import type { UserRepository } from '@db/repositories'; +import { canSkipAuth } from '@/decorators/registerController'; const jwtFromRequest = (req: Request) => { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access @@ -66,6 +61,17 @@ const staticAssets = globSync(['**/*.html', '**/*.svg', '**/*.png', '**/*.ico'], cwd: EDITOR_UI_DIST_DIR, }); +// TODO: delete this +const isPostUsersId = (req: Request, restEndpoint: string): boolean => + req.method === 'POST' && + new RegExp(`/${restEndpoint}/users/[\\w\\d-]*`).test(req.url) && + !req.url.includes('reinvite'); + +const isAuthExcluded = (url: string, ignoredEndpoints: Readonly<string[]>): boolean => + !!ignoredEndpoints + .filter(Boolean) // skip empty paths + .find((ignoredEndpoint) => url.startsWith(`/${ignoredEndpoint}`)); + /** * This sets up the auth middlewares in the correct order */ @@ -85,20 +91,12 @@ export const setupAuthMiddlewares = ( // skip authentication for preflight requests req.method === 'OPTIONS' || staticAssets.includes(req.url.slice(1)) || + canSkipAuth(req.method, req.path) || + isAuthExcluded(req.url, ignoredEndpoints) || req.url.startsWith(`/${restEndpoint}/settings`) || - req.url.startsWith(`/${restEndpoint}/login`) || - req.url.startsWith(`/${restEndpoint}/logout`) || - req.url.startsWith(`/${restEndpoint}/resolve-signup-token`) || isPostUsersId(req, restEndpoint) || - req.url.startsWith(`/${restEndpoint}/forgot-password`) || - req.url.startsWith(`/${restEndpoint}/resolve-password-token`) || - req.url.startsWith(`/${restEndpoint}/change-password`) || req.url.startsWith(`/${restEndpoint}/oauth2-credential/callback`) || - req.url.startsWith(`/${restEndpoint}/oauth1-credential/callback`) || - req.url.startsWith(`/${restEndpoint}/sso/saml${SamlUrls.metadata}`) || - req.url.startsWith(`/${restEndpoint}/sso/saml${SamlUrls.initSSO}`) || - req.url.startsWith(`/${restEndpoint}/sso/saml${SamlUrls.acs}`) || - isAuthExcluded(req.url, ignoredEndpoints) + req.url.startsWith(`/${restEndpoint}/oauth1-credential/callback`) ) { return next(); } @@ -115,43 +113,5 @@ export const setupAuthMiddlewares = ( return passportMiddleware(req, res, next); }); - app.use((req: Request | AuthenticatedRequest, res: Response, next: NextFunction) => { - // req.user is empty for public routes, so just proceed - // owner can do anything, so proceed as well - if (!req.user || (isAuthenticatedRequest(req) && req.user.globalRole.name === 'owner')) { - next(); - return; - } - // Not owner and user exists. We now protect restricted urls. - const postRestrictedUrls = [ - `/${restEndpoint}/users`, - `/${restEndpoint}/owner`, - `/${restEndpoint}/ldap/sync`, - `/${restEndpoint}/ldap/test-connection`, - ]; - const getRestrictedUrls = [`/${restEndpoint}/ldap/sync`, `/${restEndpoint}/ldap/config`]; - const putRestrictedUrls = [`/${restEndpoint}/ldap/config`]; - const trimmedUrl = req.url.endsWith('/') ? req.url.slice(0, -1) : req.url; - if ( - (req.method === 'POST' && postRestrictedUrls.includes(trimmedUrl)) || - (req.method === 'GET' && getRestrictedUrls.includes(trimmedUrl)) || - (req.method === 'PUT' && putRestrictedUrls.includes(trimmedUrl)) || - (req.method === 'DELETE' && - new RegExp(`/${restEndpoint}/users/[^/]+`, 'gm').test(trimmedUrl)) || - (req.method === 'POST' && - new RegExp(`/${restEndpoint}/users/[^/]+/reinvite`, 'gm').test(trimmedUrl)) || - new RegExp(`/${restEndpoint}/owner/[^/]+`, 'gm').test(trimmedUrl) - ) { - Logger.verbose('User attempted to access endpoint without authorization', { - endpoint: `${req.method} ${trimmedUrl}`, - userId: isAuthenticatedRequest(req) ? req.user.id : 'unknown', - }); - res.status(403).json({ status: 'error', message: 'Unauthorized' }); - return; - } - - next(); - }); - app.use(refreshExpiringCookie); }; diff --git a/packages/cli/src/middlewares/isOwner.ts b/packages/cli/src/middlewares/isOwner.ts deleted file mode 100644 index d3e3c70a0fb46..0000000000000 --- a/packages/cli/src/middlewares/isOwner.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { RequestHandler } from 'express'; -import { LoggerProxy } from 'n8n-workflow'; -import type { AuthenticatedRequest } from '@/requests'; - -export const isOwnerMiddleware: RequestHandler = (req: AuthenticatedRequest, res, next) => { - if (req.user.globalRole.name === 'owner') { - next(); - } else { - LoggerProxy.debug('Request failed because user is not owner'); - res.status(401).send('Unauthorized'); - } -}; diff --git a/packages/cli/src/posthog/index.ts b/packages/cli/src/posthog/index.ts index e513e366366cf..df390c53b39a8 100644 --- a/packages/cli/src/posthog/index.ts +++ b/packages/cli/src/posthog/index.ts @@ -44,7 +44,7 @@ export class PostHogClient { } async getFeatureFlags(user: Pick<PublicUser, 'id' | 'createdAt'>): Promise<FeatureFlags> { - if (!this.postHog) return Promise.resolve({}); + if (!this.postHog) return {}; const fullId = [this.instanceId, user.id].join('#'); diff --git a/packages/cli/src/sso/saml/middleware/samlEnabledMiddleware.ts b/packages/cli/src/sso/saml/middleware/samlEnabledMiddleware.ts index 6e4600b895f06..69015838d7f69 100644 --- a/packages/cli/src/sso/saml/middleware/samlEnabledMiddleware.ts +++ b/packages/cli/src/sso/saml/middleware/samlEnabledMiddleware.ts @@ -1,24 +1,11 @@ import type { RequestHandler } from 'express'; -import type { AuthenticatedRequest } from '@/requests'; import { isSamlLicensed, isSamlLicensedAndEnabled } from '../samlHelpers'; -export const samlLicensedOwnerMiddleware: RequestHandler = ( - req: AuthenticatedRequest, - res, - next, -) => { - if (isSamlLicensed() && req.user?.globalRole.name === 'owner') { - next(); - } else { - res.status(401).json({ status: 'error', message: 'Unauthorized' }); - } -}; - export const samlLicensedAndEnabledMiddleware: RequestHandler = (req, res, next) => { if (isSamlLicensedAndEnabled()) { next(); } else { - res.status(401).json({ status: 'error', message: 'Unauthorized' }); + res.status(403).json({ status: 'error', message: 'Unauthorized' }); } }; @@ -26,6 +13,6 @@ export const samlLicensedMiddleware: RequestHandler = (req, res, next) => { if (isSamlLicensed()) { next(); } else { - res.status(401).json({ status: 'error', message: 'Unauthorized' }); + res.status(403).json({ status: 'error', message: 'Unauthorized' }); } }; diff --git a/packages/cli/src/sso/saml/routes/saml.controller.ee.ts b/packages/cli/src/sso/saml/routes/saml.controller.ee.ts index 86e7b75cfa8c4..d93bfe5fdb1f7 100644 --- a/packages/cli/src/sso/saml/routes/saml.controller.ee.ts +++ b/packages/cli/src/sso/saml/routes/saml.controller.ee.ts @@ -1,10 +1,10 @@ import express from 'express'; -import { Get, Post, RestController } from '@/decorators'; +import { getInstanceBaseUrl } from '@/UserManagement/UserManagementHelper'; +import { Authorized, Get, NoAuthRequired, Post, RestController } from '@/decorators'; import { SamlUrls } from '../constants'; import { samlLicensedAndEnabledMiddleware, samlLicensedMiddleware, - samlLicensedOwnerMiddleware, } from '../middleware/samlEnabledMiddleware'; import { SamlService } from '../saml.service.ee'; import { SamlConfiguration } from '../types/requests'; @@ -23,11 +23,14 @@ import { } from '../serviceProvider.ee'; import { getSamlConnectionTestSuccessView } from '../views/samlConnectionTestSuccess'; import { getSamlConnectionTestFailedView } from '../views/samlConnectionTestFailed'; +import Container from 'typedi'; +import { InternalHooks } from '@/InternalHooks'; @RestController('/sso/saml') export class SamlController { constructor(private samlService: SamlService) {} + @NoAuthRequired() @Get(SamlUrls.metadata) async getServiceProviderMetadata(req: express.Request, res: express.Response) { return res @@ -39,7 +42,8 @@ export class SamlController { * GET /sso/saml/config * Return SAML config */ - @Get(SamlUrls.config, { middlewares: [samlLicensedOwnerMiddleware] }) + @Authorized('any') + @Get(SamlUrls.config, { middlewares: [samlLicensedMiddleware] }) async configGet() { const prefs = this.samlService.samlPreferences; return { @@ -53,7 +57,8 @@ export class SamlController { * POST /sso/saml/config * Set SAML config */ - @Post(SamlUrls.config, { middlewares: [samlLicensedOwnerMiddleware] }) + @Authorized(['global', 'owner']) + @Post(SamlUrls.config, { middlewares: [samlLicensedMiddleware] }) async configPost(req: SamlConfiguration.Update) { const validationResult = await validate(req.body); if (validationResult.length === 0) { @@ -71,7 +76,8 @@ export class SamlController { * POST /sso/saml/config/toggle * Set SAML config */ - @Post(SamlUrls.configToggleEnabled, { middlewares: [samlLicensedOwnerMiddleware] }) + @Authorized(['global', 'owner']) + @Post(SamlUrls.configToggleEnabled, { middlewares: [samlLicensedMiddleware] }) async toggleEnabledPost(req: SamlConfiguration.Toggle, res: express.Response) { if (req.body.loginEnabled === undefined) { throw new BadRequestError('Body should contain a boolean "loginEnabled" property'); @@ -84,6 +90,7 @@ export class SamlController { * GET /sso/saml/acs * Assertion Consumer Service endpoint */ + @NoAuthRequired() @Get(SamlUrls.acs, { middlewares: [samlLicensedMiddleware] }) async acsGet(req: SamlConfiguration.AcsRequest, res: express.Response) { return this.acsHandler(req, res, 'redirect'); @@ -93,6 +100,7 @@ export class SamlController { * POST /sso/saml/acs * Assertion Consumer Service endpoint */ + @NoAuthRequired() @Post(SamlUrls.acs, { middlewares: [samlLicensedMiddleware] }) async acsPost(req: SamlConfiguration.AcsRequest, res: express.Response) { return this.acsHandler(req, res, 'post'); @@ -119,23 +127,35 @@ export class SamlController { } } if (loginResult.authenticatedUser) { + void Container.get(InternalHooks).onUserLoginSuccess({ + user: loginResult.authenticatedUser, + authenticationMethod: 'saml', + }); // Only sign in user if SAML is enabled, otherwise treat as test connection if (isSamlLicensedAndEnabled()) { await issueCookie(res, loginResult.authenticatedUser); if (loginResult.onboardingRequired) { - return res.redirect(SamlUrls.samlOnboarding); + return res.redirect(getInstanceBaseUrl() + SamlUrls.samlOnboarding); } else { - return res.redirect(SamlUrls.defaultRedirect); + return res.redirect(getInstanceBaseUrl() + SamlUrls.defaultRedirect); } } else { return res.status(202).send(loginResult.attributes); } } + void Container.get(InternalHooks).onUserLoginFailed({ + user: loginResult.attributes.email ?? 'unknown', + authenticationMethod: 'saml', + }); throw new AuthError('SAML Authentication failed'); } catch (error) { if (isConnectionTestRequest(req)) { return res.send(getSamlConnectionTestFailedView((error as Error).message)); } + void Container.get(InternalHooks).onUserLoginFailed({ + user: 'unknown', + authenticationMethod: 'saml', + }); throw new AuthError('SAML Authentication failed: ' + (error as Error).message); } } @@ -145,6 +165,7 @@ export class SamlController { * Access URL for implementing SP-init SSO * This endpoint is available if SAML is licensed and enabled */ + @NoAuthRequired() @Get(SamlUrls.initSSO, { middlewares: [samlLicensedAndEnabledMiddleware] }) async initSsoGet(req: express.Request, res: express.Response) { return this.handleInitSSO(res); @@ -155,7 +176,8 @@ export class SamlController { * Test SAML config * This endpoint is available if SAML is licensed and the requestor is an instance owner */ - @Get(SamlUrls.configTest, { middlewares: [samlLicensedOwnerMiddleware] }) + @Authorized(['global', 'owner']) + @Get(SamlUrls.configTest, { middlewares: [samlLicensedMiddleware] }) async configTestGet(req: AuthenticatedRequest, res: express.Response) { return this.handleInitSSO(res, getServiceProviderConfigTestReturnUrl()); } diff --git a/packages/cli/src/sso/saml/saml.service.ee.ts b/packages/cli/src/sso/saml/saml.service.ee.ts index b1a764c724094..cbeb3dee3ac02 100644 --- a/packages/cli/src/sso/saml/saml.service.ee.ts +++ b/packages/cli/src/sso/saml/saml.service.ee.ts @@ -73,9 +73,8 @@ export class SamlService { validate: async (response: string) => { const valid = await validateResponse(response); if (!valid) { - return Promise.reject(new Error('Invalid SAML response')); + throw new Error('Invalid SAML response'); } - return Promise.resolve(); }, }); } @@ -212,6 +211,8 @@ export class SamlService { this._samlPreferences.metadata = fetchedMetadata; } } else if (prefs.metadata) { + // remove metadataUrl if metadata is set directly + this._samlPreferences.metadataUrl = undefined; const validationResult = await validateMetadata(prefs.metadata); if (!validationResult) { throw new Error('Invalid SAML metadata'); diff --git a/packages/cli/src/telemetry/index.ts b/packages/cli/src/telemetry/index.ts index cdc7b96f8be74..7edb6f6172f41 100644 --- a/packages/cli/src/telemetry/index.ts +++ b/packages/cli/src/telemetry/index.ts @@ -76,7 +76,7 @@ export class Telemetry { private async pulse(): Promise<unknown> { if (!this.rudderStack) { - return Promise.resolve(); + return; } const allPromises = Object.keys(this.executionCountsBuffer).map(async (workflowId) => { diff --git a/packages/cli/src/utils.ts b/packages/cli/src/utils.ts index c99ef97f500fa..c9eee2f4c857f 100644 --- a/packages/cli/src/utils.ts +++ b/packages/cli/src/utils.ts @@ -58,3 +58,31 @@ export const separate = <T>(array: T[], test: (element: T) => boolean) => { return [pass, fail]; }; + +export const webhookNotFoundErrorMessage = ( + path: string, + httpMethod?: string, + webhookMethods?: string[], +) => { + let webhookPath = path; + + if (httpMethod) { + webhookPath = `${httpMethod} ${webhookPath}`; + } + + if (webhookMethods?.length && httpMethod) { + let methods = ''; + + if (webhookMethods.length === 1) { + methods = webhookMethods[0]; + } else { + const lastMethod = webhookMethods.pop(); + + methods = `${webhookMethods.join(', ')} or ${lastMethod as string}`; + } + + return `This webhook is not registered for ${httpMethod} requests. Did you mean to make a ${methods} request?`; + } else { + return `The requested webhook "${webhookPath}" is not registered.`; + } +}; diff --git a/packages/cli/test/integration/commands/import.cmd.test.ts b/packages/cli/test/integration/commands/import.cmd.test.ts index bc00a5e53d481..93fbfeac8f35c 100644 --- a/packages/cli/test/integration/commands/import.cmd.test.ts +++ b/packages/cli/test/integration/commands/import.cmd.test.ts @@ -25,7 +25,7 @@ test('import:workflow should import active workflow and deactivate it', async () ['--separate', '--input=./test/integration/commands/importWorkflows/separate'], config, ); - const mockExit = jest.spyOn(process, 'exit').mockImplementation((number) => { + const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => { throw new Error('process.exit'); }); @@ -52,7 +52,7 @@ test('import:workflow should import active workflow from combined file and deact ['--input=./test/integration/commands/importWorkflows/combined/combined.json'], config, ); - const mockExit = jest.spyOn(process, 'exit').mockImplementation((number) => { + const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => { throw new Error('process.exit'); }); diff --git a/packages/cli/test/integration/credentials.ee.test.ts b/packages/cli/test/integration/credentials.ee.test.ts index 139975eb5bb42..61409984a5e40 100644 --- a/packages/cli/test/integration/credentials.ee.test.ts +++ b/packages/cli/test/integration/credentials.ee.test.ts @@ -415,7 +415,7 @@ describe('PUT /credentials/:id/share', () => { test('should respond 403 for non-existing credentials', async () => { const response = await authOwnerAgent - .put(`/credentials/1234567/share`) + .put('/credentials/1234567/share') .send({ shareWithIds: [member.id] }); expect(response.statusCode).toBe(403); diff --git a/packages/cli/test/integration/credentials.test.ts b/packages/cli/test/integration/credentials.test.ts index 3a2f5f8415218..3f5b7e74a7866 100644 --- a/packages/cli/test/integration/credentials.test.ts +++ b/packages/cli/test/integration/credentials.test.ts @@ -11,9 +11,8 @@ import type { Role } from '@db/entities/Role'; import type { User } from '@db/entities/User'; import { randomCredentialPayload, randomName, randomString } from './shared/random'; import * as testDb from './shared/testDb'; -import type { SaveCredentialFunction } from './shared/types'; +import type { AuthAgent, SaveCredentialFunction } from './shared/types'; import * as utils from './shared/utils'; -import type { AuthAgent } from './shared/types'; // mock that credentialsSharing is not enabled const mockIsCredentialsSharingEnabled = jest.spyOn(UserManagementHelpers, 'isSharingEnabled'); @@ -124,7 +123,7 @@ describe('POST /credentials', () => { expect(credential.name).toBe(payload.name); expect(credential.type).toBe(payload.type); - expect(credential.nodesAccess[0].nodeType).toBe(payload.nodesAccess![0].nodeType); + expect(credential.nodesAccess[0].nodeType).toBe(payload.nodesAccess[0].nodeType); expect(credential.data).not.toBe(payload.data); const sharedCredential = await Db.collections.SharedCredentials.findOneOrFail({ @@ -278,7 +277,7 @@ describe('PATCH /credentials/:id', () => { expect(credential.name).toBe(patchPayload.name); expect(credential.type).toBe(patchPayload.type); - expect(credential.nodesAccess[0].nodeType).toBe(patchPayload.nodesAccess![0].nodeType); + expect(credential.nodesAccess[0].nodeType).toBe(patchPayload.nodesAccess[0].nodeType); expect(credential.data).not.toBe(patchPayload.data); const sharedCredential = await Db.collections.SharedCredentials.findOneOrFail({ @@ -315,7 +314,7 @@ describe('PATCH /credentials/:id', () => { expect(credential.name).toBe(patchPayload.name); expect(credential.type).toBe(patchPayload.type); - expect(credential.nodesAccess[0].nodeType).toBe(patchPayload.nodesAccess![0].nodeType); + expect(credential.nodesAccess[0].nodeType).toBe(patchPayload.nodesAccess[0].nodeType); expect(credential.data).not.toBe(patchPayload.data); const sharedCredential = await Db.collections.SharedCredentials.findOneOrFail({ @@ -352,7 +351,7 @@ describe('PATCH /credentials/:id', () => { expect(credential.name).toBe(patchPayload.name); expect(credential.type).toBe(patchPayload.type); - expect(credential.nodesAccess[0].nodeType).toBe(patchPayload.nodesAccess![0].nodeType); + expect(credential.nodesAccess[0].nodeType).toBe(patchPayload.nodesAccess[0].nodeType); expect(credential.data).not.toBe(patchPayload.data); const sharedCredential = await Db.collections.SharedCredentials.findOneOrFail({ diff --git a/packages/cli/test/integration/eventbus.test.ts b/packages/cli/test/integration/eventbus.test.ts index a5b712321747d..8c0b9b86eb5a1 100644 --- a/packages/cli/test/integration/eventbus.test.ts +++ b/packages/cli/test/integration/eventbus.test.ts @@ -1,4 +1,4 @@ -import express from 'express'; +import type express from 'express'; import config from '@/config'; import axios from 'axios'; import syslog from 'syslog-client'; @@ -7,23 +7,25 @@ import { Container } from 'typedi'; import type { SuperAgentTest } from 'supertest'; import * as utils from './shared/utils'; import * as testDb from './shared/testDb'; -import { Role } from '@db/entities/Role'; -import { User } from '@db/entities/User'; +import type { Role } from '@db/entities/Role'; +import type { User } from '@db/entities/User'; +import type { + MessageEventBusDestinationSentryOptions, + MessageEventBusDestinationSyslogOptions, + MessageEventBusDestinationWebhookOptions, +} from 'n8n-workflow'; import { defaultMessageEventBusDestinationSentryOptions, defaultMessageEventBusDestinationSyslogOptions, defaultMessageEventBusDestinationWebhookOptions, - MessageEventBusDestinationSentryOptions, - MessageEventBusDestinationSyslogOptions, - MessageEventBusDestinationWebhookOptions, } from 'n8n-workflow'; import { eventBus } from '@/eventbus'; import { EventMessageGeneric } from '@/eventbus/EventMessageClasses/EventMessageGeneric'; -import { MessageEventBusDestinationSyslog } from '@/eventbus/MessageEventBusDestination/MessageEventBusDestinationSyslog.ee'; -import { MessageEventBusDestinationWebhook } from '@/eventbus/MessageEventBusDestination/MessageEventBusDestinationWebhook.ee'; -import { MessageEventBusDestinationSentry } from '@/eventbus/MessageEventBusDestination/MessageEventBusDestinationSentry.ee'; +import type { MessageEventBusDestinationSyslog } from '@/eventbus/MessageEventBusDestination/MessageEventBusDestinationSyslog.ee'; +import type { MessageEventBusDestinationWebhook } from '@/eventbus/MessageEventBusDestination/MessageEventBusDestinationWebhook.ee'; +import type { MessageEventBusDestinationSentry } from '@/eventbus/MessageEventBusDestination/MessageEventBusDestinationSentry.ee'; import { EventMessageAudit } from '@/eventbus/EventMessageClasses/EventMessageAudit'; -import { EventNamesTypes } from '@/eventbus/EventMessageClasses'; +import type { EventNamesTypes } from '@/eventbus/EventMessageClasses'; import { License } from '@/License'; jest.unmock('@/eventbus/MessageEventBus/MessageEventBus'); @@ -51,7 +53,7 @@ const testWebhookDestination: MessageEventBusDestinationWebhookOptions = { ...defaultMessageEventBusDestinationWebhookOptions, id: '88be6560-bfb4-455c-8aa1-06971e9e5522', url: 'http://localhost:3456', - method: `POST`, + method: 'POST', label: 'Test Webhook', enabled: false, subscribedEvents: ['n8n.test.message', 'n8n.audit.user.updated'], diff --git a/packages/cli/test/integration/ldap/ldap.api.test.ts b/packages/cli/test/integration/ldap/ldap.api.test.ts index 2bb766be937b9..da1e5de444bcd 100644 --- a/packages/cli/test/integration/ldap/ldap.api.test.ts +++ b/packages/cli/test/integration/ldap/ldap.api.test.ts @@ -1,4 +1,4 @@ -import express from 'express'; +import type express from 'express'; import type { Entry as LdapUser } from 'ldapts'; import { Not } from 'typeorm'; import { Container } from 'typedi'; @@ -205,9 +205,7 @@ test('GET /ldap/config route should retrieve current configuration', async () => describe('POST /ldap/test-connection', () => { test('route should success', async () => { - jest - .spyOn(LdapService.prototype, 'testConnection') - .mockImplementation(async () => Promise.resolve()); + jest.spyOn(LdapService.prototype, 'testConnection').mockResolvedValue(); const response = await authAgent(owner).post('/ldap/test-connection'); expect(response.statusCode).toBe(200); @@ -216,9 +214,7 @@ describe('POST /ldap/test-connection', () => { test('route should fail', async () => { const errorMessage = 'Invalid connection'; - jest - .spyOn(LdapService.prototype, 'testConnection') - .mockImplementation(async () => Promise.reject(new Error(errorMessage))); + jest.spyOn(LdapService.prototype, 'testConnection').mockRejectedValue(new Error(errorMessage)); const response = await authAgent(owner).post('/ldap/test-connection'); expect(response.statusCode).toBe(400); @@ -240,9 +236,7 @@ describe('POST /ldap/sync', () => { describe('dry mode', () => { const runTest = async (ldapUsers: LdapUser[]) => { - jest - .spyOn(LdapService.prototype, 'searchWithAdminBinding') - .mockImplementation(async () => Promise.resolve(ldapUsers)); + jest.spyOn(LdapService.prototype, 'searchWithAdminBinding').mockResolvedValue(ldapUsers); const response = await authAgent(owner).post('/ldap/sync').send({ type: 'dry' }); @@ -337,9 +331,7 @@ describe('POST /ldap/sync', () => { describe('live mode', () => { const runTest = async (ldapUsers: LdapUser[]) => { - jest - .spyOn(LdapService.prototype, 'searchWithAdminBinding') - .mockImplementation(async () => Promise.resolve(ldapUsers)); + jest.spyOn(LdapService.prototype, 'searchWithAdminBinding').mockResolvedValue(ldapUsers); const response = await authAgent(owner).post('/ldap/sync').send({ type: 'live' }); @@ -467,9 +459,7 @@ describe('POST /ldap/sync', () => { test('should remove user instance access once the user is disabled during synchronization', async () => { const member = await testDb.createLdapUser({ globalRole: globalMemberRole }, uniqueId()); - jest - .spyOn(LdapService.prototype, 'searchWithAdminBinding') - .mockImplementation(async () => Promise.resolve([])); + jest.spyOn(LdapService.prototype, 'searchWithAdminBinding').mockResolvedValue([]); await authAgent(owner).post('/ldap/sync').send({ type: 'live' }); @@ -508,13 +498,9 @@ describe('POST /login', () => { const authlessAgent = utils.createAgent(app); - jest - .spyOn(LdapService.prototype, 'searchWithAdminBinding') - .mockImplementation(async () => Promise.resolve([ldapUser])); + jest.spyOn(LdapService.prototype, 'searchWithAdminBinding').mockResolvedValue([ldapUser]); - jest - .spyOn(LdapService.prototype, 'validUser') - .mockImplementation(async () => Promise.resolve()); + jest.spyOn(LdapService.prototype, 'validUser').mockResolvedValue(); const response = await authlessAgent .post('/login') diff --git a/packages/cli/test/integration/license.api.test.ts b/packages/cli/test/integration/license.api.test.ts index 44d84e11502a7..3b45f507f4f12 100644 --- a/packages/cli/test/integration/license.api.test.ts +++ b/packages/cli/test/integration/license.api.test.ts @@ -1,7 +1,7 @@ import type { SuperAgentTest } from 'supertest'; import config from '@/config'; import type { User } from '@db/entities/User'; -import { ILicensePostResponse, ILicenseReadResponse } from '@/Interfaces'; +import type { ILicensePostResponse, ILicenseReadResponse } from '@/Interfaces'; import { License } from '@/License'; import * as testDb from './shared/testDb'; import * as utils from './shared/utils'; @@ -68,13 +68,13 @@ describe('POST /license/activate', () => { test('errors out properly', async () => { License.prototype.activate = jest.fn().mockImplementation(() => { - throw new Error(INVALID_ACIVATION_KEY_MESSAGE); + throw new Error(ACTIVATION_FAILED_MESSAGE); }); await authOwnerAgent .post('/license/activate') .send({ activationKey: 'abcde' }) - .expect(400, { code: 400, message: INVALID_ACIVATION_KEY_MESSAGE }); + .expect(400, { code: 400, message: ACTIVATION_FAILED_MESSAGE }); }); }); @@ -135,5 +135,5 @@ const DEFAULT_POST_RESPONSE: { data: ILicensePostResponse } = { }; const NON_OWNER_ACTIVATE_RENEW_MESSAGE = 'Only an instance owner may activate or renew a license'; -const INVALID_ACIVATION_KEY_MESSAGE = 'Invalid activation key'; +const ACTIVATION_FAILED_MESSAGE = 'Failed to activate license'; const RENEW_ERROR_MESSAGE = 'Something went wrong when trying to renew license'; diff --git a/packages/cli/test/integration/passwordReset.api.test.ts b/packages/cli/test/integration/passwordReset.api.test.ts index 60ccfa0ce5b46..85e3cb2d936ef 100644 --- a/packages/cli/test/integration/passwordReset.api.test.ts +++ b/packages/cli/test/integration/passwordReset.api.test.ts @@ -23,7 +23,7 @@ let globalOwnerRole: Role; let globalMemberRole: Role; let owner: User; let authlessAgent: SuperAgentTest; -let externalHooks = utils.mockInstance(ExternalHooks); +const externalHooks = utils.mockInstance(ExternalHooks); beforeAll(async () => { const app = await utils.initTestServer({ endpointGroups: ['passwordReset'] }); diff --git a/packages/cli/test/integration/publicApi/executions.test.ts b/packages/cli/test/integration/publicApi/executions.test.ts index 1d5debdbfb1e3..d71e0aa2e4da1 100644 --- a/packages/cli/test/integration/publicApi/executions.test.ts +++ b/packages/cli/test/integration/publicApi/executions.test.ts @@ -213,7 +213,7 @@ describe('GET /executions', () => { await testDb.createErrorExecution(workflow); - const response = await authOwnerAgent.get(`/executions`).query({ + const response = await authOwnerAgent.get('/executions').query({ status: 'success', }); @@ -254,7 +254,7 @@ describe('GET /executions', () => { await testDb.createErrorExecution(workflow); - const firstExecutionResponse = await authOwnerAgent.get(`/executions`).query({ + const firstExecutionResponse = await authOwnerAgent.get('/executions').query({ status: 'success', limit: 1, }); @@ -263,7 +263,7 @@ describe('GET /executions', () => { expect(firstExecutionResponse.body.data.length).toBe(1); expect(firstExecutionResponse.body.nextCursor).toBeDefined(); - const secondExecutionResponse = await authOwnerAgent.get(`/executions`).query({ + const secondExecutionResponse = await authOwnerAgent.get('/executions').query({ status: 'success', limit: 1, cursor: firstExecutionResponse.body.nextCursor, @@ -308,7 +308,7 @@ describe('GET /executions', () => { const errorExecution = await testDb.createErrorExecution(workflow); - const response = await authOwnerAgent.get(`/executions`).query({ + const response = await authOwnerAgent.get('/executions').query({ status: 'error', }); @@ -348,7 +348,7 @@ describe('GET /executions', () => { const waitingExecution = await testDb.createWaitingExecution(workflow); - const response = await authOwnerAgent.get(`/executions`).query({ + const response = await authOwnerAgent.get('/executions').query({ status: 'waiting', }); @@ -389,7 +389,7 @@ describe('GET /executions', () => { ); await testDb.createManyExecutions(2, workflow2, testDb.createSuccessfulExecution); - const response = await authOwnerAgent.get(`/executions`).query({ + const response = await authOwnerAgent.get('/executions').query({ workflowId: workflow.id, }); @@ -439,7 +439,7 @@ describe('GET /executions', () => { await testDb.createManyExecutions(2, firstWorkflowForUser2, testDb.createSuccessfulExecution); await testDb.createManyExecutions(2, secondWorkflowForUser2, testDb.createSuccessfulExecution); - const response = await authOwnerAgent.get(`/executions`); + const response = await authOwnerAgent.get('/executions'); expect(response.statusCode).toBe(200); expect(response.body.data.length).toBe(8); @@ -463,7 +463,7 @@ describe('GET /executions', () => { await testDb.createManyExecutions(2, firstWorkflowForUser2, testDb.createSuccessfulExecution); await testDb.createManyExecutions(2, secondWorkflowForUser2, testDb.createSuccessfulExecution); - const response = await authUser1Agent.get(`/executions`); + const response = await authUser1Agent.get('/executions'); expect(response.statusCode).toBe(200); expect(response.body.data.length).toBe(4); @@ -489,7 +489,7 @@ describe('GET /executions', () => { await testDb.shareWorkflowWithUsers(firstWorkflowForUser2, [user1]); - const response = await authUser1Agent.get(`/executions`); + const response = await authUser1Agent.get('/executions'); expect(response.statusCode).toBe(200); expect(response.body.data.length).toBe(6); diff --git a/packages/cli/test/integration/publicApi/workflows.test.ts b/packages/cli/test/integration/publicApi/workflows.test.ts index 02c76b616c765..c730cd07c4fc6 100644 --- a/packages/cli/test/integration/publicApi/workflows.test.ts +++ b/packages/cli/test/integration/publicApi/workflows.test.ts @@ -309,7 +309,7 @@ describe('GET /workflows/:id', () => { test('should fail due to invalid API Key', testWithAPIKey('get', '/workflows/2', 'abcXYZ')); test('should fail due to non-existing workflow', async () => { - const response = await authOwnerAgent.get(`/workflows/2`); + const response = await authOwnerAgent.get('/workflows/2'); expect(response.statusCode).toBe(404); }); @@ -375,7 +375,7 @@ describe('DELETE /workflows/:id', () => { test('should fail due to invalid API Key', testWithAPIKey('delete', '/workflows/2', 'abcXYZ')); test('should fail due to non-existing workflow', async () => { - const response = await authOwnerAgent.delete(`/workflows/2`); + const response = await authOwnerAgent.delete('/workflows/2'); expect(response.statusCode).toBe(404); }); @@ -447,7 +447,7 @@ describe('POST /workflows/:id/activate', () => { ); test('should fail due to non-existing workflow', async () => { - const response = await authOwnerAgent.post(`/workflows/2/activate`); + const response = await authOwnerAgent.post('/workflows/2/activate'); expect(response.statusCode).toBe(404); }); @@ -549,7 +549,7 @@ describe('POST /workflows/:id/deactivate', () => { ); test('should fail due to non-existing workflow', async () => { - const response = await authOwnerAgent.post(`/workflows/2/deactivate`); + const response = await authOwnerAgent.post('/workflows/2/deactivate'); expect(response.statusCode).toBe(404); }); @@ -709,7 +709,7 @@ describe('PUT /workflows/:id', () => { test('should fail due to invalid API Key', testWithAPIKey('put', '/workflows/1', 'abcXYZ')); test('should fail due to non-existing workflow', async () => { - const response = await authOwnerAgent.put(`/workflows/1`).send({ + const response = await authOwnerAgent.put('/workflows/1').send({ name: 'testing', nodes: [ { @@ -737,7 +737,7 @@ describe('PUT /workflows/:id', () => { }); test('should fail due to invalid body', async () => { - const response = await authOwnerAgent.put(`/workflows/1`).send({ + const response = await authOwnerAgent.put('/workflows/1').send({ nodes: [ { id: 'uuid-1234', diff --git a/packages/cli/test/integration/saml/saml.api.test.ts b/packages/cli/test/integration/saml/saml.api.test.ts index fa52693b1b325..a9bda3c53ffb4 100644 --- a/packages/cli/test/integration/saml/saml.api.test.ts +++ b/packages/cli/test/integration/saml/saml.api.test.ts @@ -3,13 +3,21 @@ import type { SuperAgentTest } from 'supertest'; import type { User } from '@db/entities/User'; import { setSamlLoginEnabled } from '@/sso/saml/samlHelpers'; import { getCurrentAuthenticationMethod, setCurrentAuthenticationMethod } from '@/sso/ssoHelpers'; +import { SamlUrls } from '@/sso/saml/constants'; import { License } from '@/License'; import { randomEmail, randomName, randomValidPassword } from '../shared/random'; import * as testDb from '../shared/testDb'; import * as utils from '../shared/utils'; import { sampleConfig } from './sampleMetadata'; +import { InternalHooks } from '@/InternalHooks'; +import { SamlService } from '@/sso/saml/saml.service.ee'; +import type { SamlUserAttributes } from '@/sso/saml/types/samlUserAttributes'; +import type { AuthenticationMethod } from 'n8n-workflow'; +let someUser: User; let owner: User; +let noAuthMemberAgent: SuperAgentTest; +let authMemberAgent: SuperAgentTest; let authOwnerAgent: SuperAgentTest; async function enableSaml(enable: boolean) { @@ -20,7 +28,10 @@ beforeAll(async () => { Container.get(License).isSamlEnabled = () => true; const app = await utils.initTestServer({ endpointGroups: ['me', 'saml'] }); owner = await testDb.createOwner(); + someUser = await testDb.createUser(); authOwnerAgent = utils.createAuthAgent(app)(owner); + authMemberAgent = utils.createAgent(app, { auth: true, user: someUser }); + noAuthMemberAgent = utils.createAgent(app, { auth: false, user: someUser }); }); afterAll(async () => { @@ -129,6 +140,216 @@ describe('Instance owner', () => { .expect(500); expect(getCurrentAuthenticationMethod()).toBe('ldap'); + await setCurrentAuthenticationMethod('saml'); }); }); }); + +describe('Check endpoint permissions', () => { + beforeEach(async () => { + await enableSaml(true); + }); + describe('Owner', () => { + test(`should be able to access ${SamlUrls.metadata}`, async () => { + await authOwnerAgent.get(`/sso/saml${SamlUrls.metadata}`).expect(200); + }); + + test(`should be able to access GET ${SamlUrls.config}`, async () => { + await authOwnerAgent.get(`/sso/saml${SamlUrls.config}`).expect(200); + }); + + test(`should be able to access POST ${SamlUrls.config}`, async () => { + await authOwnerAgent.post(`/sso/saml${SamlUrls.config}`).expect(200); + }); + + test(`should be able to access POST ${SamlUrls.configToggleEnabled}`, async () => { + await authOwnerAgent.post(`/sso/saml${SamlUrls.configToggleEnabled}`).expect(400); + }); + + test(`should be able to access GET ${SamlUrls.acs}`, async () => { + // Note that 401 here is coming from the missing SAML object, + // not from not being able to access the endpoint, so this is expected! + const response = await authOwnerAgent.get(`/sso/saml${SamlUrls.acs}`).expect(401); + expect(response.text).toContain('SAML Authentication failed'); + }); + + test(`should be able to access POST ${SamlUrls.acs}`, async () => { + // Note that 401 here is coming from the missing SAML object, + // not from not being able to access the endpoint, so this is expected! + const response = await authOwnerAgent.post(`/sso/saml${SamlUrls.acs}`).expect(401); + expect(response.text).toContain('SAML Authentication failed'); + }); + + test(`should be able to access GET ${SamlUrls.initSSO}`, async () => { + const response = await authOwnerAgent.get(`/sso/saml${SamlUrls.initSSO}`).expect(200); + }); + + test(`should be able to access GET ${SamlUrls.configTest}`, async () => { + await authOwnerAgent.get(`/sso/saml${SamlUrls.configTest}`).expect(200); + }); + }); + describe('Authenticated Member', () => { + test(`should be able to access ${SamlUrls.metadata}`, async () => { + await authMemberAgent.get(`/sso/saml${SamlUrls.metadata}`).expect(200); + }); + + test(`should be able to access GET ${SamlUrls.config}`, async () => { + await authMemberAgent.get(`/sso/saml${SamlUrls.config}`).expect(200); + }); + + test(`should NOT be able to access POST ${SamlUrls.config}`, async () => { + await authMemberAgent.post(`/sso/saml${SamlUrls.config}`).expect(403); + }); + + test(`should NOT be able to access POST ${SamlUrls.configToggleEnabled}`, async () => { + await authMemberAgent.post(`/sso/saml${SamlUrls.configToggleEnabled}`).expect(403); + }); + + test(`should be able to access GET ${SamlUrls.acs}`, async () => { + // Note that 401 here is coming from the missing SAML object, + // not from not being able to access the endpoint, so this is expected! + const response = await authMemberAgent.get(`/sso/saml${SamlUrls.acs}`).expect(401); + expect(response.text).toContain('SAML Authentication failed'); + }); + + test(`should be able to access POST ${SamlUrls.acs}`, async () => { + // Note that 401 here is coming from the missing SAML object, + // not from not being able to access the endpoint, so this is expected! + const response = await authMemberAgent.post(`/sso/saml${SamlUrls.acs}`).expect(401); + expect(response.text).toContain('SAML Authentication failed'); + }); + + test(`should be able to access GET ${SamlUrls.initSSO}`, async () => { + const response = await authMemberAgent.get(`/sso/saml${SamlUrls.initSSO}`).expect(200); + }); + + test(`should NOT be able to access GET ${SamlUrls.configTest}`, async () => { + await authMemberAgent.get(`/sso/saml${SamlUrls.configTest}`).expect(403); + }); + }); + describe('Non-Authenticated User', () => { + test(`should be able to access ${SamlUrls.metadata}`, async () => { + await noAuthMemberAgent.get(`/sso/saml${SamlUrls.metadata}`).expect(200); + }); + + test(`should NOT be able to access GET ${SamlUrls.config}`, async () => { + await noAuthMemberAgent.get(`/sso/saml${SamlUrls.config}`).expect(401); + }); + + test(`should NOT be able to access POST ${SamlUrls.config}`, async () => { + await noAuthMemberAgent.post(`/sso/saml${SamlUrls.config}`).expect(401); + }); + + test(`should NOT be able to access POST ${SamlUrls.configToggleEnabled}`, async () => { + await noAuthMemberAgent.post(`/sso/saml${SamlUrls.configToggleEnabled}`).expect(401); + }); + + test(`should be able to access GET ${SamlUrls.acs}`, async () => { + // Note that 401 here is coming from the missing SAML object, + // not from not being able to access the endpoint, so this is expected! + const response = await noAuthMemberAgent.get(`/sso/saml${SamlUrls.acs}`).expect(401); + expect(response.text).toContain('SAML Authentication failed'); + }); + + test(`should be able to access POST ${SamlUrls.acs}`, async () => { + // Note that 401 here is coming from the missing SAML object, + // not from not being able to access the endpoint, so this is expected! + const response = await noAuthMemberAgent.post(`/sso/saml${SamlUrls.acs}`).expect(401); + expect(response.text).toContain('SAML Authentication failed'); + }); + + test(`should be able to access GET ${SamlUrls.initSSO}`, async () => { + const response = await noAuthMemberAgent.get(`/sso/saml${SamlUrls.initSSO}`).expect(200); + }); + + test(`should NOT be able to access GET ${SamlUrls.configTest}`, async () => { + await noAuthMemberAgent.get(`/sso/saml${SamlUrls.configTest}`).expect(401); + }); + }); +}); + +describe('SAML login flow', () => { + beforeEach(async () => { + await enableSaml(true); + }); + + test('should trigger onUserLoginSuccess hook', async () => { + const mockedHandleSamlLogin = jest.spyOn(Container.get(SamlService), 'handleSamlLogin'); + + mockedHandleSamlLogin.mockImplementation( + async (): Promise<{ + authenticatedUser: User; + attributes: SamlUserAttributes; + onboardingRequired: false; + }> => { + return { + authenticatedUser: someUser, + attributes: { + email: someUser.email, + firstName: someUser.firstName, + lastName: someUser.lastName, + userPrincipalName: someUser.email, + }, + onboardingRequired: false, + }; + }, + ); + + const mockedHookOnUserLoginSuccess = jest.spyOn( + Container.get(InternalHooks), + 'onUserLoginSuccess', + ); + mockedHookOnUserLoginSuccess.mockImplementation( + async (userLoginData: { user: User; authenticationMethod: AuthenticationMethod }) => { + expect(userLoginData.authenticationMethod).toEqual('saml'); + return; + }, + ); + const response = await authOwnerAgent.post(`/sso/saml${SamlUrls.acs}`).expect(302); + expect(mockedHookOnUserLoginSuccess).toBeCalled(); + mockedHookOnUserLoginSuccess.mockRestore(); + mockedHandleSamlLogin.mockRestore(); + }); + + test('should trigger onUserLoginFailed hook', async () => { + const mockedHandleSamlLogin = jest.spyOn(Container.get(SamlService), 'handleSamlLogin'); + + mockedHandleSamlLogin.mockImplementation( + async (): Promise<{ + authenticatedUser: User | undefined; + attributes: SamlUserAttributes; + onboardingRequired: false; + }> => { + return { + authenticatedUser: undefined, + attributes: { + email: someUser.email, + firstName: someUser.firstName, + lastName: someUser.lastName, + userPrincipalName: someUser.email, + }, + onboardingRequired: false, + }; + }, + ); + + const mockedHookOnUserLoginFailed = jest.spyOn( + Container.get(InternalHooks), + 'onUserLoginFailed', + ); + mockedHookOnUserLoginFailed.mockImplementation( + async (userLoginData: { + user: string; + authenticationMethod: AuthenticationMethod; + reason?: string; + }) => { + expect(userLoginData.authenticationMethod).toEqual('saml'); + return; + }, + ); + const response = await authOwnerAgent.post(`/sso/saml${SamlUrls.acs}`).expect(401); + expect(mockedHookOnUserLoginFailed).toBeCalled(); + mockedHookOnUserLoginFailed.mockRestore(); + mockedHandleSamlLogin.mockRestore(); + }); +}); diff --git a/packages/cli/test/integration/shared/constants.ts b/packages/cli/test/integration/shared/constants.ts index 3608225eaa1e1..d259011696207 100644 --- a/packages/cli/test/integration/shared/constants.ts +++ b/packages/cli/test/integration/shared/constants.ts @@ -1,8 +1,8 @@ import config from '@/config'; -export const REST_PATH_SEGMENT = config.getEnv('endpoints.rest') as Readonly<string>; +export const REST_PATH_SEGMENT = config.getEnv('endpoints.rest'); -export const PUBLIC_API_REST_PATH_SEGMENT = config.getEnv('publicApi.path') as Readonly<string>; +export const PUBLIC_API_REST_PATH_SEGMENT = config.getEnv('publicApi.path'); export const AUTHLESS_ENDPOINTS: Readonly<string[]> = [ 'healthz', @@ -42,7 +42,7 @@ export const ROUTES_REQUIRING_AUTHORIZATION: Readonly<string[]> = [ 'POST /users', 'DELETE /users/123', 'POST /users/123/reinvite', - 'POST /owner/pre-setup', + 'GET /owner/pre-setup', 'POST /owner/setup', 'POST /owner/skip-setup', ]; diff --git a/packages/cli/test/integration/shared/testDb.ts b/packages/cli/test/integration/shared/testDb.ts index 95e7303010622..f855aff86b2a7 100644 --- a/packages/cli/test/integration/shared/testDb.ts +++ b/packages/cli/test/integration/shared/testDb.ts @@ -1,9 +1,6 @@ import { UserSettings } from 'n8n-core'; -import { - DataSource as Connection, - DataSourceOptions as ConnectionOptions, - Repository, -} from 'typeorm'; +import type { DataSourceOptions as ConnectionOptions, Repository } from 'typeorm'; +import { DataSource as Connection } from 'typeorm'; import { Container } from 'typedi'; import config from '@/config'; @@ -24,7 +21,7 @@ import type { TagEntity } from '@db/entities/TagEntity'; import type { User } from '@db/entities/User'; import type { WorkflowEntity } from '@db/entities/WorkflowEntity'; import { RoleRepository } from '@db/repositories'; -import { ICredentialsDb } from '@/Interfaces'; +import type { ICredentialsDb } from '@/Interfaces'; import { DB_INITIALIZATION_TIMEOUT } from './constants'; import { randomApiKey, randomEmail, randomName, randomString, randomValidPassword } from './random'; @@ -211,6 +208,7 @@ export async function createManyUsers( amount: number, attributes: Partial<User> = {}, ): Promise<User[]> { + // eslint-disable-next-line prefer-const let { email, password, firstName, lastName, globalRole, ...rest } = attributes; if (!globalRole) { globalRole = await getGlobalMemberRole(); diff --git a/packages/cli/test/integration/shared/utils.ts b/packages/cli/test/integration/shared/utils.ts index b3dcd3d2f0ab8..dbd9920a517ee 100644 --- a/packages/cli/test/integration/shared/utils.ts +++ b/packages/cli/test/integration/shared/utils.ts @@ -7,25 +7,23 @@ import { CronJob } from 'cron'; import express from 'express'; import set from 'lodash.set'; import { BinaryDataManager, UserSettings } from 'n8n-core'; -import { +import type { ICredentialType, - IDataObject, IExecuteFunctions, INode, INodeExecutionData, INodeParameters, ITriggerFunctions, ITriggerResponse, - LoggerProxy, - NodeHelpers, - toCronExpression, TriggerTime, } from 'n8n-workflow'; -import superagent from 'superagent'; +import { deepCopy } from 'n8n-workflow'; +import { LoggerProxy, NodeHelpers, toCronExpression } from 'n8n-workflow'; +import type superagent from 'superagent'; import request from 'supertest'; import { URL } from 'url'; import { mock } from 'jest-mock-extended'; -import { DeepPartial } from 'ts-essentials'; +import type { DeepPartial } from 'ts-essentials'; import config from '@/config'; import * as Db from '@/Db'; import { WorkflowEntity } from '@db/entities/WorkflowEntity'; @@ -73,7 +71,7 @@ import { v4 as uuid } from 'uuid'; import { InternalHooks } from '@/InternalHooks'; import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; import { PostHogClient } from '@/posthog'; -import { variablesController } from '@/environments/variables.controller'; +import { variablesController } from '@/environments/variables/variables.controller'; import { LdapManager } from '@/Ldap/LdapManager.ee'; import { handleLdapInit } from '@/Ldap/helpers'; import { Push } from '@/push'; @@ -368,7 +366,7 @@ export async function initNodeTypes() { outputs: ['main'], properties: [], }, - execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> { + async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> { const items = this.getInputData(); return this.prepareOutputData(items); @@ -571,7 +569,7 @@ export async function initNodeTypes() { }, ], }, - execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> { + async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> { const items = this.getInputData(); if (items.length === 0) { @@ -585,13 +583,13 @@ export async function initNodeTypes() { for (let itemIndex = 0; itemIndex < items.length; itemIndex++) { keepOnlySet = this.getNodeParameter('keepOnlySet', itemIndex, false) as boolean; item = items[itemIndex]; - const options = this.getNodeParameter('options', itemIndex, {}) as IDataObject; + const options = this.getNodeParameter('options', itemIndex, {}); const newItem: INodeExecutionData = { json: {}, }; - if (keepOnlySet !== true) { + if (!keepOnlySet) { if (item.binary !== undefined) { // Create a shallow copy of the binary data so that the old // data references which do not get changed still stay behind @@ -600,7 +598,7 @@ export async function initNodeTypes() { Object.assign(newItem.binary, item.binary); } - newItem.json = JSON.parse(JSON.stringify(item.json)); + newItem.json = deepCopy(item.json); } // Add boolean values @@ -708,7 +706,7 @@ export function createAuthAgent(app: express.Application) { * Example: http://127.0.0.1:62100/me/password ā http://127.0.0.1:62100/rest/me/password */ export function prefix(pathSegment: string) { - return function (request: superagent.SuperAgentRequest) { + return async function (request: superagent.SuperAgentRequest) { const url = new URL(request.url); // enforce consistency at call sites @@ -799,11 +797,11 @@ export function installedNodePayload(packageName: string): InstalledNodePayload }; } -export const emptyPackage = () => { +export const emptyPackage = async () => { const installedPackage = new InstalledPackages(); installedPackage.installedNodes = []; - return Promise.resolve(installedPackage); + return installedPackage; }; // ---------------------------------- diff --git a/packages/cli/test/integration/users.api.test.ts b/packages/cli/test/integration/users.api.test.ts index 759d12b8cb42a..3e3fe82db8405 100644 --- a/packages/cli/test/integration/users.api.test.ts +++ b/packages/cli/test/integration/users.api.test.ts @@ -31,6 +31,7 @@ let credentialOwnerRole: Role; let owner: User; let authlessAgent: SuperAgentTest; let authOwnerAgent: SuperAgentTest; +let authAgentFor: (user: User) => SuperAgentTest; beforeAll(async () => { const app = await utils.initTestServer({ endpointGroups: ['users'] }); @@ -49,7 +50,8 @@ beforeAll(async () => { owner = await testDb.createUser({ globalRole: globalOwnerRole }); authlessAgent = utils.createAgent(app); - authOwnerAgent = utils.createAuthAgent(app)(owner); + authAgentFor = utils.createAuthAgent(app); + authOwnerAgent = authAgentFor(owner); }); beforeEach(async () => { @@ -69,7 +71,7 @@ afterAll(async () => { }); describe('GET /users', () => { - test('should return all users', async () => { + test('should return all users (for owner)', async () => { await testDb.createUser({ globalRole: globalMemberRole }); const response = await authOwnerAgent.get('/users'); @@ -103,6 +105,14 @@ describe('GET /users', () => { expect(apiKey).not.toBeDefined(); }); }); + + test('should return all users (for member)', async () => { + const member = await testDb.createUser({ globalRole: globalMemberRole }); + const response = await authAgentFor(member).get('/users'); + + expect(response.statusCode).toBe(200); + expect(response.body.data.length).toBe(2); + }); }); describe('DELETE /users/:id', () => { @@ -504,7 +514,7 @@ describe('UserManagementMailer expect NodeMailer.verifyConnection', () => { test('not be called when SMTP not set up', async () => { const userManagementMailer = new UserManagementMailer(); // NodeMailer.verifyConnection gets called only explicitly - expect(async () => await userManagementMailer.verifyConnection()).rejects.toThrow(); + expect(async () => userManagementMailer.verifyConnection()).rejects.toThrow(); expect(NodeMailer.prototype.verifyConnection).toHaveBeenCalledTimes(0); }); @@ -516,6 +526,6 @@ describe('UserManagementMailer expect NodeMailer.verifyConnection', () => { const userManagementMailer = new UserManagementMailer(); // NodeMailer.verifyConnection gets called only explicitly - expect(async () => await userManagementMailer.verifyConnection()).not.toThrow(); + expect(async () => userManagementMailer.verifyConnection()).not.toThrow(); }); }); diff --git a/packages/cli/test/integration/variables.test.ts b/packages/cli/test/integration/variables.test.ts index 46e0814622555..d97df3e322541 100644 --- a/packages/cli/test/integration/variables.test.ts +++ b/packages/cli/test/integration/variables.test.ts @@ -5,7 +5,6 @@ import * as testDb from './shared/testDb'; import * as utils from './shared/utils'; import type { AuthAgent } from './shared/types'; -import type { ClassLike, MockedClass } from 'jest-mock'; import { License } from '@/License'; // mock that credentialsSharing is not enabled @@ -14,7 +13,7 @@ let ownerUser: User; let memberUser: User; let authAgent: AuthAgent; let variablesSpy: jest.SpyInstance<boolean>; -let licenseLike = { +const licenseLike = { isVariablesEnabled: jest.fn().mockReturnValue(true), getVariablesLimit: jest.fn().mockReturnValue(-1), }; diff --git a/packages/cli/test/integration/workflows.controller.test.ts b/packages/cli/test/integration/workflows.controller.test.ts index dd0d236ceb533..5c53cc25dba1d 100644 --- a/packages/cli/test/integration/workflows.controller.test.ts +++ b/packages/cli/test/integration/workflows.controller.test.ts @@ -1,4 +1,4 @@ -import { SuperAgentTest } from 'supertest'; +import type { SuperAgentTest } from 'supertest'; import type { IPinData } from 'n8n-workflow'; import type { User } from '@db/entities/User'; diff --git a/packages/cli/test/teardown.ts b/packages/cli/test/teardown.ts index bfc2748b1c303..07136f2ddb496 100644 --- a/packages/cli/test/teardown.ts +++ b/packages/cli/test/teardown.ts @@ -12,14 +12,14 @@ export default async () => { const query = dbType === 'postgres' ? 'SELECT datname as "Database" FROM pg_database' : 'SHOW DATABASES'; - const results: { Database: string }[] = await connection.query(query); + const results: Array<{ Database: string }> = await connection.query(query); const databases = results .filter( ({ Database: dbName }) => dbName.startsWith(`${dbType}_`) && dbName.endsWith('_n8n_test'), ) .map(({ Database: dbName }) => dbName); - const promises = databases.map((dbName) => connection.query(`DROP DATABASE ${dbName};`)); + const promises = databases.map(async (dbName) => connection.query(`DROP DATABASE ${dbName};`)); await Promise.all(promises); await connection.destroy(); }; diff --git a/packages/cli/test/unit/ActiveExecutions.test.ts b/packages/cli/test/unit/ActiveExecutions.test.ts index 19c1d316e6a5b..c635fe7d299d1 100644 --- a/packages/cli/test/unit/ActiveExecutions.test.ts +++ b/packages/cli/test/unit/ActiveExecutions.test.ts @@ -3,13 +3,9 @@ import { ActiveExecutions } from '@/ActiveExecutions'; import { mocked } from 'jest-mock'; import PCancelable from 'p-cancelable'; import { v4 as uuid } from 'uuid'; -import { - createDeferredPromise, - IDeferredPromise, - IExecuteResponsePromiseData, - IRun, -} from 'n8n-workflow'; -import { IWorkflowExecutionDataProcess } from '@/Interfaces'; +import type { IDeferredPromise, IExecuteResponsePromiseData, IRun } from 'n8n-workflow'; +import { createDeferredPromise } from 'n8n-workflow'; +import type { IWorkflowExecutionDataProcess } from '@/Interfaces'; const FAKE_EXECUTION_ID = '15'; const FAKE_SECOND_EXECUTION_ID = '20'; @@ -18,7 +14,7 @@ jest.mock('@/Db', () => { return { collections: { Execution: { - save: jest.fn(async () => Promise.resolve({ id: FAKE_EXECUTION_ID })), + save: jest.fn(async () => ({ id: FAKE_EXECUTION_ID })), update: jest.fn(), }, }, @@ -160,12 +156,12 @@ function mockFullRunData(): IRun { }; } -function mockCancelablePromise(): PCancelable<IRun> { +async function mockCancelablePromise(): PCancelable<IRun> { return new PCancelable(async (resolve) => { resolve(); }); } -function mockDeferredPromise(): Promise<IDeferredPromise<IExecuteResponsePromiseData>> { +async function mockDeferredPromise(): Promise<IDeferredPromise<IExecuteResponsePromiseData>> { return createDeferredPromise<IExecuteResponsePromiseData>(); } diff --git a/packages/cli/test/unit/ActiveWorkflowRunner.test.ts b/packages/cli/test/unit/ActiveWorkflowRunner.test.ts index efe6fe22960c8..c8694ceb88d31 100644 --- a/packages/cli/test/unit/ActiveWorkflowRunner.test.ts +++ b/packages/cli/test/unit/ActiveWorkflowRunner.test.ts @@ -1,13 +1,8 @@ import { v4 as uuid } from 'uuid'; import { mocked } from 'jest-mock'; -import { - ICredentialTypes, - INodesAndCredentials, - LoggerProxy, - NodeOperationError, - Workflow, -} from 'n8n-workflow'; +import type { ICredentialTypes, INodesAndCredentials } from 'n8n-workflow'; +import { LoggerProxy, NodeOperationError, Workflow } from 'n8n-workflow'; import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner'; import * as Db from '@/Db'; @@ -22,7 +17,7 @@ import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData' import { WorkflowRunner } from '@/WorkflowRunner'; import { mock } from 'jest-mock-extended'; -import { ExternalHooks } from '@/ExternalHooks'; +import type { ExternalHooks } from '@/ExternalHooks'; import { Container } from 'typedi'; import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; import { mockInstance } from '../integration/shared/utils'; @@ -99,12 +94,11 @@ jest.mock('@/Db', () => { return { collections: { Workflow: { - find: jest.fn(async () => Promise.resolve(generateWorkflows(databaseActiveWorkflowsCount))), + find: jest.fn(async () => generateWorkflows(databaseActiveWorkflowsCount)), findOne: jest.fn(async (searchParams) => { - const foundWorkflow = databaseActiveWorkflowsList.find( + return databaseActiveWorkflowsList.find( (workflow) => workflow.id.toString() === searchParams.where.id.toString(), ); - return Promise.resolve(foundWorkflow); }), update: jest.fn(), createQueryBuilder: jest.fn(() => { @@ -112,7 +106,7 @@ jest.mock('@/Db', () => { update: () => fakeQueryBuilder, set: () => fakeQueryBuilder, where: () => fakeQueryBuilder, - execute: () => Promise.resolve(), + execute: async () => {}, }; return fakeQueryBuilder; }), @@ -246,7 +240,7 @@ describe('ActiveWorkflowRunner', () => { const workflow = generateWorkflows(1); const additionalData = await WorkflowExecuteAdditionalData.getBase('fake-user-id'); - workflowRunnerRun.mockImplementationOnce(() => Promise.resolve('invalid-execution-id')); + workflowRunnerRun.mockResolvedValueOnce('invalid-execution-id'); await activeWorkflowRunner.runWorkflow( workflow[0], diff --git a/packages/cli/test/unit/CommunityNodeHelpers.test.ts b/packages/cli/test/unit/CommunityNodeHelpers.test.ts index 6cb69d0085e2d..951a9f4b34b1a 100644 --- a/packages/cli/test/unit/CommunityNodeHelpers.test.ts +++ b/packages/cli/test/unit/CommunityNodeHelpers.test.ts @@ -145,7 +145,7 @@ describe('executeCommand', () => { ); }); - await expect(async () => await executeCommand('ls')).rejects.toThrow( + await expect(async () => executeCommand('ls')).rejects.toThrow( RESPONSE_ERROR_MESSAGES.PACKAGE_NOT_FOUND, ); diff --git a/packages/cli/test/unit/CredentialsHelper.test.ts b/packages/cli/test/unit/CredentialsHelper.test.ts index 8ede15e4fdcad..5a11254cdb916 100644 --- a/packages/cli/test/unit/CredentialsHelper.test.ts +++ b/packages/cli/test/unit/CredentialsHelper.test.ts @@ -1,4 +1,4 @@ -import { +import type { IAuthenticateGeneric, ICredentialDataDecryptedObject, ICredentialType, @@ -7,8 +7,9 @@ import { INode, INodeProperties, INodesAndCredentials, - Workflow, } from 'n8n-workflow'; +import { deepCopy } from 'n8n-workflow'; +import { Workflow } from 'n8n-workflow'; import { CredentialsHelper } from '@/CredentialsHelper'; import { CredentialTypes } from '@/CredentialTypes'; import { Container } from 'typedi'; @@ -82,7 +83,9 @@ describe('CredentialsHelper', () => { }, credentialType: new (class TestApi implements ICredentialType { name = 'testApi'; + displayName = 'Test API'; + properties: INodeProperties[] = [ { displayName: 'User', @@ -124,7 +127,9 @@ describe('CredentialsHelper', () => { }, credentialType: new (class TestApi implements ICredentialType { name = 'testApi'; + displayName = 'Test API'; + properties: INodeProperties[] = [ { displayName: 'Access Token', @@ -154,7 +159,9 @@ describe('CredentialsHelper', () => { }, credentialType: new (class TestApi implements ICredentialType { name = 'testApi'; + displayName = 'Test API'; + properties: INodeProperties[] = [ { displayName: 'Access Token', @@ -184,7 +191,9 @@ describe('CredentialsHelper', () => { }, credentialType: new (class TestApi implements ICredentialType { name = 'testApi'; + displayName = 'Test API'; + properties: INodeProperties[] = [ { displayName: 'Access Token', @@ -215,7 +224,9 @@ describe('CredentialsHelper', () => { }, credentialType: new (class TestApi implements ICredentialType { name = 'testApi'; + displayName = 'Test API'; + properties: INodeProperties[] = [ { displayName: 'My Token', @@ -229,8 +240,8 @@ describe('CredentialsHelper', () => { credentials: ICredentialDataDecryptedObject, requestOptions: IHttpRequestOptions, ): Promise<IHttpRequestOptions> { - requestOptions.headers!['Authorization'] = `Bearer ${credentials.accessToken}`; - requestOptions.qs!['user'] = credentials.user; + requestOptions.headers!.Authorization = `Bearer ${credentials.accessToken}`; + requestOptions.qs!.user = credentials.user; return requestOptions; } })(), @@ -287,7 +298,7 @@ describe('CredentialsHelper', () => { const result = await credentialsHelper.authenticate( testData.input.credentials, testData.input.credentialType.name, - JSON.parse(JSON.stringify(incomingRequestOptions)), + deepCopy(incomingRequestOptions), workflow, node, timezone, diff --git a/packages/cli/test/unit/CurlConverterHelper.test.ts b/packages/cli/test/unit/CurlConverterHelper.test.ts index dd492bfdca167..0f4ec60795624 100644 --- a/packages/cli/test/unit/CurlConverterHelper.test.ts +++ b/packages/cli/test/unit/CurlConverterHelper.test.ts @@ -17,7 +17,8 @@ describe('CurlConverterHelper', () => { }); test('Should parse JSON content type correctly', () => { - const curl = `curl -X POST https://reqbin.com/echo/post/json -H 'Content-Type: application/json' -d '{"login":"my_login","password":"my_password"}'`; + const curl = + 'curl -X POST https://reqbin.com/echo/post/json -H \'Content-Type: application/json\' -d \'{"login":"my_login","password":"my_password"}\''; const parameters = toHttpNodeParameters(curl); expect(parameters.url).toBe('https://reqbin.com/echo/post/json'); expect(parameters.sendBody).toBe(true); @@ -31,7 +32,8 @@ describe('CurlConverterHelper', () => { }); test('Should parse multipart-form-data content type correctly', () => { - const curl = `curl -X POST https://reqbin.com/echo/post/json -v -F key1=value1 -F upload=@localfilename`; + const curl = + 'curl -X POST https://reqbin.com/echo/post/json -v -F key1=value1 -F upload=@localfilename'; const parameters = toHttpNodeParameters(curl); expect(parameters.url).toBe('https://reqbin.com/echo/post/json'); expect(parameters.sendBody).toBe(true); @@ -46,7 +48,8 @@ describe('CurlConverterHelper', () => { }); test('Should parse binary request correctly', () => { - const curl = `curl --location --request POST 'https://www.website.com' --header 'Content-Type: image/png' --data-binary '@/Users/image.png`; + const curl = + "curl --location --request POST 'https://www.website.com' --header 'Content-Type: image/png' --data-binary '@/Users/image.png"; const parameters = toHttpNodeParameters(curl); expect(parameters.url).toBe('https://www.website.com'); expect(parameters.method).toBe('POST'); @@ -74,7 +77,8 @@ describe('CurlConverterHelper', () => { }); test('Should parse header properties and keep the original case', () => { - const curl = `curl -X POST https://reqbin.com/echo/post/json -v -F key1=value1 -F upload=@localfilename -H "ACCEPT: text/javascript" -H "content-type: multipart/form-data"`; + const curl = + 'curl -X POST https://reqbin.com/echo/post/json -v -F key1=value1 -F upload=@localfilename -H "ACCEPT: text/javascript" -H "content-type: multipart/form-data"'; const parameters = toHttpNodeParameters(curl); expect(parameters.url).toBe('https://reqbin.com/echo/post/json'); expect(parameters.sendBody).toBe(true); @@ -91,7 +95,7 @@ describe('CurlConverterHelper', () => { }); test('Should parse querystring properties', () => { - const curl = `curl -G -d 'q=kitties' -d 'count=20' https://google.com/search`; + const curl = "curl -G -d 'q=kitties' -d 'count=20' https://google.com/search"; const parameters = toHttpNodeParameters(curl); expect(parameters.url).toBe('https://google.com/search'); expect(parameters.sendBody).toBe(false); @@ -105,7 +109,7 @@ describe('CurlConverterHelper', () => { }); test('Should parse basic authentication property and keep the original case', () => { - const curl = `curl https://reqbin.com/echo -u "login:password"`; + const curl = 'curl https://reqbin.com/echo -u "login:password"'; const parameters = toHttpNodeParameters(curl); expect(parameters.url).toBe('https://reqbin.com/echo'); expect(parameters.sendBody).toBe(false); @@ -119,7 +123,7 @@ describe('CurlConverterHelper', () => { }); test('Should parse location flag with --location', () => { - const curl = `curl https://reqbin.com/echo -u "login:password" --location`; + const curl = 'curl https://reqbin.com/echo -u "login:password" --location'; const parameters = toHttpNodeParameters(curl); expect(parameters.url).toBe('https://reqbin.com/echo'); expect(parameters.sendBody).toBe(false); @@ -134,7 +138,7 @@ describe('CurlConverterHelper', () => { }); test('Should parse location flag with --L', () => { - const curl = `curl https://reqbin.com/echo -u "login:password" -L`; + const curl = 'curl https://reqbin.com/echo -u "login:password" -L'; const parameters = toHttpNodeParameters(curl); expect(parameters.url).toBe('https://reqbin.com/echo'); expect(parameters.sendBody).toBe(false); @@ -149,7 +153,7 @@ describe('CurlConverterHelper', () => { }); test('Should parse location and max redirects flags with --location and --max-redirs 10', () => { - const curl = `curl https://reqbin.com/echo -u "login:password" --location --max-redirs 10`; + const curl = 'curl https://reqbin.com/echo -u "login:password" --location --max-redirs 10'; const parameters = toHttpNodeParameters(curl); expect(parameters.url).toBe('https://reqbin.com/echo'); expect(parameters.sendBody).toBe(false); @@ -165,7 +169,7 @@ describe('CurlConverterHelper', () => { }); test('Should parse proxy flag -x', () => { - const curl = `curl https://reqbin.com/echo -u "login:password" -x https://google.com`; + const curl = 'curl https://reqbin.com/echo -u "login:password" -x https://google.com'; const parameters = toHttpNodeParameters(curl); expect(parameters.url).toBe('https://reqbin.com/echo'); expect(parameters.sendBody).toBe(false); @@ -180,7 +184,7 @@ describe('CurlConverterHelper', () => { }); test('Should parse proxy flag --proxy', () => { - const curl = `curl https://reqbin.com/echo -u "login:password" -x https://google.com`; + const curl = 'curl https://reqbin.com/echo -u "login:password" -x https://google.com'; const parameters = toHttpNodeParameters(curl); expect(parameters.url).toBe('https://reqbin.com/echo'); expect(parameters.sendBody).toBe(false); @@ -195,7 +199,7 @@ describe('CurlConverterHelper', () => { }); test('Should parse include headers on output flag --include', () => { - const curl = `curl https://reqbin.com/echo -u "login:password" --include -x https://google.com`; + const curl = 'curl https://reqbin.com/echo -u "login:password" --include -x https://google.com'; const parameters = toHttpNodeParameters(curl); expect(parameters.url).toBe('https://reqbin.com/echo'); expect(parameters.sendBody).toBe(false); @@ -210,7 +214,7 @@ describe('CurlConverterHelper', () => { }); test('Should parse include headers on output flag -i', () => { - const curl = `curl https://reqbin.com/echo -u "login:password" -x https://google.com -i`; + const curl = 'curl https://reqbin.com/echo -u "login:password" -x https://google.com -i'; const parameters = toHttpNodeParameters(curl); expect(parameters.url).toBe('https://reqbin.com/echo'); expect(parameters.sendBody).toBe(false); @@ -225,7 +229,7 @@ describe('CurlConverterHelper', () => { }); test('Should parse include request flag -X', () => { - const curl = `curl -X POST https://reqbin.com/echo -u "login:password" -x https://google.com`; + const curl = 'curl -X POST https://reqbin.com/echo -u "login:password" -x https://google.com'; const parameters = toHttpNodeParameters(curl); expect(parameters.url).toBe('https://reqbin.com/echo'); expect(parameters.method).toBe('POST'); @@ -233,7 +237,8 @@ describe('CurlConverterHelper', () => { }); test('Should parse include request flag --request', () => { - const curl = `curl --request POST https://reqbin.com/echo -u "login:password" -x https://google.com`; + const curl = + 'curl --request POST https://reqbin.com/echo -u "login:password" -x https://google.com'; const parameters = toHttpNodeParameters(curl); expect(parameters.url).toBe('https://reqbin.com/echo'); expect(parameters.method).toBe('POST'); @@ -241,7 +246,8 @@ describe('CurlConverterHelper', () => { }); test('Should parse include timeout flag --connect-timeout', () => { - const curl = `curl --request POST https://reqbin.com/echo -u "login:password" --connect-timeout 20`; + const curl = + 'curl --request POST https://reqbin.com/echo -u "login:password" --connect-timeout 20'; const parameters = toHttpNodeParameters(curl); expect(parameters.url).toBe('https://reqbin.com/echo'); expect(parameters.method).toBe('POST'); @@ -250,7 +256,7 @@ describe('CurlConverterHelper', () => { }); test('Should parse download file flag -O', () => { - const curl = `curl --request POST https://reqbin.com/echo -u "login:password" -O`; + const curl = 'curl --request POST https://reqbin.com/echo -u "login:password" -O'; const parameters = toHttpNodeParameters(curl); expect(parameters.url).toBe('https://reqbin.com/echo'); expect(parameters.method).toBe('POST'); @@ -260,7 +266,7 @@ describe('CurlConverterHelper', () => { }); test('Should parse download file flag -o', () => { - const curl = `curl --request POST https://reqbin.com/echo -u "login:password" -o`; + const curl = 'curl --request POST https://reqbin.com/echo -u "login:password" -o'; const parameters = toHttpNodeParameters(curl); expect(parameters.url).toBe('https://reqbin.com/echo'); expect(parameters.method).toBe('POST'); @@ -270,7 +276,7 @@ describe('CurlConverterHelper', () => { }); test('Should parse ignore SSL flag -k', () => { - const curl = `curl --request POST https://reqbin.com/echo -u "login:password" -k`; + const curl = 'curl --request POST https://reqbin.com/echo -u "login:password" -k'; const parameters = toHttpNodeParameters(curl); expect(parameters.url).toBe('https://reqbin.com/echo'); expect(parameters.method).toBe('POST'); @@ -279,7 +285,7 @@ describe('CurlConverterHelper', () => { }); test('Should parse ignore SSL flag --insecure', () => { - const curl = `curl --request POST https://reqbin.com/echo -u "login:password" --insecure`; + const curl = 'curl --request POST https://reqbin.com/echo -u "login:password" --insecure'; const parameters = toHttpNodeParameters(curl); expect(parameters.url).toBe('https://reqbin.com/echo'); expect(parameters.method).toBe('POST'); diff --git a/packages/cli/test/unit/Events.test.ts b/packages/cli/test/unit/Events.test.ts index d68aa19216389..ce9e3e58bd8f1 100644 --- a/packages/cli/test/unit/Events.test.ts +++ b/packages/cli/test/unit/Events.test.ts @@ -1,12 +1,13 @@ -import { IRun, LoggerProxy, WorkflowExecuteMode } from 'n8n-workflow'; +import type { IRun, WorkflowExecuteMode } from 'n8n-workflow'; +import { LoggerProxy } from 'n8n-workflow'; import { QueryFailedError } from 'typeorm'; import { mock } from 'jest-mock-extended'; import config from '@/config'; import * as Db from '@/Db'; import { User } from '@db/entities/User'; -import { WorkflowStatistics } from '@db/entities/WorkflowStatistics'; -import { WorkflowStatisticsRepository } from '@db/repositories'; +import type { WorkflowStatistics } from '@db/entities/WorkflowStatistics'; +import type { WorkflowStatisticsRepository } from '@db/repositories'; import { nodeFetchedData, workflowExecutionCompleted } from '@/events/WorkflowStatistics'; import * as UserManagementHelper from '@/UserManagement/UserManagementHelper'; import { getLogger } from '@/Logger'; diff --git a/packages/cli/test/unit/Helpers.ts b/packages/cli/test/unit/Helpers.ts index 2d9f43ebf598a..2504f1dfcf3f9 100644 --- a/packages/cli/test/unit/Helpers.ts +++ b/packages/cli/test/unit/Helpers.ts @@ -1,4 +1,4 @@ -import { INodeTypeData } from 'n8n-workflow'; +import type { INodeTypeData } from 'n8n-workflow'; /** * Ensure all pending promises settle. The promise's `resolve` is placed in @@ -29,7 +29,7 @@ export function mockNodeTypesData( outputs: [], properties: [], }, - trigger: options?.addTrigger ? () => Promise.resolve(undefined) : undefined, + trigger: options?.addTrigger ? async () => undefined : undefined, }, }), acc diff --git a/packages/cli/test/unit/License.test.ts b/packages/cli/test/unit/License.test.ts index 2c6c72d26ee14..32351f2e27aa6 100644 --- a/packages/cli/test/unit/License.test.ts +++ b/packages/cli/test/unit/License.test.ts @@ -10,7 +10,7 @@ const MOCK_RENEW_OFFSET = 259200; const MOCK_INSTANCE_ID = 'instance-id'; const MOCK_ACTIVATION_KEY = 'activation-key'; const MOCK_FEATURE_FLAG = 'feat:mock'; -const MOCK_MAIN_PLAN_ID = 1234; +const MOCK_MAIN_PLAN_ID = '1b765dc4-d39d-4ffe-9885-c56dd67c4b26'; describe('License', () => { beforeAll(() => { @@ -82,12 +82,21 @@ describe('License', () => { expect(LicenseManager.prototype.getManagementJwt).toHaveBeenCalled(); }); - test('check main plan', async () => { + test('getMainPlan() returns the right entitlement', async () => { // mock entitlements response License.prototype.getCurrentEntitlements = jest.fn().mockReturnValue([ + { + id: '84a9c852-1349-478d-9ad1-b3f55510e477', + productId: '670650f2-72d8-4397-898c-c249906e2cc2', + productMetadata: {}, + features: {}, + featureOverrides: {}, + validFrom: new Date(), + validTo: new Date(), + }, { id: MOCK_MAIN_PLAN_ID, - productId: '', + productId: '670650f2-72d8-4397-898c-c249906e2cc2', productMetadata: { terms: { isMainPlan: true, @@ -104,4 +113,32 @@ describe('License', () => { const mainPlan = license.getMainPlan(); expect(mainPlan?.id).toBe(MOCK_MAIN_PLAN_ID); }); + + test('getMainPlan() returns undefined if there is no main plan', async () => { + // mock entitlements response + License.prototype.getCurrentEntitlements = jest.fn().mockReturnValue([ + { + id: '84a9c852-1349-478d-9ad1-b3f55510e477', + productId: '670650f2-72d8-4397-898c-c249906e2cc2', + productMetadata: {}, // has no `productMetadata.terms.isMainPlan`! + features: {}, + featureOverrides: {}, + validFrom: new Date(), + validTo: new Date(), + }, + { + id: 'c1aae471-c24e-4874-ad88-b97107de486c', + productId: '670650f2-72d8-4397-898c-c249906e2cc2', + productMetadata: {}, // has no `productMetadata.terms.isMainPlan`! + features: {}, + featureOverrides: {}, + validFrom: new Date(), + validTo: new Date(), + }, + ]); + jest.fn(license.getMainPlan).mockReset(); + + const mainPlan = license.getMainPlan(); + expect(mainPlan).toBeUndefined(); + }); }); diff --git a/packages/cli/test/unit/PermissionChecker.test.ts b/packages/cli/test/unit/PermissionChecker.test.ts index 71b550c50fea5..c69bc597abce2 100644 --- a/packages/cli/test/unit/PermissionChecker.test.ts +++ b/packages/cli/test/unit/PermissionChecker.test.ts @@ -1,6 +1,7 @@ import { v4 as uuid } from 'uuid'; import { Container } from 'typedi'; -import { ICredentialTypes, INodeTypes, SubworkflowOperationError, Workflow } from 'n8n-workflow'; +import type { ICredentialTypes, INodeTypes } from 'n8n-workflow'; +import { SubworkflowOperationError, Workflow } from 'n8n-workflow'; import config from '@/config'; import * as Db from '@/Db'; @@ -79,7 +80,7 @@ describe('PermissionChecker.check()', () => { ], }); - expect(() => PermissionChecker.check(workflow, userId)).not.toThrow(); + expect(async () => PermissionChecker.check(workflow, userId)).not.toThrow(); }); test('should allow if requesting user is instance owner', async () => { @@ -109,7 +110,7 @@ describe('PermissionChecker.check()', () => { ], }); - expect(async () => await PermissionChecker.check(workflow, owner.id)).not.toThrow(); + expect(async () => PermissionChecker.check(workflow, owner.id)).not.toThrow(); }); test('should allow if workflow creds are valid subset', async () => { @@ -156,7 +157,7 @@ describe('PermissionChecker.check()', () => { ], }); - expect(async () => await PermissionChecker.check(workflow, owner.id)).not.toThrow(); + expect(async () => PermissionChecker.check(workflow, owner.id)).not.toThrow(); }); test('should deny if workflow creds are not valid subset', async () => { diff --git a/packages/cli/test/unit/PostHog.test.ts b/packages/cli/test/unit/PostHog.test.ts index 47e736014e6d9..978108c39c9ad 100644 --- a/packages/cli/test/unit/PostHog.test.ts +++ b/packages/cli/test/unit/PostHog.test.ts @@ -24,7 +24,7 @@ describe('PostHog', () => { const ph = new PostHogClient(); await ph.init(instanceId); - expect(PostHog.prototype.constructor).toHaveBeenCalledWith(apiKey, {host: apiHost}); + expect(PostHog.prototype.constructor).toHaveBeenCalledWith(apiKey, { host: apiHost }); }); it('does not initialize or track if diagnostics are not enabled', async () => { @@ -78,13 +78,10 @@ describe('PostHog', () => { createdAt, }); - expect(PostHog.prototype.getAllFlags).toHaveBeenCalledWith( - `${instanceId}#${userId}`, - { - personProperties: { - created_at_timestamp: createdAt.getTime().toString(), - }, - } - ); + expect(PostHog.prototype.getAllFlags).toHaveBeenCalledWith(`${instanceId}#${userId}`, { + personProperties: { + created_at_timestamp: createdAt.getTime().toString(), + }, + }); }); -}); \ No newline at end of file +}); diff --git a/packages/cli/test/unit/WorkflowCredentials.test.ts b/packages/cli/test/unit/WorkflowCredentials.test.ts index ece7dd66351b9..78afe95a643b5 100644 --- a/packages/cli/test/unit/WorkflowCredentials.test.ts +++ b/packages/cli/test/unit/WorkflowCredentials.test.ts @@ -12,16 +12,16 @@ async function mockFind({ type: string; }): Promise<IWorkflowCredentials | null> { // Simple statement that maps a return value based on the `id` parameter - if (id === notFoundNode.credentials!!.test.id) { + if (id === notFoundNode.credentials!.test.id) { return null; } // Otherwise just build some kind of credential object and return it return { [type]: { [id]: { - id: id, + id, name: type, - type: type, + type, nodesAccess: [], data: '', }, @@ -49,7 +49,7 @@ describe('WorkflowCredentials', () => { }); test('Should return an error if any node has no credential ID', () => { - const credentials = noIdNode.credentials!!.test; + const credentials = noIdNode.credentials!.test; const expectedError = new Error( `Credentials with name "${credentials.name}" for type "test" miss an ID.`, ); @@ -58,7 +58,7 @@ describe('WorkflowCredentials', () => { }); test('Should return an error if credentials cannot be found in the DB', () => { - const credentials = notFoundNode.credentials!!.test; + const credentials = notFoundNode.credentials!.test; const expectedError = new Error( `Could not find credentials for type "test" with ID "${credentials.id}".`, ); diff --git a/packages/cli/test/unit/WorkflowExecuteAdditionalData.test.ts b/packages/cli/test/unit/WorkflowExecuteAdditionalData.test.ts index 9d364bd788939..6007c0dd74b81 100644 --- a/packages/cli/test/unit/WorkflowExecuteAdditionalData.test.ts +++ b/packages/cli/test/unit/WorkflowExecuteAdditionalData.test.ts @@ -6,7 +6,7 @@ jest.mock('@/Db', () => { return { collections: { ExecutionMetadata: { - save: jest.fn(async () => Promise.resolve([])), + save: jest.fn(async () => []), }, }, }; diff --git a/packages/cli/test/unit/WorkflowHelpers.test.ts b/packages/cli/test/unit/WorkflowHelpers.test.ts index 1f00375b63d3f..77cc85ec96bdd 100644 --- a/packages/cli/test/unit/WorkflowHelpers.test.ts +++ b/packages/cli/test/unit/WorkflowHelpers.test.ts @@ -1,4 +1,5 @@ -import { INode, LoggerProxy } from 'n8n-workflow'; +import type { INode } from 'n8n-workflow'; +import { LoggerProxy } from 'n8n-workflow'; import { WorkflowEntity } from '@db/entities/WorkflowEntity'; import { CredentialsEntity } from '@db/entities/CredentialsEntity'; import { getNodesWithInaccessibleCreds, validateWorkflowCredentialUsage } from '@/WorkflowHelpers'; diff --git a/packages/cli/test/unit/controllers/me.controller.test.ts b/packages/cli/test/unit/controllers/me.controller.test.ts index 20b8fdfb92d5e..8a622c67e4f62 100644 --- a/packages/cli/test/unit/controllers/me.controller.test.ts +++ b/packages/cli/test/unit/controllers/me.controller.test.ts @@ -1,11 +1,10 @@ -import { CookieOptions, Response } from 'express'; -import type { Repository } from 'typeorm'; +import type { CookieOptions, Response } from 'express'; import jwt from 'jsonwebtoken'; import { mock, anyObject, captor } from 'jest-mock-extended'; import type { ILogger } from 'n8n-workflow'; import type { IExternalHooksClass, IInternalHooksClass } from '@/Interfaces'; import type { User } from '@db/entities/User'; -import { UserRepository } from '@db/repositories'; +import type { UserRepository } from '@db/repositories'; import { MeController } from '@/controllers'; import { AUTH_COOKIE_NAME } from '@/constants'; import { BadRequestError } from '@/ResponseHelper'; diff --git a/packages/cli/test/unit/controllers/translation.controller.test.ts b/packages/cli/test/unit/controllers/translation.controller.test.ts index a24d462f81818..16e9645bbf44a 100644 --- a/packages/cli/test/unit/controllers/translation.controller.test.ts +++ b/packages/cli/test/unit/controllers/translation.controller.test.ts @@ -1,9 +1,9 @@ import { mock } from 'jest-mock-extended'; import type { ICredentialTypes } from 'n8n-workflow'; import type { Config } from '@/config'; +import type { TranslationRequest } from '@/controllers/translation.controller'; import { TranslationController, - TranslationRequest, CREDENTIAL_TRANSLATIONS_DIR, } from '@/controllers/translation.controller'; import { BadRequestError } from '@/ResponseHelper'; diff --git a/packages/cli/test/unit/repositories/role.repository.test.ts b/packages/cli/test/unit/repositories/role.repository.test.ts index ad37f797fc0c7..73dcc048b7d13 100644 --- a/packages/cli/test/unit/repositories/role.repository.test.ts +++ b/packages/cli/test/unit/repositories/role.repository.test.ts @@ -1,7 +1,8 @@ import { Container } from 'typedi'; import { DataSource, EntityManager } from 'typeorm'; import { mock } from 'jest-mock-extended'; -import { Role, RoleNames, RoleScopes } from '@db/entities/Role'; +import type { RoleNames, RoleScopes } from '@db/entities/Role'; +import { Role } from '@db/entities/Role'; import { RoleRepository } from '@db/repositories/role.repository'; import { mockInstance } from '../../integration/shared/utils'; import { randomInteger } from '../../integration/shared/random'; @@ -38,7 +39,7 @@ describe('RoleRepository', () => { test('should throw otherwise', async () => { entityManager.findOneOrFail.mockRejectedValueOnce(new Error()); - expect(() => roleRepository.findRoleOrFail('global', 'owner')).rejects.toThrow(); + expect(async () => roleRepository.findRoleOrFail('global', 'owner')).rejects.toThrow(); }); }); diff --git a/packages/cli/test/unit/utils.test.ts b/packages/cli/test/unit/utils.test.ts new file mode 100644 index 0000000000000..2925b4b66902d --- /dev/null +++ b/packages/cli/test/unit/utils.test.ts @@ -0,0 +1,35 @@ +import { webhookNotFoundErrorMessage } from '../../src/utils'; + +describe('utils test webhookNotFoundErrorMessage ', () => { + it('should return a message with path and method', () => { + const message = webhookNotFoundErrorMessage('webhook12345', 'GET'); + + expect(message).toEqual('The requested webhook "GET webhook12345" is not registered.'); + }); + it('should return a message with path', () => { + const message = webhookNotFoundErrorMessage('webhook12345'); + + expect(message).toEqual('The requested webhook "webhook12345" is not registered.'); + }); + it('should return a message with method with tip', () => { + const message = webhookNotFoundErrorMessage('webhook12345', 'POST', ['GET', 'PUT']); + + expect(message).toEqual( + 'This webhook is not registered for POST requests. Did you mean to make a GET or PUT request?', + ); + }); + it('should return a message with method with tip', () => { + const message = webhookNotFoundErrorMessage('webhook12345', 'POST', ['PUT']); + + expect(message).toEqual( + 'This webhook is not registered for POST requests. Did you mean to make a PUT request?', + ); + }); + it('should return a message with method with tip', () => { + const message = webhookNotFoundErrorMessage('webhook12345', 'POST', ['GET', 'PUT', 'DELETE']); + + expect(message).toEqual( + 'This webhook is not registered for POST requests. Did you mean to make a GET, PUT or DELETE request?', + ); + }); +}); diff --git a/packages/core/.eslintrc.js b/packages/core/.eslintrc.js index a1a1b8155d206..4d68086144d56 100644 --- a/packages/core/.eslintrc.js +++ b/packages/core/.eslintrc.js @@ -11,8 +11,6 @@ module.exports = { ignorePatterns: ['bin/*.js'], rules: { - '@typescript-eslint/consistent-type-imports': 'error', - // TODO: Remove this 'import/order': 'off', '@typescript-eslint/ban-ts-comment': ['error', { 'ts-ignore': true }], diff --git a/packages/core/package.json b/packages/core/package.json index c1d1a9f30f0b5..699e12149c62b 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "n8n-core", - "version": "0.164.0", + "version": "0.166.0", "description": "Core functionality of n8n", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", diff --git a/packages/core/src/BinaryDataManager/FileSystem.ts b/packages/core/src/BinaryDataManager/FileSystem.ts index df033569752f1..011a5cd5eedae 100644 --- a/packages/core/src/BinaryDataManager/FileSystem.ts +++ b/packages/core/src/BinaryDataManager/FileSystem.ts @@ -134,63 +134,58 @@ export class BinaryDataFileSystem implements IBinaryDataManager { const execsAdded: { [key: string]: number } = {}; - const proms = metaFileNames.reduce( - (prev, curr) => { - const [prefix, executionId, ts] = curr.split('_'); + const promises = metaFileNames.reduce<Array<Promise<void>>>((prev, curr) => { + const [prefix, executionId, ts] = curr.split('_'); - if (prefix !== filePrefix) { + if (prefix !== filePrefix) { + return prev; + } + + const execTimestamp = parseInt(ts, 10); + + if (execTimestamp < currentTimeValue) { + if (execsAdded[executionId]) { + // do not delete data, only meta file + prev.push(this.deleteMetaFileByPath(path.join(metaPath, curr))); return prev; } - const execTimestamp = parseInt(ts, 10); - - if (execTimestamp < currentTimeValue) { - if (execsAdded[executionId]) { - // do not delete data, only meta file - prev.push(this.deleteMetaFileByPath(path.join(metaPath, curr))); - return prev; - } - - execsAdded[executionId] = 1; - prev.push( - this.deleteBinaryDataByExecutionId(executionId).then(async () => - this.deleteMetaFileByPath(path.join(metaPath, curr)), - ), - ); - } + execsAdded[executionId] = 1; + prev.push( + this.deleteBinaryDataByExecutionId(executionId).then(async () => + this.deleteMetaFileByPath(path.join(metaPath, curr)), + ), + ); + } - return prev; - }, - [Promise.resolve()], - ); + return prev; + }, []); - return Promise.all(proms).then(() => {}); + await Promise.all(promises); } async duplicateBinaryDataByIdentifier(binaryDataId: string, prefix: string): Promise<string> { const newBinaryDataId = this.generateFileName(prefix); - return fs - .copyFile(this.resolveStoragePath(binaryDataId), this.resolveStoragePath(newBinaryDataId)) - .then(() => newBinaryDataId); + await fs.copyFile( + this.resolveStoragePath(binaryDataId), + this.resolveStoragePath(newBinaryDataId), + ); + return newBinaryDataId; } async deleteBinaryDataByExecutionId(executionId: string): Promise<void> { const regex = new RegExp(`${executionId}_*`); const filenames = await fs.readdir(this.storagePath); - const proms = filenames.reduce( - (allProms, filename) => { - if (regex.test(filename)) { - allProms.push(fs.rm(this.resolveStoragePath(filename))); - } - - return allProms; - }, - [Promise.resolve()], - ); + const promises = filenames.reduce<Array<Promise<void>>>((allProms, filename) => { + if (regex.test(filename)) { + allProms.push(fs.rm(this.resolveStoragePath(filename))); + } + return allProms; + }, []); - return Promise.all(proms).then(async () => Promise.resolve()); + await Promise.all(promises); } async deleteBinaryDataByIdentifier(identifier: string): Promise<void> { @@ -198,20 +193,17 @@ export class BinaryDataFileSystem implements IBinaryDataManager { } async persistBinaryDataForExecutionId(executionId: string): Promise<void> { - return fs.readdir(this.getBinaryDataPersistMetaPath()).then(async (metafiles) => { - const proms = metafiles.reduce( - (prev, curr) => { - if (curr.startsWith(`${PREFIX_PERSISTED_METAFILE}_${executionId}_`)) { - prev.push(fs.rm(path.join(this.getBinaryDataPersistMetaPath(), curr))); - return prev; - } - + return fs.readdir(this.getBinaryDataPersistMetaPath()).then(async (metaFiles) => { + const promises = metaFiles.reduce<Array<Promise<void>>>((prev, curr) => { + if (curr.startsWith(`${PREFIX_PERSISTED_METAFILE}_${executionId}_`)) { + prev.push(fs.rm(path.join(this.getBinaryDataPersistMetaPath(), curr))); return prev; - }, - [Promise.resolve()], - ); + } + + return prev; + }, []); - return Promise.all(proms).then(() => {}); + await Promise.all(promises); }); } @@ -227,8 +219,8 @@ export class BinaryDataFileSystem implements IBinaryDataManager { return path.join(this.storagePath, 'persistMeta'); } - private async deleteMetaFileByPath(metafilePath: string): Promise<void> { - return fs.rm(metafilePath); + private async deleteMetaFileByPath(metaFilePath: string): Promise<void> { + return fs.rm(metaFilePath); } private async deleteFromLocalStorage(identifier: string) { diff --git a/packages/core/src/BinaryDataManager/index.ts b/packages/core/src/BinaryDataManager/index.ts index b3ddb8738d6c6..bed5a08b101d0 100644 --- a/packages/core/src/BinaryDataManager/index.ts +++ b/packages/core/src/BinaryDataManager/index.ts @@ -158,38 +158,30 @@ export class BinaryDataManager { async markDataForDeletionByExecutionId(executionId: string): Promise<void> { if (this.managers[this.binaryDataMode]) { - return this.managers[this.binaryDataMode].markDataForDeletionByExecutionId(executionId); + await this.managers[this.binaryDataMode].markDataForDeletionByExecutionId(executionId); } - - return Promise.resolve(); } async markDataForDeletionByExecutionIds(executionIds: string[]): Promise<void> { if (this.managers[this.binaryDataMode]) { - return Promise.all( + await Promise.all( executionIds.map(async (id) => this.managers[this.binaryDataMode].markDataForDeletionByExecutionId(id), ), - ).then(() => {}); + ); } - - return Promise.resolve(); } async persistBinaryDataForExecutionId(executionId: string): Promise<void> { if (this.managers[this.binaryDataMode]) { - return this.managers[this.binaryDataMode].persistBinaryDataForExecutionId(executionId); + await this.managers[this.binaryDataMode].persistBinaryDataForExecutionId(executionId); } - - return Promise.resolve(); } async deleteBinaryDataByExecutionId(executionId: string): Promise<void> { if (this.managers[this.binaryDataMode]) { - return this.managers[this.binaryDataMode].deleteBinaryDataByExecutionId(executionId); + await this.managers[this.binaryDataMode].deleteBinaryDataByExecutionId(executionId); } - - return Promise.resolve(); } async duplicateBinaryData( @@ -201,7 +193,7 @@ export class BinaryDataManager { async (executionDataArray) => { if (executionDataArray) { return Promise.all( - executionDataArray.map((executionData) => { + executionDataArray.map(async (executionData) => { if (executionData.binary) { return this.duplicateBinaryDataInExecData(executionData, executionId); } @@ -218,7 +210,7 @@ export class BinaryDataManager { return Promise.all(returnInputData); } - return Promise.resolve(inputData as INodeExecutionData[][]); + return inputData as INodeExecutionData[][]; } private generateBinaryId(filename: string) { diff --git a/packages/core/src/NodeExecuteFunctions.ts b/packages/core/src/NodeExecuteFunctions.ts index ec78d8a1dc653..5d86a914a0d08 100644 --- a/packages/core/src/NodeExecuteFunctions.ts +++ b/packages/core/src/NodeExecuteFunctions.ts @@ -64,6 +64,7 @@ import type { } from 'n8n-workflow'; import { createDeferredPromise, + isObjectEmpty, NodeApiError, NodeHelpers, NodeOperationError, @@ -727,10 +728,6 @@ export async function proxyRequestToAxios( } } -function isIterator(obj: unknown): boolean { - return obj instanceof Object && Symbol.iterator in obj; -} - function convertN8nRequestToAxios(n8nRequest: IHttpRequestOptions): AxiosRequestConfig { // Destructure properties with the same name first. const { headers, method, timeout, auth, proxy, url } = n8nRequest; @@ -794,7 +791,7 @@ function convertN8nRequestToAxios(n8nRequest: IHttpRequestOptions): AxiosRequest // if there is a body and it's empty (does not have properties), // make sure not to send anything in it as some services fail when // sending GET request with empty body. - if (isIterator(body) || Object.keys(body).length > 0) { + if (typeof body === 'object' && !isObjectEmpty(body)) { axiosRequest.data = body; } } diff --git a/packages/core/src/WorkflowExecute.ts b/packages/core/src/WorkflowExecute.ts index 480a2b2f80e0c..072105c76a6e8 100644 --- a/packages/core/src/WorkflowExecute.ts +++ b/packages/core/src/WorkflowExecute.ts @@ -792,7 +792,7 @@ export class WorkflowExecute { } if (gotCancel) { - return Promise.resolve(); + return; } nodeSuccessData = null; @@ -919,7 +919,7 @@ export class WorkflowExecute { for (let tryIndex = 0; tryIndex < maxTries; tryIndex++) { if (gotCancel) { - return Promise.resolve(); + return; } try { if (tryIndex !== 0) { @@ -1175,10 +1175,8 @@ export class WorkflowExecute { outputIndex ]) { if (!workflow.nodes.hasOwnProperty(connectionData.node)) { - return Promise.reject( - new Error( - `The node "${executionNode.name}" connects to not found node "${connectionData.node}"`, - ), + throw new Error( + `The node "${executionNode.name}" connects to not found node "${connectionData.node}"`, ); } @@ -1212,7 +1210,7 @@ export class WorkflowExecute { ]); } - return Promise.resolve(); + return; })() .then(async () => { if (gotCancel && executionError === undefined) { @@ -1288,12 +1286,16 @@ export class WorkflowExecute { message: executionError.message, stack: executionError.stack, } as ExecutionError; + if (executionError.message?.includes('canceled')) { + fullRunData.status = 'canceled'; + } } else if (this.runExecutionData.waitTill!) { // eslint-disable-next-line @typescript-eslint/restrict-template-expressions Logger.verbose(`Workflow execution will wait until ${this.runExecutionData.waitTill}`, { workflowId: workflow.id, }); fullRunData.waitTill = this.runExecutionData.waitTill; + fullRunData.status = 'waiting'; } else { Logger.verbose('Workflow execution finished successfully', { workflowId: workflow.id }); fullRunData.finished = true; @@ -1306,7 +1308,6 @@ export class WorkflowExecute { // Static data of workflow changed newStaticData = workflow.staticData; } - await this.executeHook('workflowExecuteAfter', [fullRunData, newStaticData]); if (closeFunction) { diff --git a/packages/core/test/Helpers.ts b/packages/core/test/Helpers.ts index c38362b8278a6..0d6874495483a 100644 --- a/packages/core/test/Helpers.ts +++ b/packages/core/test/Helpers.ts @@ -1,9 +1,7 @@ import set from 'lodash.set'; -import { +import type { ICredentialDataDecryptedObject, - ICredentialsHelper, - IDataObject, IDeferredPromise, IExecuteWorkflowInfo, IHttpRequestHelper, @@ -20,12 +18,12 @@ import { IVersionedNodeType, IWorkflowBase, IWorkflowExecuteAdditionalData, - NodeHelpers, NodeParameterValue, - WorkflowHooks, } from 'n8n-workflow'; +import { deepCopy } from 'n8n-workflow'; +import { ICredentialsHelper, NodeHelpers, WorkflowHooks } from 'n8n-workflow'; import { Credentials } from '@/Credentials'; -import { IExecuteFunctions } from '@/Interfaces'; +import type { IExecuteFunctions } from '@/Interfaces'; export class CredentialsHelper extends ICredentialsHelper { async authenticate( @@ -381,12 +379,12 @@ class NodeTypesClass implements INodeTypes { compareData.value2 as NodeParameterValue, ); - if (compareOperationResult === true && combineOperation === 'any') { + if (compareOperationResult && combineOperation === 'any') { // If it passes and the operation is "any" we do not have to check any // other ones as it should pass anyway. So go on with the next item. returnDataTrue.push(item); continue itemLoop; - } else if (compareOperationResult === false && combineOperation === 'all') { + } else if (!compareOperationResult && combineOperation === 'all') { // If it fails and the operation is "all" we do not have to check any // other ones as it should be not pass anyway. So go on with the next item. returnDataFalse.push(item); @@ -524,7 +522,7 @@ class NodeTypesClass implements INodeTypes { outputs: ['main'], properties: [], }, - execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> { + async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> { const items = this.getInputData(); return this.prepareOutputData(items); }, @@ -570,7 +568,7 @@ class NodeTypesClass implements INodeTypes { }, ], }, - execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> { + async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> { const items = this.getInputData(); const returnData: INodeExecutionData[] = []; @@ -702,13 +700,14 @@ class NodeTypesClass implements INodeTypes { name: 'dotNotation', type: 'boolean', default: true, - description: `<p>By default, dot-notation is used in property names. This means that "a.b" will set the property "b" underneath "a" so { "a": { "b": value} }.</p><p>If that is not intended this can be deactivated, it will then set { "a.b": value } instead.</p>`, + description: + '<p>By default, dot-notation is used in property names. This means that "a.b" will set the property "b" underneath "a" so { "a": { "b": value} }.</p><p>If that is not intended this can be deactivated, it will then set { "a.b": value } instead.</p>', }, ], }, ], }, - execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> { + async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> { const items = this.getInputData(); if (items.length === 0) { @@ -722,13 +721,13 @@ class NodeTypesClass implements INodeTypes { for (let itemIndex = 0; itemIndex < items.length; itemIndex++) { keepOnlySet = this.getNodeParameter('keepOnlySet', itemIndex, false) as boolean; item = items[itemIndex]; - const options = this.getNodeParameter('options', itemIndex, {}) as IDataObject; + const options = this.getNodeParameter('options', itemIndex, {}); const newItem: INodeExecutionData = { json: {}, }; - if (keepOnlySet !== true) { + if (!keepOnlySet) { if (item.binary !== undefined) { // Create a shallow copy of the binary data so that the old // data references which do not get changed still stay behind @@ -737,7 +736,7 @@ class NodeTypesClass implements INodeTypes { Object.assign(newItem.binary, item.binary); } - newItem.json = JSON.parse(JSON.stringify(item.json)); + newItem.json = deepCopy(item.json); } // Add boolean values @@ -797,7 +796,7 @@ class NodeTypesClass implements INodeTypes { outputs: ['main'], properties: [], }, - execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> { + async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> { const items = this.getInputData(); return this.prepareOutputData(items); @@ -851,6 +850,8 @@ export function WorkflowExecuteAdditionalData( connections: {}, }; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore return { credentialsHelper: new CredentialsHelper(''), hooks: new WorkflowHooks(hookFunctions, 'trigger', '1', workflowData), diff --git a/packages/core/test/NodeExecuteFunctions.test.ts b/packages/core/test/NodeExecuteFunctions.test.ts index aff7ba036679b..f91c605b2cb3d 100644 --- a/packages/core/test/NodeExecuteFunctions.test.ts +++ b/packages/core/test/NodeExecuteFunctions.test.ts @@ -28,7 +28,7 @@ describe('NodeExecuteFunctions', () => { BinaryDataManager.instance = undefined; }); - test(`test getBinaryDataBuffer(...) & setBinaryDataBuffer(...) methods in 'default' mode`, async () => { + test("test getBinaryDataBuffer(...) & setBinaryDataBuffer(...) methods in 'default' mode", async () => { // Setup a 'default' binary data manager instance await BinaryDataManager.init({ mode: 'default', @@ -39,8 +39,8 @@ describe('NodeExecuteFunctions', () => { }); // Set our binary data buffer - let inputData: Buffer = Buffer.from('This is some binary data', 'utf8'); - let setBinaryDataBufferResponse: IBinaryData = await setBinaryDataBuffer( + const inputData: Buffer = Buffer.from('This is some binary data', 'utf8'); + const setBinaryDataBufferResponse: IBinaryData = await setBinaryDataBuffer( { mimeType: 'txt', data: 'This should be overwritten by the actual payload in the response', @@ -54,7 +54,7 @@ describe('NodeExecuteFunctions', () => { // Now, re-fetch our data. // An ITaskDataConnections object is used to share data between nodes. The top level property, 'main', represents the successful output object from a previous node. - let taskDataConnectionsInput: ITaskDataConnections = { + const taskDataConnectionsInput: ITaskDataConnections = { main: [], }; @@ -69,7 +69,7 @@ describe('NodeExecuteFunctions', () => { ]); // Now, lets fetch our data! The item will be item index 0. - let getBinaryDataBufferResponse: Buffer = await getBinaryDataBuffer( + const getBinaryDataBufferResponse: Buffer = await getBinaryDataBuffer( taskDataConnectionsInput, 0, 'data', @@ -79,7 +79,7 @@ describe('NodeExecuteFunctions', () => { expect(getBinaryDataBufferResponse).toEqual(inputData); }); - test(`test getBinaryDataBuffer(...) & setBinaryDataBuffer(...) methods in 'filesystem' mode`, async () => { + test("test getBinaryDataBuffer(...) & setBinaryDataBuffer(...) methods in 'filesystem' mode", async () => { // Setup a 'filesystem' binary data manager instance await BinaryDataManager.init({ mode: 'filesystem', @@ -90,8 +90,8 @@ describe('NodeExecuteFunctions', () => { }); // Set our binary data buffer - let inputData: Buffer = Buffer.from('This is some binary data', 'utf8'); - let setBinaryDataBufferResponse: IBinaryData = await setBinaryDataBuffer( + const inputData: Buffer = Buffer.from('This is some binary data', 'utf8'); + const setBinaryDataBufferResponse: IBinaryData = await setBinaryDataBuffer( { mimeType: 'txt', data: 'This should be overwritten with the name of the configured data manager', @@ -112,7 +112,7 @@ describe('NodeExecuteFunctions', () => { // Now, re-fetch our data. // An ITaskDataConnections object is used to share data between nodes. The top level property, 'main', represents the successful output object from a previous node. - let taskDataConnectionsInput: ITaskDataConnections = { + const taskDataConnectionsInput: ITaskDataConnections = { main: [], }; @@ -127,7 +127,7 @@ describe('NodeExecuteFunctions', () => { ]); // Now, lets fetch our data! The item will be item index 0. - let getBinaryDataBufferResponse: Buffer = await getBinaryDataBuffer( + const getBinaryDataBufferResponse: Buffer = await getBinaryDataBuffer( taskDataConnectionsInput, 0, 'data', diff --git a/packages/core/test/WorkflowExecute.test.ts b/packages/core/test/WorkflowExecute.test.ts index 5a79bac72339f..5c5eb2ba58826 100644 --- a/packages/core/test/WorkflowExecute.test.ts +++ b/packages/core/test/WorkflowExecute.test.ts @@ -1,4 +1,5 @@ -import { createDeferredPromise, IConnections, INode, IRun, Workflow } from 'n8n-workflow'; +import type { IConnections, INode, IRun } from 'n8n-workflow'; +import { createDeferredPromise, Workflow } from 'n8n-workflow'; import { WorkflowExecute } from '@/WorkflowExecute'; import * as Helpers from './Helpers'; diff --git a/packages/core/test/utils.ts b/packages/core/test/utils.ts index 1d6abbf50efaa..ca7c804dca44a 100644 --- a/packages/core/test/utils.ts +++ b/packages/core/test/utils.ts @@ -1,4 +1,5 @@ -import { ILogger, LoggerProxy } from 'n8n-workflow'; +import type { ILogger } from 'n8n-workflow'; +import { LoggerProxy } from 'n8n-workflow'; const fakeLogger = { log: () => {}, diff --git a/packages/design-system/.storybook/main.js b/packages/design-system/.storybook/main.js index 2a72dca1e6e55..d3a302a1b40a4 100644 --- a/packages/design-system/.storybook/main.js +++ b/packages/design-system/.storybook/main.js @@ -20,7 +20,6 @@ module.exports = { }, }, }, - 'storybook-addon-designs', 'storybook-addon-themes', ], webpackFinal: async (config) => { diff --git a/packages/design-system/.storybook/preview.js b/packages/design-system/.storybook/preview.js index be5e60d537fef..1e01ec1dd4fdc 100644 --- a/packages/design-system/.storybook/preview.js +++ b/packages/design-system/.storybook/preview.js @@ -5,12 +5,12 @@ import ElementUI from 'element-ui'; import lang from 'element-ui/lib/locale/lang/en'; import locale from 'element-ui/lib/locale'; -import designSystemComponents from '../src/plugins/n8nComponents'; +import { N8nPlugin } from '../src/plugin'; import Vue from 'vue'; Vue.use(ElementUI); -Vue.use(designSystemComponents); +Vue.use(N8nPlugin); locale.use(lang); @@ -58,7 +58,7 @@ export const parameters = { list: [ { name: 'dark', - class: 'theme-dark', + class: 'theme-dark-beta', color: '#000', }, ], diff --git a/packages/design-system/package.json b/packages/design-system/package.json index f0e29389e26cd..27e945f2198d8 100644 --- a/packages/design-system/package.json +++ b/packages/design-system/package.json @@ -1,6 +1,6 @@ { "name": "n8n-design-system", - "version": "0.62.0", + "version": "0.63.0", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", "author": { @@ -42,12 +42,13 @@ "@fortawesome/fontawesome-svg-core": "^1.2.36", "@fortawesome/free-solid-svg-icons": "^5.15.4", "@fortawesome/vue-fontawesome": "^2.0.9", - "@storybook/addon-actions": "^7.0.0-beta.46", - "@storybook/addon-essentials": "^7.0.0-beta.46", - "@storybook/addon-links": "^7.0.0-beta.46", + "@storybook/addon-actions": "^7.0.7", + "@storybook/addon-docs": "^7.0.7", + "@storybook/addon-essentials": "^7.0.7", + "@storybook/addon-links": "^7.0.7", "@storybook/addon-postcss": "^3.0.0-alpha.1", - "@storybook/vue": "^7.0.0-beta.46", - "@storybook/vue-webpack5": "^7.0.0-beta.46", + "@storybook/vue": "^7.0.7", + "@storybook/vue-webpack5": "^7.0.7", "@testing-library/jest-dom": "^5.16.5", "@testing-library/vue": "^5.8.3", "@types/markdown-it": "^12.2.3", @@ -63,8 +64,7 @@ "node-notifier": "^10.0.1", "sass": "^1.58.0", "sass-loader": "^13.2.0", - "storybook": "^7.0.0-beta.46", - "storybook-addon-designs": "^6.3.1", + "storybook": "^7.0.7", "storybook-addon-themes": "^6.1.0", "trim": "^1.0.1", "vite": "^4.0.4", diff --git a/packages/design-system/src/components/N8nActionDropdown/ActionDropdown.vue b/packages/design-system/src/components/N8nActionDropdown/ActionDropdown.vue index 08f0147a20575..75a785a590289 100644 --- a/packages/design-system/src/components/N8nActionDropdown/ActionDropdown.vue +++ b/packages/design-system/src/components/N8nActionDropdown/ActionDropdown.vue @@ -34,7 +34,8 @@ </template> <script lang="ts"> -import { defineComponent, PropType } from 'vue'; +import type { PropType } from 'vue'; +import { defineComponent } from 'vue'; import { Dropdown as ElDropdown, DropdownMenu as ElDropdownMenu, diff --git a/packages/design-system/src/components/N8nActionToggle/ActionToggle.vue b/packages/design-system/src/components/N8nActionToggle/ActionToggle.vue index 087770050cc78..c617ace7626d3 100644 --- a/packages/design-system/src/components/N8nActionToggle/ActionToggle.vue +++ b/packages/design-system/src/components/N8nActionToggle/ActionToggle.vue @@ -37,7 +37,8 @@ </template> <script lang="ts"> -import { defineComponent, PropType } from 'vue'; +import type { PropType } from 'vue'; +import { defineComponent } from 'vue'; import { Dropdown as ElDropdown, DropdownMenu as ElDropdownMenu, diff --git a/packages/design-system/src/components/N8nButton/overrides/ElButton.vue b/packages/design-system/src/components/N8nButton/overrides/ElButton.vue index 817e6d7a5566a..1cbb31b8200f3 100644 --- a/packages/design-system/src/components/N8nButton/overrides/ElButton.vue +++ b/packages/design-system/src/components/N8nButton/overrides/ElButton.vue @@ -13,6 +13,8 @@ const classToTypeMap = { 'el-picker-panel__link-btn': 'secondary', }; +type ButtonRef = InstanceType<typeof N8nButton>; + export default defineComponent({ components: { N8nButton, @@ -32,7 +34,7 @@ export default defineComponent({ } Object.entries(classToTypeMap).forEach(([className, mappedType]) => { - if (this.$refs.button && (this.$refs.button as Vue).$el.classList.contains(className)) { + if ((this.$refs.button as ButtonRef)?.$el.classList.contains(className)) { type = mappedType; } }); diff --git a/packages/design-system/src/components/N8nCallout/Callout.vue b/packages/design-system/src/components/N8nCallout/Callout.vue index 97c15296e0558..604cec6a25a42 100644 --- a/packages/design-system/src/components/N8nCallout/Callout.vue +++ b/packages/design-system/src/components/N8nCallout/Callout.vue @@ -1,7 +1,7 @@ <template> <div :class="classes" role="alert"> - <div :class="$style['message-section']"> - <div :class="$style.icon"> + <div :class="$style.messageSection"> + <div :class="$style.icon" v-if="!iconless"> <n8n-icon :icon="getIcon" :size="theme === 'secondary' ? 'medium' : 'large'" /> </div> <n8n-text size="small"> @@ -44,10 +44,21 @@ export default defineComponent({ type: String, default: 'info-circle', }, + iconless: { + type: Boolean, + }, + slim: { + type: Boolean, + }, }, computed: { classes(): string[] { - return ['n8n-callout', this.$style.callout, this.$style[this.theme]]; + return [ + 'n8n-callout', + this.$style.callout, + this.$style[this.theme], + this.slim ? this.$style.slim : '', + ]; }, getIcon(): string { if (Object.keys(CALLOUT_DEFAULT_ICONS).includes(this.theme)) { @@ -70,9 +81,14 @@ export default defineComponent({ border-radius: var(--border-radius-base); align-items: center; line-height: var(--font-line-height-loose); + + &.slim { + line-height: var(--font-line-height-loose); + padding: var(--spacing-3xs) var(--spacing-2xs); + } } -.message-section { +.messageSection { display: flex; align-items: center; } diff --git a/packages/design-system/src/components/N8nCallout/__tests__/__snapshots__/Callout.spec.ts.snap b/packages/design-system/src/components/N8nCallout/__tests__/__snapshots__/Callout.spec.ts.snap index 17f61ac2e7ae0..6141319c81259 100644 --- a/packages/design-system/src/components/N8nCallout/__tests__/__snapshots__/Callout.spec.ts.snap +++ b/packages/design-system/src/components/N8nCallout/__tests__/__snapshots__/Callout.spec.ts.snap @@ -2,7 +2,7 @@ exports[`components > N8nCallout > should render additional slots correctly 1`] = ` "<div role=\\"alert\\" class=\\"n8n-callout callout custom\\"> - <div class=\\"message-section\\"> + <div class=\\"messageSection\\"> <div class=\\"icon\\"> <n8n-icon-stub icon=\\"code-branch\\" size=\\"large\\"></n8n-icon-stub> </div> @@ -16,7 +16,7 @@ exports[`components > N8nCallout > should render additional slots correctly 1`] exports[`components > N8nCallout > should render custom theme correctly 1`] = ` "<div role=\\"alert\\" class=\\"n8n-callout callout custom\\"> - <div class=\\"message-section\\"> + <div class=\\"messageSection\\"> <div class=\\"icon\\"> <n8n-icon-stub icon=\\"code-branch\\" size=\\"large\\"></n8n-icon-stub> </div> @@ -29,7 +29,7 @@ exports[`components > N8nCallout > should render custom theme correctly 1`] = ` exports[`components > N8nCallout > should render danger theme correctly 1`] = ` "<div role=\\"alert\\" class=\\"n8n-callout callout danger\\"> - <div class=\\"message-section\\"> + <div class=\\"messageSection\\"> <div class=\\"icon\\"> <n8n-icon-stub icon=\\"times-circle\\" size=\\"large\\"></n8n-icon-stub> </div> @@ -42,7 +42,7 @@ exports[`components > N8nCallout > should render danger theme correctly 1`] = ` exports[`components > N8nCallout > should render info theme correctly 1`] = ` "<div role=\\"alert\\" class=\\"n8n-callout callout info\\"> - <div class=\\"message-section\\"> + <div class=\\"messageSection\\"> <div class=\\"icon\\"> <n8n-icon-stub icon=\\"info-circle\\" size=\\"large\\"></n8n-icon-stub> </div> @@ -55,7 +55,7 @@ exports[`components > N8nCallout > should render info theme correctly 1`] = ` exports[`components > N8nCallout > should render secondary theme correctly 1`] = ` "<div role=\\"alert\\" class=\\"n8n-callout callout secondary\\"> - <div class=\\"message-section\\"> + <div class=\\"messageSection\\"> <div class=\\"icon\\"> <n8n-icon-stub icon=\\"info-circle\\" size=\\"medium\\"></n8n-icon-stub> </div> @@ -68,7 +68,7 @@ exports[`components > N8nCallout > should render secondary theme correctly 1`] = exports[`components > N8nCallout > should render success theme correctly 1`] = ` "<div role=\\"alert\\" class=\\"n8n-callout callout success\\"> - <div class=\\"message-section\\"> + <div class=\\"messageSection\\"> <div class=\\"icon\\"> <n8n-icon-stub icon=\\"check-circle\\" size=\\"large\\"></n8n-icon-stub> </div> @@ -81,7 +81,7 @@ exports[`components > N8nCallout > should render success theme correctly 1`] = ` exports[`components > N8nCallout > should render warning theme correctly 1`] = ` "<div role=\\"alert\\" class=\\"n8n-callout callout warning\\"> - <div class=\\"message-section\\"> + <div class=\\"messageSection\\"> <div class=\\"icon\\"> <n8n-icon-stub icon=\\"exclamation-triangle\\" size=\\"large\\"></n8n-icon-stub> </div> diff --git a/packages/design-system/src/components/N8nCheckbox/Checkbox.vue b/packages/design-system/src/components/N8nCheckbox/Checkbox.vue index 28b704ecceeab..1568aea764c73 100644 --- a/packages/design-system/src/components/N8nCheckbox/Checkbox.vue +++ b/packages/design-system/src/components/N8nCheckbox/Checkbox.vue @@ -8,7 +8,9 @@ :value="value" @change="onChange" > + <slot></slot> <n8n-input-label + v-if="label" :label="label" :tooltipText="tooltipText" :bold="false" @@ -32,7 +34,6 @@ export default defineComponent({ props: { label: { type: String, - required: true, }, disabled: { type: Boolean, @@ -40,7 +41,6 @@ export default defineComponent({ }, tooltipText: { type: String, - required: false, }, indeterminate: { type: Boolean, diff --git a/packages/design-system/src/components/N8nCheckbox/__tests__/Checkbox.spec.ts b/packages/design-system/src/components/N8nCheckbox/__tests__/Checkbox.spec.ts new file mode 100644 index 0000000000000..0ec4f9166002c --- /dev/null +++ b/packages/design-system/src/components/N8nCheckbox/__tests__/Checkbox.spec.ts @@ -0,0 +1,31 @@ +import { render } from '@testing-library/vue'; +import N8nCheckbox from '../Checkbox.vue'; + +describe('components', () => { + describe('N8nCheckbox', () => { + it('should render without label and child content', () => { + const { container } = render(N8nCheckbox); + expect(container).toMatchSnapshot(); + }); + + it('should render with label', () => { + const { container } = render(N8nCheckbox, { props: { label: 'Checkbox' } }); + expect(container).toMatchSnapshot(); + }); + + it('should render with child', () => { + const { container } = render(N8nCheckbox, { + slots: { default: '<strong>Bold text</strong>' }, + }); + expect(container).toMatchSnapshot(); + }); + + it('should render with both child and label', () => { + const { container } = render(N8nCheckbox, { + props: { label: 'Checkbox' }, + slots: { default: '<strong>Bold text</strong>' }, + }); + expect(container).toMatchSnapshot(); + }); + }); +}); diff --git a/packages/design-system/src/components/N8nCheckbox/__tests__/__snapshots__/Checkbox.spec.ts.snap b/packages/design-system/src/components/N8nCheckbox/__tests__/__snapshots__/Checkbox.spec.ts.snap new file mode 100644 index 0000000000000..9a7a248acca0d --- /dev/null +++ b/packages/design-system/src/components/N8nCheckbox/__tests__/__snapshots__/Checkbox.spec.ts.snap @@ -0,0 +1,158 @@ +// Vitest Snapshot v1 + +exports[`components > N8nCheckbox > should render with both child and label 1`] = ` +<div> + <label + class="el-checkbox n8n-checkbox n8nCheckbox" + labelsize="medium" + > + <span + class="el-checkbox__input" + > + <span + class="el-checkbox__inner" + /> + <input + aria-hidden="false" + class="el-checkbox__original" + type="checkbox" + value="Checkbox" + /> + </span> + <span + class="el-checkbox__label" + > + <strong> + Bold text + </strong> + <div + class="container" + > + <label + class="n8n-input-label inputLabel heading medium" + > + <div + class="title" + > + <span + class="n8n-text size-medium regular" + > + Checkbox + <!----> + </span> + </div> + <!----> + <!----> + <!----> + </label> + </div> + <!----> + </span> + </label> +</div> +`; + +exports[`components > N8nCheckbox > should render with child 1`] = ` +<div> + <label + class="el-checkbox n8n-checkbox n8nCheckbox" + labelsize="medium" + > + <span + class="el-checkbox__input" + > + <span + class="el-checkbox__inner" + /> + <input + aria-hidden="false" + class="el-checkbox__original" + type="checkbox" + value="" + /> + </span> + <span + class="el-checkbox__label" + > + <strong> + Bold text + </strong> + <!----> + <!----> + </span> + </label> +</div> +`; + +exports[`components > N8nCheckbox > should render with label 1`] = ` +<div> + <label + class="el-checkbox n8n-checkbox n8nCheckbox" + labelsize="medium" + > + <span + class="el-checkbox__input" + > + <span + class="el-checkbox__inner" + /> + <input + aria-hidden="false" + class="el-checkbox__original" + type="checkbox" + value="Checkbox" + /> + </span> + <span + class="el-checkbox__label" + > + <div + class="container" + > + <label + class="n8n-input-label inputLabel heading medium" + > + <div + class="title" + > + <span + class="n8n-text size-medium regular" + > + Checkbox + <!----> + </span> + </div> + <!----> + <!----> + <!----> + </label> + </div> + <!----> + </span> + </label> +</div> +`; + +exports[`components > N8nCheckbox > should render without label and child content 1`] = ` +<div> + <label + class="el-checkbox n8n-checkbox n8nCheckbox" + labelsize="medium" + > + <span + class="el-checkbox__input" + > + <span + class="el-checkbox__inner" + /> + <input + aria-hidden="false" + class="el-checkbox__original" + type="checkbox" + value="" + /> + </span> + <!----> + </label> +</div> +`; diff --git a/packages/design-system/src/components/N8nDatatable/Datatable.vue b/packages/design-system/src/components/N8nDatatable/Datatable.vue index 87c8ddcd33759..990ea94a1d3c5 100644 --- a/packages/design-system/src/components/N8nDatatable/Datatable.vue +++ b/packages/design-system/src/components/N8nDatatable/Datatable.vue @@ -1,6 +1,7 @@ <script lang="ts"> -import { computed, defineComponent, PropType, ref, useCssModule } from 'vue'; -import { DatatableColumn, DatatableRow, DatatableRowDataType } from '../../types'; +import type { PropType } from 'vue'; +import { computed, defineComponent, ref, useCssModule } from 'vue'; +import type { DatatableColumn, DatatableRow, DatatableRowDataType } from '../../types'; import { getValueByPath } from '../../utils'; import { useI18n } from '../../composables'; import N8nSelect from '../N8nSelect'; diff --git a/packages/design-system/src/components/N8nDatatable/__tests__/data.ts b/packages/design-system/src/components/N8nDatatable/__tests__/data.ts index 03471152a2951..a9b2c5aa7e9a6 100644 --- a/packages/design-system/src/components/N8nDatatable/__tests__/data.ts +++ b/packages/design-system/src/components/N8nDatatable/__tests__/data.ts @@ -1,5 +1,6 @@ -import { defineComponent, h, PropType } from 'vue'; -import { DatatableRow } from '../../../types'; +import type { PropType } from 'vue'; +import { defineComponent, h } from 'vue'; +import type { DatatableRow } from '../../../types'; import N8nButton from '../../N8nButton'; // eslint-disable-next-line @typescript-eslint/naming-convention diff --git a/packages/design-system/src/components/N8nFormInput/FormInput.vue b/packages/design-system/src/components/N8nFormInput/FormInput.vue index 7030af847acbd..30622d9f6fdaf 100644 --- a/packages/design-system/src/components/N8nFormInput/FormInput.vue +++ b/packages/design-system/src/components/N8nFormInput/FormInput.vue @@ -95,7 +95,7 @@ import N8nCheckbox from '../N8nCheckbox'; import { Switch as ElSwitch } from 'element-ui'; import { getValidationError, VALIDATORS } from './validators'; -import { Rule, RuleGroup, IValidator, Validatable, FormState } from '../../types'; +import type { Rule, RuleGroup, IValidator, Validatable, FormState } from '../../types'; import { t } from '../../locale'; diff --git a/packages/design-system/src/components/N8nFormInput/validators.ts b/packages/design-system/src/components/N8nFormInput/validators.ts index 7577c139b42a2..855c3ba6a2050 100644 --- a/packages/design-system/src/components/N8nFormInput/validators.ts +++ b/packages/design-system/src/components/N8nFormInput/validators.ts @@ -1,4 +1,4 @@ -import { IValidator, RuleGroup, Validatable } from '../../types'; +import type { IValidator, RuleGroup, Validatable } from '../../types'; export const emailRegex = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; diff --git a/packages/design-system/src/components/N8nFormInputs/FormInputs.vue b/packages/design-system/src/components/N8nFormInputs/FormInputs.vue index 569051122f19c..e0809c1d39526 100644 --- a/packages/design-system/src/components/N8nFormInputs/FormInputs.vue +++ b/packages/design-system/src/components/N8nFormInputs/FormInputs.vue @@ -37,11 +37,13 @@ </template> <script lang="ts"> -import { defineComponent, PropType } from 'vue'; +import type { PropType } from 'vue'; +import { defineComponent } from 'vue'; import N8nFormInput from '../N8nFormInput'; import type { IFormInput } from '../../types'; import ResizeObserver from '../ResizeObserver'; -import { createEventBus, EventBus } from '../../utils'; +import type { EventBus } from '../../utils'; +import { createEventBus } from '../../utils'; export default defineComponent({ name: 'n8n-form-inputs', diff --git a/packages/design-system/src/components/N8nInfoAccordion/InfoAccordion.vue b/packages/design-system/src/components/N8nInfoAccordion/InfoAccordion.vue index ccf718e81f0ec..7cf5d10d9d233 100644 --- a/packages/design-system/src/components/N8nInfoAccordion/InfoAccordion.vue +++ b/packages/design-system/src/components/N8nInfoAccordion/InfoAccordion.vue @@ -41,7 +41,8 @@ <script lang="ts"> import N8nText from '../N8nText'; import N8nIcon from '../N8nIcon'; -import { defineComponent, PropType } from 'vue'; +import type { PropType } from 'vue'; +import { defineComponent } from 'vue'; export interface IAccordionItem { id: string; diff --git a/packages/design-system/src/components/N8nInput/Input.vue b/packages/design-system/src/components/N8nInput/Input.vue index 3408ea14ae9ed..68e617d590f16 100644 --- a/packages/design-system/src/components/N8nInput/Input.vue +++ b/packages/design-system/src/components/N8nInput/Input.vue @@ -27,6 +27,8 @@ import { Input as ElInput } from 'element-ui'; import { defineComponent } from 'vue'; +type InputRef = InstanceType<typeof ElInput>; + export default defineComponent({ name: 'n8n-input', components: { @@ -92,7 +94,7 @@ export default defineComponent({ }, methods: { focus() { - const innerInput = this.$refs.innerInput as Vue | undefined; + const innerInput = this.$refs.innerInput as InputRef | undefined; if (!innerInput) return; @@ -105,7 +107,7 @@ export default defineComponent({ inputElement.focus(); }, blur() { - const innerInput = this.$refs.innerInput as Vue | undefined; + const innerInput = this.$refs.innerInput as InputRef | undefined; if (!innerInput) return; @@ -118,7 +120,7 @@ export default defineComponent({ inputElement.blur(); }, select() { - const innerInput = this.$refs.innerInput as Vue | undefined; + const innerInput = this.$refs.innerInput as InputRef | undefined; if (!innerInput) return; diff --git a/packages/design-system/src/components/N8nMarkdown/Markdown.vue b/packages/design-system/src/components/N8nMarkdown/Markdown.vue index 2f157d6185fe6..d35f1ee575784 100644 --- a/packages/design-system/src/components/N8nMarkdown/Markdown.vue +++ b/packages/design-system/src/components/N8nMarkdown/Markdown.vue @@ -19,13 +19,15 @@ <script lang="ts"> import N8nLoading from '../N8nLoading'; -import Markdown, { PluginSimple } from 'markdown-it'; +import type { PluginSimple } from 'markdown-it'; +import Markdown from 'markdown-it'; import markdownLink from 'markdown-it-link-attributes'; import markdownEmoji from 'markdown-it-emoji'; import markdownTasklists from 'markdown-it-task-lists'; -import { defineComponent, PropType } from 'vue'; +import type { PropType } from 'vue'; +import { defineComponent } from 'vue'; import xss, { friendlyAttrValue } from 'xss'; import { escapeMarkdown } from '../../utils/markdown'; diff --git a/packages/design-system/src/components/N8nMenu/Menu.vue b/packages/design-system/src/components/N8nMenu/Menu.vue index 868a3c050d32f..66e9d61bb46bd 100644 --- a/packages/design-system/src/components/N8nMenu/Menu.vue +++ b/packages/design-system/src/components/N8nMenu/Menu.vue @@ -54,7 +54,8 @@ <script lang="ts"> import { Menu as ElMenu } from 'element-ui'; import N8nMenuItem from '../N8nMenuItem'; -import { defineComponent, PropType } from 'vue'; +import type { PropType } from 'vue'; +import { defineComponent } from 'vue'; import type { IMenuItem, RouteObject } from '../../types'; export default defineComponent({ diff --git a/packages/design-system/src/components/N8nMenuItem/MenuItem.vue b/packages/design-system/src/components/N8nMenuItem/MenuItem.vue index 4094404c10193..3dbb7b327e0af 100644 --- a/packages/design-system/src/components/N8nMenuItem/MenuItem.vue +++ b/packages/design-system/src/components/N8nMenuItem/MenuItem.vue @@ -74,7 +74,8 @@ import { Submenu as ElSubmenu, MenuItem as ElMenuItem } from 'element-ui'; import N8nTooltip from '../N8nTooltip'; import N8nIcon from '../N8nIcon'; -import { defineComponent, PropType } from 'vue'; +import type { PropType } from 'vue'; +import { defineComponent } from 'vue'; import type { IMenuItem, RouteObject } from '../../types'; export default defineComponent({ diff --git a/packages/design-system/src/components/N8nNodeCreatorNode/NodeCreatorNode.stories.ts b/packages/design-system/src/components/N8nNodeCreatorNode/NodeCreatorNode.stories.ts index 77156838bcf42..bcd38ce4510ad 100644 --- a/packages/design-system/src/components/N8nNodeCreatorNode/NodeCreatorNode.stories.ts +++ b/packages/design-system/src/components/N8nNodeCreatorNode/NodeCreatorNode.stories.ts @@ -1,5 +1,5 @@ import N8nNodeCreatorNode from './NodeCreatorNode.vue'; -import { StoryFn } from '@storybook/vue'; +import type { StoryFn } from '@storybook/vue'; export default { title: 'Modules/Node Creator Node', diff --git a/packages/design-system/src/components/N8nNodeCreatorNode/NodeCreatorNode.vue b/packages/design-system/src/components/N8nNodeCreatorNode/NodeCreatorNode.vue index ebac11ac37641..433eb7de125e6 100644 --- a/packages/design-system/src/components/N8nNodeCreatorNode/NodeCreatorNode.vue +++ b/packages/design-system/src/components/N8nNodeCreatorNode/NodeCreatorNode.vue @@ -92,16 +92,11 @@ defineEmits<{ } .nodeIcon { display: flex; - margin-right: var(--spacing-s); - - & > :global(*) { - min-width: 25px; - max-width: 25px; - } + margin-right: var(--node-icon-margin-right, var(--spacing-s)); } .name { - font-weight: var(--font-weight-bold); - font-size: var(--font-size-s); + font-weight: var(--node-creator-name-weight, var(--font-weight-bold)); + font-size: var(--node-creator-name-size, var(--font-size-s)); line-height: 1.115rem; } .description { diff --git a/packages/design-system/src/components/N8nNodeIcon/NodeIcon.vue b/packages/design-system/src/components/N8nNodeIcon/NodeIcon.vue index dd17c437a562b..1e36d9914c564 100644 --- a/packages/design-system/src/components/N8nNodeIcon/NodeIcon.vue +++ b/packages/design-system/src/components/N8nNodeIcon/NodeIcon.vue @@ -14,7 +14,7 @@ <template #content>{{ nodeTypeName }}</template> <div v-if="type !== 'unknown'" :class="$style.icon"> <img v-if="type === 'file'" :src="src" :class="$style.nodeIconImage" /> - <font-awesome-icon v-else :icon="name" :style="fontStyleData" /> + <font-awesome-icon v-else :icon="name" :class="$style.iconFa" :style="fontStyleData" /> </div> <div v-else :class="$style.nodeIconPlaceholder"> {{ nodeTypeName ? nodeTypeName.charAt(0) : '?' }} @@ -127,6 +127,11 @@ export default defineComponent({ justify-content: center; align-items: center; pointer-events: none; + + svg { + max-width: 100%; + max-height: 100%; + } } .nodeIconPlaceholder { text-align: center; diff --git a/packages/design-system/src/components/N8nPagination/Pagination.vue b/packages/design-system/src/components/N8nPagination/Pagination.vue index e6968b5c21a0b..5b0f106370005 100644 --- a/packages/design-system/src/components/N8nPagination/Pagination.vue +++ b/packages/design-system/src/components/N8nPagination/Pagination.vue @@ -1,5 +1,6 @@ <script lang="ts"> -import { DefineComponent, defineComponent } from 'vue'; +import type { DefineComponent } from 'vue'; +import { defineComponent } from 'vue'; import { Pagination as ElPagination } from 'element-ui'; export default defineComponent({ diff --git a/packages/design-system/src/components/N8nRadioButtons/RadioButtons.vue b/packages/design-system/src/components/N8nRadioButtons/RadioButtons.vue index 9fc3bfc92e9a1..1972d4fd600ea 100644 --- a/packages/design-system/src/components/N8nRadioButtons/RadioButtons.vue +++ b/packages/design-system/src/components/N8nRadioButtons/RadioButtons.vue @@ -18,7 +18,8 @@ <script lang="ts"> import RadioButton from './RadioButton.vue'; -import { defineComponent, PropType } from 'vue'; +import type { PropType } from 'vue'; +import { defineComponent } from 'vue'; export interface RadioOption { label: string; diff --git a/packages/design-system/src/components/N8nRecycleScroller/RecycleScroller.stories.ts b/packages/design-system/src/components/N8nRecycleScroller/RecycleScroller.stories.ts index d1b1ed0e0bef2..7e75a5d51a60f 100644 --- a/packages/design-system/src/components/N8nRecycleScroller/RecycleScroller.stories.ts +++ b/packages/design-system/src/components/N8nRecycleScroller/RecycleScroller.stories.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment */ import type { StoryFn } from '@storybook/vue'; import N8nRecycleScroller from './RecycleScroller.vue'; -import { ComponentInstance } from 'vue'; +import type { ComponentInstance } from 'vue'; export default { title: 'Atoms/RecycleScroller', diff --git a/packages/design-system/src/components/N8nRecycleScroller/RecycleScroller.vue b/packages/design-system/src/components/N8nRecycleScroller/RecycleScroller.vue index 408f7db5e51b6..4c4a1073aee44 100644 --- a/packages/design-system/src/components/N8nRecycleScroller/RecycleScroller.vue +++ b/packages/design-system/src/components/N8nRecycleScroller/RecycleScroller.vue @@ -1,17 +1,8 @@ <script lang="ts"> /* eslint-disable @typescript-eslint/no-use-before-define */ -import { - computed, - defineComponent, - onMounted, - onBeforeMount, - ref, - PropType, - nextTick, - watch, - ComponentPublicInstance, -} from 'vue'; +import type { PropType, ComponentPublicInstance } from 'vue'; +import { computed, defineComponent, onMounted, onBeforeMount, ref, nextTick, watch } from 'vue'; export default defineComponent({ name: 'n8n-recycle-scroller', diff --git a/packages/design-system/src/components/N8nResizeWrapper/ResizeWrapper.vue b/packages/design-system/src/components/N8nResizeWrapper/ResizeWrapper.vue index fec202b33393c..930e4eb608d2f 100644 --- a/packages/design-system/src/components/N8nResizeWrapper/ResizeWrapper.vue +++ b/packages/design-system/src/components/N8nResizeWrapper/ResizeWrapper.vue @@ -13,7 +13,8 @@ <script lang="ts"> /* eslint-disable @typescript-eslint/unbound-method */ -import { defineComponent, PropType } from 'vue'; +import type { PropType } from 'vue'; +import { defineComponent } from 'vue'; function closestNumber(value: number, divisor: number): number { const q = value / divisor; diff --git a/packages/design-system/src/components/N8nSelect/Select.vue b/packages/design-system/src/components/N8nSelect/Select.vue index 856c4a9d986b9..7ea6eeb799c71 100644 --- a/packages/design-system/src/components/N8nSelect/Select.vue +++ b/packages/design-system/src/components/N8nSelect/Select.vue @@ -35,6 +35,8 @@ import { Select as ElSelect } from 'element-ui'; import { defineComponent } from 'vue'; +type InnerSelectRef = InstanceType<typeof ElSelect>; + export interface IProps { size?: string; limitPopperWidth?: string; @@ -117,23 +119,23 @@ export default defineComponent({ }, methods: { focus() { - const select = this.$refs.innerSelect as (Vue & HTMLElement) | undefined; - if (select) { - select.focus(); + const selectRef = this.$refs.innerSelect as InnerSelectRef | undefined; + if (selectRef) { + selectRef.focus(); } }, blur() { - const select = this.$refs.innerSelect as (Vue & HTMLElement) | undefined; - if (select) { - select.blur(); + const selectRef = this.$refs.innerSelect as InnerSelectRef | undefined; + if (selectRef) { + selectRef.blur(); } }, focusOnInput() { - const select = this.$refs.innerSelect as (Vue & HTMLElement) | undefined; - if (select) { - const input = select.$refs.input as (Vue & HTMLElement) | undefined; - if (input) { - input.focus(); + const selectRef = this.$refs.innerSelect as InnerSelectRef | undefined; + if (selectRef) { + const inputRef = selectRef.$refs.input as HTMLInputElement | undefined; + if (inputRef) { + inputRef.focus(); } } }, diff --git a/packages/design-system/src/components/N8nTabs/Tabs.vue b/packages/design-system/src/components/N8nTabs/Tabs.vue index afbffc7dadbb2..8ce5387be8da8 100644 --- a/packages/design-system/src/components/N8nTabs/Tabs.vue +++ b/packages/design-system/src/components/N8nTabs/Tabs.vue @@ -47,7 +47,8 @@ </template> <script lang="ts"> -import { defineComponent, PropType } from 'vue'; +import type { PropType } from 'vue'; +import { defineComponent } from 'vue'; import N8nIcon from '../N8nIcon'; export interface N8nTabOptions { diff --git a/packages/design-system/src/components/N8nTags/Tags.vue b/packages/design-system/src/components/N8nTags/Tags.vue index 63c982106e75e..40e668f3aebba 100644 --- a/packages/design-system/src/components/N8nTags/Tags.vue +++ b/packages/design-system/src/components/N8nTags/Tags.vue @@ -22,7 +22,8 @@ import N8nTag from '../N8nTag'; import N8nLink from '../N8nLink'; import Locale from '../../mixins/locale'; -import { defineComponent, PropType } from 'vue'; +import type { PropType } from 'vue'; +import { defineComponent } from 'vue'; export interface ITag { id: string; diff --git a/packages/design-system/src/components/N8nTooltip/Tooltip.vue b/packages/design-system/src/components/N8nTooltip/Tooltip.vue index dbe2f8113d276..f9bb068ebdc1c 100644 --- a/packages/design-system/src/components/N8nTooltip/Tooltip.vue +++ b/packages/design-system/src/components/N8nTooltip/Tooltip.vue @@ -20,7 +20,8 @@ </template> <script lang="ts"> -import { defineComponent, PropType } from 'vue'; +import type { PropType } from 'vue'; +import { defineComponent } from 'vue'; import { Tooltip as ElTooltip } from 'element-ui'; import type { IN8nButton } from '@/types'; import N8nButton from '../N8nButton'; diff --git a/packages/design-system/src/components/N8nTree/Tree.vue b/packages/design-system/src/components/N8nTree/Tree.vue index 3b22eb55f2a89..80a11b6670865 100644 --- a/packages/design-system/src/components/N8nTree/Tree.vue +++ b/packages/design-system/src/components/N8nTree/Tree.vue @@ -37,7 +37,8 @@ </template> <script lang="ts"> -import { defineComponent, PropType } from 'vue'; +import type { PropType } from 'vue'; +import { defineComponent } from 'vue'; export default defineComponent({ name: 'n8n-tree', diff --git a/packages/design-system/src/components/N8nUserSelect/UserSelect.vue b/packages/design-system/src/components/N8nUserSelect/UserSelect.vue index 815d00721747b..d4c8eec96d34d 100644 --- a/packages/design-system/src/components/N8nUserSelect/UserSelect.vue +++ b/packages/design-system/src/components/N8nUserSelect/UserSelect.vue @@ -33,10 +33,11 @@ import N8nUserInfo from '../N8nUserInfo'; import N8nSelect from '../N8nSelect'; import N8nOption from '../N8nOption'; -import { IUser } from '../../types'; +import type { IUser } from '../../types'; import Locale from '../../mixins/locale'; import { t } from '../../locale'; -import { defineComponent, PropType } from 'vue'; +import type { PropType } from 'vue'; +import { defineComponent } from 'vue'; export default defineComponent({ name: 'n8n-user-select', diff --git a/packages/design-system/src/components/N8nUsersList/UsersList.stories.ts b/packages/design-system/src/components/N8nUsersList/UsersList.stories.ts index c83799055718a..fb3348e7288d6 100644 --- a/packages/design-system/src/components/N8nUsersList/UsersList.stories.ts +++ b/packages/design-system/src/components/N8nUsersList/UsersList.stories.ts @@ -1,7 +1,7 @@ import N8nUsersList from './UsersList.vue'; import { action } from '@storybook/addon-actions'; import type { StoryFn } from '@storybook/vue'; -import { IUser } from '@/types'; +import type { IUser } from '@/types'; export default { title: 'Modules/UsersList', diff --git a/packages/design-system/src/components/N8nUsersList/UsersList.vue b/packages/design-system/src/components/N8nUsersList/UsersList.vue index 52275e05f1205..9ea1075c894fd 100644 --- a/packages/design-system/src/components/N8nUsersList/UsersList.vue +++ b/packages/design-system/src/components/N8nUsersList/UsersList.vue @@ -37,7 +37,8 @@ import N8nActionToggle from '../N8nActionToggle'; import N8nBadge from '../N8nBadge'; import N8nUserInfo from '../N8nUserInfo'; import Locale from '../../mixins/locale'; -import { defineComponent, PropType } from 'vue'; +import type { PropType } from 'vue'; +import { defineComponent } from 'vue'; export default defineComponent({ name: 'n8n-users-list', diff --git a/packages/design-system/src/components/index.ts b/packages/design-system/src/components/index.ts new file mode 100644 index 0000000000000..32fdfd9e0cf7c --- /dev/null +++ b/packages/design-system/src/components/index.ts @@ -0,0 +1,50 @@ +export { default as N8nActionBox } from './N8nActionBox'; +export { default as N8nActionDropdown } from './N8nActionDropdown'; +export { default as N8nActionToggle } from './N8nActionToggle'; +export { default as N8nAlert } from './N8nAlert'; +export { default as N8nAvatar } from './N8nAvatar'; +export { default as N8nBadge } from './N8nBadge'; +export { default as N8nBlockUi } from './N8nBlockUi'; +export { default as N8nButton } from './N8nButton'; +export { N8nElButton } from './N8nButton/overrides'; +export { default as N8nCallout } from './N8nCallout'; +export { default as N8nCard } from './N8nCard'; +export { default as N8nDatatable } from './N8nDatatable'; +export { default as N8nFormBox } from './N8nFormBox'; +export { default as N8nFormInputs } from './N8nFormInputs'; +export { default as N8nFormInput } from './N8nFormInput'; +export { default as N8nHeading } from './N8nHeading'; +export { default as N8nIcon } from './N8nIcon'; +export { default as N8nIconButton } from './N8nIconButton'; +export { default as N8nInfoAccordion } from './N8nInfoAccordion'; +export { default as N8nInfoTip } from './N8nInfoTip'; +export { default as N8nInput } from './N8nInput'; +export { default as N8nInputLabel } from './N8nInputLabel'; +export { default as N8nInputNumber } from './N8nInputNumber'; +export { default as N8nLink } from './N8nLink'; +export { default as N8nLoading } from './N8nLoading'; +export { default as N8nMarkdown } from './N8nMarkdown'; +export { default as N8nMenu } from './N8nMenu'; +export { default as N8nMenuItem } from './N8nMenuItem'; +export { default as N8nNodeCreatorNode } from './N8nNodeCreatorNode'; +export { default as N8nNodeIcon } from './N8nNodeIcon'; +export { default as N8nNotice } from './N8nNotice'; +export { default as N8nOption } from './N8nOption'; +export { default as N8nPopover } from './N8nPopover'; +export { default as N8nPulse } from './N8nPulse'; +export { default as N8nRadioButtons } from './N8nRadioButtons'; +export { default as N8nSelect } from './N8nSelect'; +export { default as N8nSpinner } from './N8nSpinner'; +export { default as N8nSticky } from './N8nSticky'; +export { default as N8nTabs } from './N8nTabs'; +export { default as N8nTag } from './N8nTag'; +export { default as N8nTags } from './N8nTags'; +export { default as N8nText } from './N8nText'; +export { default as N8nTooltip } from './N8nTooltip'; +export { default as N8nTree } from './N8nTree'; +export { default as N8nUserInfo } from './N8nUserInfo'; +export { default as N8nUserSelect } from './N8nUserSelect'; +export { default as N8nUsersList } from './N8nUsersList'; +export { default as N8nResizeWrapper } from './N8nResizeWrapper'; +export { default as N8nRecycleScroller } from './N8nRecycleScroller'; +export { default as N8nCheckbox } from './N8nCheckbox'; diff --git a/packages/design-system/src/css/_tokens.dark.scss b/packages/design-system/src/css/_tokens.dark.scss index 8efd08810688e..671eeead71bfd 100644 --- a/packages/design-system/src/css/_tokens.dark.scss +++ b/packages/design-system/src/css/_tokens.dark.scss @@ -276,6 +276,7 @@ --color-json-highlight: #dcdfea; --color-code-background: #222020; + --color-code-background-readonly: #323230; --color-code-foreground: #f8f8f2; --color-code-caret: #f8f8f0; --color-code-selection: #312b25; diff --git a/packages/design-system/src/css/_tokens.scss b/packages/design-system/src/css/_tokens.scss index 944bb55f54da8..1e8762c851358 100644 --- a/packages/design-system/src/css/_tokens.scss +++ b/packages/design-system/src/css/_tokens.scss @@ -377,6 +377,7 @@ ); --color-code-background: #ffffff; + --color-code-background-readonly: #f5f2f0; --color-code-foreground: #4d4d4c; --color-code-caret: #aeafad; --color-code-selection: #d6d6d6; diff --git a/packages/design-system/src/docs/Introduction.stories.mdx b/packages/design-system/src/docs/Introduction.stories.mdx index 961486cf9ed7c..b02392c3b5710 100644 --- a/packages/design-system/src/docs/Introduction.stories.mdx +++ b/packages/design-system/src/docs/Introduction.stories.mdx @@ -1,3 +1,5 @@ +import { Meta } from '@storybook/addon-docs'; + <Meta title="Docs/Introduction" /> # Welcome to n8n Storybook diff --git a/packages/design-system/src/main.ts b/packages/design-system/src/main.ts index 65ae159c2ca85..cf75163855806 100644 --- a/packages/design-system/src/main.ts +++ b/packages/design-system/src/main.ts @@ -1,6 +1,7 @@ import * as locale from './locale'; -import designSystemComponents from './plugins/n8nComponents'; +export * from './components'; +export * from './plugin'; export * from './types'; export * from './utils'; -export { locale, designSystemComponents }; +export { locale }; diff --git a/packages/design-system/src/plugin.ts b/packages/design-system/src/plugin.ts new file mode 100644 index 0000000000000..71d7ff4df772d --- /dev/null +++ b/packages/design-system/src/plugin.ts @@ -0,0 +1,109 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import type { PluginObject } from 'vue'; +import { + N8nActionBox, + N8nActionDropdown, + N8nActionToggle, + N8nAlert, + N8nAvatar, + N8nBadge, + N8nBlockUi, + N8nButton, + N8nElButton, + N8nCallout, + N8nCard, + N8nDatatable, + N8nFormBox, + N8nFormInputs, + N8nFormInput, + N8nHeading, + N8nIcon, + N8nIconButton, + N8nInfoAccordion, + N8nInfoTip, + N8nInput, + N8nInputLabel, + N8nInputNumber, + N8nLink, + N8nLoading, + N8nMarkdown, + N8nMenu, + N8nMenuItem, + N8nNodeCreatorNode, + N8nNodeIcon, + N8nNotice, + N8nOption, + N8nPopover, + N8nPulse, + N8nRadioButtons, + N8nSelect, + N8nSpinner, + N8nSticky, + N8nTabs, + N8nTag, + N8nTags, + N8nText, + N8nTooltip, + N8nTree, + N8nUserInfo, + N8nUserSelect, + N8nUsersList, + N8nResizeWrapper, + N8nRecycleScroller, + N8nCheckbox, +} from './components'; + +export const N8nPlugin: PluginObject<{}> = { + install: (app) => { + app.component('n8n-info-accordion', N8nInfoAccordion); + app.component('n8n-action-box', N8nActionBox); + app.component('n8n-action-dropdown', N8nActionDropdown); + app.component('n8n-action-toggle', N8nActionToggle); + app.component('n8n-alert', N8nAlert); + app.component('n8n-avatar', N8nAvatar); + app.component('n8n-badge', N8nBadge); + app.component('n8n-block-ui', N8nBlockUi); + app.component('n8n-button', N8nButton); + app.component('el-button', N8nElButton); + app.component('n8n-callout', N8nCallout); + app.component('n8n-card', N8nCard); + app.component('n8n-datatable', N8nDatatable); + app.component('n8n-form-box', N8nFormBox); + app.component('n8n-form-inputs', N8nFormInputs); + app.component('n8n-form-input', N8nFormInput); + app.component('n8n-icon', N8nIcon); + app.component('n8n-icon-button', N8nIconButton); + app.component('n8n-info-tip', N8nInfoTip); + app.component('n8n-input', N8nInput); + app.component('n8n-input-label', N8nInputLabel); + app.component('n8n-input-number', N8nInputNumber); + app.component('n8n-loading', N8nLoading); + app.component('n8n-heading', N8nHeading); + app.component('n8n-link', N8nLink); + app.component('n8n-markdown', N8nMarkdown); + app.component('n8n-menu', N8nMenu); + app.component('n8n-menu-item', N8nMenuItem); + app.component('n8n-node-creator-node', N8nNodeCreatorNode); + app.component('n8n-node-icon', N8nNodeIcon); + app.component('n8n-notice', N8nNotice); + app.component('n8n-option', N8nOption); + app.component('n8n-popover', N8nPopover); + app.component('n8n-pulse', N8nPulse); + app.component('n8n-select', N8nSelect); + app.component('n8n-spinner', N8nSpinner); + app.component('n8n-sticky', N8nSticky); + app.component('n8n-radio-buttons', N8nRadioButtons); + app.component('n8n-tags', N8nTags); + app.component('n8n-tabs', N8nTabs); + app.component('n8n-tag', N8nTag); + app.component('n8n-text', N8nText); + app.component('n8n-tooltip', N8nTooltip); + app.component('n8n-user-info', N8nUserInfo); + app.component('n8n-tree', N8nTree); + app.component('n8n-users-list', N8nUsersList); + app.component('n8n-user-select', N8nUserSelect); + app.component('n8n-resize-wrapper', N8nResizeWrapper); + app.component('n8n-recycle-scroller', N8nRecycleScroller); + app.component('n8n-checkbox', N8nCheckbox); + }, +}; diff --git a/packages/design-system/src/plugins/n8nComponents.ts b/packages/design-system/src/plugins/n8nComponents.ts deleted file mode 100644 index 6c182b53f2cba..0000000000000 --- a/packages/design-system/src/plugins/n8nComponents.ts +++ /dev/null @@ -1,106 +0,0 @@ -import type { PluginObject } from 'vue'; -import N8nActionBox from '../components/N8nActionBox'; -import N8nActionDropdown from '../components/N8nActionDropdown'; -import N8nActionToggle from '../components/N8nActionToggle'; -import N8nAlert from '../components/N8nAlert'; -import N8nAvatar from '../components/N8nAvatar'; -import N8nBadge from '../components/N8nBadge'; -import N8nBlockUi from '../components/N8nBlockUi'; -import N8nButton from '../components/N8nButton'; -import { N8nElButton } from '../components/N8nButton/overrides'; -import N8nCallout from '../components/N8nCallout'; -import N8nCard from '../components/N8nCard'; -import N8nDatatable from '../components/N8nDatatable'; -import N8nFormBox from '../components/N8nFormBox'; -import N8nFormInputs from '../components/N8nFormInputs'; -import N8nFormInput from '../components/N8nFormInput'; -import N8nHeading from '../components/N8nHeading'; -import N8nIcon from '../components/N8nIcon'; -import N8nIconButton from '../components/N8nIconButton'; -import N8nInfoAccordion from '../components/N8nInfoAccordion'; -import N8nInfoTip from '../components/N8nInfoTip'; -import { default as N8nInput } from '../components/N8nInput'; -import N8nInputLabel from '../components/N8nInputLabel'; -import N8nInputNumber from '../components/N8nInputNumber'; -import N8nLink from '../components/N8nLink'; -import N8nLoading from '../components/N8nLoading'; -import N8nMarkdown from '../components/N8nMarkdown'; -import N8nMenu from '../components/N8nMenu'; -import N8nMenuItem from '../components/N8nMenuItem'; -import N8nNodeCreatorNode from '../components/N8nNodeCreatorNode'; -import N8nNodeIcon from '../components/N8nNodeIcon'; -import N8nNotice from '../components/N8nNotice'; -import N8nOption from '../components/N8nOption'; -import N8nPopover from '../components/N8nPopover'; -import N8nPulse from '../components/N8nPulse'; -import N8nRadioButtons from '../components/N8nRadioButtons'; -import N8nSelect from '../components/N8nSelect'; -import N8nSpinner from '../components/N8nSpinner'; -import N8nSticky from '../components/N8nSticky'; -import N8nTabs from '../components/N8nTabs'; -import N8nTag from '../components/N8nTag'; -import N8nTags from '../components/N8nTags'; -import N8nText from '../components/N8nText'; -import N8nTooltip from '../components/N8nTooltip'; -import N8nTree from '../components/N8nTree'; -import N8nUserInfo from '../components/N8nUserInfo'; -import N8nUserSelect from '../components/N8nUserSelect'; -import N8nUsersList from '../components/N8nUsersList'; -import N8nResizeWrapper from '../components/N8nResizeWrapper'; -import N8nRecycleScroller from '../components/N8nRecycleScroller'; - -const n8nComponentsPlugin: PluginObject<{}> = { - install: (app) => { - app.component('n8n-info-accordion', N8nInfoAccordion); - app.component('n8n-action-box', N8nActionBox); - app.component('n8n-action-dropdown', N8nActionDropdown); - app.component('n8n-action-toggle', N8nActionToggle); - app.component('n8n-alert', N8nAlert); - app.component('n8n-avatar', N8nAvatar); - app.component('n8n-badge', N8nBadge); - app.component('n8n-block-ui', N8nBlockUi); - app.component('n8n-button', N8nButton); - app.component('el-button', N8nElButton); - app.component('n8n-callout', N8nCallout); - app.component('n8n-card', N8nCard); - app.component('n8n-datatable', N8nDatatable); - app.component('n8n-form-box', N8nFormBox); - app.component('n8n-form-inputs', N8nFormInputs); - app.component('n8n-form-input', N8nFormInput); - app.component('n8n-icon', N8nIcon); - app.component('n8n-icon-button', N8nIconButton); - app.component('n8n-info-tip', N8nInfoTip); - app.component('n8n-input', N8nInput); - app.component('n8n-input-label', N8nInputLabel); - app.component('n8n-input-number', N8nInputNumber); - app.component('n8n-loading', N8nLoading); - app.component('n8n-heading', N8nHeading); - app.component('n8n-link', N8nLink); - app.component('n8n-markdown', N8nMarkdown); - app.component('n8n-menu', N8nMenu); - app.component('n8n-menu-item', N8nMenuItem); - app.component('n8n-node-creator-node', N8nNodeCreatorNode); - app.component('n8n-node-icon', N8nNodeIcon); - app.component('n8n-notice', N8nNotice); - app.component('n8n-option', N8nOption); - app.component('n8n-popover', N8nPopover); - app.component('n8n-pulse', N8nPulse); - app.component('n8n-select', N8nSelect); - app.component('n8n-spinner', N8nSpinner); - app.component('n8n-sticky', N8nSticky); - app.component('n8n-radio-buttons', N8nRadioButtons); - app.component('n8n-tags', N8nTags); - app.component('n8n-tabs', N8nTabs); - app.component('n8n-tag', N8nTag); - app.component('n8n-text', N8nText); - app.component('n8n-tooltip', N8nTooltip); - app.component('n8n-user-info', N8nUserInfo); - app.component('n8n-tree', N8nTree); - app.component('n8n-users-list', N8nUsersList); - app.component('n8n-user-select', N8nUserSelect); - app.component('n8n-resize-wrapper', N8nResizeWrapper); - app.component('n8n-recycle-scroller', N8nRecycleScroller); - }, -}; - -export default n8nComponentsPlugin; diff --git a/packages/design-system/src/styleguide/ColorCircles.vue b/packages/design-system/src/styleguide/ColorCircles.vue index dd6ef8e17daee..763bc24b1d763 100644 --- a/packages/design-system/src/styleguide/ColorCircles.vue +++ b/packages/design-system/src/styleguide/ColorCircles.vue @@ -10,7 +10,8 @@ </template> <script lang="ts"> -import { defineComponent, PropType } from 'vue'; +import type { PropType } from 'vue'; +import { defineComponent } from 'vue'; function hslToHex(h: number, s: number, l: number): string { l /= 100; diff --git a/packages/design-system/src/styleguide/Sizes.vue b/packages/design-system/src/styleguide/Sizes.vue index f24f4346505a7..dd5316c6799de 100644 --- a/packages/design-system/src/styleguide/Sizes.vue +++ b/packages/design-system/src/styleguide/Sizes.vue @@ -18,7 +18,8 @@ </template> <script lang="ts"> -import { defineComponent, PropType } from 'vue'; +import type { PropType } from 'vue'; +import { defineComponent } from 'vue'; export default defineComponent({ name: 'sizes', diff --git a/packages/design-system/src/styleguide/VariableTable.vue b/packages/design-system/src/styleguide/VariableTable.vue index fb02a3b0f09a2..9d26337900f68 100644 --- a/packages/design-system/src/styleguide/VariableTable.vue +++ b/packages/design-system/src/styleguide/VariableTable.vue @@ -16,7 +16,8 @@ </template> <script lang="ts"> -import { defineComponent, PropType } from 'vue'; +import type { PropType } from 'vue'; +import { defineComponent } from 'vue'; export default defineComponent({ name: 'variable-table', diff --git a/packages/design-system/src/types/datatable.ts b/packages/design-system/src/types/datatable.ts index aa568483424cd..8a31c2b1d7079 100644 --- a/packages/design-system/src/types/datatable.ts +++ b/packages/design-system/src/types/datatable.ts @@ -1,4 +1,4 @@ -import { VNode } from 'vue'; +import type { VNode } from 'vue'; export type DatatableRowDataType = string | number | boolean | null | undefined; diff --git a/packages/editor-ui/package.json b/packages/editor-ui/package.json index 90a2bde2060e3..a2ce0bd8ecca1 100644 --- a/packages/editor-ui/package.json +++ b/packages/editor-ui/package.json @@ -1,6 +1,6 @@ { "name": "n8n-editor-ui", - "version": "0.191.0", + "version": "0.193.0", "description": "Workflow Editor UI for n8n", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", @@ -29,6 +29,9 @@ "@codemirror/autocomplete": "^6.4.0", "@codemirror/commands": "^6.1.0", "@codemirror/lang-javascript": "^6.1.2", + "@codemirror/lang-json": "^6.0.1", + "@codemirror/lang-python": "^6.1.2", + "@codemirror/lang-sql": "^6.4.1", "@codemirror/language": "^6.2.1", "@codemirror/lint": "^6.0.0", "@codemirror/state": "^6.1.4", @@ -37,7 +40,7 @@ "@fortawesome/fontawesome-svg-core": "^1.2.35", "@fortawesome/free-regular-svg-icons": "^6.1.1", "@fortawesome/free-solid-svg-icons": "^5.15.3", - "@fortawesome/vue-fontawesome": "^2.0.2", + "@fortawesome/vue-fontawesome": "^2.0.10", "@jsplumb/browser-ui": "^5.13.2", "@jsplumb/common": "^5.13.2", "@jsplumb/connector-bezier": "^5.13.2", @@ -58,13 +61,11 @@ "jsonpath": "^1.1.1", "lodash-es": "^4.17.21", "luxon": "^3.3.0", - "monaco-editor": "^0.33.0", "n8n-design-system": "workspace:*", "n8n-workflow": "workspace:*", "normalize-wheel": "^1.0.1", "pinia": "^2.0.22", "prettier": "^2.8.3", - "prismjs": "^1.17.1", "stream-browserify": "^3.0.0", "timeago.js": "^4.0.2", "uuid": "^8.3.2", @@ -75,7 +76,6 @@ "vue-i18n": "^8.26.7", "vue-infinite-loading": "^2.4.5", "vue-json-pretty": "1.9.3", - "vue-prism-editor": "^0.3.0", "vue-router": "^3.6.5", "vue-template-compiler": "^2.7.14", "vue-typed-mixins": "^0.2.0", @@ -106,6 +106,7 @@ "@vitejs/plugin-legacy": "^3.0.1", "@vitejs/plugin-vue2": "^2.2.0", "@vitest/coverage-c8": "^0.28.5", + "@volar-plugins/eslint": "^0.0.4", "c8": "^7.12.0", "jshint": "^2.9.7", "miragejs": "^0.1.47", @@ -113,7 +114,6 @@ "sass-loader": "^10.1.1", "string-template-parser": "^1.2.6", "vite": "4.0.4", - "vite-plugin-monaco-editor": "^1.0.10", "vitest": "^0.28.5", "vue-tsc": "^1.0.24" } diff --git a/packages/editor-ui/src/App.vue b/packages/editor-ui/src/App.vue index b5baefe0673f6..2498681e81d66 100644 --- a/packages/editor-ui/src/App.vue +++ b/packages/editor-ui/src/App.vue @@ -37,7 +37,6 @@ import { showMessage } from '@/mixins/showMessage'; import { userHelpers } from '@/mixins/userHelpers'; import { loadLanguage } from './plugins/i18n'; import useGlobalLinkActions from '@/composables/useGlobalLinkActions'; -import { restApi } from '@/mixins/restApi'; import { mapStores } from 'pinia'; import { useUIStore } from './stores/ui'; import { useSettingsStore } from './stores/settings'; @@ -48,8 +47,9 @@ import { useNodeTypesStore } from './stores/nodeTypes'; import { useHistoryHelper } from '@/composables/useHistoryHelper'; import { newVersions } from '@/mixins/newVersions'; import { useRoute } from 'vue-router/composables'; +import { useVersionControlStore } from '@/stores/versionControl'; -export default mixins(newVersions, showMessage, userHelpers, restApi).extend({ +export default mixins(newVersions, showMessage, userHelpers).extend({ name: 'App', components: { LoadingView, @@ -70,6 +70,7 @@ export default mixins(newVersions, showMessage, userHelpers, restApi).extend({ useTemplatesStore, useUIStore, useUsersStore, + useVersionControlStore, ), defaultLocale(): string { return this.rootStore.defaultLocale; @@ -197,6 +198,13 @@ export default mixins(newVersions, showMessage, userHelpers, restApi).extend({ if (this.defaultLocale !== 'en') { await this.nodeTypesStore.getNodeTranslationHeaders(); } + + if ( + this.versionControlStore.isEnterpriseVersionControlEnabled && + this.usersStore.isInstanceOwner + ) { + this.versionControlStore.getPreferences(); + } }, watch: { $route(route) { diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts index f867659698961..5c4e172309a8c 100644 --- a/packages/editor-ui/src/Interface.ts +++ b/packages/editor-ui/src/Interface.ts @@ -1,7 +1,7 @@ -import { CREDENTIAL_EDIT_MODAL_KEY } from './constants'; +import type { CREDENTIAL_EDIT_MODAL_KEY } from './constants'; /* eslint-disable @typescript-eslint/no-explicit-any */ -import { IMenuItem } from 'n8n-design-system'; -import { +import type { IMenuItem } from 'n8n-design-system'; +import type { GenericValue, IConnections, ICredentialsDecrypted, @@ -17,7 +17,6 @@ import { IRun, IRunData, ITaskData, - ITelemetrySettings, IWorkflowSettings as IWorkflowSettingsWorkflow, WorkflowExecuteMode, PublicInstalledPackage, @@ -26,18 +25,23 @@ import { INodeCredentials, INodeListSearchItems, NodeParameterValueType, - INodeActionTypeDescription, IDisplayOptions, IExecutionsSummary, - IAbstractEventMessage, FeatureFlags, ExecutionStatus, ITelemetryTrackProperties, + IN8nUISettings, + IUserManagementSettings, + WorkflowSettings, } from 'n8n-workflow'; -import { SignInType } from './constants'; -import { FAKE_DOOR_FEATURES, TRIGGER_NODE_FILTER, REGULAR_NODE_FILTER } from './constants'; -import { BulkCommand, Undoable } from '@/models/history'; -import { PartialBy } from '@/utils/typeHelpers'; +import type { SignInType } from './constants'; +import type { + FAKE_DOOR_FEATURES, + TRIGGER_NODE_CREATOR_VIEW, + REGULAR_NODE_CREATOR_VIEW, +} from './constants'; +import type { BulkCommand, Undoable } from '@/models/history'; +import type { PartialBy } from '@/utils/typeHelpers'; export * from 'n8n-design-system/types'; @@ -137,43 +141,6 @@ export interface IExternalHooks { run(eventName: string, metadata?: IDataObject): Promise<void>; } -/** - * @deprecated Do not add methods to this interface. - */ -export interface IRestApi { - getActiveWorkflows(): Promise<string[]>; - getActivationError(id: string): Promise<IActivationError | undefined>; - getCurrentExecutions(filter: ExecutionsQueryFilter): Promise<IExecutionsCurrentSummaryExtended[]>; - getPastExecutions( - filter: ExecutionsQueryFilter, - limit: number, - lastId?: string, - firstId?: string, - ): Promise<IExecutionsListResponse>; - stopCurrentExecution(executionId: string): Promise<IExecutionsStopData>; - makeRestApiRequest(method: string, endpoint: string, data?: any): Promise<any>; - getCredentialTranslation(credentialType: string): Promise<object>; - removeTestWebhook(workflowId: string): Promise<boolean>; - runWorkflow(runData: IStartRunData): Promise<IExecutionPushResponse>; - createNewWorkflow(sendData: IWorkflowDataUpdate): Promise<IWorkflowDb>; - updateWorkflow(id: string, data: IWorkflowDataUpdate, forceSave?: boolean): Promise<IWorkflowDb>; - deleteWorkflow(name: string): Promise<void>; - getWorkflow(id: string): Promise<IWorkflowDb>; - getWorkflows(filter?: object): Promise<IWorkflowShortResponse[]>; - getWorkflowFromUrl(url: string): Promise<IWorkflowDb>; - getExecution(id: string): Promise<IExecutionResponse | undefined>; - deleteExecutions(sendData: IExecutionDeleteFilter): Promise<void>; - retryExecution(id: string, loadWorkflow?: boolean): Promise<boolean>; - getTimezones(): Promise<IDataObject>; - getBinaryUrl( - dataPath: string, - mode: 'view' | 'download', - fileName?: string, - mimeType?: string, - ): string; - getExecutionEvents(id: string): Promise<IAbstractEventMessage[]>; -} - export interface INodeTranslationHeaders { data: { [key: string]: { @@ -635,19 +602,12 @@ export interface IN8nPromptResponse { updated: boolean; } -export enum UserManagementAuthenticationMethod { +export const enum UserManagementAuthenticationMethod { Email = 'email', Ldap = 'ldap', Saml = 'saml', } -export interface IUserManagementConfig { - enabled: boolean; - showSetupOnFirstLoad?: boolean; - smtpSetup: boolean; - authenticationMethod: UserManagementAuthenticationMethod; -} - export interface IPermissionGroup { loginStatus?: ILogInStatus[]; role?: IRole[]; @@ -732,94 +692,14 @@ export interface ITemplatesCategory { export type WorkflowCallerPolicyDefaultOption = 'any' | 'none' | 'workflowsFromAList'; -export interface IN8nUISettings { - endpointWebhook: string; - endpointWebhookTest: string; - saveDataErrorExecution: string; - saveDataSuccessExecution: string; - saveManualExecutions: boolean; - workflowCallerPolicyDefaultOption: WorkflowCallerPolicyDefaultOption; - timezone: string; - executionTimeout: number; - maxExecutionTimeout: number; - oauthCallbackUrls: { - oauth1: string; - oauth2: string; - }; - urlBaseEditor: string; - urlBaseWebhook: string; - versionCli: string; - n8nMetadata?: { - [key: string]: string | number | undefined; - }; - versionNotifications: IVersionNotificationSettings; - instanceId: string; - personalizationSurveyEnabled: boolean; - userActivationSurveyEnabled: boolean; - telemetry: ITelemetrySettings; - userManagement: IUserManagementConfig; - defaultLocale: string; - workflowTagsDisabled: boolean; - logLevel: ILogLevel; - hiringBannerEnabled: boolean; - templates: { - enabled: boolean; - host: string; - }; - posthog: { - enabled: boolean; - apiHost: string; - apiKey: string; - autocapture: boolean; - disableSessionRecording: boolean; - debug: boolean; - }; - executionMode: string; - pushBackend: 'sse' | 'websocket'; - communityNodesEnabled: boolean; - isNpmAvailable: boolean; - publicApi: { - enabled: boolean; - latestVersion: number; - path: string; - swaggerUi: { - enabled: boolean; - }; - }; - sso: { - saml: { - loginLabel: string; - loginEnabled: boolean; - }; - ldap: { - loginLabel: string; - loginEnabled: boolean; - }; - }; - onboardingCallPromptEnabled: boolean; - allowedModules: { - builtIn?: string[]; - external?: string[]; - }; - enterprise: Record<string, boolean>; - deployment?: { - type: string | 'default' | 'n8n-internal' | 'cloud' | 'desktop_mac' | 'desktop_win'; - }; - hideUsagePage: boolean; - license: { - environment: 'development' | 'production'; - }; -} - export interface IWorkflowSettings extends IWorkflowSettingsWorkflow { errorWorkflow?: string; - saveDataErrorExecution?: string; - saveDataSuccessExecution?: string; saveManualExecutions?: boolean; timezone?: string; executionTimeout?: number; + maxExecutionTimeout?: number; callerIds?: string; - callerPolicy?: WorkflowCallerPolicyDefaultOption; + callerPolicy?: WorkflowSettings.CallerPolicy; } export interface ITimeoutHMS { @@ -830,67 +710,85 @@ export interface ITimeoutHMS { export type WorkflowTitleStatus = 'EXECUTING' | 'IDLE' | 'ERROR'; -export interface ISubcategoryItemProps { - subcategory: string; - description: string; - key?: string; - iconType: string; +export type ExtractActionKeys<T> = T extends SimplifiedNodeType ? T['name'] : never; + +export type ActionsRecord<T extends SimplifiedNodeType[]> = { + [K in ExtractActionKeys<T[number]>]: ActionTypeDescription[]; +}; + +export interface SimplifiedNodeType + extends Pick< + INodeTypeDescription, + 'displayName' | 'description' | 'name' | 'group' | 'icon' | 'iconUrl' | 'codex' | 'defaults' + > {} +export interface SubcategoryItemProps { + description?: string; + iconType?: string; icon?: string; + title?: string; + subcategory?: string; defaults?: INodeParameters; + forceIncludeNodes?: string[]; } export interface ViewItemProps { - withTopBorder: boolean; title: string; description: string; icon: string; } - -export interface INodeItemProps { - subcategory: string; - nodeType: INodeTypeDescription; +export interface LabelItemProps { + key: string; } - -export interface IActionItemProps { - subcategory: string; - nodeType: INodeActionTypeDescription; +export interface ActionTypeDescription extends SimplifiedNodeType { + displayOptions?: IDisplayOptions; + values?: IDataObject; + actionKey: string; + codex: { + label: string; + categories: string[]; + }; } -export interface ICategoryItemProps { - expanded: boolean; - category: string; +export interface CategoryItemProps { name: string; + count: number; } export interface CreateElementBase { + uuid?: string; key: string; - includedByTrigger?: boolean; - includedByRegular?: boolean; } export interface NodeCreateElement extends CreateElementBase { type: 'node'; - category?: string[]; - properties: INodeItemProps; + subcategory: string; + properties: SimplifiedNodeType; } export interface CategoryCreateElement extends CreateElementBase { type: 'category'; - properties: ICategoryItemProps; + subcategory: string; + properties: CategoryItemProps; } export interface SubcategoryCreateElement extends CreateElementBase { type: 'subcategory'; - properties: ISubcategoryItemProps; + properties: SubcategoryItemProps; } export interface ViewCreateElement extends CreateElementBase { type: 'view'; properties: ViewItemProps; } +export interface LabelCreateElement extends CreateElementBase { + type: 'label'; + subcategory: string; + properties: LabelItemProps; +} + export interface ActionCreateElement extends CreateElementBase { type: 'action'; - category: string; - properties: IActionItemProps; + subcategory: string; + properties: ActionTypeDescription; } export type INodeCreateElement = @@ -898,18 +796,12 @@ export type INodeCreateElement = | CategoryCreateElement | SubcategoryCreateElement | ViewCreateElement + | LabelCreateElement | ActionCreateElement; -export interface ICategoriesWithNodes { - [category: string]: { - [subcategory: string]: { - regularCount: number; - triggerCount: number; - nodes: INodeCreateElement[]; - }; - }; +export interface SubcategorizedNodeTypes { + [subcategory: string]: INodeCreateElement[]; } - export interface ITag { id: string; name: string; @@ -1177,9 +1069,6 @@ export interface UIState { addFirstStepOnLoad: boolean; executionSidebarAutoRefresh: boolean; } - -export type ILogLevel = 'info' | 'debug' | 'warn' | 'error' | 'verbose'; - export type IFakeDoor = { id: FAKE_DOOR_FEATURES; featureName: string; @@ -1198,7 +1087,7 @@ export type IFakeDoorLocation = | 'credentialsModal' | 'workflowShareModal'; -export type INodeFilterType = typeof REGULAR_NODE_FILTER | typeof TRIGGER_NODE_FILTER; +export type NodeFilterType = typeof REGULAR_NODE_CREATOR_VIEW | typeof TRIGGER_NODE_CREATOR_VIEW; export type NodeCreatorOpenSource = | '' @@ -1213,15 +1102,15 @@ export type NodeCreatorOpenSource = export interface INodeCreatorState { itemsFilter: string; showScrim: boolean; - rootViewHistory: INodeFilterType[]; - selectedView: INodeFilterType; + rootViewHistory: NodeFilterType[]; + selectedView: NodeFilterType; openSource: NodeCreatorOpenSource; } export interface ISettingsState { settings: IN8nUISettings; promptsData: IN8nPrompts; - userManagement: IUserManagementConfig; + userManagement: IUserManagementSettings; templatesEndpointHealthy: boolean; api: { enabled: boolean; @@ -1543,3 +1432,14 @@ export type SamlPreferencesExtractedData = { entityID: string; returnUrl: string; }; + +export type VersionControlPreferences = { + connected: boolean; + repositoryUrl: string; + authorName: string; + authorEmail: string; + branchName: string; + branchReadOnly: boolean; + branchColor: string; + publicKey?: string; +}; diff --git a/packages/editor-ui/src/__tests__/permissions.spec.ts b/packages/editor-ui/src/__tests__/permissions.spec.ts index 770cffa228770..8ee7edde431a5 100644 --- a/packages/editor-ui/src/__tests__/permissions.spec.ts +++ b/packages/editor-ui/src/__tests__/permissions.spec.ts @@ -1,5 +1,5 @@ import { parsePermissionsTable } from '@/permissions'; -import { IUser } from '@/Interface'; +import type { IUser } from '@/Interface'; describe('parsePermissionsTable()', () => { const user: IUser = { diff --git a/packages/editor-ui/src/__tests__/server/endpoints/credential.ts b/packages/editor-ui/src/__tests__/server/endpoints/credential.ts index 949004b487f27..7aba2d99d40c6 100644 --- a/packages/editor-ui/src/__tests__/server/endpoints/credential.ts +++ b/packages/editor-ui/src/__tests__/server/endpoints/credential.ts @@ -1,5 +1,6 @@ -import { Response, Server } from 'miragejs'; -import { AppSchema } from '../types'; +import type { Server } from 'miragejs'; +import { Response } from 'miragejs'; +import type { AppSchema } from '../types'; export function routesForCredentials(server: Server) { server.get('/rest/credentials', (schema: AppSchema) => { diff --git a/packages/editor-ui/src/__tests__/server/endpoints/credentialType.ts b/packages/editor-ui/src/__tests__/server/endpoints/credentialType.ts index 3f9ee6eaafec3..b50d6270e2ce7 100644 --- a/packages/editor-ui/src/__tests__/server/endpoints/credentialType.ts +++ b/packages/editor-ui/src/__tests__/server/endpoints/credentialType.ts @@ -1,5 +1,6 @@ -import { Response, Server } from 'miragejs'; -import { AppSchema } from '../types'; +import type { Server } from 'miragejs'; +import { Response } from 'miragejs'; +import type { AppSchema } from '../types'; export function routesForCredentialTypes(server: Server) { server.get('/types/credentials.json', (schema: AppSchema) => { diff --git a/packages/editor-ui/src/__tests__/server/endpoints/index.ts b/packages/editor-ui/src/__tests__/server/endpoints/index.ts index f98c9c3823292..4ba3315e425fb 100644 --- a/packages/editor-ui/src/__tests__/server/endpoints/index.ts +++ b/packages/editor-ui/src/__tests__/server/endpoints/index.ts @@ -1,4 +1,4 @@ -import { Server } from 'miragejs'; +import type { Server } from 'miragejs'; import { routesForUsers } from './user'; import { routesForCredentials } from './credential'; import { routesForCredentialTypes } from './credentialType'; diff --git a/packages/editor-ui/src/__tests__/server/endpoints/settings.ts b/packages/editor-ui/src/__tests__/server/endpoints/settings.ts index 374e548d8841c..0ffed3e14a6b1 100644 --- a/packages/editor-ui/src/__tests__/server/endpoints/settings.ts +++ b/packages/editor-ui/src/__tests__/server/endpoints/settings.ts @@ -1,6 +1,7 @@ -import { Response, Server } from 'miragejs'; -import { AppSchema } from '../types'; -import { IN8nUISettings, ISettingsState } from '@/Interface'; +import type { Server } from 'miragejs'; +import { Response } from 'miragejs'; +import type { AppSchema } from '../types'; +import type { IN8nUISettings } from 'n8n-workflow'; const defaultSettings: IN8nUISettings = { allowedModules: {}, @@ -9,9 +10,15 @@ const defaultSettings: IN8nUISettings = { endpointWebhook: '', endpointWebhookTest: '', enterprise: { + sharing: false, + ldap: false, + saml: false, + logStreaming: false, + advancedExecutionFilters: false, variables: true, + versionControl: false, }, - executionMode: '', + executionMode: 'regular', executionTimeout: 0, hideUsagePage: false, hiringBannerEnabled: false, @@ -33,8 +40,8 @@ const defaultSettings: IN8nUISettings = { }, publicApi: { enabled: false, latestVersion: 0, path: '', swaggerUi: { enabled: false } }, pushBackend: 'websocket', - saveDataErrorExecution: '', - saveDataSuccessExecution: '', + saveDataErrorExecution: 'DEFAULT', + saveDataSuccessExecution: 'DEFAULT', saveManualExecutions: false, sso: { ldap: { loginEnabled: false, loginLabel: '' }, @@ -51,6 +58,7 @@ const defaultSettings: IN8nUISettings = { enabled: true, showSetupOnFirstLoad: true, smtpSetup: true, + authenticationMethod: 'email', }, versionCli: '', versionNotifications: { @@ -60,6 +68,13 @@ const defaultSettings: IN8nUISettings = { }, workflowCallerPolicyDefaultOption: 'any', workflowTagsDisabled: false, + variables: { + limit: -1, + }, + userActivationSurveyEnabled: false, + deployment: { + type: 'default', + }, }; export function routesForSettings(server: Server) { diff --git a/packages/editor-ui/src/__tests__/server/endpoints/user.ts b/packages/editor-ui/src/__tests__/server/endpoints/user.ts index 0400b47b96ba7..57bfdc8e1faa1 100644 --- a/packages/editor-ui/src/__tests__/server/endpoints/user.ts +++ b/packages/editor-ui/src/__tests__/server/endpoints/user.ts @@ -1,5 +1,6 @@ -import { Response, Server } from 'miragejs'; -import { AppSchema } from '../types'; +import type { Server } from 'miragejs'; +import { Response } from 'miragejs'; +import type { AppSchema } from '../types'; export function routesForUsers(server: Server) { server.get('/rest/users', (schema: AppSchema) => { diff --git a/packages/editor-ui/src/__tests__/server/endpoints/variable.ts b/packages/editor-ui/src/__tests__/server/endpoints/variable.ts index bbe3b87eb1eb9..c2e017c2a9392 100644 --- a/packages/editor-ui/src/__tests__/server/endpoints/variable.ts +++ b/packages/editor-ui/src/__tests__/server/endpoints/variable.ts @@ -1,7 +1,8 @@ -import { Request, Response, Server } from 'miragejs'; -import { AppSchema } from '../types'; +import type { Request, Server } from 'miragejs'; +import { Response } from 'miragejs'; +import type { AppSchema } from '../types'; import { jsonParse } from 'n8n-workflow'; -import { EnvironmentVariable } from '@/Interface'; +import type { EnvironmentVariable } from '@/Interface'; export function routesForVariables(server: Server) { server.get('/rest/variables', (schema: AppSchema) => { diff --git a/packages/editor-ui/src/__tests__/server/factories/credentialType.ts b/packages/editor-ui/src/__tests__/server/factories/credentialType.ts index 517e4e7207a8f..7fdd221cf8b7d 100644 --- a/packages/editor-ui/src/__tests__/server/factories/credentialType.ts +++ b/packages/editor-ui/src/__tests__/server/factories/credentialType.ts @@ -1,5 +1,4 @@ import { Factory } from 'miragejs'; -import { faker } from '@faker-js/faker'; import type { ICredentialType } from 'n8n-workflow'; const credentialTypes = [ diff --git a/packages/editor-ui/src/__tests__/server/models/credential.ts b/packages/editor-ui/src/__tests__/server/models/credential.ts index 17d4f45dcf984..8020afc06d0d8 100644 --- a/packages/editor-ui/src/__tests__/server/models/credential.ts +++ b/packages/editor-ui/src/__tests__/server/models/credential.ts @@ -1,4 +1,4 @@ -import { ICredentialsResponse } from '@/Interface'; +import type { ICredentialsResponse } from '@/Interface'; import { Model } from 'miragejs'; import type { ModelDefinition } from 'miragejs/-types'; diff --git a/packages/editor-ui/src/__tests__/server/models/user.ts b/packages/editor-ui/src/__tests__/server/models/user.ts index cef64c360cf9d..2c4d22e3dbd16 100644 --- a/packages/editor-ui/src/__tests__/server/models/user.ts +++ b/packages/editor-ui/src/__tests__/server/models/user.ts @@ -1,4 +1,4 @@ -import { IUser } from '@/Interface'; +import type { IUser } from '@/Interface'; import { Model } from 'miragejs'; import type { ModelDefinition } from 'miragejs/-types'; diff --git a/packages/editor-ui/src/__tests__/server/models/variable.ts b/packages/editor-ui/src/__tests__/server/models/variable.ts index 5677a4db9b980..27a2a9b238e49 100644 --- a/packages/editor-ui/src/__tests__/server/models/variable.ts +++ b/packages/editor-ui/src/__tests__/server/models/variable.ts @@ -1,4 +1,4 @@ -import { EnvironmentVariable } from '@/Interface'; +import type { EnvironmentVariable } from '@/Interface'; import { Model } from 'miragejs'; import type { ModelDefinition } from 'miragejs/-types'; diff --git a/packages/editor-ui/src/__tests__/server/types.ts b/packages/editor-ui/src/__tests__/server/types.ts index bc3e75e4f3bad..ec32f7c97cede 100644 --- a/packages/editor-ui/src/__tests__/server/types.ts +++ b/packages/editor-ui/src/__tests__/server/types.ts @@ -1,10 +1,10 @@ -import { Registry } from 'miragejs'; +import type { Registry } from 'miragejs'; // eslint-disable-next-line import/no-unresolved -import Schema from 'miragejs/orm/schema'; +import type Schema from 'miragejs/orm/schema'; -import { models } from './models'; -import { factories } from './factories'; +import type { models } from './models'; +import type { factories } from './factories'; type AppRegistry = Registry<typeof models, typeof factories>; export type AppSchema = Schema<AppRegistry>; diff --git a/packages/editor-ui/src/__tests__/utils.ts b/packages/editor-ui/src/__tests__/utils.ts index 8e0cacf179bc8..c6d167f06c3ee 100644 --- a/packages/editor-ui/src/__tests__/utils.ts +++ b/packages/editor-ui/src/__tests__/utils.ts @@ -1,4 +1,7 @@ -import { ISettingsState, UserManagementAuthenticationMethod } from '@/Interface'; +import type { ISettingsState } from '@/Interface'; +import { UserManagementAuthenticationMethod } from '@/Interface'; +import { render } from '@testing-library/vue'; +import { PiniaVuePlugin } from 'pinia'; export const retry = (assertion: () => any, { interval = 20, timeout = 200 } = {}) => { return new Promise((resolve, reject) => { @@ -18,6 +21,12 @@ export const retry = (assertion: () => any, { interval = 20, timeout = 200 } = { }); }; +type RenderParams = Parameters<typeof render>; +export const renderComponent = (Component: RenderParams[0], renderOptions: RenderParams[1] = {}) => + render(Component, renderOptions, (vue) => { + vue.use(PiniaVuePlugin); + }); + export const waitAllPromises = () => new Promise((resolve) => setTimeout(resolve)); export const SETTINGS_STORE_DEFAULT_STATE: ISettingsState = { diff --git a/packages/editor-ui/src/api/api-keys.ts b/packages/editor-ui/src/api/api-keys.ts index 20a72895f60b8..f864ffbe17364 100644 --- a/packages/editor-ui/src/api/api-keys.ts +++ b/packages/editor-ui/src/api/api-keys.ts @@ -1,4 +1,4 @@ -import { IRestApiContext } from '@/Interface'; +import type { IRestApiContext } from '@/Interface'; import { makeRestApiRequest } from '@/utils'; export function getApiKey(context: IRestApiContext): Promise<{ apiKey: string | null }> { diff --git a/packages/editor-ui/src/api/communityNodes.ts b/packages/editor-ui/src/api/communityNodes.ts index 453217aa83074..ec8212bff36ba 100644 --- a/packages/editor-ui/src/api/communityNodes.ts +++ b/packages/editor-ui/src/api/communityNodes.ts @@ -1,5 +1,5 @@ -import { IRestApiContext } from '@/Interface'; -import { PublicInstalledPackage } from 'n8n-workflow'; +import type { IRestApiContext } from '@/Interface'; +import type { PublicInstalledPackage } from 'n8n-workflow'; import { get, post, makeRestApiRequest } from '@/utils'; export async function getInstalledCommunityNodes( diff --git a/packages/editor-ui/src/api/credentials.ee.ts b/packages/editor-ui/src/api/credentials.ee.ts index 5a9b5b6196b8b..3067b906888d4 100644 --- a/packages/editor-ui/src/api/credentials.ee.ts +++ b/packages/editor-ui/src/api/credentials.ee.ts @@ -1,6 +1,6 @@ -import { ICredentialsResponse, IRestApiContext, IShareCredentialsPayload } from '@/Interface'; +import type { ICredentialsResponse, IRestApiContext, IShareCredentialsPayload } from '@/Interface'; import { makeRestApiRequest } from '@/utils'; -import { IDataObject } from 'n8n-workflow'; +import type { IDataObject } from 'n8n-workflow'; export async function setCredentialSharedWith( context: IRestApiContext, diff --git a/packages/editor-ui/src/api/credentials.ts b/packages/editor-ui/src/api/credentials.ts index cf9d13932b94a..3bef8e9e9fe41 100644 --- a/packages/editor-ui/src/api/credentials.ts +++ b/packages/editor-ui/src/api/credentials.ts @@ -1,6 +1,10 @@ -import { ICredentialsDecryptedResponse, ICredentialsResponse, IRestApiContext } from '@/Interface'; +import type { + ICredentialsDecryptedResponse, + ICredentialsResponse, + IRestApiContext, +} from '@/Interface'; import { makeRestApiRequest } from '@/utils'; -import { +import type { ICredentialsDecrypted, ICredentialType, IDataObject, diff --git a/packages/editor-ui/src/api/curlHelper.ts b/packages/editor-ui/src/api/curlHelper.ts index c1927b3b82f88..7089201eb2e70 100644 --- a/packages/editor-ui/src/api/curlHelper.ts +++ b/packages/editor-ui/src/api/curlHelper.ts @@ -1,4 +1,4 @@ -import { CurlToJSONResponse, IRestApiContext } from '@/Interface'; +import type { CurlToJSONResponse, IRestApiContext } from '@/Interface'; import { makeRestApiRequest } from '@/utils'; export function getCurlToJson( diff --git a/packages/editor-ui/src/api/environments.ee.ts b/packages/editor-ui/src/api/environments.ee.ts index baf4280459e94..f5a386a059319 100644 --- a/packages/editor-ui/src/api/environments.ee.ts +++ b/packages/editor-ui/src/api/environments.ee.ts @@ -1,6 +1,6 @@ -import { EnvironmentVariable, IRestApiContext } from '@/Interface'; +import type { EnvironmentVariable, IRestApiContext } from '@/Interface'; import { makeRestApiRequest } from '@/utils'; -import { IDataObject } from 'n8n-workflow'; +import type { IDataObject } from 'n8n-workflow'; export async function getVariables(context: IRestApiContext): Promise<EnvironmentVariable[]> { return await makeRestApiRequest(context, 'GET', '/variables'); diff --git a/packages/editor-ui/src/api/eventbus.ee.ts b/packages/editor-ui/src/api/eventbus.ee.ts index 1b00f84630064..a979a63fe92c2 100644 --- a/packages/editor-ui/src/api/eventbus.ee.ts +++ b/packages/editor-ui/src/api/eventbus.ee.ts @@ -1,6 +1,6 @@ -import { IRestApiContext } from '@/Interface'; +import type { IRestApiContext } from '@/Interface'; import { makeRestApiRequest } from '@/utils'; -import { IDataObject, MessageEventBusDestinationOptions } from 'n8n-workflow'; +import type { IDataObject, MessageEventBusDestinationOptions } from 'n8n-workflow'; export async function saveDestinationToDb( context: IRestApiContext, diff --git a/packages/editor-ui/src/api/ldap.ts b/packages/editor-ui/src/api/ldap.ts index 06bf040f7549e..2daa45905c782 100644 --- a/packages/editor-ui/src/api/ldap.ts +++ b/packages/editor-ui/src/api/ldap.ts @@ -1,6 +1,6 @@ -import { ILdapConfig, ILdapSyncData, IRestApiContext } from '@/Interface'; +import type { ILdapConfig, ILdapSyncData, IRestApiContext } from '@/Interface'; import { makeRestApiRequest } from '@/utils'; -import { IDataObject } from 'n8n-workflow'; +import type { IDataObject } from 'n8n-workflow'; export function getLdapConfig(context: IRestApiContext): Promise<ILdapConfig> { return makeRestApiRequest(context, 'GET', '/ldap/config'); diff --git a/packages/editor-ui/src/api/settings.ts b/packages/editor-ui/src/api/settings.ts index 9bd87f28f7259..cdd19d976a1f8 100644 --- a/packages/editor-ui/src/api/settings.ts +++ b/packages/editor-ui/src/api/settings.ts @@ -1,12 +1,12 @@ -import { +import type { IRestApiContext, IN8nPrompts, IN8nValueSurveyData, - IN8nUISettings, IN8nPromptResponse, } from '../Interface'; import { makeRestApiRequest, get, post } from '@/utils'; import { N8N_IO_BASE_URL, NPM_COMMUNITY_NODE_SEARCH_API_URL } from '@/constants'; +import type { IN8nUISettings } from 'n8n-workflow'; export function getSettings(context: IRestApiContext): Promise<IN8nUISettings> { return makeRestApiRequest(context, 'GET', '/settings'); diff --git a/packages/editor-ui/src/api/sso.ts b/packages/editor-ui/src/api/sso.ts index 5fc90c8a2e4e9..867a7799f9a63 100644 --- a/packages/editor-ui/src/api/sso.ts +++ b/packages/editor-ui/src/api/sso.ts @@ -1,5 +1,5 @@ import { makeRestApiRequest } from '@/utils'; -import { +import type { IRestApiContext, SamlPreferencesLoginEnabled, SamlPreferences, diff --git a/packages/editor-ui/src/api/tags.ts b/packages/editor-ui/src/api/tags.ts index 45392781251bb..88937cf2e857a 100644 --- a/packages/editor-ui/src/api/tags.ts +++ b/packages/editor-ui/src/api/tags.ts @@ -1,4 +1,4 @@ -import { IRestApiContext, ITag } from '@/Interface'; +import type { IRestApiContext, ITag } from '@/Interface'; import { makeRestApiRequest } from '@/utils'; export async function getTags(context: IRestApiContext, withUsageCount = false): Promise<ITag[]> { diff --git a/packages/editor-ui/src/api/templates.ts b/packages/editor-ui/src/api/templates.ts index 870f962d48741..46b4bf2f3e9e5 100644 --- a/packages/editor-ui/src/api/templates.ts +++ b/packages/editor-ui/src/api/templates.ts @@ -1,4 +1,4 @@ -import { +import type { ITemplatesCategory, ITemplatesCollection, ITemplatesQuery, @@ -7,7 +7,7 @@ import { ITemplatesWorkflowResponse, IWorkflowTemplate, } from '@/Interface'; -import { IDataObject } from 'n8n-workflow'; +import type { IDataObject } from 'n8n-workflow'; import { get } from '@/utils'; function stringifyArray(arr: number[]) { diff --git a/packages/editor-ui/src/api/usage.ts b/packages/editor-ui/src/api/usage.ts index 5266e5a82b5b9..8d79f59e7761b 100644 --- a/packages/editor-ui/src/api/usage.ts +++ b/packages/editor-ui/src/api/usage.ts @@ -1,5 +1,5 @@ import { makeRestApiRequest } from '@/utils'; -import { IRestApiContext, UsageState } from '@/Interface'; +import type { IRestApiContext, UsageState } from '@/Interface'; export const getLicense = (context: IRestApiContext): Promise<UsageState['data']> => { return makeRestApiRequest(context, 'GET', '/license'); diff --git a/packages/editor-ui/src/api/users.ts b/packages/editor-ui/src/api/users.ts index 6457ec2c806ef..1b693f4fbd760 100644 --- a/packages/editor-ui/src/api/users.ts +++ b/packages/editor-ui/src/api/users.ts @@ -1,11 +1,11 @@ -import { +import type { CurrentUserResponse, IInviteResponse, IPersonalizationLatestVersion, IRestApiContext, IUserResponse, } from '@/Interface'; -import { IDataObject } from 'n8n-workflow'; +import type { IDataObject } from 'n8n-workflow'; import { makeRestApiRequest } from '@/utils/apiUtils'; export function loginCurrentUser(context: IRestApiContext): Promise<CurrentUserResponse | null> { diff --git a/packages/editor-ui/src/api/versionControl.ts b/packages/editor-ui/src/api/versionControl.ts new file mode 100644 index 0000000000000..d4b78a5654205 --- /dev/null +++ b/packages/editor-ui/src/api/versionControl.ts @@ -0,0 +1,36 @@ +import type { IRestApiContext, VersionControlPreferences } from '@/Interface'; +import { makeRestApiRequest } from '@/utils'; +import type { IDataObject } from 'n8n-workflow'; + +const versionControlApiRoot = '/version-control'; + +export const initSsh = (context: IRestApiContext, data: IDataObject): Promise<string> => { + return makeRestApiRequest(context, 'POST', `${versionControlApiRoot}/init-ssh`, data); +}; + +export const initRepository = ( + context: IRestApiContext, +): Promise<{ branches: string[]; currentBranch: string }> => { + return makeRestApiRequest(context, 'POST', `${versionControlApiRoot}/init-repository`); +}; + +export const sync = (context: IRestApiContext, data: IDataObject): Promise<void> => { + return makeRestApiRequest(context, 'POST', `${versionControlApiRoot}/push`, data); +}; + +export const getConfig = ( + context: IRestApiContext, +): Promise<{ remoteRepository: string; name: string; email: string; currentBranch: string }> => { + return makeRestApiRequest(context, 'GET', `${versionControlApiRoot}/config`); +}; + +export const setPreferences = ( + context: IRestApiContext, + preferences: Partial<VersionControlPreferences>, +): Promise<VersionControlPreferences> => { + return makeRestApiRequest(context, 'POST', `${versionControlApiRoot}/preferences`, preferences); +}; + +export const getPreferences = (context: IRestApiContext): Promise<VersionControlPreferences> => { + return makeRestApiRequest(context, 'GET', `${versionControlApiRoot}/preferences`); +}; diff --git a/packages/editor-ui/src/api/versions.ts b/packages/editor-ui/src/api/versions.ts index e6842342fd3bd..4896318bfa283 100644 --- a/packages/editor-ui/src/api/versions.ts +++ b/packages/editor-ui/src/api/versions.ts @@ -1,4 +1,4 @@ -import { IVersion } from '@/Interface'; +import type { IVersion } from '@/Interface'; import { INSTANCE_ID_HEADER } from '@/constants'; import { get } from '@/utils'; diff --git a/packages/editor-ui/src/api/workflow-webhooks.ts b/packages/editor-ui/src/api/workflow-webhooks.ts index b31192f5da1f2..601073a7b05c6 100644 --- a/packages/editor-ui/src/api/workflow-webhooks.ts +++ b/packages/editor-ui/src/api/workflow-webhooks.ts @@ -1,4 +1,4 @@ -import { IOnboardingCallPrompt, IOnboardingCallPromptResponse, IUser } from '@/Interface'; +import type { IOnboardingCallPrompt, IUser } from '@/Interface'; import { get, post } from '@/utils'; const N8N_API_BASE_URL = 'https://api.n8n.io/api'; diff --git a/packages/editor-ui/src/api/workflows.ee.ts b/packages/editor-ui/src/api/workflows.ee.ts index 499fad502cb5f..1282a8ce86b56 100644 --- a/packages/editor-ui/src/api/workflows.ee.ts +++ b/packages/editor-ui/src/api/workflows.ee.ts @@ -1,6 +1,6 @@ -import { IRestApiContext, IShareWorkflowsPayload, IWorkflowsShareResponse } from '@/Interface'; +import type { IRestApiContext, IShareWorkflowsPayload, IWorkflowsShareResponse } from '@/Interface'; import { makeRestApiRequest } from '@/utils'; -import { IDataObject } from 'n8n-workflow'; +import type { IDataObject } from 'n8n-workflow'; export async function setWorkflowSharedWith( context: IRestApiContext, diff --git a/packages/editor-ui/src/api/workflows.ts b/packages/editor-ui/src/api/workflows.ts index c641b13006f23..7b2d1133cb664 100644 --- a/packages/editor-ui/src/api/workflows.ts +++ b/packages/editor-ui/src/api/workflows.ts @@ -1,11 +1,5 @@ -import { IExecutionsCurrentSummaryExtended, IRestApiContext } from '@/Interface'; -import { - ExecutionFilters, - ExecutionOptions, - ExecutionStatus, - IDataObject, - WorkflowExecuteMode, -} from 'n8n-workflow'; +import type { IExecutionsCurrentSummaryExtended, IRestApiContext } from '@/Interface'; +import type { ExecutionFilters, ExecutionOptions, IDataObject } from 'n8n-workflow'; import { makeRestApiRequest } from '@/utils'; export async function getNewWorkflow(context: IRestApiContext, name?: string) { diff --git a/packages/editor-ui/src/components/AboutModal.vue b/packages/editor-ui/src/components/AboutModal.vue index f23621cea60f3..19b57c1fd0bb7 100644 --- a/packages/editor-ui/src/components/AboutModal.vue +++ b/packages/editor-ui/src/components/AboutModal.vue @@ -54,7 +54,7 @@ </template> <script lang="ts"> -import Vue from 'vue'; +import { defineComponent } from 'vue'; import Modal from './Modal.vue'; import { ABOUT_MODAL_KEY } from '../constants'; import { mapStores } from 'pinia'; @@ -62,7 +62,7 @@ import { useSettingsStore } from '@/stores/settings'; import { useRootStore } from '@/stores/n8nRootStore'; import { createEventBus } from '@/event-bus'; -export default Vue.extend({ +export default defineComponent({ name: 'About', components: { Modal, diff --git a/packages/editor-ui/src/components/ActivationModal.vue b/packages/editor-ui/src/components/ActivationModal.vue index 0e7d2963ef406..5a72229d684c5 100644 --- a/packages/editor-ui/src/components/ActivationModal.vue +++ b/packages/editor-ui/src/components/ActivationModal.vue @@ -35,7 +35,7 @@ </template> <script lang="ts"> -import Vue from 'vue'; +import { defineComponent } from 'vue'; import Modal from '@/components/Modal.vue'; import { @@ -51,7 +51,7 @@ import { useWorkflowsStore } from '@/stores/workflows'; import { useNodeTypesStore } from '@/stores/nodeTypes'; import { createEventBus } from '@/event-bus'; -export default Vue.extend({ +export default defineComponent({ name: 'ActivationModal', components: { Modal, diff --git a/packages/editor-ui/src/components/AskAiModal.vue b/packages/editor-ui/src/components/AskAiModal.vue index 1a678394a9854..9cd992f5166b2 100644 --- a/packages/editor-ui/src/components/AskAiModal.vue +++ b/packages/editor-ui/src/components/AskAiModal.vue @@ -23,12 +23,12 @@ </template> <script lang="ts"> -import Vue from 'vue'; +import { defineComponent } from 'vue'; import Modal from './Modal.vue'; import { ASK_AI_MODAL_KEY, ASK_AI_WAITLIST_URL } from '../constants'; import { createEventBus } from '@/event-bus'; -export default Vue.extend({ +export default defineComponent({ name: 'AskAI', components: { Modal, diff --git a/packages/editor-ui/src/components/Banner.vue b/packages/editor-ui/src/components/Banner.vue index 7ce1710f6c2c2..8b48fe6883af4 100644 --- a/packages/editor-ui/src/components/Banner.vue +++ b/packages/editor-ui/src/components/Banner.vue @@ -36,9 +36,9 @@ </template> <script lang="ts"> -import Vue from 'vue'; +import { defineComponent } from 'vue'; -export default Vue.extend({ +export default defineComponent({ name: 'Banner', data() { return { diff --git a/packages/editor-ui/src/components/BinaryDataDisplay.vue b/packages/editor-ui/src/components/BinaryDataDisplay.vue index c1f5b71355ea4..4129fbe5807b9 100644 --- a/packages/editor-ui/src/components/BinaryDataDisplay.vue +++ b/packages/editor-ui/src/components/BinaryDataDisplay.vue @@ -26,11 +26,10 @@ import BinaryDataDisplayEmbed from '@/components/BinaryDataDisplayEmbed.vue'; import { nodeHelpers } from '@/mixins/nodeHelpers'; import mixins from 'vue-typed-mixins'; -import { restApi } from '@/mixins/restApi'; import { mapStores } from 'pinia'; import { useWorkflowsStore } from '@/stores/workflows'; -export default mixins(nodeHelpers, restApi).extend({ +export default mixins(nodeHelpers).extend({ name: 'BinaryDataDisplay', components: { BinaryDataDisplayEmbed, diff --git a/packages/editor-ui/src/components/BinaryDataDisplayEmbed.vue b/packages/editor-ui/src/components/BinaryDataDisplayEmbed.vue index 04fbde24a67f4..9b534b6f1f57b 100644 --- a/packages/editor-ui/src/components/BinaryDataDisplayEmbed.vue +++ b/packages/editor-ui/src/components/BinaryDataDisplayEmbed.vue @@ -19,13 +19,15 @@ </template> <script lang="ts"> -import mixins from 'vue-typed-mixins'; -import { restApi } from '@/mixins/restApi'; -import { IBinaryData, jsonParse } from 'n8n-workflow'; +import type { IBinaryData } from 'n8n-workflow'; +import { jsonParse } from 'n8n-workflow'; import type { PropType } from 'vue'; import VueJsonPretty from 'vue-json-pretty'; +import { mapStores } from 'pinia'; +import { useWorkflowsStore } from '@/stores'; +import Vue from 'vue'; -export default mixins(restApi).extend({ +export default Vue.extend({ name: 'BinaryDataDisplayEmbed', components: { VueJsonPretty, @@ -44,6 +46,9 @@ export default mixins(restApi).extend({ jsonData: '', }; }, + computed: { + ...mapStores(useWorkflowsStore), + }, async mounted() { const { id, data, fileName, fileType, mimeType } = (this.binaryData || {}) as IBinaryData; const isJSONData = fileType === 'json'; @@ -56,7 +61,7 @@ export default mixins(restApi).extend({ } } else { try { - const binaryUrl = this.restApi().getBinaryUrl(id, 'view', fileName, mimeType); + const binaryUrl = this.workflowsStore.getBinaryUrl(id, 'view', fileName, mimeType); if (isJSONData) { this.jsonData = await (await fetch(binaryUrl)).json(); } else { diff --git a/packages/editor-ui/src/components/ChangePasswordModal.vue b/packages/editor-ui/src/components/ChangePasswordModal.vue index 735336a9fbc0a..e87a5bfc7e04e 100644 --- a/packages/editor-ui/src/components/ChangePasswordModal.vue +++ b/packages/editor-ui/src/components/ChangePasswordModal.vue @@ -33,7 +33,7 @@ import mixins from 'vue-typed-mixins'; import { showMessage } from '@/mixins/showMessage'; import Modal from './Modal.vue'; -import { IFormInputs } from '@/Interface'; +import type { IFormInputs } from '@/Interface'; import { CHANGE_PASSWORD_MODAL_KEY } from '../constants'; import { mapStores } from 'pinia'; import { useUsersStore } from '@/stores/users'; diff --git a/packages/editor-ui/src/components/CodeEdit.vue b/packages/editor-ui/src/components/CodeEdit.vue deleted file mode 100644 index 8198196d48c4f..0000000000000 --- a/packages/editor-ui/src/components/CodeEdit.vue +++ /dev/null @@ -1,281 +0,0 @@ -<template> - <el-dialog - visible - append-to-body - :close-on-click-modal="false" - width="80%" - :title="`${$locale.baseText('codeEdit.edit')} ${$locale - .nodeText() - .inputLabelDisplayName(parameter, path)}`" - :before-close="closeDialog" - > - <div class="text-editor-wrapper ignore-key-press"> - <code-editor - :value="value" - :autocomplete="loadAutocompleteData" - :readonly="readonly" - @input="$emit('valueChanged', $event)" - /> - </div> - </el-dialog> -</template> - -<script lang="ts"> -import { genericHelpers } from '@/mixins/genericHelpers'; -import { workflowHelpers } from '@/mixins/workflowHelpers'; - -import mixins from 'vue-typed-mixins'; -import { IExecutionResponse, INodeUi } from '@/Interface'; -import { - IBinaryKeyData, - IDataObject, - INodeExecutionData, - IRunExecutionData, - IWorkflowDataProxyAdditionalKeys, - WorkflowDataProxy, -} from 'n8n-workflow'; - -import { PLACEHOLDER_FILLED_AT_EXECUTION_TIME } from '@/constants'; -import { CodeEditor } from './forms'; -import { mapStores } from 'pinia'; -import { useWorkflowsStore } from '@/stores/workflows'; -import { useRootStore } from '@/stores/n8nRootStore'; -import { useNDVStore } from '@/stores/ndv'; - -export default mixins(genericHelpers, workflowHelpers).extend({ - name: 'CodeEdit', - components: { - CodeEditor, - }, - props: ['codeAutocomplete', 'parameter', 'path', 'type', 'value', 'readonly'], - computed: { - ...mapStores(useNDVStore, useRootStore, useWorkflowsStore), - }, - methods: { - loadAutocompleteData(): string[] { - if (['function', 'functionItem'].includes(this.codeAutocomplete)) { - const itemIndex = 0; - const inputName = 'main'; - const mode = 'manual'; - let runIndex = 0; - - const executedWorkflow = this.workflowsStore.getWorkflowExecution; - const workflow = this.getCurrentWorkflow(); - const activeNode: INodeUi | null = this.ndvStore.activeNode; - const parentNode = workflow.getParentNodes(activeNode!.name, inputName, 1); - const nodeConnection = workflow.getNodeConnectionIndexes( - activeNode!.name, - parentNode[0], - ) || { - sourceIndex: 0, - destinationIndex: 0, - }; - - const executionData = this.workflowsStore.getWorkflowExecution; - - let runExecutionData: IRunExecutionData; - if (!executionData || !executionData.data) { - runExecutionData = { - resultData: { - runData: {}, - }, - }; - } else { - runExecutionData = executionData.data; - if (runExecutionData.resultData.runData[activeNode!.name]) { - runIndex = runExecutionData.resultData.runData[activeNode!.name].length - 1; - } - } - - const connectionInputData = this.connectionInputData( - parentNode, - activeNode!.name, - inputName, - runIndex, - nodeConnection, - ); - - const additionalProxyKeys: IWorkflowDataProxyAdditionalKeys = { - $execution: { - id: PLACEHOLDER_FILLED_AT_EXECUTION_TIME, - mode: 'test', - resumeUrl: PLACEHOLDER_FILLED_AT_EXECUTION_TIME, - }, - - // deprecated - $executionId: PLACEHOLDER_FILLED_AT_EXECUTION_TIME, - $resumeWebhookUrl: PLACEHOLDER_FILLED_AT_EXECUTION_TIME, - }; - - const dataProxy = new WorkflowDataProxy( - workflow, - runExecutionData, - runIndex, - itemIndex, - activeNode!.name, - connectionInputData || [], - {}, - mode, - this.rootStore.timezone, - additionalProxyKeys, - ); - const proxy = dataProxy.getDataProxy(); - - const autoCompleteItems = [ - 'function $evaluateExpression(expression: string, itemIndex?: number): any {};', - 'function getNodeParameter(parameterName: string, itemIndex: number, fallbackValue?: any): any {};', - 'function getWorkflowStaticData(type: string): {};', - 'function $item(itemIndex: number, runIndex?: number): {};', - 'function $items(nodeName?: string, outputIndex?: number, runIndex?: number): {};', - ]; - - const baseKeys = [ - '$env', - '$executionId', - '$mode', - '$parameter', - '$resumeWebhookUrl', - '$vars', - '$workflow', - '$now', - '$today', - '$thisRunIndex', - 'DateTime', - 'Duration', - 'Interval', - ]; - - const functionItemKeys = ['$json', '$binary', '$position', '$thisItem', '$thisItemIndex']; - - const additionalKeys: string[] = []; - if (this.codeAutocomplete === 'functionItem') { - additionalKeys.push(...functionItemKeys); - } - - if (executedWorkflow && connectionInputData && connectionInputData.length) { - baseKeys.push(...additionalKeys); - } else { - additionalKeys.forEach((key) => { - autoCompleteItems.push(`const ${key} = {}`); - }); - } - - for (const key of baseKeys) { - autoCompleteItems.push( - `const ${key} = ${JSON.stringify(this.createSimpleRepresentation(proxy[key]))}`, - ); - } - - // Add the nodes and their simplified data - const nodes: { - [key: string]: INodeExecutionData; - } = {}; - for (const [nodeName, node] of Object.entries(workflow.nodes)) { - // To not load to much data create a simple representation. - nodes[nodeName] = { - json: {} as IDataObject, - parameter: this.createSimpleRepresentation( - proxy.$node[nodeName].parameter, - ) as IDataObject, - }; - - try { - nodes[nodeName]!.json = this.createSimpleRepresentation( - proxy.$node[nodeName].json, - ) as IDataObject; - nodes[nodeName]!.context = this.createSimpleRepresentation( - proxy.$node[nodeName].context, - ) as IDataObject; - nodes[nodeName]!.runIndex = proxy.$node[nodeName].runIndex; - if (Object.keys(proxy.$node[nodeName].binary).length) { - nodes[nodeName]!.binary = this.createSimpleRepresentation( - proxy.$node[nodeName].binary, - ) as IBinaryKeyData; - } - } catch (error) {} - } - autoCompleteItems.push(`const $node = ${JSON.stringify(nodes)}`); - autoCompleteItems.push('function $jmespath(jsonDoc: object, query: string): {};'); - - if (this.codeAutocomplete === 'function') { - if (connectionInputData) { - autoCompleteItems.push( - `const items = ${JSON.stringify( - this.createSimpleRepresentation(connectionInputData), - )}`, - ); - } else { - autoCompleteItems.push('const items: {json: {[key: string]: any}}[] = []'); - } - } else if (this.codeAutocomplete === 'functionItem') { - if (connectionInputData) { - autoCompleteItems.push('const item = $json'); - } else { - autoCompleteItems.push('const item: {[key: string]: any} = {}'); - } - } - - return autoCompleteItems; - } - - return []; - }, - closeDialog() { - // Handle the close externally as the visible parameter is an external prop - // and is so not allowed to be changed here. - this.$emit('closeDialog'); - return false; - }, - - createSimpleRepresentation( - inputData: - | object - | null - | undefined - | boolean - | string - | number - | boolean[] - | string[] - | number[] - | object[], - ): - | object - | null - | undefined - | boolean - | string - | number - | boolean[] - | string[] - | number[] - | object[] { - if (inputData === null || inputData === undefined) { - return inputData; - } else if (typeof inputData === 'string') { - return ''; - } else if (typeof inputData === 'boolean') { - return true; - } else if (typeof inputData === 'number') { - return 1; - } else if (Array.isArray(inputData)) { - return inputData.map((value) => this.createSimpleRepresentation(value)); - } else if (typeof inputData === 'object') { - const returnData: { [key: string]: object } = {}; - Object.keys(inputData).forEach((key) => { - // @ts-ignore - returnData[key] = this.createSimpleRepresentation(inputData[key]); - }); - return returnData; - } - return inputData; - }, - }, -}); -</script> - -<style scoped> -.text-editor { - min-height: 30rem; -} -</style> diff --git a/packages/editor-ui/src/components/CodeNodeEditor/CodeNodeEditor.vue b/packages/editor-ui/src/components/CodeNodeEditor/CodeNodeEditor.vue index fff8c461fa446..63ebcdec60efb 100644 --- a/packages/editor-ui/src/components/CodeNodeEditor/CodeNodeEditor.vue +++ b/packages/editor-ui/src/components/CodeNodeEditor/CodeNodeEditor.vue @@ -1,11 +1,11 @@ <template> <div - :class="$style['code-node-editor-container']" + :class="['code-node-editor', $style['code-node-editor-container'], language]" @mouseover="onMouseOver" @mouseout="onMouseOut" ref="codeNodeEditorContainer" > - <div ref="codeNodeEditor" class="ph-no-capture"></div> + <div ref="codeNodeEditor" class="code-node-editor-input ph-no-capture"></div> <n8n-button v-if="isCloud && (isEditorHovered || isEditorFocused)" size="small" @@ -19,54 +19,81 @@ </template> <script lang="ts"> +import type { PropType } from 'vue'; +import { mapStores } from 'pinia'; import mixins from 'vue-typed-mixins'; +import type { LanguageSupport } from '@codemirror/language'; +import type { Extension } from '@codemirror/state'; import { Compartment, EditorState } from '@codemirror/state'; -import { EditorView, ViewUpdate } from '@codemirror/view'; +import type { ViewUpdate } from '@codemirror/view'; +import { EditorView } from '@codemirror/view'; import { javascript } from '@codemirror/lang-javascript'; +import { json } from '@codemirror/lang-json'; +import { python } from '@codemirror/lang-python'; +import type { CodeExecutionMode, CodeNodeEditorLanguage } from 'n8n-workflow'; +import { CODE_EXECUTION_MODES, CODE_LANGUAGES } from 'n8n-workflow'; -import { baseExtensions } from './baseExtensions'; -import { linterExtension } from './linter'; -import { completerExtension } from './completer'; -import { CODE_NODE_EDITOR_THEME } from './theme'; import { workflowHelpers } from '@/mixins/workflowHelpers'; // for json field completions import { ASK_AI_MODAL_KEY, CODE_NODE_TYPE } from '@/constants'; import { codeNodeEditorEventBus } from '@/event-bus'; -import { ALL_ITEMS_PLACEHOLDER, EACH_ITEM_PLACEHOLDER } from './constants'; -import { mapStores } from 'pinia'; import { useRootStore } from '@/stores/n8nRootStore'; -import Modal from '../Modal.vue'; import { useSettingsStore } from '@/stores/settings'; +import Modal from '@/components/Modal.vue'; + +import { readOnlyEditorExtensions, writableEditorExtensions } from './baseExtensions'; +import { CODE_PLACEHOLDERS } from './constants'; +import { linterExtension } from './linter'; +import { completerExtension } from './completer'; +import { codeNodeEditorTheme } from './theme'; export default mixins(linterExtension, completerExtension, workflowHelpers).extend({ name: 'code-node-editor', components: { Modal }, props: { mode: { - type: String, - validator: (value: string): boolean => - ['runOnceForAllItems', 'runOnceForEachItem'].includes(value), + type: String as PropType<CodeExecutionMode>, + validator: (value: CodeExecutionMode): boolean => CODE_EXECUTION_MODES.includes(value), + }, + language: { + type: String as PropType<CodeNodeEditorLanguage>, + default: 'javaScript' as CodeNodeEditorLanguage, + validator: (value: CodeNodeEditorLanguage): boolean => CODE_LANGUAGES.includes(value), }, isReadOnly: { type: Boolean, default: false, }, - jsCode: { + value: { type: String, }, }, data() { return { editor: null as EditorView | null, + languageCompartment: new Compartment(), linterCompartment: new Compartment(), isEditorHovered: false, isEditorFocused: false, }; }, watch: { - mode() { + mode(newMode, previousMode: CodeExecutionMode) { this.reloadLinter(); - this.refreshPlaceholder(); + + if (this.content.trim() === CODE_PLACEHOLDERS[this.language]?.[previousMode]) { + this.refreshPlaceholder(); + } + }, + language(newLanguage, previousLanguage: CodeNodeEditorLanguage) { + if (this.content.trim() === CODE_PLACEHOLDERS[previousLanguage]?.[this.mode]) { + this.refreshPlaceholder(); + } + + const [languageSupport] = this.languageExtensions; + this.editor?.dispatch({ + effects: this.languageCompartment.reconfigure(languageSupport), + }); }, }, computed: { @@ -80,30 +107,31 @@ export default mixins(linterExtension, completerExtension, workflowHelpers).exte return this.editor.state.doc.toString(); }, placeholder(): string { - return { - runOnceForAllItems: ALL_ITEMS_PLACEHOLDER, - runOnceForEachItem: EACH_ITEM_PLACEHOLDER, - }[this.mode]; + return CODE_PLACEHOLDERS[this.language]?.[this.mode] ?? ''; }, - previousPlaceholder(): string { - return { - runOnceForAllItems: EACH_ITEM_PLACEHOLDER, - runOnceForEachItem: ALL_ITEMS_PLACEHOLDER, - }[this.mode]; + languageExtensions(): [LanguageSupport, ...Extension[]] { + switch (this.language) { + case 'json': + return [json()]; + case 'javaScript': + return [javascript(), this.autocompletionExtension('javaScript')]; + case 'python': + return [python(), this.autocompletionExtension('python')]; + } }, }, methods: { onMouseOver(event: MouseEvent) { const fromElement = event.relatedTarget as HTMLElement; - const ref = this.$refs.codeNodeEditorContainer as HTMLDivElement; + const ref = this.$refs.codeNodeEditorContainer as HTMLDivElement | undefined; - if (!ref.contains(fromElement)) this.isEditorHovered = true; + if (!ref?.contains(fromElement)) this.isEditorHovered = true; }, onMouseOut(event: MouseEvent) { const fromElement = event.relatedTarget as HTMLElement; - const ref = this.$refs.codeNodeEditorContainer as HTMLDivElement; + const ref = this.$refs.codeNodeEditorContainer as HTMLDivElement | undefined; - if (!ref.contains(fromElement)) this.isEditorHovered = false; + if (!ref?.contains(fromElement)) this.isEditorHovered = false; }, onAskAiButtonClick() { this.$telemetry.track('User clicked ask ai button', { source: 'code' }); @@ -113,25 +141,26 @@ export default mixins(linterExtension, completerExtension, workflowHelpers).exte reloadLinter() { if (!this.editor) return; - this.editor.dispatch({ - effects: this.linterCompartment.reconfigure(this.linterExtension()), - }); + const linter = this.createLinter(this.language); + if (linter) { + this.editor.dispatch({ + effects: this.linterCompartment.reconfigure(linter), + }); + } }, refreshPlaceholder() { if (!this.editor) return; - if (!this.content.trim() || this.content.trim() === this.previousPlaceholder) { - this.editor.dispatch({ - changes: { from: 0, to: this.content.length, insert: this.placeholder }, - }); - } + this.editor.dispatch({ + changes: { from: 0, to: this.content.length, insert: this.placeholder }, + }); }, highlightLine(line: number | 'final') { if (!this.editor) return; if (line === 'final') { this.editor.dispatch({ - selection: { anchor: this.content.trim().length }, + selection: { anchor: this.content.length }, }); return; } @@ -162,6 +191,7 @@ export default mixins(linterExtension, completerExtension, workflowHelpers).exte insertedText = full.slice(lastDotIndex + 1); } + // TODO: Still has to get updated for Python and JSON this.$telemetry.track('User autocompleted code', { instance_id: this.rootStore.instanceId, node_type: CODE_NODE_TYPE, @@ -174,45 +204,56 @@ export default mixins(linterExtension, completerExtension, workflowHelpers).exte }, }, destroyed() { - codeNodeEditorEventBus.off('error-line-number', this.highlightLine); + if (!this.isReadOnly) codeNodeEditorEventBus.off('error-line-number', this.highlightLine); }, mounted() { - codeNodeEditorEventBus.on('error-line-number', this.highlightLine); - - const stateBasedExtensions = [ - this.linterCompartment.of(this.linterExtension()), - EditorState.readOnly.of(this.isReadOnly), - EditorView.domEventHandlers({ - focus: () => { - this.isEditorFocused = true; - }, - blur: () => { - this.isEditorFocused = false; - }, - }), - EditorView.updateListener.of((viewUpdate: ViewUpdate) => { - if (!viewUpdate.docChanged) return; - - this.trackCompletion(viewUpdate); - - this.$emit('valueChanged', this.content); - }), - ]; + if (!this.isReadOnly) codeNodeEditorEventBus.on('error-line-number', this.highlightLine); // empty on first load, default param value - if (this.jsCode === '') { + if (!this.value) { this.$emit('valueChanged', this.placeholder); } + const { isReadOnly, language } = this; + const extensions: Extension[] = [ + ...readOnlyEditorExtensions, + EditorState.readOnly.of(isReadOnly), + EditorView.editable.of(!isReadOnly), + codeNodeEditorTheme({ isReadOnly }), + ]; + + if (!isReadOnly) { + const linter = this.createLinter(language); + if (linter) { + extensions.push(this.linterCompartment.of(linter)); + } + + extensions.push( + ...writableEditorExtensions, + EditorView.domEventHandlers({ + focus: () => { + this.isEditorFocused = true; + }, + blur: () => { + this.isEditorFocused = false; + }, + }), + EditorView.updateListener.of((viewUpdate) => { + if (!viewUpdate.docChanged) return; + + this.trackCompletion(viewUpdate); + + this.$emit('valueChanged', this.editor?.state.doc.toString()); + }), + ); + } + + const [languageSupport, ...otherExtensions] = this.languageExtensions; + extensions.push(this.languageCompartment.of(languageSupport), ...otherExtensions); + const state = EditorState.create({ - doc: this.jsCode === '' ? this.placeholder : this.jsCode, - extensions: [ - ...baseExtensions, - ...stateBasedExtensions, - CODE_NODE_EDITOR_THEME, - javascript(), - this.autocompletionExtension(), - ], + doc: this.value || this.placeholder, + extensions, }); this.editor = new EditorView({ @@ -226,6 +267,10 @@ export default mixins(linterExtension, completerExtension, workflowHelpers).exte <style lang="scss" module> .code-node-editor-container { position: relative; + + & > div { + height: 100%; + } } .ask-ai-button { diff --git a/packages/editor-ui/src/components/CodeNodeEditor/baseExtensions.ts b/packages/editor-ui/src/components/CodeNodeEditor/baseExtensions.ts index c5a3c4816a0fc..1d0c4872df6a8 100644 --- a/packages/editor-ui/src/components/CodeNodeEditor/baseExtensions.ts +++ b/packages/editor-ui/src/components/CodeNodeEditor/baseExtensions.ts @@ -18,21 +18,26 @@ import { deleteCharBackward, } from '@codemirror/commands'; import { lintGutter } from '@codemirror/lint'; +import type { Extension } from '@codemirror/state'; import { codeInputHandler } from '@/plugins/codemirror/inputHandlers/code.inputHandler'; -export const baseExtensions = [ +export const readOnlyEditorExtensions: readonly Extension[] = [ lineNumbers(), - highlightActiveLineGutter(), + EditorView.lineWrapping, highlightSpecialChars(), +]; + +export const writableEditorExtensions: readonly Extension[] = [ history(), - foldGutter(), lintGutter(), + foldGutter(), codeInputHandler(), dropCursor(), indentOnInput(), bracketMatching(), highlightActiveLine(), + highlightActiveLineGutter(), keymap.of([ { key: 'Enter', run: insertNewlineAndIndent }, { key: 'Tab', run: acceptCompletion }, @@ -42,5 +47,4 @@ export const baseExtensions = [ { key: 'Backspace', run: deleteCharBackward, shift: deleteCharBackward }, indentWithTab, ]), - EditorView.lineWrapping, ]; diff --git a/packages/editor-ui/src/components/CodeNodeEditor/completer.ts b/packages/editor-ui/src/components/CodeNodeEditor/completer.ts index efebc8e76d7ac..d4a9875d43ba2 100644 --- a/packages/editor-ui/src/components/CodeNodeEditor/completer.ts +++ b/packages/editor-ui/src/components/CodeNodeEditor/completer.ts @@ -33,7 +33,12 @@ export const completerExtension = mixins( jsonFieldCompletions, ).extend({ methods: { - autocompletionExtension(): Extension { + autocompletionExtension(language: 'javaScript' | 'python'): Extension { + const completions = []; + if (language === 'javaScript') { + completions.push(jsSnippets, localCompletionSource); + } + return autocompletion({ compareCompletions: (a: Completion, b: Completion) => { if (/\.json$|id$|id['"]\]$/.test(a.label)) return 0; @@ -41,8 +46,7 @@ export const completerExtension = mixins( return a.label.localeCompare(b.label); }, override: [ - jsSnippets, - localCompletionSource, + ...completions, // core this.itemCompletions, diff --git a/packages/editor-ui/src/components/CodeNodeEditor/completions/base.completions.ts b/packages/editor-ui/src/components/CodeNodeEditor/completions/base.completions.ts index a9401714ebce3..38ecdfb02a312 100644 --- a/packages/editor-ui/src/components/CodeNodeEditor/completions/base.completions.ts +++ b/packages/editor-ui/src/components/CodeNodeEditor/completions/base.completions.ts @@ -7,7 +7,7 @@ import type { CodeNodeEditorMixin } from '../types'; import { mapStores } from 'pinia'; import { useWorkflowsStore } from '@/stores/workflows'; -function getAutocompletableNodeNames(nodes: INodeUi[]) { +function getAutoCompletableNodeNames(nodes: INodeUi[]) { return nodes .filter((node: INodeUi) => !NODE_TYPES_EXCLUDED_FROM_AUTOCOMPLETION.includes(node.type)) .map((node: INodeUi) => node.name); @@ -49,54 +49,55 @@ export const baseCompletions = (Vue as CodeNodeEditorMixin).extend({ * - Complete `$` to `$json $binary $itemIndex` in single-item mode. */ baseCompletions(context: CompletionContext): CompletionResult | null { - const preCursor = context.matchBefore(/\$\w*/); + const prefix = this.language === 'python' ? '_' : '$'; + const preCursor = context.matchBefore(new RegExp(`\\${prefix}\\w*`)); if (!preCursor || (preCursor.from === preCursor.to && !context.explicit)) return null; const TOP_LEVEL_COMPLETIONS_IN_BOTH_MODES: Completion[] = [ { - label: '$execution', + label: `${prefix}execution`, info: this.$locale.baseText('codeNodeEditor.completer.$execution'), }, - { label: '$input', info: this.$locale.baseText('codeNodeEditor.completer.$input') }, + { label: `${prefix}input`, info: this.$locale.baseText('codeNodeEditor.completer.$input') }, { - label: '$prevNode', + label: `${prefix}prevNode`, info: this.$locale.baseText('codeNodeEditor.completer.$prevNode'), }, { - label: '$workflow', + label: `${prefix}workflow`, info: this.$locale.baseText('codeNodeEditor.completer.$workflow'), }, { - label: '$vars', + label: `${prefix}vars`, info: this.$locale.baseText('codeNodeEditor.completer.$vars'), }, { - label: '$now', + label: `${prefix}now`, info: this.$locale.baseText('codeNodeEditor.completer.$now'), }, { - label: '$today', + label: `${prefix}today`, info: this.$locale.baseText('codeNodeEditor.completer.$today'), }, { - label: '$jmespath()', + label: `${prefix}jmespath()`, info: this.$locale.baseText('codeNodeEditor.completer.$jmespath'), }, { - label: '$if()', + label: `${prefix}if()`, info: this.$locale.baseText('codeNodeEditor.completer.$if'), }, { - label: '$min()', + label: `${prefix}min()`, info: this.$locale.baseText('codeNodeEditor.completer.$min'), }, { - label: '$max()', + label: `${prefix}max()`, info: this.$locale.baseText('codeNodeEditor.completer.$max'), }, { - label: '$runIndex', + label: `${prefix}runIndex`, info: this.$locale.baseText('codeNodeEditor.completer.$runIndex'), }, ]; @@ -104,9 +105,9 @@ export const baseCompletions = (Vue as CodeNodeEditorMixin).extend({ const options: Completion[] = TOP_LEVEL_COMPLETIONS_IN_BOTH_MODES.map(addVarType); options.push( - ...getAutocompletableNodeNames(this.workflowsStore.allNodes).map((nodeName) => { + ...getAutoCompletableNodeNames(this.workflowsStore.allNodes).map((nodeName) => { return { - label: `$('${nodeName}')`, + label: `${prefix}('${nodeName}')`, type: 'variable', info: this.$locale.baseText('codeNodeEditor.completer.$()', { interpolate: { nodeName }, @@ -117,10 +118,10 @@ export const baseCompletions = (Vue as CodeNodeEditorMixin).extend({ if (this.mode === 'runOnceForEachItem') { const TOP_LEVEL_COMPLETIONS_IN_SINGLE_ITEM_MODE = [ - { label: '$json' }, - { label: '$binary' }, + { label: `${prefix}json` }, + { label: `${prefix}binary` }, { - label: '$itemIndex', + label: `${prefix}itemIndex`, info: this.$locale.baseText('codeNodeEditor.completer.$itemIndex'), }, ]; @@ -138,14 +139,15 @@ export const baseCompletions = (Vue as CodeNodeEditorMixin).extend({ * Complete `$(` to `$('nodeName')`. */ nodeSelectorCompletions(context: CompletionContext): CompletionResult | null { - const preCursor = context.matchBefore(/\$\(.*/); + const prefix = this.language === 'python' ? '_' : '$'; + const preCursor = context.matchBefore(new RegExp(`\\${prefix}\\(.*`)); if (!preCursor || (preCursor.from === preCursor.to && !context.explicit)) return null; - const options: Completion[] = getAutocompletableNodeNames(this.workflowsStore.allNodes).map( + const options: Completion[] = getAutoCompletableNodeNames(this.workflowsStore.allNodes).map( (nodeName) => { return { - label: `$('${nodeName}')`, + label: `${prefix}('${nodeName}')`, type: 'variable', info: this.$locale.baseText('codeNodeEditor.completer.$()', { interpolate: { nodeName }, diff --git a/packages/editor-ui/src/components/CodeNodeEditor/completions/itemField.completions.ts b/packages/editor-ui/src/components/CodeNodeEditor/completions/itemField.completions.ts index 505a4ed02cf08..c52d520b6ab83 100644 --- a/packages/editor-ui/src/components/CodeNodeEditor/completions/itemField.completions.ts +++ b/packages/editor-ui/src/components/CodeNodeEditor/completions/itemField.completions.ts @@ -50,10 +50,11 @@ export const itemFieldCompletions = (Vue as CodeNodeEditorMixin).extend({ * - Complete `$input.item.` to `.json .binary`. */ inputMethodCompletions(context: CompletionContext): CompletionResult | null { + const prefix = this.language === 'python' ? '_' : '$'; const patterns = { - first: /\$input\.first\(\)\..*/, - last: /\$input\.last\(\)\..*/, - item: /\$input\.item\..*/, + first: new RegExp(`\\${prefix}input\\.first\\(\\)\\..*`), + last: new RegExp(`\\${prefix}input\\.last\\(\\)\\..*`), + item: new RegExp(`\\${prefix}item\\.first\\(\\)\\..*`), all: /\$input\.all\(\)\[(?<index>\w+)\]\..*/, }; @@ -64,11 +65,11 @@ export const itemFieldCompletions = (Vue as CodeNodeEditorMixin).extend({ let replacementBase = ''; - if (name === 'item') replacementBase = '$input.item'; + if (name === 'item') replacementBase = `${prefix}input.item`; - if (name === 'first') replacementBase = '$input.first()'; + if (name === 'first') replacementBase = `${prefix}input.first()`; - if (name === 'last') replacementBase = '$input.last()'; + if (name === 'last') replacementBase = `${prefix}input.last()`; if (name === 'all') { const match = preCursor.text.match(regex); @@ -77,7 +78,7 @@ export const itemFieldCompletions = (Vue as CodeNodeEditorMixin).extend({ const { index } = match.groups; - replacementBase = `$input.all()[${index}]`; + replacementBase = `${prefix}input.all()[${index}]`; } const options: Completion[] = [ diff --git a/packages/editor-ui/src/components/CodeNodeEditor/completions/require.completions.ts b/packages/editor-ui/src/components/CodeNodeEditor/completions/require.completions.ts index c9cccc174fc1a..081c2c6bb8363 100644 --- a/packages/editor-ui/src/components/CodeNodeEditor/completions/require.completions.ts +++ b/packages/editor-ui/src/components/CodeNodeEditor/completions/require.completions.ts @@ -1,5 +1,5 @@ import Vue from 'vue'; -import { AUTOCOMPLETABLE_BUILT_IN_MODULES } from '../constants'; +import { AUTOCOMPLETABLE_BUILT_IN_MODULES_JS } from '../constants'; import type { Completion, CompletionContext, CompletionResult } from '@codemirror/autocomplete'; import type { CodeNodeEditorMixin } from '../types'; import { useSettingsStore } from '@/stores/settings'; @@ -25,7 +25,7 @@ export const requireCompletions = (Vue as CodeNodeEditorMixin).extend({ if (allowedModules.builtIn) { if (allowedModules.builtIn.includes('*')) { - options.push(...AUTOCOMPLETABLE_BUILT_IN_MODULES.map(toOption)); + options.push(...AUTOCOMPLETABLE_BUILT_IN_MODULES_JS.map(toOption)); } else if (allowedModules?.builtIn?.length > 0) { options.push(...allowedModules.builtIn.map(toOption)); } diff --git a/packages/editor-ui/src/components/CodeNodeEditor/constants.ts b/packages/editor-ui/src/components/CodeNodeEditor/constants.ts index 20ce31b03b617..177f113bf2a69 100644 --- a/packages/editor-ui/src/components/CodeNodeEditor/constants.ts +++ b/packages/editor-ui/src/components/CodeNodeEditor/constants.ts @@ -1,9 +1,10 @@ import { STICKY_NODE_TYPE } from '@/constants'; import type { Diagnostic } from '@codemirror/lint'; +import type { CodeExecutionMode, CodeNodeEditorLanguage } from 'n8n-workflow'; export const NODE_TYPES_EXCLUDED_FROM_AUTOCOMPLETION = [STICKY_NODE_TYPE]; -export const AUTOCOMPLETABLE_BUILT_IN_MODULES = [ +export const AUTOCOMPLETABLE_BUILT_IN_MODULES_JS = [ 'console', 'constants', 'crypto', @@ -34,20 +35,32 @@ export const DEFAULT_LINTER_DELAY_IN_MS = 300; */ export const OFFSET_FOR_SCRIPT_WRAPPER = 'module.exports = async function() {'.length; -export const ALL_ITEMS_PLACEHOLDER = ` -// Loop over input items and add a new field -// called 'myNewField' to the JSON of each one +export const CODE_PLACEHOLDERS: Partial< + Record<CodeNodeEditorLanguage, Record<CodeExecutionMode, string>> +> = { + javaScript: { + runOnceForAllItems: ` +// Loop over input items and add a new field called 'myNewField' to the JSON of each one for (const item of $input.all()) { item.json.myNewField = 1; } -return $input.all(); -`.trim(); - -export const EACH_ITEM_PLACEHOLDER = ` -// Add a new field called 'myNewField' to the -// JSON of the item +return $input.all();`.trim(), + runOnceForEachItem: ` +// Add a new field called 'myNewField' to the JSON of the item $input.item.json.myNewField = 1; -return $input.item; -`.trim(); +return $input.item;`.trim(), + }, + python: { + runOnceForAllItems: ` +# Loop over input items and add a new field called 'myNewField' to the JSON of each one +for item in _input.all(): + item.json.myNewField = 1 +return _input.all()`.trim(), + runOnceForEachItem: ` +# Add a new field called 'myNewField' to the JSON of the item +_input.item.json.myNewField = 1 +return _input.item`.trim(), + }, +}; diff --git a/packages/editor-ui/src/components/CodeNodeEditor/linter.ts b/packages/editor-ui/src/components/CodeNodeEditor/linter.ts index d33a5c89506de..d7fed97dbe5c0 100644 --- a/packages/editor-ui/src/components/CodeNodeEditor/linter.ts +++ b/packages/editor-ui/src/components/CodeNodeEditor/linter.ts @@ -1,22 +1,27 @@ import Vue from 'vue'; -import { Diagnostic, linter as createLinter } from '@codemirror/lint'; +import type { Diagnostic } from '@codemirror/lint'; +import { linter as createLinter } from '@codemirror/lint'; +import { jsonParseLinter } from '@codemirror/lang-json'; +import type { EditorView } from '@codemirror/view'; import * as esprima from 'esprima-next'; +import type { Node } from 'estree'; +import type { CodeNodeEditorLanguage } from 'n8n-workflow'; -import { - DEFAULT_LINTER_DELAY_IN_MS, - DEFAULT_LINTER_SEVERITY, - OFFSET_FOR_SCRIPT_WRAPPER, -} from './constants'; +import { DEFAULT_LINTER_DELAY_IN_MS, DEFAULT_LINTER_SEVERITY } from './constants'; +import { OFFSET_FOR_SCRIPT_WRAPPER } from './constants'; import { walk } from './utils'; - -import type { EditorView } from '@codemirror/view'; -import type { Node } from 'estree'; import type { CodeNodeEditorMixin, RangeNode } from './types'; export const linterExtension = (Vue as CodeNodeEditorMixin).extend({ methods: { - linterExtension() { - return createLinter(this.lintSource, { delay: DEFAULT_LINTER_DELAY_IN_MS }); + createLinter(language: CodeNodeEditorLanguage) { + switch (language) { + case 'javaScript': + return createLinter(this.lintSource, { delay: DEFAULT_LINTER_DELAY_IN_MS }); + case 'json': + return createLinter(jsonParseLinter()); + } + return undefined; }, lintSource(editorView: EditorView): Diagnostic[] { diff --git a/packages/editor-ui/src/components/CodeNodeEditor/theme.ts b/packages/editor-ui/src/components/CodeNodeEditor/theme.ts index 96fc32e0b3f4b..166d214be89ac 100644 --- a/packages/editor-ui/src/components/CodeNodeEditor/theme.ts +++ b/packages/editor-ui/src/components/CodeNodeEditor/theme.ts @@ -29,7 +29,11 @@ const BASE_STYLING = { const cssStyleDeclaration = getComputedStyle(document.documentElement); -export const CODE_NODE_EDITOR_THEME = [ +interface ThemeSettings { + isReadOnly?: boolean; +} + +export const codeNodeEditorTheme = ({ isReadOnly }: ThemeSettings) => [ EditorView.theme({ '&': { 'font-size': BASE_STYLING.fontSize, @@ -37,6 +41,7 @@ export const CODE_NODE_EDITOR_THEME = [ borderRadius: cssStyleDeclaration.getPropertyValue('--border-radius-base'), backgroundColor: 'var(--color-code-background)', color: 'var(--color-code-foreground)', + height: '100%', }, '.cm-content': { fontFamily: BASE_STYLING.fontFamily, @@ -48,6 +53,9 @@ export const CODE_NODE_EDITOR_THEME = [ '&.cm-focused .cm-selectionBackgroundm .cm-selectionBackground, .cm-content ::selection': { backgroundColor: 'var(--color-code-selection)', }, + '&.cm-editor': { + ...(isReadOnly ? { backgroundColor: 'var(--color-code-background-readonly)' } : {}), + }, '&.cm-editor.cm-focused': { outline: 'none', borderColor: 'var(--color-secondary)', @@ -59,7 +67,9 @@ export const CODE_NODE_EDITOR_THEME = [ backgroundColor: 'var(--color-code-lineHighlight)', }, '.cm-gutters': { - backgroundColor: 'var(--color-code-gutterBackground)', + backgroundColor: isReadOnly + ? 'var(--color-code-background-readonly)' + : 'var(--color-code-gutterBackground)', color: 'var(--color-code-gutterForeground)', borderRadius: 'var(--border-radius-base)', }, @@ -69,7 +79,8 @@ export const CODE_NODE_EDITOR_THEME = [ }, '.cm-scroller': { overflow: 'auto', - maxHeight: BASE_STYLING.maxHeight, + maxHeight: '100%', + ...(isReadOnly ? {} : { minHeight: '10em' }), }, '.cm-diagnosticAction': { backgroundColor: BASE_STYLING.diagnosticButton.backgroundColor, diff --git a/packages/editor-ui/src/components/CodeNodeEditor/types.ts b/packages/editor-ui/src/components/CodeNodeEditor/types.ts index 38d5e669b89b4..f4a0e634f32f1 100644 --- a/packages/editor-ui/src/components/CodeNodeEditor/types.ts +++ b/packages/editor-ui/src/components/CodeNodeEditor/types.ts @@ -1,13 +1,14 @@ import type { EditorView } from '@codemirror/view'; import type { I18nClass } from '@/plugins/i18n'; -import type { Workflow } from 'n8n-workflow'; +import type { Workflow, CodeExecutionMode, CodeNodeEditorLanguage } from 'n8n-workflow'; import type { Node } from 'estree'; export type CodeNodeEditorMixin = Vue.VueConstructor< Vue & { $locale: I18nClass; editor: EditorView | null; - mode: 'runOnceForAllItems' | 'runOnceForEachItem'; + mode: CodeExecutionMode; + language: CodeNodeEditorLanguage; getCurrentWorkflow(): Workflow; } >; diff --git a/packages/editor-ui/src/components/CodeNodeEditor/utils.ts b/packages/editor-ui/src/components/CodeNodeEditor/utils.ts index b6bd5b473038b..be8560a61e352 100644 --- a/packages/editor-ui/src/components/CodeNodeEditor/utils.ts +++ b/packages/editor-ui/src/components/CodeNodeEditor/utils.ts @@ -1,4 +1,4 @@ -import * as esprima from 'esprima-next'; +import type * as esprima from 'esprima-next'; import type { Completion } from '@codemirror/autocomplete'; import type { Node } from 'estree'; import type { RangeNode } from './types'; diff --git a/packages/editor-ui/src/components/CollectionParameter.vue b/packages/editor-ui/src/components/CollectionParameter.vue index edcadf6472933..919da56fd74e5 100644 --- a/packages/editor-ui/src/components/CollectionParameter.vue +++ b/packages/editor-ui/src/components/CollectionParameter.vue @@ -46,16 +46,17 @@ </template> <script lang="ts"> -import { INodeUi, IUpdateInformation } from '@/Interface'; +import type { INodeUi, IUpdateInformation } from '@/Interface'; -import { deepCopy, INodeProperties, INodePropertyOptions } from 'n8n-workflow'; +import type { INodeProperties, INodePropertyOptions } from 'n8n-workflow'; +import { deepCopy } from 'n8n-workflow'; import { nodeHelpers } from '@/mixins/nodeHelpers'; import { get } from 'lodash-es'; import mixins from 'vue-typed-mixins'; -import { Component } from 'vue'; +import type { Component } from 'vue'; import { mapStores } from 'pinia'; import { useNDVStore } from '@/stores/ndv'; diff --git a/packages/editor-ui/src/components/CollectionsCarousel.vue b/packages/editor-ui/src/components/CollectionsCarousel.vue index 00505c2781c65..1672ac976e0fc 100644 --- a/packages/editor-ui/src/components/CollectionsCarousel.vue +++ b/packages/editor-ui/src/components/CollectionsCarousel.vue @@ -26,8 +26,8 @@ </template> <script lang="ts"> -import { PropType } from 'vue'; -import { ITemplatesCollection } from '@/Interface'; +import type { PropType } from 'vue'; +import type { ITemplatesCollection } from '@/Interface'; import Card from '@/components/CollectionWorkflowCard.vue'; import CollectionCard from '@/components/CollectionCard.vue'; import VueAgile from 'vue-agile'; @@ -35,6 +35,8 @@ import VueAgile from 'vue-agile'; import { genericHelpers } from '@/mixins/genericHelpers'; import mixins from 'vue-typed-mixins'; +type SliderRef = InstanceType<typeof VueAgile>; + export default mixins(genericHelpers).extend({ name: 'CollectionsCarousel', props: { @@ -97,22 +99,23 @@ export default mixins(genericHelpers).extend({ }, mounted() { this.$nextTick(() => { - const slider = this.$refs.slider; - if (!slider) { + const sliderRef = this.$refs.slider as SliderRef | undefined; + if (!sliderRef) { return; } - // @ts-ignore - this.listElement = slider.$el.querySelector('.agile__list'); + + this.listElement = sliderRef.$el.querySelector('.agile__list'); if (this.listElement) { this.listElement.addEventListener('scroll', this.updateCarouselScroll); } }); }, beforeDestroy() { - if (this.$refs.slider) { - // @ts-ignore - this.$refs.slider.destroy(); + const sliderRef = this.$refs.slider as SliderRef | undefined; + if (sliderRef) { + sliderRef.destroy(); } + window.removeEventListener('scroll', this.updateCarouselScroll); }, }); diff --git a/packages/editor-ui/src/components/CommunityPackageCard.vue b/packages/editor-ui/src/components/CommunityPackageCard.vue index eb11303650006..8fef13ae282ea 100644 --- a/packages/editor-ui/src/components/CommunityPackageCard.vue +++ b/packages/editor-ui/src/components/CommunityPackageCard.vue @@ -63,7 +63,7 @@ <script lang="ts"> import { useUIStore } from '@/stores/ui'; -import { PublicInstalledPackage } from 'n8n-workflow'; +import type { PublicInstalledPackage } from 'n8n-workflow'; import { mapStores } from 'pinia'; import mixins from 'vue-typed-mixins'; import { NPM_PACKAGE_DOCS_BASE_URL, COMMUNITY_PACKAGE_MANAGE_ACTIONS } from '../constants'; diff --git a/packages/editor-ui/src/components/ContactPromptModal.vue b/packages/editor-ui/src/components/ContactPromptModal.vue index 3978430ae61d1..20e5c91b92e9c 100644 --- a/packages/editor-ui/src/components/ContactPromptModal.vue +++ b/packages/editor-ui/src/components/ContactPromptModal.vue @@ -35,7 +35,7 @@ <script lang="ts"> import mixins from 'vue-typed-mixins'; -import { IN8nPromptResponse } from '@/Interface'; +import type { IN8nPromptResponse } from '@/Interface'; import { VALID_EMAIL_REGEX } from '@/constants'; import { workflowHelpers } from '@/mixins/workflowHelpers'; import Modal from './Modal.vue'; diff --git a/packages/editor-ui/src/components/CredentialCard.vue b/packages/editor-ui/src/components/CredentialCard.vue index 55d69ddd91fd1..c274a9c3e8a8c 100644 --- a/packages/editor-ui/src/components/CredentialCard.vue +++ b/packages/editor-ui/src/components/CredentialCard.vue @@ -32,12 +32,13 @@ <script lang="ts"> import mixins from 'vue-typed-mixins'; -import { ICredentialsResponse, IUser } from '@/Interface'; -import { ICredentialType } from 'n8n-workflow'; +import type { ICredentialsResponse, IUser } from '@/Interface'; +import type { ICredentialType } from 'n8n-workflow'; import { EnterpriseEditionFeature } from '@/constants'; import { showMessage } from '@/mixins/showMessage'; import CredentialIcon from '@/components/CredentialIcon.vue'; -import { getCredentialPermissions, IPermissions } from '@/permissions'; +import type { IPermissions } from '@/permissions'; +import { getCredentialPermissions } from '@/permissions'; import dateformat from 'dateformat'; import { mapStores } from 'pinia'; import { useUIStore } from '@/stores/ui'; diff --git a/packages/editor-ui/src/components/CredentialEdit/AuthTypeSelector.vue b/packages/editor-ui/src/components/CredentialEdit/AuthTypeSelector.vue index 1302e574966d4..a462bca0ba578 100644 --- a/packages/editor-ui/src/components/CredentialEdit/AuthTypeSelector.vue +++ b/packages/editor-ui/src/components/CredentialEdit/AuthTypeSelector.vue @@ -1,6 +1,6 @@ <script setup lang="ts"> import ParameterInputFull from '@/components/ParameterInputFull.vue'; -import { IUpdateInformation, NodeAuthenticationOption } from '@/Interface'; +import type { IUpdateInformation, NodeAuthenticationOption } from '@/Interface'; import { useNDVStore } from '@/stores/ndv'; import { useNodeTypesStore } from '@/stores/nodeTypes'; import { @@ -9,7 +9,7 @@ import { getNodeAuthOptions, isAuthRelatedParameter, } from '@/utils'; -import { INodeProperties, INodeTypeDescription, NodeParameterValue } from 'n8n-workflow'; +import type { INodeProperties, INodeTypeDescription, NodeParameterValue } from 'n8n-workflow'; import { computed, onMounted, ref } from 'vue'; import Vue from 'vue'; diff --git a/packages/editor-ui/src/components/CredentialEdit/CredentialConfig.vue b/packages/editor-ui/src/components/CredentialEdit/CredentialConfig.vue index e2255bd90012e..306de0582dafe 100644 --- a/packages/editor-ui/src/components/CredentialEdit/CredentialConfig.vue +++ b/packages/editor-ui/src/components/CredentialEdit/CredentialConfig.vue @@ -58,6 +58,7 @@ :buttonTitle="$locale.baseText('credentialEdit.credentialConfig.retryCredentialTest')" :buttonLoading="isRetesting" @click="$emit('retest')" + data-test-id="credentials-config-container-test-success" /> <template v-if="credentialPermissions.updateConnection"> @@ -128,18 +129,16 @@ </template> <script lang="ts"> -import { ICredentialType, INodeTypeDescription } from 'n8n-workflow'; +import type { ICredentialType, INodeTypeDescription } from 'n8n-workflow'; import { getAppNameFromCredType, isCommunityPackageName } from '@/utils'; import Banner from '../Banner.vue'; import CopyInput from '../CopyInput.vue'; import CredentialInputs from './CredentialInputs.vue'; import OauthButton from './OauthButton.vue'; -import { restApi } from '@/mixins/restApi'; import { addCredentialTranslation } from '@/plugins/i18n'; -import mixins from 'vue-typed-mixins'; import { BUILTIN_CREDENTIALS_DOCS_URL, DOCS_DOMAIN, EnterpriseEditionFeature } from '@/constants'; -import { IPermissions } from '@/permissions'; +import type { IPermissions } from '@/permissions'; import { mapStores } from 'pinia'; import { useUIStore } from '@/stores/ui'; import { useWorkflowsStore } from '@/stores/workflows'; @@ -147,12 +146,12 @@ import { useRootStore } from '@/stores/n8nRootStore'; import { useNDVStore } from '@/stores/ndv'; import { useCredentialsStore } from '@/stores/credentials'; import { useNodeTypesStore } from '@/stores/nodeTypes'; -import { ICredentialsResponse, IUpdateInformation, NodeAuthenticationOption } from '@/Interface'; -import ParameterInputFull from '@/components/ParameterInputFull.vue'; +import type { ICredentialsResponse } from '@/Interface'; import AuthTypeSelector from '@/components/CredentialEdit/AuthTypeSelector.vue'; import GoogleAuthButton from './GoogleAuthButton.vue'; +import Vue from 'vue'; -export default mixins(restApi).extend({ +export default Vue.extend({ name: 'CredentialConfig', components: { AuthTypeSelector, @@ -160,7 +159,6 @@ export default mixins(restApi).extend({ CopyInput, CredentialInputs, OauthButton, - ParameterInputFull, GoogleAuthButton, }, props: { @@ -226,7 +224,9 @@ export default mixins(restApi).extend({ if (this.$locale.exists(key)) return; - const credTranslation = await this.restApi().getCredentialTranslation(this.credentialType.name); + const credTranslation = await this.credentialsStore.getCredentialTranslation( + this.credentialType.name, + ); addCredentialTranslation( { [this.credentialType.name]: credTranslation }, diff --git a/packages/editor-ui/src/components/CredentialEdit/CredentialEdit.vue b/packages/editor-ui/src/components/CredentialEdit/CredentialEdit.vue index 29b970f90cf90..6614c32a4cedb 100644 --- a/packages/editor-ui/src/components/CredentialEdit/CredentialEdit.vue +++ b/packages/editor-ui/src/components/CredentialEdit/CredentialEdit.vue @@ -111,9 +111,9 @@ <script lang="ts"> import Vue from 'vue'; -import type { ICredentialsResponse, IUser, NewCredentialsModal } from '@/Interface'; +import type { ICredentialsResponse, IUser } from '@/Interface'; -import { +import type { CredentialInformation, ICredentialDataDecryptedObject, ICredentialNodeAccess, @@ -125,8 +125,8 @@ import { INodeProperties, INodeTypeDescription, ITelemetryTrackProperties, - NodeHelpers, } from 'n8n-workflow'; +import { NodeHelpers } from 'n8n-workflow'; import CredentialIcon from '../CredentialIcon.vue'; import mixins from 'vue-typed-mixins'; @@ -140,10 +140,12 @@ import SaveButton from '../SaveButton.vue'; import Modal from '../Modal.vue'; import InlineNameEdit from '../InlineNameEdit.vue'; import { CREDENTIAL_EDIT_MODAL_KEY, EnterpriseEditionFeature } from '@/constants'; -import { IDataObject } from 'n8n-workflow'; +import type { IDataObject } from 'n8n-workflow'; import FeatureComingSoon from '../FeatureComingSoon.vue'; -import { getCredentialPermissions, IPermissions } from '@/permissions'; -import { createEventBus, IMenuItem } from 'n8n-design-system'; +import type { IPermissions } from '@/permissions'; +import { getCredentialPermissions } from '@/permissions'; +import type { IMenuItem } from 'n8n-design-system'; +import { createEventBus } from 'n8n-design-system'; import { mapStores } from 'pinia'; import { useUIStore } from '@/stores/ui'; import { useSettingsStore } from '@/stores/settings'; @@ -254,7 +256,9 @@ export default mixins(showMessage, nodeHelpers).extend({ setTimeout(() => { if (this.credentialId) { - if (!this.requiredPropertiesFilled) { + if (!this.requiredPropertiesFilled && this.credentialPermissions.isOwner === true) { + // sharees can't see properties, so this check would always fail for them + // if the credential contains required fields. this.showValidationWarning = true; } else { this.retestCredential(); @@ -345,6 +349,10 @@ export default mixins(showMessage, nodeHelpers).extend({ }; }, isCredentialTestable(): boolean { + // Sharees can always test since they can't see the data. + if (this.credentialPermissions.isOwner === false) { + return true; + } if (this.isOAuthType || !this.requiredPropertiesFilled) { return false; } @@ -672,18 +680,18 @@ export default mixins(showMessage, nodeHelpers).extend({ scrollToTop() { setTimeout(() => { - const content = this.$refs.content as Element; - if (content) { - content.scrollTop = 0; + const contentRef = this.$refs.content as Element | undefined; + if (contentRef) { + contentRef.scrollTop = 0; } }, 0); }, scrollToBottom() { setTimeout(() => { - const content = this.$refs.content as Element; - if (content) { - content.scrollTop = content.scrollHeight; + const contentRef = this.$refs.content as Element | undefined; + if (contentRef) { + contentRef.scrollTop = contentRef.scrollHeight; } }, 0); }, diff --git a/packages/editor-ui/src/components/CredentialEdit/CredentialInfo.vue b/packages/editor-ui/src/components/CredentialEdit/CredentialInfo.vue index 5b329fd1e1279..bb798284fc343 100644 --- a/packages/editor-ui/src/components/CredentialEdit/CredentialInfo.vue +++ b/packages/editor-ui/src/components/CredentialEdit/CredentialInfo.vue @@ -68,12 +68,12 @@ </template> <script lang="ts"> -import Vue from 'vue'; +import { defineComponent } from 'vue'; import TimeAgo from '../TimeAgo.vue'; -import { INodeTypeDescription } from 'n8n-workflow'; +import type { INodeTypeDescription } from 'n8n-workflow'; -export default Vue.extend({ +export default defineComponent({ name: 'CredentialInfo', props: ['nodesWithAccess', 'nodeAccess', 'currentCredential', 'credentialPermissions'], components: { diff --git a/packages/editor-ui/src/components/CredentialEdit/CredentialInputs.vue b/packages/editor-ui/src/components/CredentialEdit/CredentialInputs.vue index 7b5451f03380a..fd471e5f5735d 100644 --- a/packages/editor-ui/src/components/CredentialEdit/CredentialInputs.vue +++ b/packages/editor-ui/src/components/CredentialEdit/CredentialInputs.vue @@ -24,12 +24,12 @@ </template> <script lang="ts"> -import Vue from 'vue'; -import { IParameterLabel } from 'n8n-workflow'; -import { IUpdateInformation } from '@/Interface'; +import { defineComponent } from 'vue'; +import type { IParameterLabel } from 'n8n-workflow'; +import type { IUpdateInformation } from '@/Interface'; import ParameterInputExpanded from '../ParameterInputExpanded.vue'; -export default Vue.extend({ +export default defineComponent({ name: 'CredentialsInput', props: [ 'credentialProperties', diff --git a/packages/editor-ui/src/components/CredentialEdit/CredentialSharing.ee.vue b/packages/editor-ui/src/components/CredentialEdit/CredentialSharing.ee.vue index 5bea9fa37d62f..6d3a5db2708c7 100644 --- a/packages/editor-ui/src/components/CredentialEdit/CredentialSharing.ee.vue +++ b/packages/editor-ui/src/components/CredentialEdit/CredentialSharing.ee.vue @@ -80,7 +80,7 @@ </template> <script lang="ts"> -import { IUser, IUserListAction, UIState } from '@/Interface'; +import type { IUser, IUserListAction } from '@/Interface'; import mixins from 'vue-typed-mixins'; import { showMessage } from '@/mixins/showMessage'; import { mapStores } from 'pinia'; @@ -90,7 +90,6 @@ import { useUIStore } from '@/stores/ui'; import { useCredentialsStore } from '@/stores/credentials'; import { useUsageStore } from '@/stores/usage'; import { EnterpriseEditionFeature, VIEWS } from '@/constants'; -import { BaseTextKey } from '@/plugins/i18n'; export default mixins(showMessage).extend({ name: 'CredentialSharing', diff --git a/packages/editor-ui/src/components/CredentialEdit/OauthButton.vue b/packages/editor-ui/src/components/CredentialEdit/OauthButton.vue index ca0586de99b81..129752c06e0ed 100644 --- a/packages/editor-ui/src/components/CredentialEdit/OauthButton.vue +++ b/packages/editor-ui/src/components/CredentialEdit/OauthButton.vue @@ -12,7 +12,6 @@ <script lang="ts" setup> import GoogleAuthButton from './GoogleAuthButton.vue'; -import Vue from 'vue'; defineProps<{ isGoogleOAuthType: boolean; diff --git a/packages/editor-ui/src/components/CredentialIcon.vue b/packages/editor-ui/src/components/CredentialIcon.vue index b6efc3ca30eb3..5041d365efff3 100644 --- a/packages/editor-ui/src/components/CredentialIcon.vue +++ b/packages/editor-ui/src/components/CredentialIcon.vue @@ -10,11 +10,11 @@ import { useCredentialsStore } from '@/stores/credentials'; import { useRootStore } from '@/stores/n8nRootStore'; import { useNodeTypesStore } from '@/stores/nodeTypes'; -import { ICredentialType, INodeTypeDescription } from 'n8n-workflow'; +import type { ICredentialType, INodeTypeDescription } from 'n8n-workflow'; import { mapStores } from 'pinia'; -import Vue from 'vue'; +import { defineComponent } from 'vue'; -export default Vue.extend({ +export default defineComponent({ props: { credentialTypeName: { type: String, diff --git a/packages/editor-ui/src/components/CredentialsSelect.vue b/packages/editor-ui/src/components/CredentialsSelect.vue index 559bed74dd1ae..7e55e962ab8ed 100644 --- a/packages/editor-ui/src/components/CredentialsSelect.vue +++ b/packages/editor-ui/src/components/CredentialsSelect.vue @@ -55,14 +55,17 @@ </template> <script lang="ts"> -import { ICredentialType } from 'n8n-workflow'; -import Vue from 'vue'; +import type { ICredentialType } from 'n8n-workflow'; +import { defineComponent } from 'vue'; import ScopesNotice from '@/components/ScopesNotice.vue'; import NodeCredentials from '@/components/NodeCredentials.vue'; import { mapStores } from 'pinia'; import { useCredentialsStore } from '@/stores/credentials'; +import type { N8nSelect } from 'n8n-design-system'; -export default Vue.extend({ +type N8nSelectRef = InstanceType<typeof N8nSelect>; + +export default defineComponent({ name: 'CredentialsSelect', components: { ScopesNotice, @@ -93,9 +96,9 @@ export default Vue.extend({ }, methods: { focus() { - const select = this.$refs.innerSelect as (Vue & HTMLElement) | undefined; - if (select) { - select.focus(); + const selectRef = this.$refs.innerSelect as N8nSelectRef | undefined; + if (selectRef) { + selectRef.focus(); } }, /** diff --git a/packages/editor-ui/src/components/CredentialsSelectModal.vue b/packages/editor-ui/src/components/CredentialsSelectModal.vue index b2470bd0f6e7f..8298633f39f79 100644 --- a/packages/editor-ui/src/components/CredentialsSelectModal.vue +++ b/packages/editor-ui/src/components/CredentialsSelectModal.vue @@ -80,9 +80,9 @@ export default mixins(externalHooks).extend({ this.loading = false; setTimeout(() => { - const element = this.$refs.select as HTMLSelectElement; - if (element) { - element.focus(); + const elementRef = this.$refs.select as HTMLSelectElement | undefined; + if (elementRef) { + elementRef.focus(); } }, 0); }, diff --git a/packages/editor-ui/src/components/DeleteUserModal.vue b/packages/editor-ui/src/components/DeleteUserModal.vue index ba21d42ae6021..2b644b1d11512 100644 --- a/packages/editor-ui/src/components/DeleteUserModal.vue +++ b/packages/editor-ui/src/components/DeleteUserModal.vue @@ -74,7 +74,7 @@ import mixins from 'vue-typed-mixins'; import { showMessage } from '@/mixins/showMessage'; import Modal from './Modal.vue'; -import { IUser } from '../Interface'; +import type { IUser } from '../Interface'; import { mapStores } from 'pinia'; import { useUsersStore } from '@/stores/users'; import { createEventBus } from '@/event-bus'; diff --git a/packages/editor-ui/src/components/Draggable.vue b/packages/editor-ui/src/components/Draggable.vue index 0e9631776aae8..ce5c9ea0c48ff 100644 --- a/packages/editor-ui/src/components/Draggable.vue +++ b/packages/editor-ui/src/components/Draggable.vue @@ -16,15 +16,15 @@ </template> <script lang="ts"> -import { XYPosition } from '@/Interface'; +import type { XYPosition } from '@/Interface'; import { useNDVStore } from '@/stores/ndv'; import { mapStores } from 'pinia'; -import Vue from 'vue'; +import { defineComponent } from 'vue'; // @ts-ignore import Teleport from 'vue2-teleport'; -export default Vue.extend({ +export default defineComponent({ name: 'draggable', components: { Teleport, diff --git a/packages/editor-ui/src/components/DraggableTarget.vue b/packages/editor-ui/src/components/DraggableTarget.vue index 742ff4d515cf4..bec71082ec500 100644 --- a/packages/editor-ui/src/components/DraggableTarget.vue +++ b/packages/editor-ui/src/components/DraggableTarget.vue @@ -7,9 +7,10 @@ <script lang="ts"> import { useNDVStore } from '@/stores/ndv'; import { mapStores } from 'pinia'; -import Vue, { PropType } from 'vue'; +import { defineComponent } from 'vue'; +import type { PropType } from 'vue'; -export default Vue.extend({ +export default defineComponent({ props: { type: { type: String, @@ -55,10 +56,10 @@ export default Vue.extend({ }, methods: { onMouseMove(e: MouseEvent) { - const target = this.$refs.target as HTMLElement; + const targetRef = this.$refs.target as HTMLElement | undefined; - if (target && this.isDragging) { - const dim = target.getBoundingClientRect(); + if (targetRef && this.isDragging) { + const dim = targetRef.getBoundingClientRect(); this.hovering = e.clientX >= dim.left && diff --git a/packages/editor-ui/src/components/DuplicateWorkflowDialog.vue b/packages/editor-ui/src/components/DuplicateWorkflowDialog.vue index 3628ee0c95e55..653030aec09c3 100644 --- a/packages/editor-ui/src/components/DuplicateWorkflowDialog.vue +++ b/packages/editor-ui/src/components/DuplicateWorkflowDialog.vue @@ -56,16 +56,17 @@ import { workflowHelpers } from '@/mixins/workflowHelpers'; import { showMessage } from '@/mixins/showMessage'; import TagsDropdown from '@/components/TagsDropdown.vue'; import Modal from './Modal.vue'; -import { restApi } from '@/mixins/restApi'; import { mapStores } from 'pinia'; import { useSettingsStore } from '@/stores/settings'; import { useWorkflowsStore } from '@/stores/workflows'; -import { IWorkflowDataUpdate } from '@/Interface'; -import { getWorkflowPermissions, IPermissions } from '@/permissions'; +import type { IWorkflowDataUpdate } from '@/Interface'; +import type { IPermissions } from '@/permissions'; +import { getWorkflowPermissions } from '@/permissions'; import { useUsersStore } from '@/stores/users'; import { createEventBus } from '@/event-bus'; +import { useCredentialsStore } from '@/stores'; -export default mixins(showMessage, workflowHelpers, restApi).extend({ +export default mixins(showMessage, workflowHelpers).extend({ components: { TagsDropdown, Modal }, name: 'DuplicateWorkflow', props: ['modalName', 'isActive', 'data'], @@ -87,7 +88,7 @@ export default mixins(showMessage, workflowHelpers, restApi).extend({ this.$nextTick(() => this.focusOnNameInput()); }, computed: { - ...mapStores(useUsersStore, useSettingsStore, useWorkflowsStore), + ...mapStores(useCredentialsStore, useUsersStore, useSettingsStore, useWorkflowsStore), workflowPermissions(): IPermissions { const isEmptyWorkflow = this.data.id === PLACEHOLDER_EMPTY_WORKFLOW_ID; const isCurrentWorkflowEmpty = @@ -115,9 +116,9 @@ export default mixins(showMessage, workflowHelpers, restApi).extend({ this.dropdownBus.emit('focus'); }, focusOnNameInput() { - const input = this.$refs.nameInput as HTMLElement; - if (input && input.focus) { - input.focus(); + const inputRef = this.$refs.nameInput as HTMLElement | undefined; + if (inputRef && inputRef.focus) { + inputRef.focus(); } }, onTagsBlur() { @@ -150,7 +151,7 @@ export default mixins(showMessage, workflowHelpers, restApi).extend({ let workflowToUpdate: IWorkflowDataUpdate | undefined; if (currentWorkflowId !== PLACEHOLDER_EMPTY_WORKFLOW_ID) { const { createdAt, updatedAt, usedCredentials, ...workflow } = - await this.restApi().getWorkflow(this.data.id); + await this.workflowsStore.fetchWorkflow(this.data.id); workflowToUpdate = workflow; this.removeForeignCredentialsFromWorkflow( diff --git a/packages/editor-ui/src/components/EnterpriseEdition.ee.vue b/packages/editor-ui/src/components/EnterpriseEdition.ee.vue index dd418473797de..5e73ce911a3d3 100644 --- a/packages/editor-ui/src/components/EnterpriseEdition.ee.vue +++ b/packages/editor-ui/src/components/EnterpriseEdition.ee.vue @@ -6,12 +6,12 @@ </template> <script lang="ts"> -import Vue from 'vue'; -import { EnterpriseEditionFeature } from '@/constants'; +import { defineComponent } from 'vue'; +import type { EnterpriseEditionFeature } from '@/constants'; import { mapStores } from 'pinia'; import { useSettingsStore } from '@/stores/settings'; -export default Vue.extend({ +export default defineComponent({ name: 'EnterpriseEdition', props: { features: { diff --git a/packages/editor-ui/src/components/Error/NodeErrorView.vue b/packages/editor-ui/src/components/Error/NodeErrorView.vue index c04c6209f7a34..60faab4ba5cfd 100644 --- a/packages/editor-ui/src/components/Error/NodeErrorView.vue +++ b/packages/editor-ui/src/components/Error/NodeErrorView.vue @@ -124,9 +124,8 @@ import { copyPaste } from '@/mixins/copyPaste'; import { showMessage } from '@/mixins/showMessage'; import mixins from 'vue-typed-mixins'; import { MAX_DISPLAY_DATA_SIZE } from '@/constants'; -import { INodeUi } from '@/Interface'; -import { INodeProperties, INodePropertyCollection, INodePropertyOptions } from 'n8n-workflow'; +import type { INodeProperties, INodePropertyCollection, INodePropertyOptions } from 'n8n-workflow'; import { sanitizeHtml } from '@/utils'; import { mapStores } from 'pinia'; import { useNDVStore } from '@/stores/ndv'; diff --git a/packages/editor-ui/src/components/ExecutionFilter.vue b/packages/editor-ui/src/components/ExecutionFilter.vue index 1f76ddf99f8e0..bfe08d61eed10 100644 --- a/packages/editor-ui/src/components/ExecutionFilter.vue +++ b/packages/editor-ui/src/components/ExecutionFilter.vue @@ -1,5 +1,5 @@ <script lang="ts" setup> -import { computed, reactive, onBeforeMount } from 'vue'; +import { computed, reactive, onBeforeMount, ref } from 'vue'; import debounce from 'lodash/debounce'; import type { PopoverPlacement } from 'element-ui/types/popover'; import type { @@ -14,6 +14,7 @@ import { EnterpriseEditionFeature } from '@/constants'; import { useSettingsStore } from '@/stores/settings'; import { useUsageStore } from '@/stores/usage'; import { useUIStore } from '@/stores/ui'; +import { useTelemetry } from '@/composables'; export type ExecutionFilterProps = { workflows?: IWorkflowShortResponse[]; @@ -25,6 +26,9 @@ const DATE_TIME_MASK = 'yyyy-MM-dd HH:mm'; const settingsStore = useSettingsStore(); const usageStore = useUsageStore(); const uiStore = useUIStore(); + +const telemetry = useTelemetry(); + const props = withDefaults(defineProps<ExecutionFilterProps>(), { popoverPlacement: 'bottom', }); @@ -33,6 +37,7 @@ const emit = defineEmits<{ }>(); const debouncedEmit = debounce(emit, 500); +const isCustomDataFilterTracked = ref(false); const isAdvancedExecutionFilterEnabled = computed(() => settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.AdvancedExecutionFilters), ); @@ -109,6 +114,12 @@ const onFilterMetaChange = (index: number, prop: keyof ExecutionFilterMetadata, }; } filter.metadata[index][prop] = value; + + if (!isCustomDataFilterTracked.value) { + telemetry.track('User filtered executions with custom data'); + isCustomDataFilterTracked.value = true; + } + debouncedEmit('filterChanged', filter); }; @@ -129,6 +140,7 @@ const goToUpgrade = () => { }; onBeforeMount(() => { + isCustomDataFilterTracked.value = false; emit('filterChanged', filter); }); </script> diff --git a/packages/editor-ui/src/components/ExecutionsList.vue b/packages/editor-ui/src/components/ExecutionsList.vue index 7fb081d318961..64fac16595537 100644 --- a/packages/editor-ui/src/components/ExecutionsList.vue +++ b/packages/editor-ui/src/components/ExecutionsList.vue @@ -271,11 +271,10 @@ import WorkflowActivator from '@/components/WorkflowActivator.vue'; import ExecutionFilter from '@/components/ExecutionFilter.vue'; import { externalHooks } from '@/mixins/externalHooks'; import { VIEWS, WAIT_TIME_UNLIMITED } from '@/constants'; -import { restApi } from '@/mixins/restApi'; import { genericHelpers } from '@/mixins/genericHelpers'; import { executionHelpers } from '@/mixins/executionsHelpers'; import { showMessage } from '@/mixins/showMessage'; -import { +import type { IExecutionsCurrentSummaryExtended, IExecutionDeleteFilter, IExecutionsListResponse, @@ -292,625 +291,620 @@ import { useWorkflowsStore } from '@/stores/workflows'; import { isEmpty, setPageTitle } from '@/utils'; import { executionFilterToQueryFilter } from '@/utils/executionUtils'; -export default mixins(externalHooks, genericHelpers, executionHelpers, restApi, showMessage).extend( - { - name: 'ExecutionsList', - components: { - ExecutionTime, - WorkflowActivator, - ExecutionFilter, - }, - data() { - return { - finishedExecutions: [] as IExecutionsSummary[], - finishedExecutionsCount: 0, - finishedExecutionsCountEstimated: false, +export default mixins(externalHooks, genericHelpers, executionHelpers, showMessage).extend({ + name: 'ExecutionsList', + components: { + ExecutionTime, + WorkflowActivator, + ExecutionFilter, + }, + data() { + return { + finishedExecutions: [] as IExecutionsSummary[], + finishedExecutionsCount: 0, + finishedExecutionsCountEstimated: false, + + allVisibleSelected: false, + allExistingSelected: false, + autoRefresh: true, + autoRefreshInterval: undefined as undefined | NodeJS.Timer, - allVisibleSelected: false, - allExistingSelected: false, - autoRefresh: true, - autoRefreshInterval: undefined as undefined | NodeJS.Timer, + filter: {} as ExecutionFilterType, - filter: {} as ExecutionFilterType, + isDataLoading: false, - isDataLoading: false, + requestItemsPerRequest: 10, + + selectedItems: {} as { [key: string]: boolean }, + + stoppingExecutions: [] as string[], + workflows: [] as IWorkflowShortResponse[], + }; + }, + mounted() { + setPageTitle(`n8n - ${this.pageTitle}`); + }, + async created() { + await this.loadWorkflows(); + //await this.refreshData(); + this.handleAutoRefreshToggle(); + + this.$externalHooks().run('executionsList.openDialog'); + this.$telemetry.track('User opened Executions log', { + workflow_id: this.workflowsStore.workflowId, + }); + }, + beforeDestroy() { + if (this.autoRefreshInterval) { + clearInterval(this.autoRefreshInterval); + this.autoRefreshInterval = undefined; + } + }, + computed: { + ...mapStores(useUIStore, useWorkflowsStore), + activeExecutions(): IExecutionsCurrentSummaryExtended[] { + return this.workflowsStore.activeExecutions; + }, + combinedExecutions(): IExecutionsSummary[] { + const returnData: IExecutionsSummary[] = []; - requestItemsPerRequest: 10, + if (['all', 'running'].includes(this.filter.status)) { + returnData.push(...this.activeExecutions); + } + if (['all', 'error', 'success', 'waiting'].includes(this.filter.status)) { + returnData.push(...this.finishedExecutions); + } - selectedItems: {} as { [key: string]: boolean }, + return returnData.filter( + (execution) => + this.filter.workflowId === 'all' || execution.workflowId === this.filter.workflowId, + ); + }, + numSelected(): number { + if (this.allExistingSelected) { + return this.finishedExecutionsCount; + } - stoppingExecutions: [] as string[], - workflows: [] as IWorkflowShortResponse[], - }; + return Object.keys(this.selectedItems).length; + }, + workflowFilterCurrent(): ExecutionsQueryFilter { + const filter: ExecutionsQueryFilter = {}; + if (this.filter.workflowId !== 'all') { + filter.workflowId = this.filter.workflowId; + } + return filter; + }, + workflowFilterPast(): ExecutionsQueryFilter { + return executionFilterToQueryFilter(this.filter); }, - mounted() { - setPageTitle(`n8n - ${this.pageTitle}`); + pageTitle() { + return this.$locale.baseText('executionsList.workflowExecutions'); }, - async created() { - await this.loadWorkflows(); - //await this.refreshData(); - this.handleAutoRefreshToggle(); - - this.$externalHooks().run('executionsList.openDialog'); - this.$telemetry.track('User opened Executions log', { - workflow_id: this.workflowsStore.workflowId, + }, + methods: { + closeDialog() { + this.$emit('closeModal'); + }, + displayExecution(execution: IExecutionsSummary) { + const route = this.$router.resolve({ + name: VIEWS.EXECUTION_PREVIEW, + params: { name: execution.workflowId, executionId: execution.id }, }); + window.open(route.href, '_blank'); }, - beforeDestroy() { + handleAutoRefreshToggle() { if (this.autoRefreshInterval) { + // Clear any previously existing intervals (if any - there shouldn't) clearInterval(this.autoRefreshInterval); this.autoRefreshInterval = undefined; } - }, - computed: { - ...mapStores(useUIStore, useWorkflowsStore), - activeExecutions(): IExecutionsCurrentSummaryExtended[] { - return this.workflowsStore.activeExecutions; - }, - combinedExecutions(): IExecutionsSummary[] { - const returnData: IExecutionsSummary[] = []; - - if (['all', 'running'].includes(this.filter.status)) { - returnData.push(...this.activeExecutions); - } - if (['all', 'error', 'success', 'waiting'].includes(this.filter.status)) { - returnData.push(...this.finishedExecutions); - } - return returnData.filter( - (execution) => - this.filter.workflowId === 'all' || execution.workflowId === this.filter.workflowId, - ); - }, - numSelected(): number { - if (this.allExistingSelected) { - return this.finishedExecutionsCount; - } - - return Object.keys(this.selectedItems).length; - }, - workflowFilterCurrent(): ExecutionsQueryFilter { - const filter: ExecutionsQueryFilter = {}; - if (this.filter.workflowId !== 'all') { - filter.workflowId = this.filter.workflowId; - } - return filter; - }, - workflowFilterPast(): ExecutionsQueryFilter { - return executionFilterToQueryFilter(this.filter); - }, - pageTitle() { - return this.$locale.baseText('executionsList.workflowExecutions'); - }, + if (this.autoRefresh) { + this.autoRefreshInterval = setInterval(() => this.loadAutoRefresh(), 4 * 1000); // refresh data every 4 secs + } }, - methods: { - closeDialog() { - this.$emit('closeModal'); - }, - displayExecution(execution: IExecutionsSummary) { - const route = this.$router.resolve({ - name: VIEWS.EXECUTION_PREVIEW, - params: { name: execution.workflowId, executionId: execution.id }, - }); - window.open(route.href, '_blank'); - }, - handleAutoRefreshToggle() { - if (this.autoRefreshInterval) { - // Clear any previously existing intervals (if any - there shouldn't) - clearInterval(this.autoRefreshInterval); - this.autoRefreshInterval = undefined; - } + handleCheckAllExistingChange() { + this.allExistingSelected = !this.allExistingSelected; + this.allVisibleSelected = !this.allExistingSelected; + this.handleCheckAllVisibleChange(); + }, + handleCheckAllVisibleChange() { + this.allVisibleSelected = !this.allVisibleSelected; + if (!this.allVisibleSelected) { + this.allExistingSelected = false; + Vue.set(this, 'selectedItems', {}); + } else { + this.selectAllVisibleExecutions(); + } + }, + handleCheckboxChanged(executionId: string) { + if (this.selectedItems[executionId]) { + Vue.delete(this.selectedItems, executionId); + } else { + Vue.set(this.selectedItems, executionId, true); + } + this.allVisibleSelected = + Object.keys(this.selectedItems).length === this.combinedExecutions.length; + this.allExistingSelected = + Object.keys(this.selectedItems).length === this.finishedExecutionsCount; + }, + async handleDeleteSelected() { + const deleteExecutions = await this.confirmMessage( + this.$locale.baseText('executionsList.confirmMessage.message', { + interpolate: { numSelected: this.numSelected.toString() }, + }), + this.$locale.baseText('executionsList.confirmMessage.headline'), + 'warning', + this.$locale.baseText('executionsList.confirmMessage.confirmButtonText'), + this.$locale.baseText('executionsList.confirmMessage.cancelButtonText'), + ); + + if (!deleteExecutions) { + return; + } - if (this.autoRefresh) { - this.autoRefreshInterval = setInterval(() => this.loadAutoRefresh(), 4 * 1000); // refresh data every 4 secs - } - }, - handleCheckAllExistingChange() { - this.allExistingSelected = !this.allExistingSelected; - this.allVisibleSelected = !this.allExistingSelected; - this.handleCheckAllVisibleChange(); - }, - handleCheckAllVisibleChange() { - this.allVisibleSelected = !this.allVisibleSelected; - if (!this.allVisibleSelected) { - this.allExistingSelected = false; - Vue.set(this, 'selectedItems', {}); - } else { - this.selectAllVisibleExecutions(); - } - }, - handleCheckboxChanged(executionId: string) { - if (this.selectedItems[executionId]) { - Vue.delete(this.selectedItems, executionId); - } else { - Vue.set(this.selectedItems, executionId, true); - } - this.allVisibleSelected = - Object.keys(this.selectedItems).length === this.combinedExecutions.length; - this.allExistingSelected = - Object.keys(this.selectedItems).length === this.finishedExecutionsCount; - }, - async handleDeleteSelected() { - const deleteExecutions = await this.confirmMessage( - this.$locale.baseText('executionsList.confirmMessage.message', { - interpolate: { numSelected: this.numSelected.toString() }, - }), - this.$locale.baseText('executionsList.confirmMessage.headline'), - 'warning', - this.$locale.baseText('executionsList.confirmMessage.confirmButtonText'), - this.$locale.baseText('executionsList.confirmMessage.cancelButtonText'), - ); + this.isDataLoading = true; - if (!deleteExecutions) { - return; - } + const sendData: IExecutionDeleteFilter = {}; + if (this.allExistingSelected) { + sendData.deleteBefore = this.finishedExecutions[0].startedAt as Date; + } else { + sendData.ids = Object.keys(this.selectedItems); + } - this.isDataLoading = true; + sendData.filters = this.workflowFilterPast; - const sendData: IExecutionDeleteFilter = {}; - if (this.allExistingSelected) { - sendData.deleteBefore = this.finishedExecutions[0].startedAt as Date; - } else { - sendData.ids = Object.keys(this.selectedItems); - } + try { + await this.workflowsStore.deleteExecutions(sendData); + } catch (error) { + this.isDataLoading = false; + this.$showError( + error, + this.$locale.baseText('executionsList.showError.handleDeleteSelected.title'), + ); - sendData.filters = this.workflowFilterPast; + return; + } + this.isDataLoading = false; - try { - await this.restApi().deleteExecutions(sendData); - } catch (error) { - this.isDataLoading = false; - this.$showError( - error, - this.$locale.baseText('executionsList.showError.handleDeleteSelected.title'), - ); + this.$showMessage({ + title: this.$locale.baseText('executionsList.showMessage.handleDeleteSelected.title'), + type: 'success', + }); - return; + this.handleClearSelection(); + this.refreshData(); + }, + handleClearSelection(): void { + this.allVisibleSelected = false; + this.allExistingSelected = false; + Vue.set(this, 'selectedItems', {}); + }, + onFilterChanged(filter: ExecutionFilterType) { + this.filter = filter; + this.refreshData(); + this.handleClearSelection(); + }, + handleActionItemClick(commandData: { command: string; execution: IExecutionsSummary }) { + if (['currentlySaved', 'original'].includes(commandData.command)) { + let loadWorkflow = false; + if (commandData.command === 'currentlySaved') { + loadWorkflow = true; } - this.isDataLoading = false; - this.$showMessage({ - title: this.$locale.baseText('executionsList.showMessage.handleDeleteSelected.title'), - type: 'success', + this.retryExecution(commandData.execution, loadWorkflow); + + this.$telemetry.track('User clicked retry execution button', { + workflow_id: this.workflowsStore.workflowId, + execution_id: commandData.execution.id, + retry_type: loadWorkflow ? 'current' : 'original', }); + } + if (commandData.command === 'delete') { + this.deleteExecution(commandData.execution); + } + }, + getWorkflowName(workflowId: string): string | undefined { + return this.workflows.find((data) => data.id === workflowId)?.name; + }, + async loadActiveExecutions(): Promise<void> { + const activeExecutions = isEmpty(this.workflowFilterCurrent.metadata) + ? await this.workflowsStore.getCurrentExecutions(this.workflowFilterCurrent) + : []; + for (const activeExecution of activeExecutions) { + if (activeExecution.workflowId && !activeExecution.workflowName) { + activeExecution.workflowName = this.getWorkflowName(activeExecution.workflowId); + } + } - this.handleClearSelection(); - this.refreshData(); - }, - handleClearSelection(): void { - this.allVisibleSelected = false; - this.allExistingSelected = false; - Vue.set(this, 'selectedItems', {}); - }, - onFilterChanged(filter: ExecutionFilterType) { - this.filter = filter; - this.refreshData(); - this.handleClearSelection(); - }, - handleActionItemClick(commandData: { command: string; execution: IExecutionsSummary }) { - if (['currentlySaved', 'original'].includes(commandData.command)) { - let loadWorkflow = false; - if (commandData.command === 'currentlySaved') { - loadWorkflow = true; - } + this.workflowsStore.activeExecutions = activeExecutions; + this.workflowsStore.addToCurrentExecutions(activeExecutions); + }, + async loadAutoRefresh(): Promise<void> { + const filter: ExecutionsQueryFilter = this.workflowFilterPast; + // We cannot use firstId here as some executions finish out of order. Let's say + // You have execution ids 500 to 505 running. + // Suppose 504 finishes before 500, 501, 502 and 503. + // iF you use firstId, filtering id >= 504 you won't + // ever get ids 500, 501, 502 and 503 when they finish + const promises = [this.workflowsStore.getPastExecutions(filter, this.requestItemsPerRequest)]; + if (isEmpty(filter.metadata)) { + promises.push(this.workflowsStore.getCurrentExecutions({})); + } - this.retryExecution(commandData.execution, loadWorkflow); + const results = await Promise.all(promises); - this.$telemetry.track('User clicked retry execution button', { - workflow_id: this.workflowsStore.workflowId, - execution_id: commandData.execution.id, - retry_type: loadWorkflow ? 'current' : 'original', - }); - } - if (commandData.command === 'delete') { - this.deleteExecution(commandData.execution); + for (const activeExecution of results[1]) { + if ( + activeExecution.workflowId !== undefined && + activeExecution.workflowName === undefined + ) { + activeExecution.workflowName = this.getWorkflowName(activeExecution.workflowId); } - }, - getWorkflowName(workflowId: string): string | undefined { - return this.workflows.find((data) => data.id === workflowId)?.name; - }, - async loadActiveExecutions(): Promise<void> { - const activeExecutions = isEmpty(this.workflowFilterCurrent.metadata) - ? await this.restApi().getCurrentExecutions(this.workflowFilterCurrent) - : []; - for (const activeExecution of activeExecutions) { - if (activeExecution.workflowId && !activeExecution.workflowName) { - activeExecution.workflowName = this.getWorkflowName(activeExecution.workflowId); + } + + this.workflowsStore.activeExecutions = results[1]; + + // execution IDs are typed as string, int conversion is necessary so we can order. + const alreadyPresentExecutions = [...this.finishedExecutions]; + const alreadyPresentExecutionIds = alreadyPresentExecutions.map((exec) => + parseInt(exec.id, 10), + ); + let lastId = 0; + const gaps = [] as number[]; + for (let i = results[0].results.length - 1; i >= 0; i--) { + const currentItem = results[0].results[i]; + const currentId = parseInt(currentItem.id, 10); + if (lastId !== 0 && !isNaN(currentId)) { + // We are doing this iteration to detect possible gaps. + // The gaps are used to remove executions that finished + // and were deleted from database but were displaying + // in this list while running. + if (currentId - lastId > 1) { + // We have some gaps. + const range = _range(lastId + 1, currentId); + gaps.push(...range); } } + lastId = parseInt(currentItem.id, 10) || 0; + + // Check new results from end to start + // Add new items accordingly. + const executionIndex = alreadyPresentExecutionIds.indexOf(currentId); + if (executionIndex !== -1) { + // Execution that we received is already present. - this.workflowsStore.activeExecutions = activeExecutions; - this.workflowsStore.addToCurrentExecutions(activeExecutions); - }, - async loadAutoRefresh(): Promise<void> { - const filter: ExecutionsQueryFilter = this.workflowFilterPast; - // We cannot use firstId here as some executions finish out of order. Let's say - // You have execution ids 500 to 505 running. - // Suppose 504 finishes before 500, 501, 502 and 503. - // iF you use firstId, filtering id >= 504 you won't - // ever get ids 500, 501, 502 and 503 when they finish - const pastExecutionsPromise: Promise<IExecutionsListResponse> = - this.restApi().getPastExecutions(filter, this.requestItemsPerRequest); - const currentExecutionsPromise: Promise<IExecutionsCurrentSummaryExtended[]> = isEmpty( - filter.metadata, - ) - ? this.restApi().getCurrentExecutions({}) - : Promise.resolve([]); - - const results = await Promise.all([pastExecutionsPromise, currentExecutionsPromise]); - - for (const activeExecution of results[1]) { if ( - activeExecution.workflowId !== undefined && - activeExecution.workflowName === undefined + alreadyPresentExecutions[executionIndex].finished === false && + currentItem.finished === true ) { - activeExecution.workflowName = this.getWorkflowName(activeExecution.workflowId); + // Concurrency stuff. This might happen if the execution finishes + // prior to saving all information to database. Somewhat rare but + // With auto refresh and several executions, it happens sometimes. + // So we replace the execution data so it displays correctly. + alreadyPresentExecutions[executionIndex] = currentItem; } - } - - this.workflowsStore.activeExecutions = results[1]; - // execution IDs are typed as string, int conversion is necessary so we can order. - const alreadyPresentExecutions = [...this.finishedExecutions]; - const alreadyPresentExecutionIds = alreadyPresentExecutions.map((exec) => - parseInt(exec.id, 10), - ); - let lastId = 0; - const gaps = [] as number[]; - for (let i = results[0].results.length - 1; i >= 0; i--) { - const currentItem = results[0].results[i]; - const currentId = parseInt(currentItem.id, 10); - if (lastId !== 0 && !isNaN(currentId)) { - // We are doing this iteration to detect possible gaps. - // The gaps are used to remove executions that finished - // and were deleted from database but were displaying - // in this list while running. - if (currentId - lastId > 1) { - // We have some gaps. - const range = _range(lastId + 1, currentId); - gaps.push(...range); - } - } - lastId = parseInt(currentItem.id, 10) || 0; - - // Check new results from end to start - // Add new items accordingly. - const executionIndex = alreadyPresentExecutionIds.indexOf(currentId); - if (executionIndex !== -1) { - // Execution that we received is already present. - - if ( - alreadyPresentExecutions[executionIndex].finished === false && - currentItem.finished === true - ) { - // Concurrency stuff. This might happen if the execution finishes - // prior to saving all information to database. Somewhat rare but - // With auto refresh and several executions, it happens sometimes. - // So we replace the execution data so it displays correctly. - alreadyPresentExecutions[executionIndex] = currentItem; - } - - continue; - } + continue; + } - // Find the correct position to place this newcomer - let j; - for (j = alreadyPresentExecutions.length - 1; j >= 0; j--) { - if (currentId < parseInt(alreadyPresentExecutions[j].id, 10)) { - alreadyPresentExecutions.splice(j + 1, 0, currentItem); - break; - } - } - if (j === -1) { - alreadyPresentExecutions.unshift(currentItem); + // Find the correct position to place this newcomer + let j; + for (j = alreadyPresentExecutions.length - 1; j >= 0; j--) { + if (currentId < parseInt(alreadyPresentExecutions[j].id, 10)) { + alreadyPresentExecutions.splice(j + 1, 0, currentItem); + break; } } - const alreadyPresentExecutionsFiltered = alreadyPresentExecutions.filter( - (execution) => - !gaps.includes(parseInt(execution.id, 10)) && lastId >= parseInt(execution.id, 10), - ); - this.finishedExecutionsCount = results[0].count; - this.finishedExecutionsCountEstimated = results[0].estimated; - - Vue.set(this, 'finishedExecutions', alreadyPresentExecutionsFiltered); - this.workflowsStore.addToCurrentExecutions(alreadyPresentExecutionsFiltered); - - this.adjustSelectionAfterMoreItemsLoaded(); - }, - async loadFinishedExecutions(): Promise<void> { - if (this.filter.status === 'running') { - this.finishedExecutions = []; - this.finishedExecutionsCount = 0; - this.finishedExecutionsCountEstimated = false; - return; + if (j === -1) { + alreadyPresentExecutions.unshift(currentItem); } - const data = await this.restApi().getPastExecutions( - this.workflowFilterPast, - this.requestItemsPerRequest, - ); - this.finishedExecutions = data.results; - this.finishedExecutionsCount = data.count; - this.finishedExecutionsCountEstimated = data.estimated; - - this.workflowsStore.addToCurrentExecutions(data.results); + } + const alreadyPresentExecutionsFiltered = alreadyPresentExecutions.filter( + (execution) => + !gaps.includes(parseInt(execution.id, 10)) && lastId >= parseInt(execution.id, 10), + ); + this.finishedExecutionsCount = results[0].count; + this.finishedExecutionsCountEstimated = results[0].estimated; - if (this.finishedExecutions.length === 0) { - this.handleClearSelection(); - } - }, - async loadMore() { - if (this.filter.status === 'running') { - return; - } + Vue.set(this, 'finishedExecutions', alreadyPresentExecutionsFiltered); + this.workflowsStore.addToCurrentExecutions(alreadyPresentExecutionsFiltered); - this.isDataLoading = true; + this.adjustSelectionAfterMoreItemsLoaded(); + }, + async loadFinishedExecutions(): Promise<void> { + if (this.filter.status === 'running') { + this.finishedExecutions = []; + this.finishedExecutionsCount = 0; + this.finishedExecutionsCountEstimated = false; + return; + } + const data = await this.workflowsStore.getPastExecutions( + this.workflowFilterPast, + this.requestItemsPerRequest, + ); + this.finishedExecutions = data.results; + this.finishedExecutionsCount = data.count; + this.finishedExecutionsCountEstimated = data.estimated; - const filter = this.workflowFilterPast; - let lastId: string | undefined; + this.workflowsStore.addToCurrentExecutions(data.results); - if (this.finishedExecutions.length !== 0) { - const lastItem = this.finishedExecutions.slice(-1)[0]; - lastId = lastItem.id; - } + if (this.finishedExecutions.length === 0) { + this.handleClearSelection(); + } + }, + async loadMore() { + if (this.filter.status === 'running') { + return; + } - let data: IExecutionsListResponse; - try { - data = await this.restApi().getPastExecutions( - filter, - this.requestItemsPerRequest, - lastId, - ); - } catch (error) { - this.isDataLoading = false; - this.$showError(error, this.$locale.baseText('executionsList.showError.loadMore.title')); - return; - } + this.isDataLoading = true; - data.results = data.results.map((execution) => { - // @ts-ignore - return { ...execution, mode: execution.mode }; - }); + const filter = this.workflowFilterPast; + let lastId: string | undefined; - this.finishedExecutions.push(...data.results); - this.finishedExecutionsCount = data.count; - this.finishedExecutionsCountEstimated = data.estimated; + if (this.finishedExecutions.length !== 0) { + const lastItem = this.finishedExecutions.slice(-1)[0]; + lastId = lastItem.id; + } + let data: IExecutionsListResponse; + try { + data = await this.workflowsStore.getPastExecutions( + filter, + this.requestItemsPerRequest, + lastId, + ); + } catch (error) { this.isDataLoading = false; + this.$showError(error, this.$locale.baseText('executionsList.showError.loadMore.title')); + return; + } - this.workflowsStore.addToCurrentExecutions(data.results); - - this.adjustSelectionAfterMoreItemsLoaded(); - }, - async loadWorkflows() { - try { - const workflows = await this.restApi().getWorkflows(); - workflows.sort((a, b) => { - if (a.name.toLowerCase() < b.name.toLowerCase()) { - return -1; - } - if (a.name.toLowerCase() > b.name.toLowerCase()) { - return 1; - } - return 0; - }); + data.results = data.results.map((execution) => { + // @ts-ignore + return { ...execution, mode: execution.mode }; + }); - // @ts-ignore - workflows.unshift({ - id: 'all', - name: this.$locale.baseText('executionsList.allWorkflows'), - }); + this.finishedExecutions.push(...data.results); + this.finishedExecutionsCount = data.count; + this.finishedExecutionsCountEstimated = data.estimated; - Vue.set(this, 'workflows', workflows); - } catch (error) { - this.$showError( - error, - this.$locale.baseText('executionsList.showError.loadWorkflows.title'), - ); - } - }, - async retryExecution(execution: IExecutionsSummary, loadWorkflow?: boolean) { - this.isDataLoading = true; - - try { - const retrySuccessful = await this.restApi().retryExecution(execution.id, loadWorkflow); - - if (retrySuccessful) { - this.$showMessage({ - title: this.$locale.baseText('executionsList.showMessage.retrySuccessfulTrue.title'), - type: 'success', - }); - } else { - this.$showMessage({ - title: this.$locale.baseText('executionsList.showMessage.retrySuccessfulFalse.title'), - type: 'error', - }); + this.isDataLoading = false; + + this.workflowsStore.addToCurrentExecutions(data.results); + + this.adjustSelectionAfterMoreItemsLoaded(); + }, + async loadWorkflows() { + try { + const workflows = await this.workflowsStore.fetchAllWorkflows(); + workflows.sort((a, b) => { + if (a.name.toLowerCase() < b.name.toLowerCase()) { + return -1; + } + if (a.name.toLowerCase() > b.name.toLowerCase()) { + return 1; } + return 0; + }); - this.isDataLoading = false; - } catch (error) { - this.$showError( - error, - this.$locale.baseText('executionsList.showError.retryExecution.title'), - ); + // @ts-ignore + workflows.unshift({ + id: 'all', + name: this.$locale.baseText('executionsList.allWorkflows'), + }); - this.isDataLoading = false; - } - }, - async refreshData() { - this.isDataLoading = true; - - try { - await Promise.all([this.loadActiveExecutions(), this.loadFinishedExecutions()]); - } catch (error) { - this.$showError( - error, - this.$locale.baseText('executionsList.showError.refreshData.title'), - ); - } + Vue.set(this, 'workflows', workflows); + } catch (error) { + this.$showError( + error, + this.$locale.baseText('executionsList.showError.loadWorkflows.title'), + ); + } + }, + async retryExecution(execution: IExecutionsSummary, loadWorkflow?: boolean) { + this.isDataLoading = true; - this.isDataLoading = false; - }, - getStatus(execution: IExecutionsSummary): ExecutionStatus { - if (execution.status) { - return execution.status; + try { + const retrySuccessful = await this.workflowsStore.retryExecution( + execution.id, + loadWorkflow, + ); + + if (retrySuccessful) { + this.$showMessage({ + title: this.$locale.baseText('executionsList.showMessage.retrySuccessfulTrue.title'), + type: 'success', + }); } else { - // this should not happen but just in case - let status: ExecutionStatus = 'unknown'; - if (execution.waitTill) { - status = 'waiting'; - } else if (execution.stoppedAt === undefined) { - status = 'running'; - } else if (execution.finished) { - status = 'success'; - } else if (execution.stoppedAt !== null) { - status = 'failed'; - } else { - status = 'unknown'; - } - return status; + this.$showMessage({ + title: this.$locale.baseText('executionsList.showMessage.retrySuccessfulFalse.title'), + type: 'error', + }); } - }, - getRowClass(execution: IExecutionsSummary): string { - return [this.$style.execRow, this.$style[this.getStatus(execution)]].join(' '); - }, - getStatusText(entry: IExecutionsSummary): string { - const status = this.getStatus(entry); - let text = ''; - - if (status === 'waiting') { - text = this.$locale.baseText('executionsList.waiting'); - } else if (status === 'canceled') { - text = this.$locale.baseText('executionsList.canceled'); - } else if (status === 'crashed') { - text = this.$locale.baseText('executionsList.error'); - } else if (status === 'new') { - text = this.$locale.baseText('executionsList.running'); - } else if (status === 'running') { - text = this.$locale.baseText('executionsList.running'); - } else if (status === 'success') { - text = this.$locale.baseText('executionsList.succeeded'); - } else if (status === 'failed') { - text = this.$locale.baseText('executionsList.error'); + + this.isDataLoading = false; + } catch (error) { + this.$showError( + error, + this.$locale.baseText('executionsList.showError.retryExecution.title'), + ); + + this.isDataLoading = false; + } + }, + async refreshData() { + this.isDataLoading = true; + + try { + await Promise.all([this.loadActiveExecutions(), this.loadFinishedExecutions()]); + } catch (error) { + this.$showError(error, this.$locale.baseText('executionsList.showError.refreshData.title')); + } + + this.isDataLoading = false; + }, + getStatus(execution: IExecutionsSummary): ExecutionStatus { + if (execution.status) { + return execution.status; + } else { + // this should not happen but just in case + let status: ExecutionStatus = 'unknown'; + if (execution.waitTill) { + status = 'waiting'; + } else if (execution.stoppedAt === undefined) { + status = 'running'; + } else if (execution.finished) { + status = 'success'; + } else if (execution.stoppedAt !== null) { + status = 'failed'; } else { - text = this.$locale.baseText('executionsList.unknown'); + status = 'unknown'; } + return status; + } + }, + getRowClass(execution: IExecutionsSummary): string { + return [this.$style.execRow, this.$style[this.getStatus(execution)]].join(' '); + }, + getStatusText(entry: IExecutionsSummary): string { + const status = this.getStatus(entry); + let text = ''; + + if (status === 'waiting') { + text = this.$locale.baseText('executionsList.waiting'); + } else if (status === 'canceled') { + text = this.$locale.baseText('executionsList.canceled'); + } else if (status === 'crashed') { + text = this.$locale.baseText('executionsList.error'); + } else if (status === 'new') { + text = this.$locale.baseText('executionsList.running'); + } else if (status === 'running') { + text = this.$locale.baseText('executionsList.running'); + } else if (status === 'success') { + text = this.$locale.baseText('executionsList.succeeded'); + } else if (status === 'failed') { + text = this.$locale.baseText('executionsList.error'); + } else { + text = this.$locale.baseText('executionsList.unknown'); + } - return text; - }, - getStatusTextTranslationPath(entry: IExecutionsSummary): string { - const status = this.getStatus(entry); - let path = ''; - - if (status === 'waiting') { - path = 'executionsList.statusWaiting'; - } else if (status === 'canceled') { - path = 'executionsList.statusCanceled'; - } else if (status === 'crashed') { - path = 'executionsList.statusText'; - } else if (status === 'new') { - path = 'executionsList.statusRunning'; - } else if (status === 'running') { - path = 'executionsList.statusRunning'; - } else if (status === 'success') { - path = 'executionsList.statusText'; - } else if (status === 'failed') { - path = 'executionsList.statusText'; + return text; + }, + getStatusTextTranslationPath(entry: IExecutionsSummary): string { + const status = this.getStatus(entry); + let path = ''; + + if (status === 'waiting') { + path = 'executionsList.statusWaiting'; + } else if (status === 'canceled') { + path = 'executionsList.statusCanceled'; + } else if (['crashed', 'failed', 'success'].includes(status)) { + if (!entry.stoppedAt) { + path = 'executionsList.statusTextWithoutTime'; } else { - path = 'executionsList.statusUnknown'; + path = 'executionsList.statusText'; } + } else if (status === 'new') { + path = 'executionsList.statusRunning'; + } else if (status === 'running') { + path = 'executionsList.statusRunning'; + } else { + path = 'executionsList.statusUnknown'; + } - return path; - }, - getStatusTooltipText(entry: IExecutionsSummary): string { - const status = this.getStatus(entry); - let text = ''; + return path; + }, + getStatusTooltipText(entry: IExecutionsSummary): string { + const status = this.getStatus(entry); + let text = ''; - if (status === 'waiting' && this.isWaitTillIndefinite(entry)) { - text = this.$locale.baseText( - 'executionsList.statusTooltipText.theWorkflowIsWaitingIndefinitely', - ); - } + if (status === 'waiting' && this.isWaitTillIndefinite(entry)) { + text = this.$locale.baseText( + 'executionsList.statusTooltipText.theWorkflowIsWaitingIndefinitely', + ); + } - return text; - }, - async stopExecution(activeExecutionId: string) { - try { - // Add it to the list of currently stopping executions that we - // can show the user in the UI that it is in progress - this.stoppingExecutions.push(activeExecutionId); + return text; + }, + async stopExecution(activeExecutionId: string) { + try { + // Add it to the list of currently stopping executions that we + // can show the user in the UI that it is in progress + this.stoppingExecutions.push(activeExecutionId); - await this.restApi().stopCurrentExecution(activeExecutionId); + await this.workflowsStore.stopCurrentExecution(activeExecutionId); - // Remove it from the list of currently stopping executions - const index = this.stoppingExecutions.indexOf(activeExecutionId); - this.stoppingExecutions.splice(index, 1); + // Remove it from the list of currently stopping executions + const index = this.stoppingExecutions.indexOf(activeExecutionId); + this.stoppingExecutions.splice(index, 1); - this.$showMessage({ - title: this.$locale.baseText('executionsList.showMessage.stopExecution.title'), - message: this.$locale.baseText('executionsList.showMessage.stopExecution.message', { - interpolate: { activeExecutionId }, - }), - type: 'success', - }); + this.$showMessage({ + title: this.$locale.baseText('executionsList.showMessage.stopExecution.title'), + message: this.$locale.baseText('executionsList.showMessage.stopExecution.message', { + interpolate: { activeExecutionId }, + }), + type: 'success', + }); - this.refreshData(); - } catch (error) { - this.$showError( - error, - this.$locale.baseText('executionsList.showError.stopExecution.title'), - ); - } - }, - isExecutionRetriable(execution: IExecutionsSummary): boolean { - return ( - execution.stoppedAt !== undefined && - !execution.finished && - execution.retryOf === undefined && - execution.retrySuccessId === undefined && - !execution.waitTill + this.refreshData(); + } catch (error) { + this.$showError( + error, + this.$locale.baseText('executionsList.showError.stopExecution.title'), ); - }, - async deleteExecution(execution: IExecutionsSummary) { - this.isDataLoading = true; - try { - await this.restApi().deleteExecutions({ ids: [execution.id] }); - await this.refreshData(); - - if (this.allVisibleSelected) { - Vue.set(this, 'selectedItems', {}); - this.selectAllVisibleExecutions(); - } - } catch (error) { - this.$showError( - error, - this.$locale.baseText('executionsList.showError.handleDeleteSelected.title'), - ); - } - this.isDataLoading = true; - }, - isWaitTillIndefinite(execution: IExecutionsSummary): boolean { - if (!execution.waitTill) { - return false; - } - return new Date(execution.waitTill).toISOString() === WAIT_TIME_UNLIMITED; - }, - isRunning(execution: IExecutionsSummary): boolean { - return this.getStatus(execution) === 'running'; - }, - selectAllVisibleExecutions() { - this.combinedExecutions.forEach((execution: IExecutionsSummary) => { - Vue.set(this.selectedItems, execution.id, true); - }); - }, - adjustSelectionAfterMoreItemsLoaded() { - if (this.allExistingSelected) { - this.allVisibleSelected = true; + } + }, + isExecutionRetriable(execution: IExecutionsSummary): boolean { + return ( + execution.stoppedAt !== undefined && + !execution.finished && + execution.retryOf === undefined && + execution.retrySuccessId === undefined && + !execution.waitTill + ); + }, + async deleteExecution(execution: IExecutionsSummary) { + this.isDataLoading = true; + try { + await this.workflowsStore.deleteExecutions({ ids: [execution.id] }); + await this.refreshData(); + + if (this.allVisibleSelected) { + Vue.set(this, 'selectedItems', {}); this.selectAllVisibleExecutions(); } - }, + } catch (error) { + this.$showError( + error, + this.$locale.baseText('executionsList.showError.handleDeleteSelected.title'), + ); + } + this.isDataLoading = true; + }, + isWaitTillIndefinite(execution: IExecutionsSummary): boolean { + if (!execution.waitTill) { + return false; + } + return new Date(execution.waitTill).toISOString() === WAIT_TIME_UNLIMITED; + }, + isRunning(execution: IExecutionsSummary): boolean { + return this.getStatus(execution) === 'running'; + }, + selectAllVisibleExecutions() { + this.combinedExecutions.forEach((execution: IExecutionsSummary) => { + Vue.set(this.selectedItems, execution.id, true); + }); + }, + adjustSelectionAfterMoreItemsLoaded() { + if (this.allExistingSelected) { + this.allVisibleSelected = true; + this.selectAllVisibleExecutions(); + } }, }, -); +}); </script> <style module lang="scss"> diff --git a/packages/editor-ui/src/components/ExecutionsModal.vue b/packages/editor-ui/src/components/ExecutionsModal.vue index 7da0c9da3b471..e30fd2b01201c 100644 --- a/packages/editor-ui/src/components/ExecutionsModal.vue +++ b/packages/editor-ui/src/components/ExecutionsModal.vue @@ -7,13 +7,13 @@ </template> <script lang="ts"> -import Vue from 'vue'; +import { defineComponent } from 'vue'; import ExecutionsList from '@/components/ExecutionsList.vue'; import Modal from '@/components/Modal.vue'; import { EXECUTIONS_MODAL_KEY } from '@/constants'; import { createEventBus } from '@/event-bus'; -export default Vue.extend({ +export default defineComponent({ name: 'ExecutionsModal', components: { Modal, diff --git a/packages/editor-ui/src/components/ExecutionsView/ExecutionCard.vue b/packages/editor-ui/src/components/ExecutionsView/ExecutionCard.vue index 54b550d563994..7778795f91c2e 100644 --- a/packages/editor-ui/src/components/ExecutionsView/ExecutionCard.vue +++ b/packages/editor-ui/src/components/ExecutionsView/ExecutionCard.vue @@ -81,15 +81,15 @@ </template> <script lang="ts"> -import { IExecutionsSummary } from '@/Interface'; +import type { IExecutionsSummary } from '@/Interface'; import mixins from 'vue-typed-mixins'; -import { executionHelpers, IExecutionUIData } from '@/mixins/executionsHelpers'; +import type { IExecutionUIData } from '@/mixins/executionsHelpers'; +import { executionHelpers } from '@/mixins/executionsHelpers'; import { VIEWS } from '@/constants'; import { showMessage } from '@/mixins/showMessage'; -import { restApi } from '@/mixins/restApi'; import ExecutionTime from '@/components/ExecutionTime.vue'; -export default mixins(executionHelpers, showMessage, restApi).extend({ +export default mixins(executionHelpers, showMessage).extend({ name: 'execution-card', components: { ExecutionTime, diff --git a/packages/editor-ui/src/components/ExecutionsView/ExecutionPreview.vue b/packages/editor-ui/src/components/ExecutionsView/ExecutionPreview.vue index bd5774a47ad3c..c4f8c7c11e049 100644 --- a/packages/editor-ui/src/components/ExecutionsView/ExecutionPreview.vue +++ b/packages/editor-ui/src/components/ExecutionsView/ExecutionPreview.vue @@ -127,17 +127,18 @@ <script lang="ts"> import mixins from 'vue-typed-mixins'; -import { restApi } from '@/mixins/restApi'; import { showMessage } from '@/mixins/showMessage'; import WorkflowPreview from '@/components/WorkflowPreview.vue'; -import { executionHelpers, IExecutionUIData } from '@/mixins/executionsHelpers'; +import type { IExecutionUIData } from '@/mixins/executionsHelpers'; +import { executionHelpers } from '@/mixins/executionsHelpers'; import { VIEWS } from '@/constants'; import { mapStores } from 'pinia'; import { useUIStore } from '@/stores/ui'; import { Dropdown as ElDropdown } from 'element-ui'; -import { IAbstractEventMessage } from 'n8n-workflow'; -export default mixins(restApi, showMessage, executionHelpers).extend({ +type RetryDropdownRef = InstanceType<typeof ElDropdown> & { hide: () => void }; + +export default mixins(showMessage, executionHelpers).extend({ name: 'execution-preview', components: { ElDropdown, @@ -182,9 +183,9 @@ export default mixins(restApi, showMessage, executionHelpers).extend({ }, onRetryButtonBlur(event: FocusEvent): void { // Hide dropdown when clicking outside of current document - const retryDropdown = this.$refs.retryDropdown as (Vue & { hide: () => void }) | undefined; - if (retryDropdown && event.relatedTarget === null) { - retryDropdown.hide(); + const retryDropdownRef = this.$refs.retryDropdown as RetryDropdownRef | undefined; + if (retryDropdownRef && event.relatedTarget === null) { + retryDropdownRef.hide(); } }, }, diff --git a/packages/editor-ui/src/components/ExecutionsView/ExecutionsInfoAccordion.vue b/packages/editor-ui/src/components/ExecutionsView/ExecutionsInfoAccordion.vue index d6571fb049241..56a708099eff0 100644 --- a/packages/editor-ui/src/components/ExecutionsView/ExecutionsInfoAccordion.vue +++ b/packages/editor-ui/src/components/ExecutionsView/ExecutionsInfoAccordion.vue @@ -42,7 +42,8 @@ import { useUIStore } from '@/stores/ui'; import { useWorkflowsStore } from '@/stores/workflows'; import { mapStores } from 'pinia'; import { PLACEHOLDER_EMPTY_WORKFLOW_ID, WORKFLOW_SETTINGS_MODAL_KEY } from '@/constants'; -import { deepCopy, IWorkflowSettings } from 'n8n-workflow'; +import type { IWorkflowSettings } from 'n8n-workflow'; +import { deepCopy } from 'n8n-workflow'; import mixins from 'vue-typed-mixins'; import { workflowHelpers } from '@/mixins/workflowHelpers'; diff --git a/packages/editor-ui/src/components/ExecutionsView/ExecutionsLandingPage.vue b/packages/editor-ui/src/components/ExecutionsView/ExecutionsLandingPage.vue index d41d2c29773a1..2c58834db9f8d 100644 --- a/packages/editor-ui/src/components/ExecutionsView/ExecutionsLandingPage.vue +++ b/packages/editor-ui/src/components/ExecutionsView/ExecutionsLandingPage.vue @@ -27,10 +27,10 @@ import { PLACEHOLDER_EMPTY_WORKFLOW_ID, VIEWS } from '@/constants'; import { useUIStore } from '@/stores/ui'; import { useWorkflowsStore } from '@/stores/workflows'; import { mapStores } from 'pinia'; -import Vue from 'vue'; +import { defineComponent } from 'vue'; import ExecutionsInfoAccordion from './ExecutionsInfoAccordion.vue'; -export default Vue.extend({ +export default defineComponent({ name: 'executions-landing-page', components: { ExecutionsInfoAccordion, diff --git a/packages/editor-ui/src/components/ExecutionsView/ExecutionsList.vue b/packages/editor-ui/src/components/ExecutionsView/ExecutionsList.vue index 69566f4f236df..0a17411a81cee 100644 --- a/packages/editor-ui/src/components/ExecutionsView/ExecutionsList.vue +++ b/packages/editor-ui/src/components/ExecutionsView/ExecutionsList.vue @@ -1,8 +1,8 @@ <template> - <div :class="$style.container" v-if="!loading"> + <div :class="$style.container"> <executions-sidebar :executions="executions" - :loading="loading" + :loading="loading && !executions.length" :loadingMore="loadingMore" :temporaryExecution="temporaryExecution" @reloadExecutions="setExecutions" @@ -32,27 +32,26 @@ import { VIEWS, WEBHOOK_NODE_TYPE, } from '@/constants'; -import { +import type { ExecutionFilterType, IExecutionsListResponse, INodeUi, ITag, IWorkflowDb, } from '@/Interface'; -import { +import type { IExecutionsSummary, IConnection, IConnections, IDataObject, INodeTypeDescription, INodeTypeNameVersion, - NodeHelpers, } from 'n8n-workflow'; +import { NodeHelpers } from 'n8n-workflow'; import mixins from 'vue-typed-mixins'; -import { restApi } from '@/mixins/restApi'; import { showMessage } from '@/mixins/showMessage'; import { v4 as uuid } from 'uuid'; -import { Route } from 'vue-router'; +import type { Route } from 'vue-router'; import { executionHelpers } from '@/mixins/executionsHelpers'; import { range as _range } from 'lodash-es'; import { debounceHelper } from '@/mixins/debounce'; @@ -71,13 +70,7 @@ const MAX_LOADING_ATTEMPTS = 5; // Number of executions fetched on each page const LOAD_MORE_PAGE_SIZE = 100; -export default mixins( - restApi, - showMessage, - executionHelpers, - debounceHelper, - workflowHelpers, -).extend({ +export default mixins(showMessage, executionHelpers, debounceHelper, workflowHelpers).extend({ name: 'executions-list', components: { ExecutionsSidebar, @@ -225,7 +218,7 @@ export default mixins( let data: IExecutionsListResponse; try { - data = await this.restApi().getPastExecutions(this.requestFilter, limit, lastId); + data = await this.workflowsStore.getPastExecutions(this.requestFilter, limit, lastId); } catch (error) { this.loadingMore = false; this.$showError(error, this.$locale.baseText('executionsList.showError.loadMore.title')); @@ -260,7 +253,7 @@ export default mixins( this.executions[executionIndex - 1] || this.executions[0]; - await this.restApi().deleteExecutions({ ids: [this.$route.params.executionId] }); + await this.workflowsStore.deleteExecutions({ ids: [this.$route.params.executionId] }); if (this.temporaryExecution?.id === this.$route.params.executionId) { this.temporaryExecution = null; } @@ -300,7 +293,7 @@ export default mixins( const activeExecutionId = this.$route.params.executionId; try { - await this.restApi().stopCurrentExecution(activeExecutionId); + await this.workflowsStore.stopCurrentExecution(activeExecutionId); this.$showMessage({ title: this.$locale.baseText('executionsList.showMessage.stopExecution.title'), @@ -498,7 +491,7 @@ export default mixins( let data: IWorkflowDb | undefined; try { - data = await this.restApi().getWorkflow(workflowId); + data = await this.workflowsStore.fetchWorkflow(workflowId); } catch (error) { this.$showError(error, this.$locale.baseText('nodeView.showError.openWorkflow.title')); return; @@ -644,7 +637,7 @@ export default mixins( } }, async loadActiveWorkflows(): Promise<void> { - this.workflowsStore.activeWorkflows = await this.restApi().getActiveWorkflows(); + await this.workflowsStore.fetchActiveWorkflows(); }, async onRetryExecution(payload: { execution: IExecutionsSummary; command: string }) { const loadWorkflow = payload.command === 'current-workflow'; @@ -665,7 +658,10 @@ export default mixins( }, async retryExecution(execution: IExecutionsSummary, loadWorkflow?: boolean) { try { - const retrySuccessful = await this.restApi().retryExecution(execution.id, loadWorkflow); + const retrySuccessful = await this.workflowsStore.retryExecution( + execution.id, + loadWorkflow, + ); if (retrySuccessful === true) { this.$showMessage({ diff --git a/packages/editor-ui/src/components/ExecutionsView/ExecutionsSidebar.vue b/packages/editor-ui/src/components/ExecutionsView/ExecutionsSidebar.vue index 4a126a33d37da..9db935ee07ed0 100644 --- a/packages/editor-ui/src/components/ExecutionsView/ExecutionsSidebar.vue +++ b/packages/editor-ui/src/components/ExecutionsView/ExecutionsSidebar.vue @@ -25,12 +25,10 @@ data-test-id="current-executions-list" @scroll="loadMore(20)" > - <div v-if="loading" class="mr-m"> - <n8n-loading :class="$style.loader" variant="p" :rows="1" /> - <n8n-loading :class="$style.loader" variant="p" :rows="1" /> - <n8n-loading :class="$style.loader" variant="p" :rows="1" /> + <div v-if="loading" class="mr-l"> + <n8n-loading variant="rect" /> </div> - <div v-if="executions.length === 0" :class="$style.noResultsContainer"> + <div v-if="!loading && executions.length === 0" :class="$style.noResultsContainer"> <n8n-text color="text-base" size="medium" align="center"> {{ $locale.baseText('executionsLandingPage.noResults') }} </n8n-text> @@ -54,7 +52,7 @@ @retryExecution="onRetryExecution" /> <div v-if="loadingMore" class="mr-m"> - <n8n-loading :class="$style.loader" variant="p" :rows="1" /> + <n8n-loading variant="p" :rows="1" /> </div> </div> <div :class="$style.infoAccordion"> @@ -69,15 +67,17 @@ import ExecutionsInfoAccordion from '@/components/ExecutionsView/ExecutionsInfoA import ExecutionFilter from '@/components/ExecutionFilter.vue'; import { VIEWS } from '@/constants'; import type { IExecutionsSummary } from 'n8n-workflow'; -import { Route } from 'vue-router'; -import Vue from 'vue'; -import { PropType } from 'vue'; +import type { Route } from 'vue-router'; +import { defineComponent } from 'vue'; +import type { PropType } from 'vue'; import { mapStores } from 'pinia'; import { useUIStore } from '@/stores/ui'; import { useWorkflowsStore } from '@/stores/workflows'; -import { ExecutionFilterType } from '@/Interface'; +import type { ExecutionFilterType } from '@/Interface'; -export default Vue.extend({ +type ExecutionCardRef = InstanceType<typeof ExecutionCard>; + +export default defineComponent({ name: 'executions-sidebar', components: { ExecutionCard, @@ -144,10 +144,11 @@ export default Vue.extend({ methods: { loadMore(limit = 20): void { if (!this.loading) { - const executionsList = this.$refs.executionList as HTMLElement; - if (executionsList) { + const executionsListRef = this.$refs.executionList as HTMLElement | undefined; + if (executionsListRef) { const diff = - executionsList.offsetHeight - (executionsList.scrollHeight - executionsList.scrollTop); + executionsListRef.offsetHeight - + (executionsListRef.scrollHeight - executionsListRef.scrollTop); if (diff > -10 && diff < 10) { this.$emit('loadMore', limit); } @@ -178,16 +179,16 @@ export default Vue.extend({ } }, checkListSize(): void { - const sidebarContainer = this.$refs.container as HTMLElement; - const currentExecutionCard = this.$refs[ + const sidebarContainerRef = this.$refs.container as HTMLElement | undefined; + const currentExecutionCardRefs = this.$refs[ `execution-${this.workflowsStore.activeWorkflowExecution?.id}` - ] as Vue[]; + ] as ExecutionCardRef[] | undefined; // Find out how many execution card can fit into list // and load more if needed - if (sidebarContainer && currentExecutionCard?.length) { - const cardElement = currentExecutionCard[0].$el as HTMLElement; - const listCapacity = Math.ceil(sidebarContainer.clientHeight / cardElement.clientHeight); + if (sidebarContainerRef && currentExecutionCardRefs?.length) { + const cardElement = currentExecutionCardRefs[0].$el as HTMLElement; + const listCapacity = Math.ceil(sidebarContainerRef.clientHeight / cardElement.clientHeight); if (listCapacity > this.executions.length) { this.$emit('loadMore', listCapacity - this.executions.length); @@ -195,21 +196,21 @@ export default Vue.extend({ } }, scrollToActiveCard(): void { - const executionsList = this.$refs.executionList as HTMLElement; - const currentExecutionCard = this.$refs[ + const executionsListRef = this.$refs.executionList as HTMLElement | undefined; + const currentExecutionCardRefs = this.$refs[ `execution-${this.workflowsStore.activeWorkflowExecution?.id}` - ] as Vue[]; + ] as ExecutionCardRef[] | undefined; if ( - executionsList && - currentExecutionCard?.length && + executionsListRef && + currentExecutionCardRefs?.length && this.workflowsStore.activeWorkflowExecution ) { - const cardElement = currentExecutionCard[0].$el as HTMLElement; + const cardElement = currentExecutionCardRefs[0].$el as HTMLElement; const cardRect = cardElement.getBoundingClientRect(); const LIST_HEADER_OFFSET = 200; - if (cardRect.top > executionsList.offsetHeight) { - executionsList.scrollTo({ top: cardRect.top - LIST_HEADER_OFFSET }); + if (cardRect.top > executionsListRef.offsetHeight) { + executionsListRef.scrollTo({ top: cardRect.top - LIST_HEADER_OFFSET }); } } }, @@ -288,3 +289,10 @@ export default Vue.extend({ text-align: center; } </style> + +<style lang="scss" scoped> +:deep(.el-skeleton__item) { + height: 60px; + border-radius: 0; +} +</style> diff --git a/packages/editor-ui/src/components/ExpandableInput/ExpandableInputBase.vue b/packages/editor-ui/src/components/ExpandableInput/ExpandableInputBase.vue index 39bf7a0187348..5d0c57ef24e23 100644 --- a/packages/editor-ui/src/components/ExpandableInput/ExpandableInputBase.vue +++ b/packages/editor-ui/src/components/ExpandableInput/ExpandableInputBase.vue @@ -6,9 +6,9 @@ </template> <script lang="ts"> -import Vue from 'vue'; +import { defineComponent } from 'vue'; -export default Vue.extend({ +export default defineComponent({ name: 'ExpandableInputBase', props: ['value', 'placeholder', 'staticSize'], computed: { diff --git a/packages/editor-ui/src/components/ExpandableInput/ExpandableInputEdit.vue b/packages/editor-ui/src/components/ExpandableInput/ExpandableInputEdit.vue index 198e64f4d7a00..a7454d79b1dd0 100644 --- a/packages/editor-ui/src/components/ExpandableInput/ExpandableInputEdit.vue +++ b/packages/editor-ui/src/components/ExpandableInput/ExpandableInputEdit.vue @@ -16,11 +16,12 @@ </template> <script lang="ts"> -import Vue, { PropType } from 'vue'; +import { defineComponent } from 'vue'; import ExpandableInputBase from './ExpandableInputBase.vue'; -import { EventBus } from '@/event-bus'; +import type { PropType } from 'vue'; +import type { EventBus } from '@/event-bus'; -export default Vue.extend({ +export default defineComponent({ components: { ExpandableInputBase }, name: 'ExpandableInputEdit', props: { diff --git a/packages/editor-ui/src/components/ExpandableInput/ExpandableInputPreview.vue b/packages/editor-ui/src/components/ExpandableInput/ExpandableInputPreview.vue index f30d91b18252d..7b0d23087e882 100644 --- a/packages/editor-ui/src/components/ExpandableInput/ExpandableInputPreview.vue +++ b/packages/editor-ui/src/components/ExpandableInput/ExpandableInputPreview.vue @@ -12,10 +12,10 @@ </template> <script lang="ts"> -import Vue from 'vue'; +import { defineComponent } from 'vue'; import ExpandableInputBase from './ExpandableInputBase.vue'; -export default Vue.extend({ +export default defineComponent({ components: { ExpandableInputBase }, name: 'ExpandableInputPreview', props: ['value'], diff --git a/packages/editor-ui/src/components/ExpressionEdit.vue b/packages/editor-ui/src/components/ExpressionEdit.vue index 32daae6fda8bb..a8d896cee968f 100644 --- a/packages/editor-ui/src/components/ExpressionEdit.vue +++ b/packages/editor-ui/src/components/ExpressionEdit.vue @@ -78,7 +78,7 @@ import ExpressionEditorModalInput from '@/components/ExpressionEditorModal/Expre import ExpressionEditorModalOutput from '@/components/ExpressionEditorModal/ExpressionEditorModalOutput.vue'; import VariableSelector from '@/components/VariableSelector.vue'; -import { IVariableItemSelected } from '@/Interface'; +import type { IVariableItemSelected } from '@/Interface'; import { externalHooks } from '@/mixins/externalHooks'; import { genericHelpers } from '@/mixins/genericHelpers'; diff --git a/packages/editor-ui/src/components/ExpressionEditorModal/ExpressionEditorModalOutput.vue b/packages/editor-ui/src/components/ExpressionEditorModal/ExpressionEditorModalOutput.vue index 642773e9089d9..8c1d9f3ad3bf1 100644 --- a/packages/editor-ui/src/components/ExpressionEditorModal/ExpressionEditorModalOutput.vue +++ b/packages/editor-ui/src/components/ExpressionEditorModal/ExpressionEditorModalOutput.vue @@ -3,9 +3,10 @@ </template> <script lang="ts"> -import Vue, { PropType } from 'vue'; import { EditorView } from '@codemirror/view'; import { EditorState } from '@codemirror/state'; +import { defineComponent } from 'vue'; +import type { PropType } from 'vue'; import { highlighter } from '@/plugins/codemirror/resolvableHighlighter'; import { outputTheme } from './theme'; @@ -13,7 +14,7 @@ import { outputTheme } from './theme'; import type { Plaintext, Resolved, Segment } from '@/types/expressions'; import { forceParse } from '@/utils/forceParse'; -export default Vue.extend({ +export default defineComponent({ name: 'ExpressionEditorModalOutput', props: { segments: { diff --git a/packages/editor-ui/src/components/ExpressionParameterInput.vue b/packages/editor-ui/src/components/ExpressionParameterInput.vue index 51e58eef9dc9d..f26dee0ebe2a6 100644 --- a/packages/editor-ui/src/components/ExpressionParameterInput.vue +++ b/packages/editor-ui/src/components/ExpressionParameterInput.vue @@ -66,7 +66,7 @@ <script lang="ts"> import { mapStores } from 'pinia'; -import Vue from 'vue'; +import { defineComponent } from 'vue'; import { useNDVStore } from '@/stores/ndv'; import { useWorkflowsStore } from '@/stores/workflows'; @@ -79,7 +79,9 @@ import { EXPRESSIONS_DOCS_URL } from '@/constants'; import type { Segment } from '@/types/expressions'; import type { TargetItem } from '@/Interface'; -export default Vue.extend({ +type InlineExpressionEditorInputRef = InstanceType<typeof InlineExpressionEditorInput>; + +export default defineComponent({ name: 'ExpressionParameterInput', components: { InlineExpressionEditorInput, @@ -115,7 +117,11 @@ export default Vue.extend({ return (this.hoveringItem?.itemIndex ?? 0) + 1; }, hoveringItem(): TargetItem | null { - return this.ndvStore.hoveringItem; + if (this.ndvStore.isInputParentOfActiveNode) { + return this.ndvStore.hoveringItem; + } + + return null; }, isDragging(): boolean { return this.ndvStore.isDraggableDragging; @@ -123,9 +129,10 @@ export default Vue.extend({ }, methods: { focus() { - const inlineInput = this.$refs.inlineInput as (Vue & HTMLElement) | undefined; - - if (inlineInput?.$el) inlineInput.focus(); + const inlineInputRef = this.$refs.inlineInput as InlineExpressionEditorInputRef | undefined; + if (inlineInputRef?.$el) { + inlineInputRef.focus(); + } }, onFocus() { this.isFocused = true; diff --git a/packages/editor-ui/src/components/FeatureComingSoon.vue b/packages/editor-ui/src/components/FeatureComingSoon.vue index b14c895535e7f..637f1028ecbae 100644 --- a/packages/editor-ui/src/components/FeatureComingSoon.vue +++ b/packages/editor-ui/src/components/FeatureComingSoon.vue @@ -29,15 +29,15 @@ </template> <script lang="ts"> -import { IFakeDoor } from '@/Interface'; +import type { IFakeDoor } from '@/Interface'; import { useRootStore } from '@/stores/n8nRootStore'; import { useSettingsStore } from '@/stores/settings'; import { useUIStore } from '@/stores/ui'; import { useUsersStore } from '@/stores/users'; import { mapStores } from 'pinia'; -import Vue from 'vue'; +import { defineComponent } from 'vue'; -export default Vue.extend({ +export default defineComponent({ name: 'FeatureComingSoon', props: { featureId: { diff --git a/packages/editor-ui/src/components/FixedCollectionParameter.vue b/packages/editor-ui/src/components/FixedCollectionParameter.vue index fdae7a368f466..b817034551054 100644 --- a/packages/editor-ui/src/components/FixedCollectionParameter.vue +++ b/packages/editor-ui/src/components/FixedCollectionParameter.vue @@ -112,21 +112,21 @@ </template> <script lang="ts"> -import Vue, { Component, PropType } from 'vue'; -import { IUpdateInformation } from '@/Interface'; +import { defineComponent } from 'vue'; +import type { Component, PropType } from 'vue'; +import type { IUpdateInformation } from '@/Interface'; -import { +import type { INodeParameters, INodeProperties, INodePropertyCollection, NodeParameterValue, - deepCopy, - isINodePropertyCollectionList, } from 'n8n-workflow'; +import { deepCopy, isINodePropertyCollectionList } from 'n8n-workflow'; import { get } from 'lodash-es'; -export default Vue.extend({ +export default defineComponent({ name: 'FixedCollectionParameter', props: { nodeValues: { diff --git a/packages/editor-ui/src/components/GoBackButton.vue b/packages/editor-ui/src/components/GoBackButton.vue index 9c54a2157bd31..6dcd409b971be 100644 --- a/packages/editor-ui/src/components/GoBackButton.vue +++ b/packages/editor-ui/src/components/GoBackButton.vue @@ -7,9 +7,9 @@ <script lang="ts"> import { VIEWS } from '@/constants'; -import Vue from 'vue'; +import { defineComponent } from 'vue'; -export default Vue.extend({ +export default defineComponent({ name: 'GoBackButton', data() { return { diff --git a/packages/editor-ui/src/components/HoverableNodeIcon.vue b/packages/editor-ui/src/components/HoverableNodeIcon.vue index 2dda3cd8af556..5b6e5a634cc9c 100644 --- a/packages/editor-ui/src/components/HoverableNodeIcon.vue +++ b/packages/editor-ui/src/components/HoverableNodeIcon.vue @@ -40,10 +40,10 @@ </template> <script lang="ts"> -import Vue from 'vue'; +import { defineComponent } from 'vue'; -import { ITemplatesNode } from '@/Interface'; -import { INodeTypeDescription } from 'n8n-workflow'; +import type { ITemplatesNode } from '@/Interface'; +import type { INodeTypeDescription } from 'n8n-workflow'; import { mapStores } from 'pinia'; import { useRootStore } from '@/stores/n8nRootStore'; @@ -54,7 +54,7 @@ interface NodeIconData { fileBuffer?: string; } -export default Vue.extend({ +export default defineComponent({ name: 'HoverableNodeIcon', props: { circle: { diff --git a/packages/editor-ui/src/components/HtmlEditor/HtmlEditor.vue b/packages/editor-ui/src/components/HtmlEditor/HtmlEditor.vue index 23b7168b962d3..0d6488d9b8ad2 100644 --- a/packages/editor-ui/src/components/HtmlEditor/HtmlEditor.vue +++ b/packages/editor-ui/src/components/HtmlEditor/HtmlEditor.vue @@ -18,7 +18,9 @@ import { indentOnInput, LanguageSupport, } from '@codemirror/language'; -import { EditorState, Extension } from '@codemirror/state'; +import type { Extension } from '@codemirror/state'; +import { EditorState } from '@codemirror/state'; +import type { ViewUpdate } from '@codemirror/view'; import { dropCursor, EditorView, @@ -26,7 +28,6 @@ import { highlightActiveLineGutter, keymap, lineNumbers, - ViewUpdate, } from '@codemirror/view'; import { n8nCompletionSources } from '@/plugins/codemirror/completions/addCompletions'; @@ -169,11 +170,12 @@ export default mixins(expressionManager).extend({ methods: { root() { - const root = this.$refs.htmlEditor as HTMLDivElement | undefined; - - if (!root) throw new Error('Expected div with ref "htmlEditor"'); + const rootRef = this.$refs.htmlEditor as HTMLDivElement | undefined; + if (!rootRef) { + throw new Error('Expected div with ref "htmlEditor"'); + } - return root; + return rootRef; }, isMissingHtmlTags() { diff --git a/packages/editor-ui/src/components/ImportCurlModal.vue b/packages/editor-ui/src/components/ImportCurlModal.vue index 4baa06600ebdb..03b30cbd258d6 100644 --- a/packages/editor-ui/src/components/ImportCurlModal.vue +++ b/packages/editor-ui/src/components/ImportCurlModal.vue @@ -48,7 +48,7 @@ import { } from '../constants'; import { showMessage } from '@/mixins/showMessage'; import mixins from 'vue-typed-mixins'; -import { INodeUi } from '@/Interface'; +import type { INodeUi } from '@/Interface'; import { mapStores } from 'pinia'; import { useUIStore } from '@/stores/ui'; import { useNDVStore } from '@/stores/ndv'; diff --git a/packages/editor-ui/src/components/InlineExpressionEditor/InlineExpressionEditorOutput.vue b/packages/editor-ui/src/components/InlineExpressionEditor/InlineExpressionEditorOutput.vue index 89aa0833528c0..b64a70a2df3ab 100644 --- a/packages/editor-ui/src/components/InlineExpressionEditor/InlineExpressionEditorOutput.vue +++ b/packages/editor-ui/src/components/InlineExpressionEditor/InlineExpressionEditorOutput.vue @@ -3,16 +3,18 @@ </template> <script lang="ts"> -import Vue, { PropType } from 'vue'; import { EditorView } from '@codemirror/view'; import { EditorState } from '@codemirror/state'; +import { defineComponent } from 'vue'; +import type { PropType } from 'vue'; + import { highlighter } from '@/plugins/codemirror/resolvableHighlighter'; import { outputTheme } from './theme'; import type { Plaintext, Resolved, Segment } from '@/types/expressions'; -export default Vue.extend({ +export default defineComponent({ name: 'InlineExpressionEditorOutput', props: { segments: { diff --git a/packages/editor-ui/src/components/InlineNameEdit.vue b/packages/editor-ui/src/components/InlineNameEdit.vue index 4ec839fbd3cf0..269c7d865659a 100644 --- a/packages/editor-ui/src/components/InlineNameEdit.vue +++ b/packages/editor-ui/src/components/InlineNameEdit.vue @@ -65,9 +65,9 @@ export default mixins(showMessage).extend({ this.isNameEdit = true; setTimeout(() => { - const input = this.$refs.nameInput as HTMLInputElement; - if (input) { - input.focus(); + const inputRef = this.$refs.nameInput as HTMLInputElement | undefined; + if (inputRef) { + inputRef.focus(); } }, 0); }, diff --git a/packages/editor-ui/src/components/InlineTextEdit.vue b/packages/editor-ui/src/components/InlineTextEdit.vue index 2876ab25b0214..ec2bd9f975e6f 100644 --- a/packages/editor-ui/src/components/InlineTextEdit.vue +++ b/packages/editor-ui/src/components/InlineTextEdit.vue @@ -21,12 +21,12 @@ </template> <script lang="ts"> -import Vue from 'vue'; +import { defineComponent } from 'vue'; import ExpandableInputEdit from '@/components/ExpandableInput/ExpandableInputEdit.vue'; import ExpandableInputPreview from '@/components/ExpandableInput/ExpandableInputPreview.vue'; import { createEventBus } from '@/event-bus'; -export default Vue.extend({ +export default defineComponent({ name: 'InlineTextEdit', components: { ExpandableInputEdit, ExpandableInputPreview }, props: ['isEditEnabled', 'value', 'placeholder', 'maxLength', 'previewValue'], diff --git a/packages/editor-ui/src/components/InputPanel.vue b/packages/editor-ui/src/components/InputPanel.vue index c52d2a1e666cf..f4be3c9daf8f8 100644 --- a/packages/editor-ui/src/components/InputPanel.vue +++ b/packages/editor-ui/src/components/InputPanel.vue @@ -117,7 +117,7 @@ }}</n8n-text> </template> - <template #recovered-artifical-output-data> + <template #recovered-artificial-output-data> <div :class="$style.recoveredOutputData"> <n8n-text tag="div" :bold="true" color="text-dark" size="large">{{ $locale.baseText('executionDetails.executionFailed.recoveredNodeTitle') @@ -131,8 +131,8 @@ </template> <script lang="ts"> -import { INodeUi } from '@/Interface'; -import { IConnectedNode, INodeTypeDescription, Workflow } from 'n8n-workflow'; +import type { INodeUi } from '@/Interface'; +import type { IConnectedNode, INodeTypeDescription, Workflow } from 'n8n-workflow'; import RunData from './RunData.vue'; import { workflowHelpers } from '@/mixins/workflowHelpers'; import mixins from 'vue-typed-mixins'; diff --git a/packages/editor-ui/src/components/IntersectionObserver.vue b/packages/editor-ui/src/components/IntersectionObserver.vue index 7632a95ad95d0..e548ec91f6e3a 100644 --- a/packages/editor-ui/src/components/IntersectionObserver.vue +++ b/packages/editor-ui/src/components/IntersectionObserver.vue @@ -5,9 +5,9 @@ </template> <script lang="ts"> -import Vue from 'vue'; +import { defineComponent } from 'vue'; -export default Vue.extend({ +export default defineComponent({ name: 'IntersectionObserver', props: ['threshold', 'enabled'], data() { diff --git a/packages/editor-ui/src/components/InviteUsersModal.vue b/packages/editor-ui/src/components/InviteUsersModal.vue index 7e4422826c322..a6fca050e841d 100644 --- a/packages/editor-ui/src/components/InviteUsersModal.vue +++ b/packages/editor-ui/src/components/InviteUsersModal.vue @@ -69,7 +69,7 @@ import mixins from 'vue-typed-mixins'; import { showMessage } from '@/mixins/showMessage'; import { copyPaste } from '@/mixins/copyPaste'; import Modal from './Modal.vue'; -import { IFormInputs, IInviteResponse, IUser } from '@/Interface'; +import type { IFormInputs, IInviteResponse, IUser } from '@/Interface'; import { VALID_EMAIL_REGEX, INVITE_USER_MODAL_KEY } from '@/constants'; import { ROLE } from '@/utils'; import { mapStores } from 'pinia'; diff --git a/packages/editor-ui/src/components/Logo.vue b/packages/editor-ui/src/components/Logo.vue index e72b9640b6fbf..64eeed691369d 100644 --- a/packages/editor-ui/src/components/Logo.vue +++ b/packages/editor-ui/src/components/Logo.vue @@ -5,9 +5,9 @@ <script lang="ts"> import { useRootStore } from '@/stores/n8nRootStore'; import { mapStores } from 'pinia'; -import Vue from 'vue'; +import { defineComponent } from 'vue'; -export default Vue.extend({ +export default defineComponent({ computed: { ...mapStores(useRootStore), basePath(): string { diff --git a/packages/editor-ui/src/components/MainHeader/MainHeader.vue b/packages/editor-ui/src/components/MainHeader/MainHeader.vue index 9eb708afa6b5d..732ca454aa96f 100644 --- a/packages/editor-ui/src/components/MainHeader/MainHeader.vue +++ b/packages/editor-ui/src/components/MainHeader/MainHeader.vue @@ -25,9 +25,9 @@ import { STICKY_NODE_TYPE, VIEWS, } from '@/constants'; -import { IExecutionsSummary, INodeUi, ITabBarItem } from '@/Interface'; +import type { IExecutionsSummary, INodeUi, ITabBarItem } from '@/Interface'; import { workflowHelpers } from '@/mixins/workflowHelpers'; -import { Route } from 'vue-router'; +import type { Route } from 'vue-router'; import { mapStores } from 'pinia'; import { useUIStore } from '@/stores/ui'; import { useNDVStore } from '@/stores/ndv'; diff --git a/packages/editor-ui/src/components/MainHeader/TabBar.vue b/packages/editor-ui/src/components/MainHeader/TabBar.vue index dadb9b62c3f81..a15f4e6c23496 100644 --- a/packages/editor-ui/src/components/MainHeader/TabBar.vue +++ b/packages/editor-ui/src/components/MainHeader/TabBar.vue @@ -12,13 +12,14 @@ </template> <script lang="ts"> -import Vue, { PropType } from 'vue'; -import { ITabBarItem } from '@/Interface'; +import { defineComponent } from 'vue'; +import type { PropType } from 'vue'; +import type { ITabBarItem } from '@/Interface'; import { MAIN_HEADER_TABS } from '@/constants'; import { mapStores } from 'pinia'; import { useUIStore } from '@/stores/ui'; -export default Vue.extend({ +export default defineComponent({ name: 'tab-bar', data() { return { diff --git a/packages/editor-ui/src/components/MainHeader/WorkflowDetails.vue b/packages/editor-ui/src/components/MainHeader/WorkflowDetails.vue index b7c1ceddd8591..30bde2f8f67a7 100644 --- a/packages/editor-ui/src/components/MainHeader/WorkflowDetails.vue +++ b/packages/editor-ui/src/components/MainHeader/WorkflowDetails.vue @@ -123,7 +123,6 @@ </template> <script lang="ts"> -import Vue from 'vue'; import mixins from 'vue-typed-mixins'; import { DUPLICATE_MODAL_KEY, @@ -145,10 +144,10 @@ import SaveButton from '@/components/SaveButton.vue'; import TagsDropdown from '@/components/TagsDropdown.vue'; import InlineTextEdit from '@/components/InlineTextEdit.vue'; import BreakpointsObserver from '@/components/BreakpointsObserver.vue'; -import { IUser, IWorkflowDataUpdate, IWorkflowDb, IWorkflowToShare } from '@/Interface'; +import type { IUser, IWorkflowDataUpdate, IWorkflowDb, IWorkflowToShare } from '@/Interface'; import { saveAs } from 'file-saver'; -import { titleChange } from '@/mixins/titleChange'; +import { useTitleChange } from '@/composables/useTitleChange'; import type { MessageBoxInputData } from 'element-ui/types/message-box'; import { mapStores } from 'pinia'; import { useUIStore } from '@/stores/ui'; @@ -156,10 +155,10 @@ import { useSettingsStore } from '@/stores/settings'; import { useWorkflowsStore } from '@/stores/workflows'; import { useRootStore } from '@/stores/n8nRootStore'; import { useTagsStore } from '@/stores/tags'; -import { getWorkflowPermissions, IPermissions } from '@/permissions'; +import type { IPermissions } from '@/permissions'; +import { getWorkflowPermissions } from '@/permissions'; import { useUsersStore } from '@/stores/users'; import { useUsageStore } from '@/stores/usage'; -import { BaseTextKey } from '@/plugins/i18n'; import { createEventBus } from '@/event-bus'; const hasChanged = (prev: string[], curr: string[]) => { @@ -171,7 +170,7 @@ const hasChanged = (prev: string[], curr: string[]) => { return curr.reduce((accu, val) => accu || !set.has(val), false); }; -export default mixins(workflowHelpers, titleChange).extend({ +export default mixins(workflowHelpers).extend({ name: 'WorkflowDetails', components: { TagsContainer, @@ -183,6 +182,11 @@ export default mixins(workflowHelpers, titleChange).extend({ InlineTextEdit, BreakpointsObserver, }, + setup() { + return { + ...useTitleChange(), + }; + }, data() { return { isTagsEditEnabled: false, @@ -420,9 +424,9 @@ export default mixins(workflowHelpers, titleChange).extend({ this.$root.$emit('importWorkflowData', { data: workflowData }); }; - const input = this.$refs.importFile as HTMLInputElement; - if (input !== null && input.files !== null && input.files.length !== 0) { - reader.readAsText(input!.files[0]!); + const inputRef = this.$refs.importFile as HTMLInputElement | undefined; + if (inputRef?.files && inputRef.files.length !== 0) { + reader.readAsText(inputRef.files[0]); } }, async onWorkflowMenuSelect(action: string): Promise<void> { @@ -505,17 +509,14 @@ export default mixins(workflowHelpers, titleChange).extend({ } try { - await this.restApi().deleteWorkflow(this.currentWorkflowId); + await this.workflowsStore.deleteWorkflow(this.currentWorkflowId); } catch (error) { - this.$showError( - error, - this.$locale.baseText('mainSidebar.showError.stopExecution.title'), - ); + this.$showError(error, this.$locale.baseText('generic.deleteWorkflowError')); return; } this.uiStore.stateIsDirty = false; // Reset tab title since workflow is deleted. - this.$titleReset(); + this.titleReset(); this.$showMessage({ title: this.$locale.baseText('mainSidebar.showMessage.handleSelect1.title'), type: 'success', diff --git a/packages/editor-ui/src/components/MainSidebar.vue b/packages/editor-ui/src/components/MainSidebar.vue index 8809d660a259a..4f2c445659e5b 100644 --- a/packages/editor-ui/src/components/MainSidebar.vue +++ b/packages/editor-ui/src/components/MainSidebar.vue @@ -9,13 +9,12 @@ > <div id="collapse-change-button" - :class="{ - ['clickable']: true, - [$style.sideMenuCollapseButton]: true, - [$style.expandedButton]: !isCollapsed, - }" + :class="['clickable', $style.sideMenuCollapseButton]" @click="toggleCollapse" - ></div> + > + <n8n-icon v-if="isCollapsed" icon="chevron-right" size="xsmall" class="ml-5xs" /> + <n8n-icon v-else icon="chevron-left" size="xsmall" class="mr-5xs" /> + </div> <n8n-menu :items="mainMenuItems" :collapsed="isCollapsed" @select="handleSelect"> <template #header> <div :class="$style.logo"> @@ -26,19 +25,39 @@ /> </div> </template> - <template #menuSuffix v-if="hasVersionUpdates"> - <div :class="$style.updates" @click="openUpdatesPanel"> - <div :class="$style.giftContainer"> - <GiftNotificationIcon /> + <template #menuSuffix> + <div v-if="hasVersionUpdates || versionControlStore.state.currentBranch"> + <div v-if="hasVersionUpdates" :class="$style.updates" @click="openUpdatesPanel"> + <div :class="$style.giftContainer"> + <GiftNotificationIcon /> + </div> + <n8n-text + :class="{ ['ml-xs']: true, [$style.expanded]: fullyExpanded }" + color="text-base" + > + {{ nextVersions.length > 99 ? '99+' : nextVersions.length }} update{{ + nextVersions.length > 1 ? 's' : '' + }} + </n8n-text> + </div> + <div :class="$style.sync" v-if="versionControlStore.state.currentBranch"> + <span> + <n8n-icon icon="code-branch" class="mr-xs" /> + {{ currentBranch }} + </span> + <n8n-button + :title=" + $locale.baseText('settings.versionControl.sync.prompt.title', { + interpolate: { branch: currentBranch }, + }) + " + icon="sync" + type="tertiary" + :size="isCollapsed ? 'mini' : 'small'" + square + @click="sync" + /> </div> - <n8n-text - :class="{ ['ml-xs']: true, [$style.expanded]: fullyExpanded }" - color="text-base" - > - {{ nextVersions.length > 99 ? '99+' : nextVersions.length }} update{{ - nextVersions.length > 1 ? 's' : '' - }} - </n8n-text> </div> </template> <template #footer v-if="showUserArea"> @@ -91,15 +110,13 @@ </template> <script lang="ts"> -import { IExecutionResponse, IMenuItem, IVersion } from '../Interface'; +import type { IExecutionResponse, IMenuItem, IVersion } from '../Interface'; import GiftNotificationIcon from './GiftNotificationIcon.vue'; import WorkflowSettings from '@/components/WorkflowSettings.vue'; import { genericHelpers } from '@/mixins/genericHelpers'; -import { restApi } from '@/mixins/restApi'; import { showMessage } from '@/mixins/showMessage'; -import { titleChange } from '@/mixins/titleChange'; import { workflowHelpers } from '@/mixins/workflowHelpers'; import { workflowRun } from '@/mixins/workflowRun'; @@ -115,13 +132,12 @@ import { useUsersStore } from '@/stores/users'; import { useWorkflowsStore } from '@/stores/workflows'; import { useRootStore } from '@/stores/n8nRootStore'; import { useVersionsStore } from '@/stores/versions'; -import { isNavigationFailure, NavigationFailureType, Route } from 'vue-router'; +import { isNavigationFailure } from 'vue-router'; +import { useVersionControlStore } from '@/stores/versionControl'; export default mixins( genericHelpers, - restApi, showMessage, - titleChange, workflowHelpers, workflowRun, userHelpers, @@ -147,7 +163,11 @@ export default mixins( useUsersStore, useVersionsStore, useWorkflowsStore, + useVersionControlStore, ), + currentBranch(): string { + return this.versionControlStore.state.currentBranch; + }, hasVersionUpdates(): boolean { return this.versionsStore.hasVersionUpdates; }, @@ -462,6 +482,29 @@ export default mixins( }); } }, + async sync() { + const prompt = await this.$prompt( + this.$locale.baseText('settings.versionControl.sync.prompt.description', { + interpolate: { branch: this.versionControlStore.state.currentBranch }, + }), + this.$locale.baseText('settings.versionControl.sync.prompt.title', { + interpolate: { branch: this.versionControlStore.state.currentBranch }, + }), + { + confirmButtonText: 'Sync', + cancelButtonText: 'Cancel', + inputPlaceholder: this.$locale.baseText( + 'settings.versionControl.sync.prompt.placeholder', + ), + inputPattern: /^.+$/, + inputErrorMessage: this.$locale.baseText('settings.versionControl.sync.prompt.error'), + }, + ); + + if (prompt.value) { + this.versionControlStore.sync({ commitMessage: prompt.value }); + } + }, }, }); </script> @@ -502,39 +545,16 @@ export default mixins( z-index: 999; display: flex; justify-content: center; - align-items: flex-end; + align-items: center; color: var(--color-text-base); background-color: var(--color-foreground-xlight); width: 20px; height: 20px; border: var(--border-width-base) var(--border-style-base) var(--color-foreground-base); - text-align: center; border-radius: 50%; - &::before { - display: block; - position: relative; - left: px; - top: -2.5px; - transform: rotate(270deg); - content: '\e6df'; - font-family: element-icons; - font-size: var(--font-size-2xs); - font-weight: bold; - color: var(--color-text-base); - } - - &.expandedButton { - &::before { - transform: rotate(90deg); - left: 0px; - } - } - &:hover { - &::before { - color: var(--color-primary-shade-1); - } + color: var(--color-primary-shade-1); } } @@ -600,4 +620,27 @@ export default mixins( display: none; } } + +.sync { + display: flex; + align-items: center; + justify-content: space-between; + padding: var(--spacing-s) var(--spacing-s) var(--spacing-s) var(--spacing-l); + margin: 0 calc(var(--spacing-l) * -1) calc(var(--spacing-m) * -1); + background: var(--color-background-light); + border-top: 1px solid var(--color-foreground-light); + font-size: var(--font-size-2xs); + + span { + color: var(--color-text-light); + } + + .sideMenuCollapsed & { + justify-content: center; + margin-left: calc(var(--spacing-xl) * -1); + > span { + display: none; + } + } +} </style> diff --git a/packages/editor-ui/src/components/Modal.vue b/packages/editor-ui/src/components/Modal.vue index a37ff05d8ec53..0321d9e0d4a95 100644 --- a/packages/editor-ui/src/components/Modal.vue +++ b/packages/editor-ui/src/components/Modal.vue @@ -43,12 +43,13 @@ </template> <script lang="ts"> -import Vue, { PropType } from 'vue'; +import { defineComponent } from 'vue'; +import type { PropType } from 'vue'; import { useUIStore } from '@/stores/ui'; import { mapStores } from 'pinia'; -import { EventBus } from '@/event-bus'; +import type { EventBus } from '@/event-bus'; -export default Vue.extend({ +export default defineComponent({ name: 'Modal', props: { name: { diff --git a/packages/editor-ui/src/components/ModalDrawer.vue b/packages/editor-ui/src/components/ModalDrawer.vue index 25802cbe64951..f3b864851831f 100644 --- a/packages/editor-ui/src/components/ModalDrawer.vue +++ b/packages/editor-ui/src/components/ModalDrawer.vue @@ -21,10 +21,11 @@ <script lang="ts"> import { useUIStore } from '@/stores/ui'; import { mapStores } from 'pinia'; -import Vue, { PropType } from 'vue'; -import { EventBus } from '@/event-bus'; +import { defineComponent } from 'vue'; +import type { PropType } from 'vue'; +import type { EventBus } from '@/event-bus'; -export default Vue.extend({ +export default defineComponent({ name: 'ModalDrawer', props: { name: { diff --git a/packages/editor-ui/src/components/ModalRoot.vue b/packages/editor-ui/src/components/ModalRoot.vue index 0696e2b706392..a2bfd81016e6c 100644 --- a/packages/editor-ui/src/components/ModalRoot.vue +++ b/packages/editor-ui/src/components/ModalRoot.vue @@ -12,11 +12,11 @@ </template> <script lang="ts"> -import Vue from 'vue'; +import { defineComponent } from 'vue'; import { useUIStore } from '@/stores/ui'; import { mapStores } from 'pinia'; -export default Vue.extend({ +export default defineComponent({ name: 'ModalRoot', props: { name: { diff --git a/packages/editor-ui/src/components/Modals.vue b/packages/editor-ui/src/components/Modals.vue index f9866042bd54d..3c01907bce00d 100644 --- a/packages/editor-ui/src/components/Modals.vue +++ b/packages/editor-ui/src/components/Modals.vue @@ -119,7 +119,7 @@ </template> <script lang="ts"> -import Vue from 'vue'; +import { defineComponent } from 'vue'; import { ABOUT_MODAL_KEY, CHANGE_PASSWORD_MODAL_KEY, @@ -171,7 +171,7 @@ import WorkflowShareModal from './WorkflowShareModal.ee.vue'; import WorkflowSuccessModal from './UserActivationSurveyModal.vue'; import EventDestinationSettingsModal from '@/components/SettingsLogStreaming/EventDestinationSettingsModal.ee.vue'; -export default Vue.extend({ +export default defineComponent({ name: 'Modals', components: { AboutModal, diff --git a/packages/editor-ui/src/components/MultipleParameter.vue b/packages/editor-ui/src/components/MultipleParameter.vue index 3d8e912d0a8f7..2202d49e66559 100644 --- a/packages/editor-ui/src/components/MultipleParameter.vue +++ b/packages/editor-ui/src/components/MultipleParameter.vue @@ -84,15 +84,17 @@ </template> <script lang="ts"> -import Vue, { PropType } from 'vue'; -import { IUpdateInformation } from '@/Interface'; -import { deepCopy, INodeParameters, INodeProperties } from 'n8n-workflow'; +import { defineComponent } from 'vue'; +import type { PropType } from 'vue'; +import type { IUpdateInformation } from '@/Interface'; +import type { INodeParameters, INodeProperties } from 'n8n-workflow'; +import { deepCopy } from 'n8n-workflow'; import CollectionParameter from '@/components/CollectionParameter.vue'; import ParameterInputFull from '@/components/ParameterInputFull.vue'; import { get } from 'lodash-es'; -export default Vue.extend({ +export default defineComponent({ name: 'MultipleParameter', components: { CollectionParameter, diff --git a/packages/editor-ui/src/components/NDVDraggablePanels.vue b/packages/editor-ui/src/components/NDVDraggablePanels.vue index 25fd9662404ca..b3d58ed2686ac 100644 --- a/packages/editor-ui/src/components/NDVDraggablePanels.vue +++ b/packages/editor-ui/src/components/NDVDraggablePanels.vue @@ -37,10 +37,10 @@ </template> <script lang="ts"> -import Vue, { PropType } from 'vue'; +import type { PropType } from 'vue'; import { get } from 'lodash-es'; -import { INodeTypeDescription } from 'n8n-workflow'; +import type { INodeTypeDescription } from 'n8n-workflow'; import PanelDragButton from './PanelDragButton.vue'; import { LOCAL_STORAGE_MAIN_PANEL_RELATIVE_WIDTH, MAIN_NODE_PANEL_WIDTH } from '@/constants'; @@ -48,7 +48,6 @@ import mixins from 'vue-typed-mixins'; import { debounceHelper } from '@/mixins/debounce'; import { mapStores } from 'pinia'; import { useNDVStore } from '@/stores/ndv'; -import { NodePanelType } from '@/Interface'; const SIDE_MARGIN = 24; const SIDE_PANELS_MARGIN = 80; @@ -437,6 +436,10 @@ export default mixins(debounceHelper).extend({ visibility: hidden; } +.double-width { + left: 90%; +} + .dragButtonContainer { position: absolute; top: -12px; diff --git a/packages/editor-ui/src/components/Node.vue b/packages/editor-ui/src/components/Node.vue index 325c72786fc95..3751cb798238e 100644 --- a/packages/editor-ui/src/components/Node.vue +++ b/packages/editor-ui/src/components/Node.vue @@ -182,7 +182,8 @@ import { nodeHelpers } from '@/mixins/nodeHelpers'; import { workflowHelpers } from '@/mixins/workflowHelpers'; import { pinData } from '@/mixins/pinData'; -import { IDataObject, INodeTypeDescription, ITaskData, NodeHelpers } from 'n8n-workflow'; +import type { INodeTypeDescription, ITaskData } from 'n8n-workflow'; +import { NodeHelpers } from 'n8n-workflow'; import NodeIcon from '@/components/NodeIcon.vue'; import TitledList from '@/components/TitledList.vue'; @@ -191,12 +192,7 @@ import mixins from 'vue-typed-mixins'; import { get } from 'lodash-es'; import { getStyleTokenValue, getTriggerNodeServiceName } from '@/utils'; -import { - IExecutionsSummary, - INodeUi, - INodeUpdatePropertiesInformation, - XYPosition, -} from '@/Interface'; +import type { IExecutionsSummary, INodeUi, XYPosition } from '@/Interface'; import { debounceHelper } from '@/mixins/debounce'; import { mapStores } from 'pinia'; import { useUIStore } from '@/stores/ui'; diff --git a/packages/editor-ui/src/components/Node/NodeCreation.vue b/packages/editor-ui/src/components/Node/NodeCreation.vue index edae0d921b58d..5c50b92528146 100644 --- a/packages/editor-ui/src/components/Node/NodeCreation.vue +++ b/packages/editor-ui/src/components/Node/NodeCreation.vue @@ -17,6 +17,7 @@ <div :class="[$style.addStickyButton, showStickyButton ? $style.visibleButton : '']" @click="addStickyNote" + data-test-id="add-sticky-button" > <n8n-icon-button size="medium" @@ -36,7 +37,7 @@ </template> <script lang="ts"> -import Vue from 'vue'; +import { defineComponent } from 'vue'; import { getMidCanvasPosition } from '@/utils/nodeViewUtils'; import { DEFAULT_STICKY_HEIGHT, @@ -47,7 +48,7 @@ import { import { mapStores } from 'pinia'; import { useUIStore } from '@/stores/ui'; -export default Vue.extend({ +export default defineComponent({ name: 'node-creation', components: { NodeCreator: () => import('@/components/Node/NodeCreator/NodeCreator.vue'), diff --git a/packages/editor-ui/src/components/Node/NodeCreator/CategorizedItems.vue b/packages/editor-ui/src/components/Node/NodeCreator/CategorizedItems.vue deleted file mode 100644 index 06dd3245d8e9e..0000000000000 --- a/packages/editor-ui/src/components/Node/NodeCreator/CategorizedItems.vue +++ /dev/null @@ -1,690 +0,0 @@ -<template> - <transition :name="`panel-slide-${state.transitionDirection}`"> - <div - ref="mainPanelContainer" - tabindex="0" - data-test-id="categorized-items" - :class="$style.categorizedItems" - :key="`${activeSubcategoryTitle + selectedViewType}_transition`" - @keydown.capture="nodeFilterKeyDown" - > - <div - :class="{ - [$style.header]: true, - [$style.headerWithBackground]: activeSubcategory, - }" - data-test-id="categorized-items-subcategory" - > - <button - :class="$style.backButton" - @click="onBackButton" - v-if="isViewNavigated || activeSubcategory" - > - <font-awesome-icon :class="$style.subcategoryBackIcon" icon="arrow-left" size="2x" /> - </button> - <div v-if="isRootView && $slots.header"> - <slot name="header" /> - </div> - <template v-if="activeSubcategory"> - <n8n-node-icon - :class="$style.nodeIcon" - v-if="showSubcategoryIcon && activeSubcategory.properties.icon" - :type="activeSubcategory.properties.iconType || 'unknown'" - :src="activeSubcategory.properties.icon" - :name="activeSubcategory.properties.icon" - :color="activeSubcategory.properties.color" - :circle="false" - :showTooltip="false" - :size="16" - /> - <span v-text="activeSubcategoryTitle" /> - </template> - </div> - <div - v-if="isRootView && $slots.description" - :class="{ - [$style.description]: true, - [$style.descriptionOffset]: isViewNavigated || activeSubcategory, - }" - > - <slot name="description" /> - </div> - - <search-bar - v-if="alwaysShowSearch || isSearchVisible" - :key="nodeCreatorStore.selectedView" - :value="searchFilter" - :placeholder=" - searchPlaceholder - ? searchPlaceholder - : $locale.baseText('nodeCreator.searchBar.searchNodes') - " - @input="onNodeFilterChange" - /> - - <div :class="$style.scrollable" ref="scrollableContainer"> - <item-iterator - :elements="searchFilter.length === 0 ? renderedItems : mergedFilteredNodes" - :activeIndex="activeSubcategory ? activeSubcategoryIndex : activeIndex" - :with-actions-getter="withActionsGetter" - :with-description-getter="withDescriptionGetter" - :lazyRender="true" - @selected="selected" - @actionsOpen="$listeners.actionsOpen" - @nodeTypeSelected="$listeners.nodeTypeSelected" - /> - <div v-if="searchFilter.length > 0 && mergedFilteredNodes.length === 0"> - <slot name="noResults" /> - </div> - <div :class="$style.footer" v-else-if="$slots.footer"> - <slot name="footer" /> - </div> - </div> - </div> - </transition> -</template> - -<script lang="ts" setup> -import { - computed, - reactive, - watch, - getCurrentInstance, - toRefs, - ref, - onUnmounted, - nextTick, -} from 'vue'; -import { camelCase } from 'lodash-es'; -import { externalHooks } from '@/mixins/externalHooks'; -import { INodeTypeDescription } from 'n8n-workflow'; -import ItemIterator from './ItemIterator.vue'; -import SearchBar from './SearchBar.vue'; -import { - INodeCreateElement, - ISubcategoryItemProps, - ICategoryItemProps, - SubcategoryCreateElement, - NodeCreateElement, - CategoryCreateElement, - INodeItemProps, -} from '@/Interface'; -import { BaseTextKey } from '@/plugins/i18n'; -import { sublimeSearch, matchesNodeType, matchesSelectType } from '@/utils'; -import { useWorkflowsStore } from '@/stores/workflows'; -import { useRootStore } from '@/stores/n8nRootStore'; -import { useNodeCreatorStore } from '@/stores/nodeCreator'; - -export interface Props { - showSubcategoryIcon?: boolean; - alwaysShowSearch?: boolean; - hideOtherCategoryItems?: boolean; - - lazyRender?: boolean; - searchPlaceholder?: string; - withActionsGetter?: (element: NodeCreateElement) => boolean; - withDescriptionGetter?: (element: NodeCreateElement) => boolean; - searchItems?: INodeCreateElement[]; - firstLevelItems?: INodeCreateElement[]; - categorizedItems: INodeCreateElement[]; - subcategoryOverride?: SubcategoryCreateElement | undefined; - allItems?: INodeCreateElement[]; -} - -const OTHER_RESULT_CATEGORY = 'searchAll'; -const props = withDefaults(defineProps<Props>(), { - allItems: () => [], - searchItems: () => [], - firstLevelItems: () => [], -}); - -const emit = defineEmits<{ - (event: 'subcategoryClose', value: INodeCreateElement[]): void; - (event: 'onSubcategorySelected', value: INodeCreateElement): void; - (event: 'nodeTypeSelected', value: string[]): void; - - (event: 'actionSelected', value: INodeCreateElement): void; - (event: 'actionsOpen', value: INodeTypeDescription): void; -}>(); - -const instance = getCurrentInstance(); -const { $externalHooks } = new externalHooks(); - -const { defaultLocale } = useRootStore(); -const { workflowId } = useWorkflowsStore(); -const nodeCreatorStore = useNodeCreatorStore(); - -const state = reactive({ - activeCategories: [] as string[], - // Keep track of activated subcategories so we could traverse back more than one level - activeSubcategoryHistory: [] as Array<{ - scrollPosition: number; - subcategory: INodeCreateElement; - activeIndex: number; - filter: string; - }>, - activeIndex: 0, - activeSubcategoryIndex: 0, - mainPanelContainer: null as HTMLElement | null, - transitionDirection: 'in', -}); -const searchBar = ref<InstanceType<typeof SearchBar>>(); -const scrollableContainer = ref<InstanceType<typeof HTMLElement>>(); - -const activeSubcategory = computed<INodeCreateElement | null>(() => { - return ( - state.activeSubcategoryHistory[state.activeSubcategoryHistory.length - 1]?.subcategory || null - ); -}); - -const categoriesKeys = computed(() => - props.categorizedItems.filter((item) => item.type === 'category').map((item) => item.key), -); -const activeSubcategoryTitle = computed<string>(() => { - if (!activeSubcategory.value || !activeSubcategory.value.properties) return ''; - - const subcategory = (activeSubcategory.value.properties as ISubcategoryItemProps).subcategory; - const subcategoryName = camelCase(subcategory); - - const titleLocaleKey = `nodeCreator.subcategoryTitles.${subcategoryName}` as BaseTextKey; - const nameLocaleKey = `nodeCreator.subcategoryNames.${subcategoryName}` as BaseTextKey; - - const titleLocale = instance?.proxy?.$locale.baseText(titleLocaleKey) as string; - const nameLocale = instance?.proxy?.$locale.baseText(nameLocaleKey) as string; - // If resolved title locale is same as the locale key it means it doesn't exist - // so we fallback to the subcategoryName - if (titleLocale === titleLocaleKey) - return nameLocale === nameLocaleKey ? subcategory : nameLocale; - - return titleLocale; -}); - -const searchFilter = computed<string>(() => nodeCreatorStore.itemsFilter.toLowerCase().trim()); - -const selectedViewType = computed(() => nodeCreatorStore.selectedView); - -const filteredNodeTypes = computed<INodeCreateElement[]>(() => { - const filter = searchFilter.value; - - let returnItems: INodeCreateElement[] = []; - if (defaultLocale !== 'en') { - returnItems = props.searchItems.filter((el: INodeCreateElement) => { - return ( - filter && - matchesSelectType(el, nodeCreatorStore.selectedView) && - matchesNodeType(el, filter) - ); - }); - } else { - const matchingNodes = - subcategorizedItems.value.length > 0 ? subcategorizedItems.value : props.searchItems; - - returnItems = getFilteredNodes(matchingNodes); - } - return returnItems; -}); - -const isViewNavigated = computed(() => nodeCreatorStore.rootViewHistory.length > 1); - -const globalFilteredNodeTypes = computed<INodeCreateElement[]>(() => { - const result = getFilteredNodes(props.allItems).reduce((acc, item) => { - if (acc.find((el) => trimTriggerNodeName(el.key) === trimTriggerNodeName(item.key))) { - return acc; - } - - return [...acc, item]; - }, [] as INodeCreateElement[]); - - return result; -}); - -const otherCategoryNodes = computed(() => { - const nodes = []; - - // Get diff of nodes between `globalFilteredNodeTypes` and `filteredNodeTypes` - for (const node of globalFilteredNodeTypes.value) { - const isNodeInFiltered = filteredNodeTypes.value.find( - (el) => trimTriggerNodeName(el.key) === trimTriggerNodeName(node.key), - ); - - if (!isNodeInFiltered) nodes.push(node); - } - - return nodes; -}); - -const mergedFilteredNodes = computed<INodeCreateElement[]>(() => { - if (props.hideOtherCategoryItems) return filteredNodeTypes.value; - - const isExpanded = state.activeCategories.includes(OTHER_RESULT_CATEGORY); - const searchCategory: CategoryCreateElement = { - type: 'category', - key: OTHER_RESULT_CATEGORY, - properties: { - category: OTHER_RESULT_CATEGORY, - name: `Results in other categories (${otherCategoryNodes.value.length})`, - expanded: isExpanded, - }, - }; - const nodeTypes = [...filteredNodeTypes.value]; - - if (otherCategoryNodes.value.length > 0) { - nodeTypes.push(searchCategory); - } - if (isExpanded) { - nodeTypes.push(...otherCategoryNodes.value); - } - - return nodeTypes; -}); - -const isRootView = computed(() => activeSubcategory.value === null); - -const subcategorizedItems = computed<INodeCreateElement[]>(() => { - if (!activeSubcategory.value) return []; - - const items = props.searchItems.filter((el: INodeCreateElement) => { - if (!activeSubcategory.value) return false; - - const subcategories = Object.values( - (el.properties as INodeItemProps).nodeType.codex?.subcategories || {}, - ).flat(); - return subcategories.includes(activeSubcategory.value.key); - }); - - return items.filter((el: INodeCreateElement) => - matchesSelectType(el, nodeCreatorStore.selectedView), - ); -}); - -const filteredCategorizedItems = computed<INodeCreateElement[]>(() => { - let categoriesCount = 0; - const reducedItems = props.categorizedItems.reduce( - (acc: INodeCreateElement[], el: INodeCreateElement) => { - if (el.type === 'category') { - el.properties.expanded = state.activeCategories.includes(el.key); - categoriesCount++; - return [...acc, el]; - } - - if (el.type === 'action' && state.activeCategories.includes(el.category)) { - return [...acc, el]; - } - - return acc; - }, - [], - ); - - // If there is only one category we don't show it - if (categoriesCount <= 1) - return reducedItems.filter((el: INodeCreateElement) => el.type !== 'category'); - - return reducedItems; -}); - -const renderedItems = computed<INodeCreateElement[]>(() => { - if (props.firstLevelItems.length > 0 && activeSubcategory.value === null) - return props.firstLevelItems; - - // If active subcategory is * then we show all items - if (activeSubcategory.value?.key === '*') return props.searchItems; - // Otherwise we show only items that match the subcategory - if (subcategorizedItems.value.length > 0) return subcategorizedItems.value; - - // Finally if none of the above is true we show the categorized items - return filteredCategorizedItems.value; -}); - -const isSearchVisible = computed<boolean>(() => { - if (subcategorizedItems.value.length === 0) return true; - - return subcategorizedItems.value.length > 9; -}); - -// Methods -function trimTriggerNodeName(nodeName: string) { - return nodeName.toLowerCase().replace('trigger', ''); -} -function getFilteredNodes(items: INodeCreateElement[]) { - // In order to support the old search we need to remove the 'trigger' part - const trimmedFilter = searchFilter.value.toLowerCase().replace('trigger', ''); - return ( - sublimeSearch<INodeCreateElement>(trimmedFilter, items, [ - { key: 'properties.nodeType.displayName', weight: 2 }, - { key: 'properties.nodeType.codex.alias', weight: 1 }, - ]) || [] - ).map(({ item }) => item); -} -function getScrollTop() { - return scrollableContainer.value?.scrollTop || 0; -} -function setScrollTop(scrollTop: number) { - if (scrollableContainer.value) { - scrollableContainer.value.scrollTop = scrollTop; - } -} -function onNodeFilterChange(filter: string) { - nodeCreatorStore.setFilter(filter); -} - -function nodeFilterKeyDown(e: KeyboardEvent) { - // We only want to propagate 'Escape' as it closes the node-creator and - // 'Tab' which toggles it - if (!['Escape', 'Tab'].includes(e.key)) e.stopPropagation(); - - // Prevent cursors position change - if (['ArrowUp', 'ArrowDown'].includes(e.key)) e.preventDefault(); - - if (activeSubcategory.value) { - const activeList = - searchFilter.value.length > 0 ? filteredNodeTypes.value : renderedItems.value; - const activeNodeType = activeList[state.activeSubcategoryIndex]; - - if (e.key === 'ArrowDown' && activeSubcategory.value) { - state.activeSubcategoryIndex++; - state.activeSubcategoryIndex = Math.min(state.activeSubcategoryIndex, activeList.length - 1); - } else if (e.key === 'ArrowUp' && activeSubcategory.value) { - state.activeSubcategoryIndex--; - state.activeSubcategoryIndex = Math.max(state.activeSubcategoryIndex, 0); - } else if (e.key === 'Enter') { - selected(activeNodeType); - } else if ( - e.key === 'ArrowLeft' && - activeNodeType?.type === 'category' && - (activeNodeType.properties as ICategoryItemProps).expanded - ) { - selected(activeNodeType); - } else if (e.key === 'ArrowLeft') { - onBackButton(); - } else if ( - e.key === 'ArrowRight' && - activeNodeType?.type === 'category' && - !(activeNodeType.properties as ICategoryItemProps).expanded - ) { - selected(activeNodeType); - } else if (e.key === 'ArrowRight' && ['node', 'action'].includes(activeNodeType?.type)) { - selected(activeNodeType); - } - return; - } - - const activeList = searchFilter.value.length > 0 ? filteredNodeTypes.value : renderedItems.value; - const activeNodeType = activeList[state.activeIndex]; - - if (e.key === 'ArrowDown') { - state.activeIndex++; - // Make sure that we stop at the last nodeType - state.activeIndex = Math.min(state.activeIndex, activeList.length - 1); - } else if (e.key === 'ArrowUp') { - state.activeIndex--; - // Make sure that we do not get before the first nodeType - state.activeIndex = Math.max(state.activeIndex, 0); - } else if (e.key === 'Enter' && activeNodeType) { - selected(activeNodeType); - } else if (e.key === 'ArrowRight' && activeNodeType?.type === 'subcategory') { - selected(activeNodeType); - } else if (e.key === 'ArrowRight' && activeNodeType?.type === 'view') { - selected(activeNodeType); - } else if ( - e.key === 'ArrowRight' && - activeNodeType?.type === 'category' && - !(activeNodeType.properties as ICategoryItemProps).expanded - ) { - selected(activeNodeType); - } else if ( - e.key === 'ArrowLeft' && - activeNodeType?.type === 'category' && - (activeNodeType.properties as ICategoryItemProps).expanded - ) { - selected(activeNodeType); - } else if (e.key === 'ArrowLeft' && isViewNavigated.value) { - onBackButton(); - } else if (e.key === 'ArrowRight' && ['node', 'action'].includes(activeNodeType?.type)) { - selected(activeNodeType); - } -} -function selected(element: INodeCreateElement) { - const typeHandler = { - category: () => onCategorySelected(element), - subcategory: () => onSubcategorySelected(element), - node: () => onNodeSelected(element as NodeCreateElement), - action: () => onActionSelected(element), - view: () => onViewSelected(element), - }; - - typeHandler[element.type](); -} -function onViewSelected(view: Record<string, any>) { - state.transitionDirection = 'in'; - state.activeIndex = 0; - nodeCreatorStore.setSelectedView(view.key); - nodeCreatorStore.setFilter(''); -} - -function onNodeSelected(element: NodeCreateElement) { - const hasActions = (element.properties.nodeType?.actions?.length || 0) > 0; - if (props.withActionsGetter && props.withActionsGetter(element) === true && hasActions) { - emit('actionsOpen', element.properties.nodeType); - return; - } - emit('nodeTypeSelected', [element.key]); -} - -function onCategorySelected(element: CategoryCreateElement) { - const categoryKey = element.properties.category; - if (state.activeCategories.includes(categoryKey)) { - state.activeCategories = state.activeCategories.filter( - (active: string) => active !== categoryKey, - ); - } else { - state.activeCategories = [...state.activeCategories, categoryKey]; - instance?.proxy.$telemetry.trackNodesPanel('nodeCreateList.onCategoryExpanded', { - category_name: categoryKey, - workflow_id: workflowId, - }); - } -} -function onActionSelected(element: INodeCreateElement) { - emit('actionSelected', element); -} - -function onSubcategorySelected(selected: INodeCreateElement, track = true) { - state.transitionDirection = 'in'; - // Store the current subcategory UI details in the state - // so we could revert it when the user closes the subcategory - state.activeSubcategoryHistory.push({ - subcategory: selected, - activeIndex: state.activeSubcategoryIndex, - scrollPosition: getScrollTop(), - filter: nodeCreatorStore.itemsFilter, - }); - nodeCreatorStore.setFilter(''); - emit('onSubcategorySelected', selected); - state.activeSubcategoryIndex = 0; - - if (track) { - instance?.proxy.$telemetry.trackNodesPanel('nodeCreateList.onSubcategorySelected', { - selected, - workflow_id: workflowId, - }); - } -} - -async function onBackButton() { - state.transitionDirection = 'out'; - // Switching views - if (isRootView.value && isViewNavigated.value) { - nodeCreatorStore.closeCurrentView(); - return; - } - - const poppedSubCategory = state.activeSubcategoryHistory.pop(); - onNodeFilterChange(poppedSubCategory?.filter || ''); - await nextTick(); - emit( - 'subcategoryClose', - state.activeSubcategoryHistory.map((el) => el.subcategory), - ); - await nextTick(); - setScrollTop(poppedSubCategory?.scrollPosition || 0); - state.activeSubcategoryIndex = poppedSubCategory?.activeIndex || 0; -} - -watch( - () => props.subcategoryOverride, - (subcategory) => { - if (subcategory) onSubcategorySelected(subcategory, false); - }, -); -watch( - () => props.categorizedItems, - () => { - state.activeCategories = [...categoriesKeys.value, OTHER_RESULT_CATEGORY]; - }, -); - -onUnmounted(() => { - nodeCreatorStore.setFilter(''); -}); - -watch(filteredNodeTypes, (returnItems) => { - $externalHooks().run('nodeCreateList.filteredNodeTypesComputed', { - nodeFilter: nodeCreatorStore.itemsFilter, - result: returnItems, - selectedType: nodeCreatorStore.selectedView, - }); -}); - -watch(isSearchVisible, (isVisible) => { - if (isVisible === false) { - // Focus the root container when search is hidden to make sure - // keyboard navigation still works - nextTick(() => state.mainPanelContainer?.focus()); - } -}); -watch( - () => nodeCreatorStore.itemsFilter, - (newValue, oldValue) => { - // Reset the index whenver the filter-value changes - state.activeIndex = 0; - state.activeSubcategoryIndex = 0; - $externalHooks().run('nodeCreateList.nodeFilterChanged', { - oldValue, - newValue, - selectedType: nodeCreatorStore.selectedView, - filteredNodes: filteredNodeTypes.value, - }); - instance?.proxy.$telemetry.trackNodesPanel('nodeCreateList.nodeFilterChanged', { - oldValue, - newValue, - selectedType: nodeCreatorStore.selectedView, - filteredNodes: filteredNodeTypes.value, - workflow_id: workflowId, - }); - }, -); - -const { activeSubcategoryIndex, activeIndex, mainPanelContainer } = toRefs(state); -</script> - -<style lang="scss" module> -:global(.panel-slide-in-leave-active), -:global(.panel-slide-in-enter-active), -:global(.panel-slide-out-leave-active), -:global(.panel-slide-out-enter-active) { - transition: transform 300ms ease; - position: absolute; - left: 0; - right: 0; -} - -:global(.panel-slide-out-enter), -:global(.panel-slide-in-leave-to) { - transform: translateX(0); - z-index: -1; -} - -:global(.panel-slide-out-leave-to), -:global(.panel-slide-in-enter) { - transform: translateX(100%); - // Make sure the leaving panel stays on top - // for the slide-out panel effect - z-index: 1; -} -.nodeIcon { - --node-icon-size: 16px; - margin-right: var(--spacing-s); -} -.categorizedItems { - background: white; - height: 100%; - background-color: $node-creator-background-color; - &:before { - box-sizing: border-box; - content: ''; - border-left: 1px solid $node-creator-border-color; - width: 1px; - position: absolute; - height: 100%; - } -} -.footer { - font-size: var(--font-size-2xs); - color: var(--color-text-base); - margin: 0 var(--spacing-xs) 0; - padding: var(--spacing-4xs) 0; - line-height: var(--font-line-height-regular); - border-top: 1px solid #dbdfe7; - z-index: 1; - margin-top: -1px; -} -.header { - font-size: var(--font-size-l); - font-weight: var(--font-weight-bold); - line-height: var(--font-line-height-compact); - - display: flex; - align-items: center; - padding: var(--spacing-s) var(--spacing-s) var(--spacing-2xs); - - &.headerWithBackground { - border-bottom: $node-creator-border-color solid 1px; - height: 50px; - background-color: $node-creator-subcategory-panel-header-bacground-color; - padding: var(--spacing-s) var(--spacing-s); - } -} -.description { - padding: 0 var(--spacing-s) var(--spacing-2xs) var(--spacing-s); - margin-top: -4px; -} -.descriptionOffset { - margin-left: calc(var(--spacing-xl) + var(--spacing-4xs)); -} -.backButton { - background: transparent; - border: none; - cursor: pointer; - padding: 0 var(--spacing-xs) 0 0; -} - -.subcategoryBackIcon { - color: $node-creator-arrow-color; - height: 16px; - padding: 0; -} - -.scrollable { - height: calc(100% - 120px); - padding-top: 1px; - padding-bottom: var(--spacing-xl); - overflow-y: auto; - overflow-x: visible; - - scrollbar-width: none; /* Firefox 64 */ - &::-webkit-scrollbar { - display: none; - } -} -</style> diff --git a/packages/editor-ui/src/components/Node/NodeCreator/CategoryItem.vue b/packages/editor-ui/src/components/Node/NodeCreator/CategoryItem.vue deleted file mode 100644 index 9dc5eba3fb0d1..0000000000000 --- a/packages/editor-ui/src/components/Node/NodeCreator/CategoryItem.vue +++ /dev/null @@ -1,41 +0,0 @@ -<template> - <div :class="$style.category"> - <span :class="$style.name" v-text="item.name" /> - <font-awesome-icon v-if="item.expanded" icon="chevron-down" :class="$style.arrow" /> - <font-awesome-icon :class="$style.arrow" icon="chevron-up" v-else /> - </div> -</template> - -<script lang="ts" setup> -import { ICategoryItemProps } from '@/Interface'; - -export interface Props { - item: ICategoryItemProps; -} -defineProps<Props>(); -</script> - -<style lang="scss" module> -.category { - font-size: 11px; - font-weight: var(--font-weight-bold); - letter-spacing: 1px; - line-height: 11px; - padding: 10px 0; - margin: 0 var(--spacing-xs); - border-bottom: 1px solid $node-creator-border-color; - display: flex; - text-transform: uppercase; - cursor: pointer; -} - -.name { - flex-grow: 1; -} - -.arrow { - font-size: var(--font-size-2xs); - width: 12px; - color: $node-creator-arrow-color; -} -</style> diff --git a/packages/editor-ui/src/components/Node/NodeCreator/ItemIterator.vue b/packages/editor-ui/src/components/Node/NodeCreator/ItemIterator.vue deleted file mode 100644 index 8889fb88b40bb..0000000000000 --- a/packages/editor-ui/src/components/Node/NodeCreator/ItemIterator.vue +++ /dev/null @@ -1,210 +0,0 @@ -<template> - <div - :class="$style.itemIterator" - name="accordion" - @before-enter="beforeEnter" - @enter="enter" - @before-leave="beforeLeave" - @leave="leave" - > - <div - v-for="(item, index) in renderedItems" - :key="`${item.key}-${index}`" - data-test-id="item-iterator-item" - :class="{ - clickable: !disabled, - [$style[item.type]]: true, - [$style.active]: activeIndex === index && !disabled, - [$style.iteratorItem]: true, - }" - ref="iteratorItems" - @click="wrappedEmit('selected', item)" - > - <category-item v-if="item.type === 'category'" :item="item.properties" /> - - <subcategory-item v-else-if="item.type === 'subcategory'" :item="item.properties" /> - - <node-item - v-else-if="item.type === 'node'" - :nodeType="item.properties.nodeType" - :allow-actions="withActionsGetter && withActionsGetter(item)" - :allow-description="withDescriptionGetter && withDescriptionGetter(item)" - @dragstart="wrappedEmit('dragstart', item, $event)" - @dragend="wrappedEmit('dragend', item, $event)" - @nodeTypeSelected="$listeners.nodeTypeSelected" - @actionsOpen="$listeners.actionsOpen" - /> - - <action-item - v-else-if="item.type === 'action'" - :nodeType="item.properties.nodeType" - :action="item.properties.nodeType" - @dragstart="wrappedEmit('dragstart', item, $event)" - @dragend="wrappedEmit('dragend', item, $event)" - /> - - <view-item v-else-if="item.type === 'view'" :view="item.properties" /> - </div> - <aside - v-for="item in elements.length" - v-show="renderedItems.length < item" - :key="item" - :class="$style.loadingItem" - > - <n8n-loading :loading="true" :rows="1" variant="p" /> - </aside> - </div> -</template> - -<script setup lang="ts"> -import { INodeCreateElement, NodeCreateElement } from '@/Interface'; -import NodeItem from './NodeItem.vue'; -import SubcategoryItem from './SubcategoryItem.vue'; -import CategoryItem from './CategoryItem.vue'; -import ActionItem from './ActionItem.vue'; -import ViewItem from './ViewItem.vue'; -import { reactive, toRefs, onMounted, watch, onUnmounted, ref } from 'vue'; - -export interface Props { - elements: INodeCreateElement[]; - activeIndex?: number; - disabled?: boolean; - lazyRender?: boolean; - withActionsGetter?: (element: NodeCreateElement) => boolean; - withDescriptionGetter?: (element: NodeCreateElement) => boolean; - enableGlobalCategoriesCounter?: boolean; -} - -const props = withDefaults(defineProps<Props>(), { - elements: () => [], -}); - -const emit = defineEmits<{ - (event: 'selected', element: INodeCreateElement, $e?: Event): void; - (event: 'dragstart', element: INodeCreateElement, $e: Event): void; - (event: 'dragend', element: INodeCreateElement, $e: Event): void; -}>(); - -const state = reactive({ - renderedItems: [] as INodeCreateElement[], - renderAnimationRequest: 0, -}); -const iteratorItems = ref<HTMLElement[]>([]); - -function wrappedEmit( - event: 'selected' | 'dragstart' | 'dragend', - element: INodeCreateElement, - $e?: Event, -) { - if (props.disabled) return; - - emit((event as 'selected') || 'dragstart' || 'dragend', element, $e); -} - -// Lazy render large items lists to prevent the browser from freezing -// when loading many items. -function renderItems() { - if (props.elements.length <= 20 || props.lazyRender === false) { - state.renderedItems = props.elements; - return; - } - - if (state.renderedItems.length < props.elements.length) { - state.renderedItems.push( - ...props.elements.slice(state.renderedItems.length, state.renderedItems.length + 10), - ); - state.renderAnimationRequest = window.requestAnimationFrame(renderItems); - } -} - -function beforeEnter(el: HTMLElement) { - el.style.height = '0'; -} - -function enter(el: HTMLElement) { - el.style.height = `${el.scrollHeight}px`; -} - -function beforeLeave(el: HTMLElement) { - el.style.height = `${el.scrollHeight}px`; -} - -function leave(el: HTMLElement) { - el.style.height = '0'; -} - -onMounted(() => { - renderItems(); -}); - -onUnmounted(() => { - window.cancelAnimationFrame(state.renderAnimationRequest); - state.renderedItems = []; -}); - -// Make sure the active item is always visible -// scroll if needed -watch( - () => props.activeIndex, - async () => { - if (props.activeIndex === undefined) return; - iteratorItems.value[props.activeIndex]?.scrollIntoView({ block: 'nearest' }); - }, -); - -// Trigger elements re-render when they change -watch( - () => props.elements, - async () => { - window.cancelAnimationFrame(state.renderAnimationRequest); - state.renderedItems = []; - renderItems(); - }, -); - -const { renderedItems } = toRefs(state); -</script> - -<style lang="scss" module> -.loadingItem { - height: 48px; - margin: 0 var(--search-margin, var(--spacing-s)); -} -.iteratorItem { - // Make sure border is fully visible - margin-left: 1px; - position: relative; - &::before { - content: ''; - position: absolute; - left: -1px; - top: 0; - bottom: 0; - border-left: 2px solid transparent; - } - &:hover::before { - border-color: $node-creator-item-hover-border-color; - } - - &.active::before { - border-color: $color-primary !important; - } - - &.category.singleCategory { - display: none; - } -} -.itemIterator { - > *:last-child { - margin-bottom: var(--spacing-2xl); - } -} -.action { - &:last-of-type { - margin-bottom: var(--spacing-s); - } -} -.node + .category { - margin-top: var(--spacing-s); -} -</style> diff --git a/packages/editor-ui/src/components/Node/NodeCreator/ActionItem.vue b/packages/editor-ui/src/components/Node/NodeCreator/ItemTypes/ActionItem.vue similarity index 73% rename from packages/editor-ui/src/components/Node/NodeCreator/ActionItem.vue rename to packages/editor-ui/src/components/Node/NodeCreator/ItemTypes/ActionItem.vue index 15c18a2e1c4df..dafdba7cc23c9 100644 --- a/packages/editor-ui/src/components/Node/NodeCreator/ActionItem.vue +++ b/packages/editor-ui/src/components/Node/NodeCreator/ItemTypes/ActionItem.vue @@ -1,13 +1,12 @@ <template> <n8n-node-creator-node - :key="`${action.actionKey}_${action.displayName}`" - @click="onActionClick(action)" @dragstart="onDragStart" @dragend="onDragEnd" draggable :class="$style.action" :title="action.displayName" :isTrigger="isTriggerAction(action)" + data-keyboard-nav="true" > <template #dragContent> <div :class="$style.draggableDataTransfer" ref="draggableDataTransfer" /> @@ -23,22 +22,27 @@ <script setup lang="ts"> import { reactive, computed, toRefs, getCurrentInstance } from 'vue'; -import { INodeTypeDescription, INodeActionTypeDescription } from 'n8n-workflow'; +import type { ActionTypeDescription, SimplifiedNodeType } from '@/Interface'; +import { WEBHOOK_NODE_TYPE } from '@/constants'; + import { getNewNodePosition, NODE_SIZE } from '@/utils/nodeViewUtils'; -import { IUpdateInformation } from '@/Interface'; import NodeIcon from '@/components/NodeIcon.vue'; -import { useNodeCreatorStore } from '@/stores/nodeCreator'; + +import { useViewStacks } from '../composables/useViewStacks'; +import { useActions } from '../composables/useActions'; export interface Props { - nodeType: INodeTypeDescription; - action: INodeActionTypeDescription; + nodeType: SimplifiedNodeType; + action: ActionTypeDescription; } const props = defineProps<Props>(); + const instance = getCurrentInstance(); const telemetry = instance?.proxy.$telemetry; -const { getActionData, getNodeTypesWithManualTrigger, setAddedNodeActionParameters } = - useNodeCreatorStore(); + +const { getActionData, getNodeTypesWithManualTrigger, setAddedNodeActionParameters } = useActions(); +const { activeViewStack } = useViewStacks(); const state = reactive({ dragging: false, @@ -49,11 +53,6 @@ const state = reactive({ storeWatcher: null as Function | null, draggableDataTransfer: null as Element | null, }); -const emit = defineEmits<{ - (event: 'actionSelected', action: IUpdateInformation): void; - (event: 'dragstart', $e: DragEvent): void; - (event: 'dragend', $e: DragEvent): void; -}>(); const draggableStyle = computed<{ top: string; left: string }>(() => ({ top: `${state.draggablePosition.y}px`, @@ -62,11 +61,8 @@ const draggableStyle = computed<{ top: string; left: string }>(() => ({ const actionData = computed(() => getActionData(props.action)); -const isTriggerAction = (action: INodeActionTypeDescription) => - action.name?.toLowerCase().includes('trigger'); -function onActionClick(actionItem: INodeActionTypeDescription) { - emit('actionSelected', getActionData(actionItem)); -} +const isTriggerAction = (action: ActionTypeDescription) => + action.name?.toLowerCase().includes('trigger') || action.name === WEBHOOK_NODE_TYPE; function onDragStart(event: DragEvent): void { /** @@ -84,14 +80,18 @@ function onDragStart(event: DragEvent): void { 'nodeTypeName', getNodeTypesWithManualTrigger(actionData.value?.key).join(','), ); - - state.storeWatcher = setAddedNodeActionParameters(actionData.value, telemetry); + if (telemetry) { + state.storeWatcher = setAddedNodeActionParameters( + actionData.value, + telemetry, + activeViewStack.rootView, + ); + } document.body.addEventListener('dragend', onDragEnd); } state.dragging = true; state.draggablePosition = { x, y }; - emit('dragstart', event); } function onDragOver(event: DragEvent): void { @@ -109,8 +109,6 @@ function onDragEnd(event: DragEvent): void { document.body.removeEventListener('dragend', onDragEnd); document.body.removeEventListener('dragover', onDragOver); - emit('dragend', event); - state.dragging = false; setTimeout(() => { state.draggablePosition = { x: -100, y: -100 }; @@ -121,25 +119,19 @@ const { draggableDataTransfer, dragging } = toRefs(state); <style lang="scss" module> .action { - margin-left: 15px; - margin-right: 12px; - + --node-creator-name-size: var(--font-size-2xs); + --node-creator-name-weight: var(--font-weight-normal); --trigger-icon-background-color: #{$trigger-icon-background-color}; --trigger-icon-border-color: #{$trigger-icon-border-color}; -} -.nodeIcon { + --node-icon-size: 20px; + --node-icon-margin-right: var(--spacing-xs); + + margin-left: var(--spacing-s); margin-right: var(--spacing-s); + padding: var(--spacing-2xs) 0; } - -.apiHint { - font-size: var(--font-size-2xs); - color: var(--color-text-base); - padding-top: var(--spacing-s); - line-height: var(--font-line-height-regular); - border-top: 1px solid #dbdfe7; - z-index: 1; - // Prevent double borders when the last category is collapsed - margin-top: -1px; +.nodeIcon { + margin-right: var(--spacing-xs); } .draggable { diff --git a/packages/editor-ui/src/components/Node/NodeCreator/ItemTypes/CategoryItem.vue b/packages/editor-ui/src/components/Node/NodeCreator/ItemTypes/CategoryItem.vue new file mode 100644 index 0000000000000..b42dd251fab8d --- /dev/null +++ b/packages/editor-ui/src/components/Node/NodeCreator/ItemTypes/CategoryItem.vue @@ -0,0 +1,81 @@ +<script lang="ts" setup> +import { computed } from 'vue'; + +export interface Props { + expanded?: boolean; + active?: boolean; + count?: number; + name: string; + isTrigger?: boolean; +} +const props = withDefaults(defineProps<Props>(), { + expanded: true, +}); + +const categoryName = computed(() => { + const itemsCount = props.count || 0; + return itemsCount > 0 ? `${props.name} (${itemsCount})` : props.name; +}); +</script> + +<template> + <div + :class="$style.categoryWrapper" + v-on="$listeners" + data-keyboard-nav="true" + data-test-id="node-creator-category-item" + > + <div :class="{ [$style.category]: true, [$style.active]: active }"> + <span :class="$style.name"> + <span v-text="categoryName" /> + <font-awesome-icon icon="bolt" v-if="isTrigger" size="xs" :class="$style.triggerIcon" /> + <slot /> + </span> + <font-awesome-icon v-if="expanded" icon="chevron-down" :class="$style.arrow" /> + <font-awesome-icon :class="$style.arrow" icon="chevron-up" v-else /> + </div> + </div> +</template> + +<style lang="scss" module> +.triggerIcon { + color: var(--color-primary); + margin-left: var(--spacing-3xs); +} +.category { + font-size: var(--font-size-s); + font-weight: var(--font-weight-bold); + line-height: var(--font-line-height-compact); + padding: var(--spacing-2xs) var(--spacing-s); + border-bottom: 1px solid $node-creator-border-color; + display: flex; + cursor: pointer; + + position: relative; + &::before { + content: ''; + position: absolute; + left: 0; + top: 0; + bottom: 0; + border-left: 2px solid transparent; + } + &:hover::before { + border-color: $node-creator-item-hover-border-color; + } + &.active::before { + border-color: $color-primary; + } +} + +.name { + flex-grow: 1; + color: var(--color-text-dark); +} + +.arrow { + font-size: var(--font-size-2xs); + width: 12px; + color: $node-creator-arrow-color; +} +</style> diff --git a/packages/editor-ui/src/components/Node/NodeCreator/ItemTypes/LabelItem.vue b/packages/editor-ui/src/components/Node/NodeCreator/ItemTypes/LabelItem.vue new file mode 100644 index 0000000000000..d3d3b3585c337 --- /dev/null +++ b/packages/editor-ui/src/components/Node/NodeCreator/ItemTypes/LabelItem.vue @@ -0,0 +1,31 @@ +<template> + <div :class="$style.label"> + <span :class="$style.name" v-text="item.key" /> + </div> +</template> + +<script lang="ts" setup> +import type { LabelItemProps } from '@/Interface'; + +export interface Props { + item: LabelItemProps; +} +defineProps<Props>(); +</script> + +<style lang="scss" module> +.label { + margin-left: var(--spacing-s); + margin-right: var(--spacing-s); + margin-bottom: var(--spacing-4xs); + letter-spacing: 1px; + padding-top: var(--spacing-s); + font-style: normal; + font-weight: 700; + font-size: 10px; + line-height: 12px; + text-transform: uppercase; + color: var(--color-text-base); + cursor: default; +} +</style> diff --git a/packages/editor-ui/src/components/Node/NodeCreator/NodeItem.vue b/packages/editor-ui/src/components/Node/NodeCreator/ItemTypes/NodeItem.vue similarity index 66% rename from packages/editor-ui/src/components/Node/NodeCreator/NodeItem.vue rename to packages/editor-ui/src/components/Node/NodeCreator/ItemTypes/NodeItem.vue index 62dd517abc4fb..874c83cfce3c9 100644 --- a/packages/editor-ui/src/components/Node/NodeCreator/NodeItem.vue +++ b/packages/editor-ui/src/components/Node/NodeCreator/ItemTypes/NodeItem.vue @@ -4,12 +4,11 @@ :draggable="!showActionArrow" @dragstart="onDragStart" @dragend="onDragEnd" - @click.stop="onClick" :class="$style.nodeItem" - :description="allowDescription ? description : ''" + :description="subcategory !== DEFAULT_SUBCATEGORY ? description : ''" :title="displayName" - :isTrigger="!allowActions && isTriggerNode" :show-action-arrow="showActionArrow" + :is-trigger="isTrigger" > <template #icon> <node-icon :nodeType="nodeType" /> @@ -39,62 +38,59 @@ </template> <script setup lang="ts"> -import { reactive, computed, toRefs, getCurrentInstance } from 'vue'; -import { INodeTypeDescription } from 'n8n-workflow'; +import { computed, ref, getCurrentInstance } from 'vue'; +import type { SimplifiedNodeType } from '@/Interface'; +import { COMMUNITY_NODES_INSTALLATION_DOCS_URL, DEFAULT_SUBCATEGORY } from '@/constants'; -import { getNewNodePosition, NODE_SIZE } from '@/utils/nodeViewUtils'; import { isCommunityPackageName } from '@/utils'; -import { COMMUNITY_NODES_INSTALLATION_DOCS_URL } from '@/constants'; +import { getNewNodePosition, NODE_SIZE } from '@/utils/nodeViewUtils'; import { useNodeCreatorStore } from '@/stores/nodeCreator'; - import NodeIcon from '@/components/NodeIcon.vue'; +import { useActions } from '../composables/useActions'; + export interface Props { - nodeType: INodeTypeDescription; + nodeType: SimplifiedNodeType; + subcategory?: string; active?: boolean; - allowActions?: boolean; - allowDescription?: boolean; } const props = withDefaults(defineProps<Props>(), { active: false, - allowActions: false, - allowDescription: false, }); -const emit = defineEmits<{ - (event: 'dragstart', $e: DragEvent): void; - (event: 'dragend', $e: DragEvent): void; - (event: 'nodeTypeSelected', value: string[]): void; - (event: 'actionsOpen', value: INodeTypeDescription): void; -}>(); - +const { actions } = useNodeCreatorStore(); +const { getNodeTypesWithManualTrigger } = useActions(); const instance = getCurrentInstance(); -const state = reactive({ - dragging: false, - draggablePosition: { - x: -100, - y: -100, - }, - draggableDataTransfer: null as Element | null, -}); + +const dragging = ref(false); +const draggablePosition = ref({ x: -100, y: -100 }); +const draggableDataTransfer = ref(null as Element | null); + const description = computed<string>(() => { return instance?.proxy.$locale.headerText({ key: `headers.${shortNodeType.value}.description`, fallback: props.nodeType.description, }) as string; }); -const showActionArrow = computed(() => props.allowActions && hasActions.value); +const showActionArrow = computed(() => hasActions.value); + +const hasActions = computed(() => { + return nodeActions.value.length > 1; +}); -const hasActions = computed<boolean>(() => (props.nodeType.actions?.length || 0) > 0); +const nodeActions = computed(() => { + const nodeActions = actions[props.nodeType.name] || []; + return nodeActions; +}); const shortNodeType = computed<string>( () => instance?.proxy.$locale.shortNodeType(props.nodeType.name) || '', ); const draggableStyle = computed<{ top: string; left: string }>(() => ({ - top: `${state.draggablePosition.y}px`, - left: `${state.draggablePosition.x}px`, + top: `${draggablePosition.value.y}px`, + left: `${draggablePosition.value.x}px`, })); const isCommunityNode = computed<boolean>(() => isCommunityPackageName(props.nodeType.name)); @@ -105,21 +101,13 @@ const displayName = computed<any>(() => { return instance?.proxy.$locale.headerText({ key: `headers.${shortNodeType}.displayName`, - fallback: - props.allowActions && props.nodeType.actions?.length - ? displayName.replace('Trigger', '') - : displayName, + fallback: hasActions.value ? displayName.replace('Trigger', '') : displayName, }); }); -const isTriggerNode = computed<boolean>(() => - props.nodeType.displayName.toLowerCase().includes('trigger'), -); - -function onClick() { - if (hasActions.value && props.allowActions) emit('actionsOpen', props.nodeType); - else emit('nodeTypeSelected', [props.nodeType.name]); -} +const isTrigger = computed<boolean>(() => { + return props.nodeType.group.includes('trigger') && !hasActions.value; +}); function onDragStart(event: DragEvent): void { /** * Workaround for firefox, that doesn't attach the pageX and pageY coordinates to "ondrag" event. @@ -133,36 +121,33 @@ function onDragStart(event: DragEvent): void { if (event.dataTransfer) { event.dataTransfer.effectAllowed = 'copy'; event.dataTransfer.dropEffect = 'copy'; - event.dataTransfer.setDragImage(state.draggableDataTransfer as Element, 0, 0); + event.dataTransfer.setDragImage(draggableDataTransfer.value as Element, 0, 0); event.dataTransfer.setData( 'nodeTypeName', - useNodeCreatorStore().getNodeTypesWithManualTrigger(props.nodeType.name).join(','), + getNodeTypesWithManualTrigger(props.nodeType.name).join(','), ); } - state.dragging = true; - state.draggablePosition = { x, y }; - emit('dragstart', event); + dragging.value = true; + draggablePosition.value = { x, y }; } function onDragOver(event: DragEvent): void { - if (!state.dragging || (event.pageX === 0 && event.pageY === 0)) { + if (!dragging.value || (event.pageX === 0 && event.pageY === 0)) { return; } const [x, y] = getNewNodePosition([], [event.pageX - NODE_SIZE / 2, event.pageY - NODE_SIZE / 2]); - state.draggablePosition = { x, y }; + draggablePosition.value = { x, y }; } function onDragEnd(event: DragEvent): void { document.body.removeEventListener('dragover', onDragOver); - emit('dragend', event); - - state.dragging = false; + dragging.value = false; setTimeout(() => { - state.draggablePosition = { x: -100, y: -100 }; + draggablePosition.value = { x: -100, y: -100 }; }, 300); } @@ -171,11 +156,6 @@ function onCommunityNodeTooltipClick(event: MouseEvent) { instance?.proxy.$telemetry.track('user clicked cnr docs link', { source: 'nodes panel node' }); } } - -defineExpose({ - onClick, -}); -const { dragging, draggableDataTransfer } = toRefs(state); </script> <style lang="scss" module> .nodeItem { diff --git a/packages/editor-ui/src/components/Node/NodeCreator/SubcategoryItem.vue b/packages/editor-ui/src/components/Node/NodeCreator/ItemTypes/SubcategoryItem.vue similarity index 78% rename from packages/editor-ui/src/components/Node/NodeCreator/SubcategoryItem.vue rename to packages/editor-ui/src/components/Node/NodeCreator/ItemTypes/SubcategoryItem.vue index 903b45cb3e38c..caf102f675c46 100644 --- a/packages/editor-ui/src/components/Node/NodeCreator/SubcategoryItem.vue +++ b/packages/editor-ui/src/components/Node/NodeCreator/ItemTypes/SubcategoryItem.vue @@ -13,15 +13,15 @@ </template> <script setup lang="ts"> -import { ISubcategoryItemProps } from '@/Interface'; +import type { SubcategoryItemProps } from '@/Interface'; import { camelCase } from 'lodash-es'; import { computed } from 'vue'; export interface Props { - item: ISubcategoryItemProps; + item: SubcategoryItemProps; } const props = defineProps<Props>(); -const subcategoryName = computed(() => camelCase(props.item.subcategory)); +const subcategoryName = computed(() => camelCase(props.item.subcategory || props.item.title)); </script> <style lang="scss" module> @@ -30,9 +30,4 @@ const subcategoryName = computed(() => camelCase(props.item.subcategory)); margin-left: 15px; margin-right: 12px; } -.withTopBorder { - border-top: 1px solid var(--color-foreground-base); - margin-top: var(--spacing-m); - padding-top: var(--spacing-l); -} </style> diff --git a/packages/editor-ui/src/components/Node/NodeCreator/ItemTypes/ViewItem.vue b/packages/editor-ui/src/components/Node/NodeCreator/ItemTypes/ViewItem.vue new file mode 100644 index 0000000000000..98fde73d25ff8 --- /dev/null +++ b/packages/editor-ui/src/components/Node/NodeCreator/ItemTypes/ViewItem.vue @@ -0,0 +1,31 @@ +<template> + <n8n-node-creator-node + :class="$style.view" + :title="view.title" + :isTrigger="false" + :description="view.description" + :showActionArrow="true" + > + <template #icon> + <n8n-node-icon type="icon" :name="view.icon" :circle="false" :showTooltip="false" /> + </template> + </n8n-node-creator-node> +</template> + +<script setup lang="ts"> +import type { ViewItemProps } from '@/Interface'; + +export interface Props { + view: ViewItemProps; +} + +defineProps<Props>(); +</script> + +<style lang="scss" module> +.view { + --action-arrow-color: var(--color-text-light); + margin-left: var(--spacing-s); + margin-right: var(--spacing-xs); +} +</style> diff --git a/packages/editor-ui/src/components/Node/NodeCreator/MainPanel.vue b/packages/editor-ui/src/components/Node/NodeCreator/MainPanel.vue deleted file mode 100644 index 1947318e37b81..0000000000000 --- a/packages/editor-ui/src/components/Node/NodeCreator/MainPanel.vue +++ /dev/null @@ -1,409 +0,0 @@ -<template> - <div :class="{ [$style.mainPanel]: true, [$style.isRoot]: isRoot }"> - <CategorizedItems - :subcategoryOverride="nodeAppSubcategory" - :alwaysShowSearch="isActionsActive" - :hideOtherCategoryItems="isActionsActive" - :categorizedItems="computedCategorizedItems" - :searchItems="searchItems" - :withActionsGetter="shouldShowNodeActions" - :withDescriptionGetter="shouldShowNodeDescription" - :firstLevelItems="firstLevelItems" - :showSubcategoryIcon="isActionsActive" - :allItems="transformCreateElements(mergedAppNodes)" - :searchPlaceholder="searchPlaceholder" - @subcategoryClose="onSubcategoryClose" - @onSubcategorySelected="onSubcategorySelected" - @nodeTypeSelected="onNodeTypeSelected" - @actionsOpen="setActiveActionsNodeType" - @actionSelected="onActionSelected" - > - <template #noResults> - <no-results - data-test-id="categorized-no-results" - :showRequest="!isActionsActive" - :show-icon="!isActionsActive" - > - <template #title v-if="!isActionsActive"> - <p v-text="$locale.baseText('nodeCreator.noResults.weDidntMakeThatYet')" /> - </template> - - <template v-if="isActionsActive" #action> - <p - v-if="containsAPIAction" - v-html="getCustomAPICallHintLocale('apiCallNoResult')" - class="clickable" - @click.stop="addHttpNode(true)" - /> - <p v-else v-text="$locale.baseText('nodeCreator.noResults.noMatchingActions')" /> - </template> - - <template v-else #action> - {{ $locale.baseText('nodeCreator.noResults.dontWorryYouCanProbablyDoItWithThe') }} - <n8n-link v-if="[REGULAR_NODE_FILTER].includes(selectedView)" @click="addHttpNode"> - {{ $locale.baseText('nodeCreator.noResults.httpRequest') }} - </n8n-link> - - <n8n-link v-if="[TRIGGER_NODE_FILTER].includes(selectedView)" @click="addWebHookNode()"> - {{ $locale.baseText('nodeCreator.noResults.webhook') }} - </n8n-link> - {{ $locale.baseText('nodeCreator.noResults.node') }} - </template> - </no-results> - </template> - - <template #header> - <p - v-if="isRoot && activeView && activeView.title" - v-text="activeView.title" - :class="$style.title" - /> - </template> - <template #description> - <p - v-if="isRoot && activeView && activeView.description" - v-text="activeView.description" - :class="$style.description" - /> - </template> - <template #footer v-if="activeNodeActions && containsAPIAction"> - <span - v-html="getCustomAPICallHintLocale('apiCall')" - class="clickable" - @click.stop="addHttpNode(true)" - /> - </template> - </CategorizedItems> - </div> -</template> - -<script setup lang="ts"> -import { reactive, toRefs, getCurrentInstance, computed, onUnmounted, ref } from 'vue'; -import { - INodeTypeDescription, - INodeActionTypeDescription, - INodeTypeNameVersion, -} from 'n8n-workflow'; -import { - INodeCreateElement, - NodeCreateElement, - IActionItemProps, - SubcategoryCreateElement, - IUpdateInformation, -} from '@/Interface'; -import { - CORE_NODES_CATEGORY, - WEBHOOK_NODE_TYPE, - EMAIL_IMAP_NODE_TYPE, - CUSTOM_API_CALL_NAME, - HTTP_REQUEST_NODE_TYPE, - STICKY_NODE_TYPE, - REGULAR_NODE_FILTER, - TRIGGER_NODE_FILTER, - N8N_NODE_TYPE, -} from '@/constants'; -import CategorizedItems from './CategorizedItems.vue'; -import { useNodeCreatorStore } from '@/stores/nodeCreator'; -import { getCategoriesWithNodes, getCategorizedList } from '@/utils'; -import { externalHooks } from '@/mixins/externalHooks'; -import { useNodeTypesStore } from '@/stores/nodeTypes'; -import { BaseTextKey } from '@/plugins/i18n'; -import NoResults from './NoResults.vue'; -import { useRootStore } from '@/stores/n8nRootStore'; -import useMainPanelView from './useMainPanelView'; - -const instance = getCurrentInstance(); - -const emit = defineEmits({ - nodeTypeSelected: (nodeTypes: string[]) => true, -}); - -const state = reactive({ - isRoot: true, - selectedSubcategory: '', - activeNodeActions: null as INodeTypeDescription | null, -}); -const { baseUrl } = useRootStore(); -const { $externalHooks } = new externalHooks(); -const { - mergedAppNodes, - getActionData, - getNodeTypesWithManualTrigger, - setAddedNodeActionParameters, -} = useNodeCreatorStore(); -const { activeView } = useMainPanelView(); -const telemetry = instance?.proxy.$telemetry; -const { isTriggerNode } = useNodeTypesStore(); -const containsAPIAction = computed( - () => - state.activeNodeActions?.properties.some((p) => - p.options?.find((o) => o.name === CUSTOM_API_CALL_NAME), - ) === true, -); - -const selectedView = computed(() => useNodeCreatorStore().selectedView); -const computedCategorizedItems = computed(() => { - if (isActionsActive.value) { - return sortActions(getCategorizedList(computedCategoriesWithNodes.value, true)); - } - - return getCategorizedList(computedCategoriesWithNodes.value, true); -}); - -const nodeAppSubcategory = computed<SubcategoryCreateElement | undefined>(() => { - if (!state.activeNodeActions) return undefined; - - const icon = state.activeNodeActions.iconUrl - ? `${baseUrl}${state.activeNodeActions.iconUrl}` - : state.activeNodeActions.icon?.split(':')[1]; - - return { - type: 'subcategory', - key: state.activeNodeActions.name, - properties: { - subcategory: state.activeNodeActions.displayName, - description: '', - iconType: state.activeNodeActions.iconUrl ? 'file' : 'icon', - icon, - color: state.activeNodeActions.defaults.color, - }, - }; -}); -const searchPlaceholder = computed(() => { - const nodeNameTitle = state.activeNodeActions?.displayName?.trim() as string; - const actionsSearchPlaceholder = instance?.proxy.$locale.baseText( - 'nodeCreator.actionsCategory.searchActions', - { interpolate: { nodeNameTitle } }, - ); - - return isActionsActive.value ? actionsSearchPlaceholder : undefined; -}); - -const filteredMergedAppNodes = computed(() => { - const WHITELISTED_APP_CORE_NODES = [EMAIL_IMAP_NODE_TYPE, WEBHOOK_NODE_TYPE]; - - if (isAppEventSubcategory.value) - return mergedAppNodes.filter((node) => { - const isTrigger = isTriggerNode(node.name); - const isRegularNode = !isTrigger; - const isStickyNode = node.name === STICKY_NODE_TYPE; - const isCoreNode = - node.codex?.categories?.includes(CORE_NODES_CATEGORY) && - !WHITELISTED_APP_CORE_NODES.includes(node.name); - const hasActions = (node.actions || []).length > 0; - - // Never show core nodes and sticky node in the Apps subcategory - if (isCoreNode || isStickyNode) return false; - - // Only show nodes without action within their view - if (!hasActions) { - return isRegularNode - ? selectedView.value === REGULAR_NODE_FILTER - : selectedView.value === TRIGGER_NODE_FILTER; - } - - return true; - }); - - return mergedAppNodes; -}); - -const computedCategoriesWithNodes = computed(() => { - if (!state.activeNodeActions) return getCategoriesWithNodes(filteredMergedAppNodes.value); - - return getCategoriesWithNodes(selectedNodeActions.value, state.activeNodeActions.displayName); -}); - -const selectedNodeActions = computed<INodeActionTypeDescription[]>( - () => state.activeNodeActions?.actions ?? [], -); -const isAppEventSubcategory = computed(() => state.selectedSubcategory === '*'); -const isActionsActive = computed(() => state.activeNodeActions !== null); -const firstLevelItems = computed(() => (isRoot.value ? activeView.value.items : [])); - -const searchItems = computed<INodeCreateElement[]>(() => { - return state.activeNodeActions - ? transformCreateElements(selectedNodeActions.value, 'action') - : transformCreateElements(filteredMergedAppNodes.value); -}); - -// If the user is in the root view, we want to show trigger nodes first -// otherwise we want to show them last -function sortActions(nodeCreateElements: INodeCreateElement[]): INodeCreateElement[] { - const elements = { - trigger: [] as INodeCreateElement[], - regular: [] as INodeCreateElement[], - }; - - nodeCreateElements.forEach((el) => { - const isTriggersCategory = el.type === 'category' && el.key === 'Triggers'; - const isTriggerAction = el.type === 'action' && el.category === 'Triggers'; - - elements[isTriggersCategory || isTriggerAction ? 'trigger' : 'regular'].push(el); - }); - - if (selectedView.value === TRIGGER_NODE_FILTER) { - return [...elements.trigger, ...elements.regular]; - } - - return [...elements.regular, ...elements.trigger]; -} - -function transformCreateElements( - createElements: Array<INodeTypeDescription | INodeActionTypeDescription>, - type: 'node' | 'action' = 'node', -): INodeCreateElement[] { - const sorted = [...createElements]; - - sorted.sort((a, b) => { - const textA = a.displayName.toLowerCase(); - const textB = b.displayName.toLowerCase(); - return textA < textB ? -1 : textA > textB ? 1 : 0; - }); - - return sorted.map((nodeType) => { - const hasTriggerActions = nodeType.actions?.find((action) => action.name.includes('trigger')); - const hasRgeularActions = nodeType.actions?.find((action) => !action.name.includes('trigger')); - - return { - type, - category: nodeType.codex?.categories, - key: nodeType.name, - properties: { - nodeType, - subcategory: state.activeNodeActions?.displayName ?? '', - }, - includedByTrigger: hasTriggerActions || nodeType.group.includes('trigger'), - includedByRegular: hasRgeularActions || !nodeType.group.includes('trigger'), - } as INodeCreateElement; - }); -} - -function onNodeTypeSelected(nodeTypes: string[]) { - emit( - 'nodeTypeSelected', - nodeTypes.length === 1 ? getNodeTypesWithManualTrigger(nodeTypes[0]) : nodeTypes, - ); -} -function getCustomAPICallHintLocale(key: string) { - if (!state.activeNodeActions) return ''; - - const nodeNameTitle = state.activeNodeActions.displayName; - return instance?.proxy.$locale.baseText(`nodeCreator.actionsList.${key}` as BaseTextKey, { - interpolate: { nodeNameTitle }, - }); -} - -function setActiveActionsNodeType(nodeType: INodeTypeDescription | null) { - state.activeNodeActions = nodeType; - - if (nodeType) trackActionsView(); -} - -function onActionSelected(actionCreateElement: INodeCreateElement) { - const action = (actionCreateElement.properties as IActionItemProps).nodeType; - const actionUpdateData = getActionData(action); - emit('nodeTypeSelected', getNodeTypesWithManualTrigger(actionUpdateData.key)); - setAddedNodeActionParameters(actionUpdateData, telemetry); -} -function addWebHookNode() { - emit('nodeTypeSelected', [WEBHOOK_NODE_TYPE]); -} - -function addHttpNode(isAction: boolean) { - const updateData = { - name: '', - key: HTTP_REQUEST_NODE_TYPE, - value: { - authentication: 'predefinedCredentialType', - }, - } as IUpdateInformation; - - emit('nodeTypeSelected', [HTTP_REQUEST_NODE_TYPE]); - if (isAction) { - setAddedNodeActionParameters(updateData, telemetry, false); - - const app_identifier = state.activeNodeActions?.name; - $externalHooks().run('nodeCreateList.onActionsCustmAPIClicked', { app_identifier }); - telemetry?.trackNodesPanel('nodeCreateList.onActionsCustmAPIClicked', { app_identifier }); - } -} - -function onSubcategorySelected(subcategory: INodeCreateElement) { - state.isRoot = false; - state.selectedSubcategory = subcategory.key; -} -function onSubcategoryClose(activeSubcategories: INodeCreateElement[]) { - if (isActionsActive.value === true) setActiveActionsNodeType(null); - - state.isRoot = activeSubcategories.length === 0; - state.selectedSubcategory = activeSubcategories[activeSubcategories.length - 1]?.key ?? ''; -} - -function shouldShowNodeDescription(node: NodeCreateElement) { - return (node.category || []).includes(CORE_NODES_CATEGORY); -} - -function shouldShowNodeActions(node: INodeCreateElement) { - if (state.isRoot && useNodeCreatorStore().itemsFilter === '') return false; - - return true; -} - -function trackActionsView() { - const trigger_action_count = selectedNodeActions.value.filter((action) => - action.name.toLowerCase().includes('trigger'), - ).length; - - const trackingPayload = { - app_identifier: state.activeNodeActions?.name, - actions: selectedNodeActions.value.map((action) => action.displayName), - regular_action_count: selectedNodeActions.value.length - trigger_action_count, - trigger_action_count, - }; - - $externalHooks().run('nodeCreateList.onViewActions', trackingPayload); - telemetry?.trackNodesPanel('nodeCreateList.onViewActions', trackingPayload); -} - -onUnmounted(() => { - useNodeCreatorStore().resetRootViewHistory(); -}); -const { isRoot, activeNodeActions } = toRefs(state); -</script> - -<style lang="scss" module> -.mainPanel { - --node-icon-color: var(--color-text-base); - height: 100%; - display: flex; - flex-direction: column; - - // Remove node item border on the root level - &.isRoot { - --node-item-border: none; - } -} -.itemCreator { - height: calc(100% - 120px); - padding-top: 1px; - overflow-y: auto; - overflow-x: visible; - - &::-webkit-scrollbar { - display: none; - } -} - -.title { - font-size: var(--font-size-l); - line-height: var(--font-line-height-xloose); - font-weight: var(--font-weight-bold); - color: var(--color-text-dark); -} -.description { - font-size: var(--font-size-s); - line-height: var(--font-line-height-loose); - color: var(--color-text-base); -} -</style> diff --git a/packages/editor-ui/src/components/Node/NodeCreator/Modes/ActionsMode.vue b/packages/editor-ui/src/components/Node/NodeCreator/Modes/ActionsMode.vue new file mode 100644 index 0000000000000..4ca6b4cda9d36 --- /dev/null +++ b/packages/editor-ui/src/components/Node/NodeCreator/Modes/ActionsMode.vue @@ -0,0 +1,366 @@ +<script setup lang="ts"> +import { computed, getCurrentInstance, onMounted, defineComponent } from 'vue'; +import type { VNode, PropType } from 'vue'; +import type { + INodeCreateElement, + ActionTypeDescription, + NodeFilterType, + IUpdateInformation, + ActionCreateElement, +} from '@/Interface'; +import { + HTTP_REQUEST_NODE_TYPE, + REGULAR_NODE_CREATOR_VIEW, + TRIGGER_NODE_CREATOR_VIEW, + CUSTOM_API_CALL_KEY, + AUTO_INSERT_ACTION_EXPERIMENT, +} from '@/constants'; + +import { usePostHog } from '@/stores/posthog'; +import { useUsersStore } from '@/stores/users'; +import { useWebhooksStore } from '@/stores/webhooks'; +import { runExternalHook } from '@/utils'; + +import { useActions } from '../composables/useActions'; +import { useKeyboardNavigation } from '../composables/useKeyboardNavigation'; +import { useViewStacks } from '../composables/useViewStacks'; + +import ItemsRenderer from '../Renderers/ItemsRenderer.vue'; +import CategorizedItemsRenderer from '../Renderers/CategorizedItemsRenderer.vue'; + +const emit = defineEmits({ + nodeTypeSelected: (nodeTypes: string[]) => true, +}); +const instance = getCurrentInstance(); +const telemetry = instance?.proxy.$telemetry; + +const { userActivated } = useUsersStore(); +const { popViewStack, updateCurrentViewStack } = useViewStacks(); +const { registerKeyHook } = useKeyboardNavigation(); +const { + getNodeTypesWithManualTrigger, + setAddedNodeActionParameters, + getActionData, + getPlaceholderTriggerActions, + parseCategoryActions, + actionsCategoryLocales, +} = useActions(); + +// We only inject labels if search is empty +const parsedTriggerActions = computed(() => + parseActions(actions.value, actionsCategoryLocales.value.triggers, false), +); +const parsedActionActions = computed(() => + parseActions(actions.value, actionsCategoryLocales.value.actions, !search.value), +); +const parsedTriggerActionsBaseline = computed(() => + parseActions( + useViewStacks().activeViewStack.baselineItems || [], + actionsCategoryLocales.value.triggers, + false, + ), +); +const parsedActionActionsBaseline = computed(() => + parseActions( + useViewStacks().activeViewStack.baselineItems || [], + actionsCategoryLocales.value.actions, + !search.value, + ), +); + +// Because the placeholder items are inserted into the slots, we need to +// add the placeholder count to the category name manually +const triggerCategoryName = computed(() => + parsedTriggerActions.value.length || search.value + ? actionsCategoryLocales.value.triggers + : `${actionsCategoryLocales.value.triggers} (${placeholderTriggerActions.length})`, +); + +const actions = computed(() => { + return (useViewStacks().activeViewStack.items || []).filter( + (p) => (p as ActionCreateElement).properties.actionKey !== CUSTOM_API_CALL_KEY, + ); +}); + +const search = computed(() => useViewStacks().activeViewStack.search); + +const subcategory = computed(() => useViewStacks().activeViewStack.subcategory); + +const rootView = computed(() => useViewStacks().activeViewStack.rootView); + +const placeholderTriggerActions = getPlaceholderTriggerActions(subcategory.value || ''); + +const hasNoTriggerActions = computed( + () => + parseCategoryActions( + useViewStacks().activeViewStack.baselineItems || [], + actionsCategoryLocales.value.triggers, + !search.value, + ).length === 0, +); + +const containsAPIAction = computed(() => { + const actions = useViewStacks().activeViewStack.baselineItems || []; + + const result = actions.some((p) => { + return ((p as ActionCreateElement).properties.actionKey ?? '') === CUSTOM_API_CALL_KEY; + }); + + return result === true; +}); + +const isTriggerRootView = computed(() => rootView.value === TRIGGER_NODE_CREATOR_VIEW); + +registerKeyHook('ActionsKeyRight', { + keyboardKeys: ['ArrowRight', 'Enter'], + condition: (type) => type === 'action', + handler: onKeySelect, +}); + +registerKeyHook('ActionsKeyLeft', { + keyboardKeys: ['ArrowLeft'], + condition: (type) => type === 'action', + handler: arrowLeft, +}); + +function parseActions(base: INodeCreateElement[], locale: string, withLabels = false) { + return parseCategoryActions(base, locale, withLabels); +} + +function arrowLeft() { + popViewStack(); +} + +function onKeySelect(activeItemId: string) { + const mergedActions = [...actions.value, ...placeholderTriggerActions]; + const activeAction = mergedActions.find((a) => a.uuid === activeItemId); + + if (activeAction) onSelected(activeAction); +} + +function onSelected(actionCreateElement: INodeCreateElement) { + const actionData = getActionData(actionCreateElement.properties as ActionTypeDescription); + const isPlaceholderTriggerAction = placeholderTriggerActions.some( + (p) => p.key === actionCreateElement.key, + ); + const includeNodeWithPlaceholderTrigger = usePostHog().isVariantEnabled( + AUTO_INSERT_ACTION_EXPERIMENT.name, + AUTO_INSERT_ACTION_EXPERIMENT.variant, + ); + + if (includeNodeWithPlaceholderTrigger && isPlaceholderTriggerAction && isTriggerRootView) { + const actionNode = actions.value[0].key; + + emit('nodeTypeSelected', [actionData.key as string, actionNode]); + } else { + emit('nodeTypeSelected', getNodeTypesWithManualTrigger(actionData.key)); + } + + if (telemetry) setAddedNodeActionParameters(actionData, telemetry, rootView.value); +} + +function trackActionsView() { + const activeViewStack = useViewStacks().activeViewStack; + + const trigger_action_count = (activeViewStack.baselineItems || [])?.filter((action) => + action.key.toLowerCase().includes('trigger'), + ).length; + + const appIdentifier = [...actions.value, ...placeholderTriggerActions][0].key; + + const trackingPayload = { + app_identifier: appIdentifier, + actions: (activeViewStack.baselineItems || [])?.map( + (action) => (action as ActionCreateElement).properties.displayName, + ), + regular_action_count: (activeViewStack.baselineItems || [])?.length - trigger_action_count, + trigger_action_count, + }; + + runExternalHook('nodeCreateList.onViewActions', useWebhooksStore(), trackingPayload); + telemetry?.trackNodesPanel('nodeCreateList.onViewActions', trackingPayload); +} + +function resetSearch() { + updateCurrentViewStack({ search: '' }); +} + +function addHttpNode() { + const updateData = { + name: '', + key: HTTP_REQUEST_NODE_TYPE, + value: { + authentication: 'predefinedCredentialType', + }, + } as IUpdateInformation; + + emit('nodeTypeSelected', [HTTP_REQUEST_NODE_TYPE]); + if (telemetry) setAddedNodeActionParameters(updateData); + + const app_identifier = actions.value[0].key; + runExternalHook('nodeCreateList.onActionsCustmAPIClicked', useWebhooksStore(), { + app_identifier, + }); + telemetry?.trackNodesPanel('nodeCreateList.onActionsCustmAPIClicked', { app_identifier }); +} + +// Anonymous component to handle triggers and actions rendering order +const OrderSwitcher = defineComponent({ + props: { + rootView: { + type: String as PropType<NodeFilterType>, + }, + }, + render(h): VNode { + const triggers = this.$slots?.triggers?.[0]; + const actions = this.$slots?.actions?.[0]; + + return h( + 'div', + {}, + this.rootView === REGULAR_NODE_CREATOR_VIEW ? [actions, triggers] : [triggers, actions], + ); + }, +}); + +onMounted(() => { + trackActionsView(); +}); +</script> + +<template> + <div :class="$style.container"> + <OrderSwitcher :rootView="rootView"> + <template #triggers v-if="isTriggerRootView || parsedTriggerActionsBaseline.length !== 0"> + <!-- Triggers Category --> + <CategorizedItemsRenderer + :elements="parsedTriggerActions" + :category="triggerCategoryName" + :mouseOverTooltip="$locale.baseText('nodeCreator.actionsTooltip.triggersStartWorkflow')" + isTriggerCategory + :expanded="isTriggerRootView || parsedActionActions.length === 0" + @selected="onSelected" + > + <!-- Empty state --> + <template #empty> + <template v-if="hasNoTriggerActions"> + <n8n-callout + theme="info" + iconless + slim + data-test-id="actions-panel-no-triggers-callout" + > + <span + v-html=" + $locale.baseText('nodeCreator.actionsCallout.noTriggerItems', { + interpolate: { nodeName: subcategory }, + }) + " + /> + </n8n-callout> + <ItemsRenderer @selected="onSelected" :elements="placeholderTriggerActions" /> + </template> + + <template v-else> + <p + :class="$style.resetSearch" + v-html="$locale.baseText('nodeCreator.actionsCategory.noMatchingTriggers')" + @click="resetSearch" + /> + </template> + </template> + </CategorizedItemsRenderer> + </template> + <template #actions v-if="!isTriggerRootView || parsedActionActionsBaseline.length !== 0"> + <!-- Actions Category --> + <CategorizedItemsRenderer + :elements="parsedActionActions" + :category="actionsCategoryLocales.actions" + :mouseOverTooltip="$locale.baseText('nodeCreator.actionsTooltip.actionsPerformStep')" + :expanded="!isTriggerRootView || parsedTriggerActions.length === 0" + @selected="onSelected" + > + <template> + <n8n-callout + theme="info" + iconless + v-if="!userActivated && isTriggerRootView" + slim + data-test-id="actions-panel-activation-callout" + > + <span v-html="$locale.baseText('nodeCreator.actionsCallout.triggersStartWorkflow')" /> + </n8n-callout> + </template> + <!-- Empty state --> + <template #empty> + <n8n-info-tip theme="info" type="note" v-if="!search" :class="$style.actionsEmpty"> + <span + v-html=" + $locale.baseText('nodeCreator.actionsCallout.noActionItems', { + interpolate: { nodeName: subcategory }, + }) + " + /> + </n8n-info-tip> + <template v-else> + <p + :class="$style.resetSearch" + v-html="$locale.baseText('nodeCreator.actionsCategory.noMatchingActions')" + @click="resetSearch" + data-test-id="actions-panel-no-matching-actions" + /> + </template> + </template> + </CategorizedItemsRenderer> + </template> + </OrderSwitcher> + <div :class="$style.apiHint" v-if="containsAPIAction"> + <span + @click.prevent="addHttpNode" + v-html=" + $locale.baseText('nodeCreator.actionsList.apiCall', { + interpolate: { node: subcategory }, + }) + " + /> + </div> + </div> +</template> + +<style lang="scss" module> +.container { + display: flex; + flex-direction: column; + padding-bottom: var(--spacing-3xl); +} + +.resetSearch { + cursor: pointer; + line-height: var(--font-line-height-regular); + font-weight: var(--font-weight-regular); + font-size: var(--font-size-2xs); + padding: var(--spacing-2xs) var(--spacing-s) 0; + color: var(--color-text-base); + + i { + font-weight: var(--font-weight-bold); + font-style: normal; + text-decoration: underline; + } +} +.actionsEmpty { + padding: var(--spacing-2xs) var(--spacing-xs) var(--spacing-s); + font-weight: var(--font-weight-regular); + + strong { + font-weight: var(--font-weight-bold); + } +} +.apiHint { + padding: 0 var(--spacing-s) var(--spacing-xl); + font-size: var(--font-size-2xs); + color: var(--color-text-base); + line-height: var(--font-line-height-regular); + z-index: 1; +} +</style> diff --git a/packages/editor-ui/src/components/Node/NodeCreator/Modes/NodesMode.vue b/packages/editor-ui/src/components/Node/NodeCreator/Modes/NodesMode.vue new file mode 100644 index 0000000000000..3ea3af59584c8 --- /dev/null +++ b/packages/editor-ui/src/components/Node/NodeCreator/Modes/NodesMode.vue @@ -0,0 +1,213 @@ +<script setup lang="ts"> +import { camelCase } from 'lodash-es'; +import { getCurrentInstance, computed } from 'vue'; +import type { INodeCreateElement, NodeFilterType } from '@/Interface'; +import { TRIGGER_NODE_CREATOR_VIEW, HTTP_REQUEST_NODE_TYPE, WEBHOOK_NODE_TYPE } from '@/constants'; + +import type { BaseTextKey } from '@/plugins/i18n'; +import { useRootStore } from '@/stores/n8nRootStore'; +import { useNodeCreatorStore } from '@/stores/nodeCreator'; + +import { TriggerView, RegularView } from '../viewsData'; +import { transformNodeType } from '../utils'; +import { useViewStacks } from '../composables/useViewStacks'; +import { useActions } from '../composables/useActions'; +import { useKeyboardNavigation } from '../composables/useKeyboardNavigation'; +import ItemsRenderer from '../Renderers/ItemsRenderer.vue'; +import CategorizedItemsRenderer from '../Renderers/CategorizedItemsRenderer.vue'; +import NoResults from '../Panel/NoResults.vue'; + +export interface Props { + rootView: 'trigger' | 'action'; +} + +const emit = defineEmits({ + nodeTypeSelected: (nodeTypes: string[]) => true, +}); + +const instance = getCurrentInstance(); +const { mergedNodes, actions } = useNodeCreatorStore(); +const { baseUrl } = useRootStore(); +const { getNodeTypesWithManualTrigger } = useActions(); +const { pushViewStack, popViewStack } = useViewStacks(); + +const { registerKeyHook } = useKeyboardNavigation(); + +const activeViewStack = computed(() => useViewStacks().activeViewStack); +const globalSearchItemsDiff = computed(() => useViewStacks().globalSearchItemsDiff); + +function selectNodeType(nodeTypes: string[]) { + emit( + 'nodeTypeSelected', + nodeTypes.length === 1 ? getNodeTypesWithManualTrigger(nodeTypes[0]) : nodeTypes, + ); +} + +function onSelected(item: INodeCreateElement) { + if (item.type === 'subcategory') { + const title = instance?.proxy.$locale.baseText( + `nodeCreator.subcategoryNames.${camelCase(item.properties.title)}` as BaseTextKey, + ); + + pushViewStack({ + subcategory: item.key, + title, + mode: 'nodes', + rootView: activeViewStack.value.rootView, + forceIncludeNodes: item.properties.forceIncludeNodes, + baseFilter: baseSubcategoriesFilter, + itemsMapper: subcategoriesMapper, + }); + + instance?.proxy.$telemetry.trackNodesPanel('nodeCreateList.onSubcategorySelected', { + subcategory: item.key, + }); + } + + if (item.type === 'node') { + const nodeActions = actions?.[item.key] || []; + if (nodeActions.length <= 1) { + selectNodeType([item.key]); + return; + } + + const icon = item.properties.iconUrl + ? `${baseUrl}${item.properties.iconUrl}` + : item.properties.icon?.split(':')[1]; + + const transformedActions = nodeActions?.map((a) => + transformNodeType(a, item.properties.displayName, 'action'), + ); + + pushViewStack({ + subcategory: item.properties.displayName, + title: item.properties.displayName, + nodeIcon: { + color: item.properties.defaults?.color || '', + icon, + iconType: item.properties.iconUrl ? 'file' : 'icon', + }, + + rootView: activeViewStack.value.rootView, + hasSearch: true, + mode: 'actions', + items: transformedActions, + }); + } + + if (item.type === 'view') { + const view = + item.key === TRIGGER_NODE_CREATOR_VIEW + ? TriggerView(instance?.proxy?.$locale) + : RegularView(instance?.proxy?.$locale); + + pushViewStack({ + title: view.title, + subtitle: view?.subtitle ?? '', + items: view.items as INodeCreateElement[], + hasSearch: true, + rootView: view.value as NodeFilterType, + mode: 'nodes', + // Root search should include all nodes + searchItems: mergedNodes, + }); + } +} + +function subcategoriesMapper(item: INodeCreateElement) { + if (item.type !== 'node') return item; + + const hasTriggerGroup = item.properties.group.includes('trigger'); + const nodeActions = actions?.[item.key] || []; + const hasActions = nodeActions.length > 0; + + if (hasTriggerGroup && hasActions) { + if (item.properties?.codex) { + // Store the original name in the alias so we can search for it + item.properties.codex.alias = [ + ...(item.properties.codex?.alias || []), + item.properties.displayName, + ]; + } + item.properties.displayName = item.properties.displayName.replace(' Trigger', ''); + } + return item; +} + +function baseSubcategoriesFilter(item: INodeCreateElement) { + if (item.type !== 'node') return false; + + const hasTriggerGroup = item.properties.group.includes('trigger'); + const nodeActions = actions?.[item.key] || []; + const hasActions = nodeActions.length > 0; + + const isTriggerRootView = activeViewStack.value.rootView === TRIGGER_NODE_CREATOR_VIEW; + if (isTriggerRootView) { + return hasActions || hasTriggerGroup; + } + + return hasActions || !hasTriggerGroup; +} + +function arrowLeft() { + popViewStack(); +} + +function onKeySelect(activeItemId: string) { + const mergedItems = [ + ...(activeViewStack.value.items || []), + ...(globalSearchItemsDiff.value || []), + ]; + + const item = mergedItems.find((i) => i.uuid === activeItemId); + if (!item) return; + + onSelected(item as INodeCreateElement); +} + +registerKeyHook('MainViewArrowRight', { + keyboardKeys: ['ArrowRight', 'Enter'], + condition: (type) => ['subcategory', 'node', 'view'].includes(type), + handler: onKeySelect, +}); + +registerKeyHook('MainViewArrowLeft', { + keyboardKeys: ['ArrowLeft'], + condition: (type) => ['subcategory', 'node', 'view'].includes(type), + handler: arrowLeft, +}); +</script> + +<template> + <span> + <!-- Main Node Items --> + <ItemsRenderer :elements="activeViewStack.items" @selected="onSelected" :class="$style.items"> + <template + #empty + v-if="(activeViewStack.items || []).length === 0 && globalSearchItemsDiff.length === 0" + > + <NoResults + :rootView="activeViewStack.rootView" + showIcon + showRequest + @addWebhookNode="selectNodeType([WEBHOOK_NODE_TYPE])" + @addHttpNode="selectNodeType([HTTP_REQUEST_NODE_TYPE])" + /> + </template> + </ItemsRenderer> + <!-- Results in other categories --> + <CategorizedItemsRenderer + v-if="globalSearchItemsDiff.length > 0" + :elements="globalSearchItemsDiff" + :category="$locale.baseText('nodeCreator.categoryNames.otherCategories')" + @selected="onSelected" + > + </CategorizedItemsRenderer> + </span> +</template> + +<style lang="scss" module> +.items { + margin-bottom: var(--spacing-s); +} +</style> diff --git a/packages/editor-ui/src/components/Node/NodeCreator/NodeCreator.vue b/packages/editor-ui/src/components/Node/NodeCreator/NodeCreator.vue index 77415e2f063aa..d1b8ef6072c9a 100644 --- a/packages/editor-ui/src/components/Node/NodeCreator/NodeCreator.vue +++ b/packages/editor-ui/src/components/Node/NodeCreator/NodeCreator.vue @@ -1,11 +1,10 @@ <template> <div> - <aside :class="{ 'node-creator-scrim': true, active: nodeCreatorStore.showScrim }" /> - + <aside :class="{ [$style.nodeCreatorScrim]: true, [$style.active]: showScrim }" /> <slide-transition> <div v-if="active" - class="node-creator" + :class="$style.nodeCreator" ref="nodeCreator" v-click-outside="onClickOutside" @dragover="onDragOver" @@ -14,38 +13,51 @@ @mouseup="onMouseUp" data-test-id="node-creator" > - <main-panel @nodeTypeSelected="$listeners.nodeTypeSelected" /> + <NodesListPanel @nodeTypeSelected="$listeners.nodeTypeSelected" /> </div> </slide-transition> </div> </template> <script setup lang="ts"> -import { watch, reactive, toRefs } from 'vue'; -import SlideTransition from '@/components/transitions/SlideTransition.vue'; +import { watch, reactive, toRefs, computed } from 'vue'; -import MainPanel from './MainPanel.vue'; +import { useNodeTypesStore } from '@/stores/nodeTypes'; import { useNodeCreatorStore } from '@/stores/nodeCreator'; +import SlideTransition from '@/components/transitions/SlideTransition.vue'; + +import { useViewStacks } from './composables/useViewStacks'; +import { useKeyboardNavigation } from './composables/useKeyboardNavigation'; +import { useActionsGenerator } from './composables/useActionsGeneration'; +import NodesListPanel from './Panel/NodesListPanel.vue'; export interface Props { active?: boolean; } const props = defineProps<Props>(); - +const { resetViewStacks } = useViewStacks(); +const { registerKeyHook } = useKeyboardNavigation(); const emit = defineEmits<{ (event: 'closeNodeCreator'): void; + (event: 'nodeTypeSelected', value: string[]): void; }>(); -const nodeCreatorStore = useNodeCreatorStore(); +const { setShowScrim, setActions, setMergeNodes } = useNodeCreatorStore(); +const { generateMergedNodesAndActions } = useActionsGenerator(); + const state = reactive({ nodeCreator: null as HTMLElement | null, mousedownInsideEvent: null as MouseEvent | null, }); +const showScrim = computed(() => useNodeCreatorStore().showScrim); + +const viewStacksLength = computed(() => useViewStacks().viewStacks.length); + function onClickOutside(event: Event) { // We need to prevent cases where user would click inside the node creator - // and try to drag undraggable element. In that case the click event would + // and try to drag non-draggable element. In that case the click event would // be fired and the node creator would be closed. So we stop that if we detect // that the click event originated from inside the node creator. And fire click even on the // original target. @@ -93,21 +105,49 @@ function onDrop(event: DragEvent) { watch( () => props.active, (isActive) => { - if (isActive === false) nodeCreatorStore.setShowScrim(false); + if (isActive === false) { + setShowScrim(false); + resetViewStacks(); + } }, ); +// Close node creator when the last view stacks is closed +watch(viewStacksLength, (viewStacksLength) => { + if (viewStacksLength === 0) { + emit('closeNodeCreator'); + setShowScrim(false); + } +}); + +registerKeyHook('NodeCreatorCloseEscape', { + keyboardKeys: ['Escape'], + handler: () => emit('closeNodeCreator'), +}); +registerKeyHook('NodeCreatorCloseTab', { + keyboardKeys: ['Tab'], + handler: () => emit('closeNodeCreator'), +}); + +watch( + () => useNodeTypesStore().visibleNodeTypes, + (nodeTypes) => { + const { actions, mergedNodes } = generateMergedNodesAndActions(nodeTypes); + + setActions(actions); + setMergeNodes(mergedNodes); + }, + { immediate: true }, +); const { nodeCreator } = toRefs(state); </script> -<style scoped lang="scss"> -::v-deep *, -*:before, -*:after { - box-sizing: border-box; +<style module lang="scss"> +:global(strong) { + font-weight: var(--font-weight-bold); } - -.node-creator { +.nodeCreator { + --node-icon-color: var(--color-text-base); position: fixed; top: $header-height; bottom: 0; @@ -117,7 +157,7 @@ const { nodeCreator } = toRefs(state); color: $node-creator-text-color; } -.node-creator-scrim { +.nodeCreatorScrim { position: fixed; top: $header-height; right: 0; diff --git a/packages/editor-ui/src/components/Node/NodeCreator/NoResults.vue b/packages/editor-ui/src/components/Node/NodeCreator/Panel/NoResults.vue similarity index 66% rename from packages/editor-ui/src/components/Node/NodeCreator/NoResults.vue rename to packages/editor-ui/src/components/Node/NodeCreator/Panel/NoResults.vue index 31b3fb9f144f9..d2a30e0e5fe0d 100644 --- a/packages/editor-ui/src/components/Node/NodeCreator/NoResults.vue +++ b/packages/editor-ui/src/components/Node/NodeCreator/Panel/NoResults.vue @@ -1,12 +1,24 @@ <template> - <div :class="{ [$style.noResults]: true, [$style.iconless]: !showIcon }"> + <div + :class="{ [$style.noResults]: true, [$style.iconless]: !showIcon }" + data-test-id="node-creator-no-results" + > <div :class="$style.icon" v-if="showIcon"> <no-results-icon /> </div> <div :class="$style.title"> <slot name="title" /> + <p v-text="$locale.baseText('nodeCreator.noResults.weDidntMakeThatYet')" /> <div :class="$style.action"> - <slot name="action" /> + {{ $locale.baseText('nodeCreator.noResults.dontWorryYouCanProbablyDoItWithThe') }} + <n8n-link v-if="rootView === REGULAR_NODE_CREATOR_VIEW" @click="$emit('addHttpNode')"> + {{ $locale.baseText('nodeCreator.noResults.httpRequest') }} + </n8n-link> + + <n8n-link v-if="rootView === TRIGGER_NODE_CREATOR_VIEW" @click="$emit('addWebhookNode')"> + {{ $locale.baseText('nodeCreator.noResults.webhook') }} + </n8n-link> + {{ $locale.baseText('nodeCreator.noResults.node') }} </div> </div> @@ -30,12 +42,19 @@ </template> <script setup lang="ts"> -import { REQUEST_NODE_FORM_URL } from '@/constants'; +import { + REQUEST_NODE_FORM_URL, + REGULAR_NODE_CREATOR_VIEW, + TRIGGER_NODE_CREATOR_VIEW, +} from '@/constants'; +import type { NodeFilterType } from '@/Interface'; + import NoResultsIcon from './NoResultsIcon.vue'; export interface Props { showIcon?: boolean; showRequest?: boolean; + rootView?: NodeFilterType; } defineProps<Props>(); diff --git a/packages/editor-ui/src/components/Node/NodeCreator/NoResultsIcon.vue b/packages/editor-ui/src/components/Node/NodeCreator/Panel/NoResultsIcon.vue similarity index 100% rename from packages/editor-ui/src/components/Node/NodeCreator/NoResultsIcon.vue rename to packages/editor-ui/src/components/Node/NodeCreator/Panel/NoResultsIcon.vue diff --git a/packages/editor-ui/src/components/Node/NodeCreator/Panel/NodesListPanel.vue b/packages/editor-ui/src/components/Node/NodeCreator/Panel/NodesListPanel.vue new file mode 100644 index 0000000000000..e5c89341627d2 --- /dev/null +++ b/packages/editor-ui/src/components/Node/NodeCreator/Panel/NodesListPanel.vue @@ -0,0 +1,258 @@ +<script setup lang="ts"> +import { getCurrentInstance, computed, onMounted, onUnmounted, watch } from 'vue'; +import type { INodeCreateElement } from '@/Interface'; +import { TRIGGER_NODE_CREATOR_VIEW } from '@/constants'; + +import { useNodeCreatorStore } from '@/stores/nodeCreator'; + +import { TriggerView, RegularView } from '../viewsData'; +import { useViewStacks } from '../composables/useViewStacks'; +import { useKeyboardNavigation } from '../composables/useKeyboardNavigation'; +import SearchBar from './SearchBar.vue'; +import ActionsRenderer from '../Modes/ActionsMode.vue'; +import NodesRenderer from '../Modes/NodesMode.vue'; + +const instance = getCurrentInstance(); + +const { mergedNodes } = useNodeCreatorStore(); +const { pushViewStack, popViewStack, updateCurrentViewStack } = useViewStacks(); +const { setActiveItemIndex, attachKeydownEvent, detachKeydownEvent } = useKeyboardNavigation(); + +const activeViewStack = computed(() => useViewStacks().activeViewStack); + +const viewStacks = computed(() => useViewStacks().viewStacks); + +const isActionsMode = computed(() => useViewStacks().activeViewStackMode === 'actions'); +const searchPlaceholder = computed(() => + isActionsMode.value + ? instance?.proxy?.$locale.baseText('nodeCreator.actionsCategory.searchActions', { + interpolate: { node: activeViewStack.value.title as string }, + }) + : instance?.proxy?.$locale.baseText('nodeCreator.searchBar.searchNodes'), +); + +const nodeCreatorView = computed(() => useNodeCreatorStore().selectedView); + +function onSearch(value: string) { + if (activeViewStack.value.uuid) { + updateCurrentViewStack({ search: value }); + setActiveItemIndex(activeViewStack.value.activeIndex ?? 0); + } +} + +function onTransitionEnd() { + // For actions, set the active focus to the first action, not category + const newStackIndex = activeViewStack.value.mode === 'actions' ? 1 : 0; + setActiveItemIndex(activeViewStack.value.activeIndex || 0 || newStackIndex); +} + +onMounted(() => { + attachKeydownEvent(); + setActiveItemIndex(activeViewStack.value.activeIndex ?? 0); +}); + +onUnmounted(() => { + detachKeydownEvent(); +}); + +watch( + () => nodeCreatorView.value, + (selectedView) => { + const view = + selectedView === TRIGGER_NODE_CREATOR_VIEW + ? TriggerView(instance?.proxy?.$locale) + : RegularView(instance?.proxy?.$locale); + + pushViewStack({ + title: view.title, + subtitle: view?.subtitle ?? '', + items: view.items as INodeCreateElement[], + hasSearch: true, + mode: 'nodes', + rootView: selectedView, + // Root search should include all nodes + searchItems: mergedNodes, + }); + }, + { immediate: true }, +); + +function onBackButton() { + popViewStack(); +} +</script> + +<template> + <transition + v-if="viewStacks.length > 0" + :name="`panel-slide-${activeViewStack.transitionDirection}`" + @afterLeave="onTransitionEnd" + > + <aside :class="$style.nodesListPanel" @keydown.capture.stop :key="`${activeViewStack.uuid}`"> + <header + :class="{ [$style.header]: true, [$style.hasBg]: !activeViewStack.subtitle }" + data-test-id="nodes-list-header" + > + <div :class="$style.top"> + <button :class="$style.backButton" @click="onBackButton" v-if="viewStacks.length > 1"> + <font-awesome-icon :class="$style.backButtonIcon" icon="arrow-left" size="2x" /> + </button> + <n8n-node-icon + v-if="activeViewStack.nodeIcon" + :class="$style.nodeIcon" + :type="activeViewStack.nodeIcon.iconType || 'unknown'" + :src="activeViewStack.nodeIcon.icon" + :name="activeViewStack.nodeIcon.icon" + :color="activeViewStack.nodeIcon.color" + :circle="false" + :showTooltip="false" + :size="16" + /> + <p :class="$style.title" v-text="activeViewStack.title" v-if="activeViewStack.title" /> + </div> + <p + v-if="activeViewStack.subtitle" + :class="{ [$style.subtitle]: true, [$style.offsetSubtitle]: viewStacks.length > 1 }" + v-text="activeViewStack.subtitle" + /> + </header> + <search-bar + v-if="activeViewStack.hasSearch" + :class="$style.searchBar" + :placeholder=" + searchPlaceholder + ? searchPlaceholder + : $locale.baseText('nodeCreator.searchBar.searchNodes') + " + @input="onSearch" + :value="activeViewStack.search" + /> + <div :class="$style.renderedItems"> + <!-- Actions mode --> + <ActionsRenderer v-if="isActionsMode && activeViewStack.subcategory" v-on="$listeners" /> + + <!-- Nodes Mode --> + <NodesRenderer v-else :rootView="nodeCreatorView" v-on="$listeners" /> + </div> + </aside> + </transition> +</template> + +<style lang="scss" module> +:global(.panel-slide-in-leave-active), +:global(.panel-slide-in-enter-active), +:global(.panel-slide-out-leave-active), +:global(.panel-slide-out-enter-active) { + transition: transform 200ms ease; + position: absolute; + left: 0; + right: 0; +} + +:global(.panel-slide-out-enter), +:global(.panel-slide-in-leave-to) { + transform: translateX(0); + z-index: -1; +} + +:global(.panel-slide-out-leave-to), +:global(.panel-slide-in-enter) { + transform: translateX(100%); + // Make sure the leaving panel stays on top + // for the slide-out panel effect + z-index: 1; +} +.backButton { + background: transparent; + border: none; + cursor: pointer; + padding: 0 var(--spacing-xs) 0 0; +} + +.backButtonIcon { + color: $node-creator-arrow-color; + height: 16px; + padding: 0; +} +.nodeIcon { + --node-icon-size: 16px; + margin-right: var(--spacing-s); +} +.renderedItems { + overflow: auto; + height: 100%; + display: flex; + flex-direction: column; + scrollbar-width: none; /* Firefox 64 */ + padding-bottom: var(--spacing-xl); + &::-webkit-scrollbar { + display: none; + } +} +.searchBar { + flex-shrink: 0; +} +.nodesListPanel { + background: var(--color-background-xlight); + height: 100%; + background-color: $node-creator-background-color; + width: 385px; + display: flex; + flex-direction: column; + + &:before { + box-sizing: border-box; + content: ''; + border-left: 1px solid $node-creator-border-color; + width: 1px; + position: absolute; + height: 100%; + } +} +.footer { + font-size: var(--font-size-2xs); + color: var(--color-text-base); + margin: 0 var(--spacing-xs) 0; + padding: var(--spacing-4xs) 0; + line-height: var(--font-line-height-regular); + border-top: 1px solid var(--color-foreground-base); + z-index: 1; + margin-top: -1px; +} +.top { + display: flex; + align-items: center; +} +.header { + font-size: var(--font-size-l); + font-weight: var(--font-weight-bold); + line-height: var(--font-line-height-compact); + + padding: var(--spacing-s) var(--spacing-s); + + &.hasBg { + border-bottom: $node-creator-border-color solid 1px; + background-color: $node-creator-subcategory-panel-header-bacground-color; + } +} +.title { + line-height: 24px; + font-weight: var(--font-weight-bold); + font-size: var(--font-size-l); + + .hasBg & { + font-size: var(--font-size-s-m); + line-height: 22px; + } +} +.subtitle { + margin-top: var(--spacing-4xs); + font-size: var(--font-size-s); + line-height: 19px; + color: var(--color-text-base); + font-weight: var(--font-weight-regular); +} +.offsetSubtitle { + margin-left: calc(var(--spacing-xl) + var(--spacing-4xs)); +} +</style> diff --git a/packages/editor-ui/src/components/Node/NodeCreator/SearchBar.vue b/packages/editor-ui/src/components/Node/NodeCreator/Panel/SearchBar.vue similarity index 91% rename from packages/editor-ui/src/components/Node/NodeCreator/SearchBar.vue rename to packages/editor-ui/src/components/Node/NodeCreator/Panel/SearchBar.vue index 596b14e3fc7a0..926dee4768922 100644 --- a/packages/editor-ui/src/components/Node/NodeCreator/SearchBar.vue +++ b/packages/editor-ui/src/components/Node/NodeCreator/Panel/SearchBar.vue @@ -25,13 +25,12 @@ <script setup lang="ts"> import { onMounted, reactive, toRefs, onBeforeUnmount } from 'vue'; -import { externalHooks } from '@/mixins/externalHooks'; -import { EventBus } from '@/event-bus'; +import { useWebhooksStore } from '@/stores/webhooks'; +import { runExternalHook } from '@/utils'; export interface Props { placeholder: string; value: string; - eventBus?: EventBus; } withDefaults(defineProps<Props>(), { @@ -43,8 +42,6 @@ const emit = defineEmits<{ (event: 'input', value: string): void; }>(); -const { $externalHooks } = new externalHooks(); - const state = reactive({ inputRef: null as HTMLInputElement | null, }); @@ -63,7 +60,7 @@ function clear() { } onMounted(() => { - $externalHooks().run('nodeCreator_searchBar.mount', { inputRef: state.inputRef }); + runExternalHook('nodeCreator_searchBar.mount', useWebhooksStore(), { inputRef: state.inputRef }); setTimeout(focus, 0); }); diff --git a/packages/editor-ui/src/components/Node/NodeCreator/Renderers/CategorizedItemsRenderer.vue b/packages/editor-ui/src/components/Node/NodeCreator/Renderers/CategorizedItemsRenderer.vue new file mode 100644 index 0000000000000..9c418c25c5e4f --- /dev/null +++ b/packages/editor-ui/src/components/Node/NodeCreator/Renderers/CategorizedItemsRenderer.vue @@ -0,0 +1,154 @@ +<script setup lang="ts"> +import { computed, watch, ref, getCurrentInstance } from 'vue'; +import type { INodeCreateElement } from '@/Interface'; + +import { useWorkflowsStore } from '@/stores/workflows'; + +import { useKeyboardNavigation } from '../composables/useKeyboardNavigation'; +import { useViewStacks } from '../composables/useViewStacks'; +import ItemsRenderer from './ItemsRenderer.vue'; +import CategoryItem from '../ItemTypes/CategoryItem.vue'; + +export interface Props { + elements: INodeCreateElement[]; + category: string; + disabled?: boolean; + activeIndex?: number; + isTriggerCategory?: boolean; + mouseOverTooltip?: string; + expanded?: boolean; +} + +const props = withDefaults(defineProps<Props>(), { + elements: () => [], +}); + +const instance = getCurrentInstance(); + +const { popViewStack } = useViewStacks(); +const { registerKeyHook } = useKeyboardNavigation(); +const { workflowId } = useWorkflowsStore(); + +const activeItemId = computed(() => useKeyboardNavigation()?.activeItemId); +const actionCount = computed(() => props.elements.filter(({ type }) => type === 'action').length); +const expanded = ref(props.expanded ?? false); + +function toggleExpanded() { + setExpanded(!expanded.value); +} + +function setExpanded(isExpanded: boolean) { + expanded.value = isExpanded; + + if (expanded.value) { + instance?.proxy.$telemetry.trackNodesPanel('nodeCreateList.onCategoryExpanded', { + category_name: props.category, + workflow_id: workflowId, + }); + } +} + +function arrowRight() { + if (expanded.value) return; + + setExpanded(true); +} + +function arrowLeft() { + if (!expanded.value) { + popViewStack(); + return; + } + + setExpanded(false); +} + +watch( + () => props.elements, + () => { + setExpanded(true); + }, +); + +registerKeyHook(`CategoryRight_${props.category}`, { + keyboardKeys: ['ArrowRight'], + condition: (type, activeItemId) => type === 'category' && props.category === activeItemId, + handler: arrowRight, +}); +registerKeyHook(`CategoryToggle_${props.category}`, { + keyboardKeys: ['Enter'], + condition: (type, activeItemId) => type === 'category' && props.category === activeItemId, + handler: toggleExpanded, +}); + +registerKeyHook(`CategoryLeft_${props.category}`, { + keyboardKeys: ['ArrowLeft'], + condition: (type, activeItemId) => type === 'category' && props.category === activeItemId, + handler: arrowLeft, +}); +</script> + +<template> + <div :class="$style.categorizedItemsRenderer" :data-category-collapsed="!expanded"> + <CategoryItem + :class="$style.categoryItem" + :name="category" + :disabled="disabled" + :active="activeItemId === category" + :count="actionCount" + :expanded="expanded" + :isTrigger="isTriggerCategory" + data-keyboard-nav-type="category" + :data-keyboard-nav-id="category" + @click="toggleExpanded" + > + <span :class="$style.mouseOverTooltip" v-if="mouseOverTooltip"> + <n8n-tooltip placement="top" :popper-class="$style.tooltipPopper"> + <n8n-icon icon="question-circle" size="small" /> + <template #content> + <div v-html="mouseOverTooltip" /> + </template> + </n8n-tooltip> + </span> + </CategoryItem> + <div :class="$style.contentSlot" v-if="expanded && actionCount > 0 && $slots.default"> + <slot /> + </div> + <!-- Pass through listeners & empty slot to ItemsRenderer --> + <ItemsRenderer + v-if="expanded" + :elements="elements" + v-on="$listeners" + :isTrigger="isTriggerCategory" + > + <template #default> </template> + <template #empty> + <slot name="empty" v-bind="{ elements }" /> + </template> + </ItemsRenderer> + </div> +</template> + +<style lang="scss" module> +.mouseOverTooltip { + opacity: 0; + margin-left: var(--spacing-3xs); + &:hover { + color: var(--color-primary); + } + + .categorizedItemsRenderer:hover & { + opacity: 1; + } +} +.tooltipPopper { + max-width: 260px; +} +.contentSlot { + padding: 0 var(--spacing-s) var(--spacing-3xs); + margin-top: var(--spacing-xs); +} +.categorizedItemsRenderer { + padding-bottom: var(--spacing-s); +} +</style> diff --git a/packages/editor-ui/src/components/Node/NodeCreator/Renderers/ItemsRenderer.vue b/packages/editor-ui/src/components/Node/NodeCreator/Renderers/ItemsRenderer.vue new file mode 100644 index 0000000000000..52e3a383df8ab --- /dev/null +++ b/packages/editor-ui/src/components/Node/NodeCreator/Renderers/ItemsRenderer.vue @@ -0,0 +1,215 @@ +<script setup lang="ts"> +import type { INodeCreateElement } from '@/Interface'; +import { onMounted, watch, onUnmounted, ref, computed } from 'vue'; + +import { useKeyboardNavigation } from '../composables/useKeyboardNavigation'; +import NodeItem from '../ItemTypes/NodeItem.vue'; +import SubcategoryItem from '../ItemTypes/SubcategoryItem.vue'; +import LabelItem from '../ItemTypes/LabelItem.vue'; +import ActionItem from '../ItemTypes/ActionItem.vue'; +import ViewItem from '../ItemTypes/ViewItem.vue'; +export interface Props { + elements: INodeCreateElement[]; + activeIndex?: number; + disabled?: boolean; + lazyRender?: boolean; +} + +const LAZY_LOAD_THRESHOLD = 20; +const LAZY_LOAD_ITEMS_PER_TICK = 5; +const props = withDefaults(defineProps<Props>(), { + elements: () => [], + lazyRender: true, +}); + +const emit = defineEmits<{ + (event: 'selected', element: INodeCreateElement, $e?: Event): void; + (event: 'dragstart', element: INodeCreateElement, $e: Event): void; + (event: 'dragend', element: INodeCreateElement, $e: Event): void; +}>(); + +const renderedItems = ref<INodeCreateElement[]>([]); +const renderAnimationRequest = ref<number>(0); + +const activeItemId = computed(() => useKeyboardNavigation()?.activeItemId); + +// Lazy render large items lists to prevent the browser from freezing +// when loading many items. +function renderItems() { + if (props.elements.length <= LAZY_LOAD_THRESHOLD || props.lazyRender === false) { + renderedItems.value = props.elements; + return; + } + + if (renderedItems.value.length < props.elements.length) { + renderedItems.value.push( + ...props.elements.slice( + renderedItems.value.length, + renderedItems.value.length + LAZY_LOAD_ITEMS_PER_TICK, + ), + ); + renderAnimationRequest.value = window.requestAnimationFrame(renderItems); + } +} + +function wrappedEmit( + event: 'selected' | 'dragstart' | 'dragend', + element: INodeCreateElement, + $e?: Event, +) { + if (props.disabled) return; + + emit((event as 'selected') || 'dragstart' || 'dragend', element, $e); +} + +function beforeEnter(el: HTMLElement) { + el.style.height = '0'; +} + +function enter(el: HTMLElement) { + el.style.height = `${el.scrollHeight}px`; +} + +function beforeLeave(el: HTMLElement) { + el.style.height = `${el.scrollHeight}px`; +} + +function leave(el: HTMLElement) { + el.style.height = '0'; +} + +onMounted(() => { + renderItems(); +}); + +onUnmounted(() => { + window.cancelAnimationFrame(renderAnimationRequest.value); + renderedItems.value = []; +}); + +// Make sure the active item is always visible +// scroll if needed +watch( + () => props.elements, + () => { + window.cancelAnimationFrame(renderAnimationRequest.value); + renderedItems.value = []; + renderItems(); + }, +); +</script> + +<template> + <div + v-if="elements.length > 0" + :class="$style.itemsRenderer" + name="accordion" + @before-enter="beforeEnter" + @enter="enter" + @before-leave="beforeLeave" + @leave="leave" + > + <slot /> + <div + v-for="item in elements" + :key="item.uuid" + data-test-id="item-iterator-item" + :class="{ + clickable: !disabled, + [$style.active]: activeItemId === item.uuid, + [$style.iteratorItem]: true, + [$style[item.type]]: true, + }" + ref="iteratorItems" + :data-keyboard-nav-type="item.type !== 'label' ? item.type : undefined" + :data-keyboard-nav-id="item.uuid" + @click="wrappedEmit('selected', item)" + > + <template v-if="renderedItems.includes(item)"> + <label-item v-if="item.type === 'label'" :item="item" /> + <subcategory-item v-if="item.type === 'subcategory'" :item="item.properties" /> + + <node-item + v-if="item.type === 'node'" + :nodeType="item.properties" + :active="true" + :subcategory="item.subcategory" + /> + + <action-item + v-if="item.type === 'action'" + :nodeType="item.properties" + :action="item.properties" + :active="true" + /> + + <view-item + v-else-if="item.type === 'view'" + :view="item.properties" + :class="$style.viewItem" + /> + </template> + + <n8n-loading :loading="true" :rows="1" variant="p" :class="$style.itemSkeleton" v-else /> + </div> + </div> + <div :class="$style.empty" v-else> + <slot name="empty" /> + </div> +</template> + +<style lang="scss" module> +.itemSkeleton { + height: 50px; +} +.iteratorItem { + // Make sure border is fully visible + margin-left: 1px; + position: relative; + &::before { + content: ''; + position: absolute; + left: -1px; + top: 0; + bottom: 0; + border-left: 2px solid transparent; + } + &:not(.label):not(.category):hover::before { + border-color: $node-creator-item-hover-border-color; + } + + &.active:not(.category)::before { + border-color: $color-primary; + } +} +.empty { + :global([role='alert']) { + margin: var(--spacing-xs) var(--spacing-s); + } +} +.itemsRenderer { + display: flex; + flex-direction: column; + + scrollbar-width: none; /* Firefox 64 */ + & > *::-webkit-scrollbar { + display: none; + } +} +.view { + margin-top: var(--spacing-s); + padding-top: var(--spacing-xs); + position: relative; + + &::after { + content: ''; + position: absolute; + left: var(--spacing-s); + right: var(--spacing-s); + top: 0; + margin: auto; + bottom: 0; + border-top: 1px solid var(--color-foreground-base); + } +} +</style> diff --git a/packages/editor-ui/src/components/Node/NodeCreator/ViewItem.vue b/packages/editor-ui/src/components/Node/NodeCreator/ViewItem.vue deleted file mode 100644 index b331301137922..0000000000000 --- a/packages/editor-ui/src/components/Node/NodeCreator/ViewItem.vue +++ /dev/null @@ -1,45 +0,0 @@ -<template> - <n8n-node-creator-node - :class="{ - [$style.view]: true, - [$style.withTopBorder]: view.withTopBorder, - }" - :title="view.title" - :isTrigger="false" - :description="view.description" - :showActionArrow="true" - > - <template #icon> - <n8n-node-icon - type="icon" - :name="view.icon" - :circle="false" - :showTooltip="false" - ></n8n-node-icon> - </template> - </n8n-node-creator-node> -</template> - -<script setup lang="ts"> -import { ViewItemProps } from '@/Interface'; - -export interface Props { - view: ViewItemProps; -} - -defineProps<Props>(); -</script> - -<style lang="scss" module> -.view { - --action-arrow-color: var(--color-text-light); - margin-left: 15px; - margin-right: 12px; - padding: 11px 4px 11px 0; -} -.withTopBorder { - border-top: 1px solid var(--color-foreground-base); - margin-top: var(--spacing-xs); - padding-top: var(--spacing-l); -} -</style> diff --git a/packages/editor-ui/src/components/Node/NodeCreator/__tests__/CategoryItem.test.ts b/packages/editor-ui/src/components/Node/NodeCreator/__tests__/CategoryItem.test.ts new file mode 100644 index 0000000000000..a566eb6b57d0c --- /dev/null +++ b/packages/editor-ui/src/components/Node/NodeCreator/__tests__/CategoryItem.test.ts @@ -0,0 +1,31 @@ +import { render, screen } from '@testing-library/vue'; +import CategoryItem from '../ItemTypes/CategoryItem.vue'; + +describe('CategoryItem', () => { + it('should allow expand and collapse', async () => { + const { container, updateProps } = render(CategoryItem, { props: { name: 'Category Test' } }); + + expect(container.querySelector('[data-icon="chevron-down"]')).toBeInTheDocument(); + await updateProps({ expanded: false }); + expect(container.querySelector('[data-icon="chevron-down"]')).not.toBeInTheDocument(); + expect(container.querySelector('[data-icon="chevron-up"]')).toBeInTheDocument(); + }); + + it('should show count', async () => { + const { updateProps } = render(CategoryItem, { props: { name: 'Category Test', count: 10 } }); + + expect(screen.getByText('Category Test (10)')).toBeInTheDocument(); + await updateProps({ count: 0 }); + expect(screen.getByText('Category Test')).toBeInTheDocument(); + }); + + it('should show trigger icon', async () => { + const { updateProps, container } = render(CategoryItem, { + props: { name: 'Category Test', isTrigger: true }, + }); + + expect(container.querySelector('[data-icon="bolt"]')).toBeInTheDocument(); + await updateProps({ isTrigger: false }); + expect(container.querySelector('[data-icon="bolt"]')).not.toBeInTheDocument(); + }); +}); diff --git a/packages/editor-ui/src/components/Node/NodeCreator/__tests__/ItemsRenderer.test.ts b/packages/editor-ui/src/components/Node/NodeCreator/__tests__/ItemsRenderer.test.ts new file mode 100644 index 0000000000000..39d236f4b846f --- /dev/null +++ b/packages/editor-ui/src/components/Node/NodeCreator/__tests__/ItemsRenderer.test.ts @@ -0,0 +1,82 @@ +import Vue from 'vue'; +import { PiniaVuePlugin } from 'pinia'; +import { createTestingPinia } from '@pinia/testing'; +import { render, fireEvent } from '@testing-library/vue'; +import { + mockSubcategoryCreateElement, + mockLabelCreateElement, + mockNodeCreateElement, +} from './utils'; +import ItemsRenderer from '../Renderers/ItemsRenderer.vue'; +import { mockActionCreateElement } from './utils'; +import { mockViewCreateElement } from './utils'; + +describe('ItemsRenderer', () => { + it('should render items', async () => { + const items = [ + mockSubcategoryCreateElement({ title: 'Subcategory 1' }), + mockLabelCreateElement('subcategory', { key: 'label1' }), + mockNodeCreateElement('subcategory', { displayName: 'Node 1', name: 'node1' }), + mockNodeCreateElement('subcategory', { displayName: 'Node 2', name: 'node2' }), + mockNodeCreateElement('subcategory', { displayName: 'Node 3', name: 'node3' }), + mockLabelCreateElement('subcategory', { key: 'label2' }), + mockNodeCreateElement('subcategory', { displayName: 'Node 2', name: 'node2' }), + mockNodeCreateElement('subcategory', { displayName: 'Node 3', name: 'node3' }), + mockSubcategoryCreateElement({ title: 'Subcategory 2' }), + ]; + const { container } = render( + ItemsRenderer, + { + pinia: createTestingPinia(), + props: { elements: items }, + }, + (vue) => { + vue.use(PiniaVuePlugin); + }, + ); + // + await Vue.nextTick(); + + const nodeItems = container.querySelectorAll('.iteratorItem .nodeItem'); + const labels = container.querySelectorAll('.iteratorItem .label'); + const subCategories = container.querySelectorAll('.iteratorItem .subCategory'); + + expect(nodeItems.length).toBe(5); + expect(labels.length).toBe(2); + expect(subCategories.length).toBe(2); + }); + + it('should fire selected events on click', async () => { + const items = [ + mockSubcategoryCreateElement(), + mockNodeCreateElement(), + mockActionCreateElement(), + mockViewCreateElement(), + ]; + const { container, emitted } = render( + ItemsRenderer, + { + pinia: createTestingPinia(), + props: { elements: items }, + }, + (vue) => { + vue.use(PiniaVuePlugin); + }, + ); + // + await Vue.nextTick(); + + const itemTypes = { + node: container.querySelector('.iteratorItem .nodeItem'), + subcategory: container.querySelector('.iteratorItem .subCategory'), + action: container.querySelector('.iteratorItem .action'), + view: container.querySelector('.iteratorItem .view'), + }; + + for (const [index, itemType] of Object.keys(itemTypes).entries()) { + const itemElement = itemTypes[itemType as keyof typeof itemTypes]; + await fireEvent.click(itemElement!); + expect(emitted().selected[index][0].type).toBe(itemType); + } + }); +}); diff --git a/packages/editor-ui/src/components/Node/NodeCreator/__tests__/NodesListPanel.test.ts b/packages/editor-ui/src/components/Node/NodeCreator/__tests__/NodesListPanel.test.ts new file mode 100644 index 0000000000000..cb827ab22195d --- /dev/null +++ b/packages/editor-ui/src/components/Node/NodeCreator/__tests__/NodesListPanel.test.ts @@ -0,0 +1,286 @@ +import Vue, { defineComponent, watch } from 'vue'; +import type { PropType } from 'vue'; +import { PiniaVuePlugin, createPinia } from 'pinia'; +import { render, screen, fireEvent } from '@testing-library/vue'; +import type { INodeTypeDescription } from 'n8n-workflow'; +import { useNodeCreatorStore } from '@/stores/nodeCreator'; +import { mockSimplifiedNodeType } from './utils'; +import NodesListPanel from '../Panel/NodesListPanel.vue'; +import { REGULAR_NODE_CREATOR_VIEW } from '@/constants'; +import type { NodeFilterType } from '@/Interface'; + +function TelemetryPlugin(vue: typeof Vue): void { + Object.defineProperty(vue, '$telemetry', { + get() { + return { + trackNodesPanel: () => {}, + }; + }, + }); + Object.defineProperty(vue.prototype, '$telemetry', { + get() { + return { + trackNodesPanel: () => {}, + }; + }, + }); +} + +function getWrapperComponent(setup: () => void) { + const wrapperComponent = defineComponent({ + props: { + nodeTypes: { + type: Array as PropType<INodeTypeDescription[]>, + required: false, + }, + }, + components: { + NodesListPanel, + }, + setup, + template: '<NodesListPanel @nodeTypeSelected="e => $emit(\'nodeTypeSelected\', e)" />', + }); + + return render( + wrapperComponent, + { + pinia: createPinia(), + }, + (vue) => { + vue.use(PiniaVuePlugin); + vue.use(TelemetryPlugin); + }, + ); +} + +describe('NodesListPanel', () => { + describe('should render nodes', () => { + it('should render trigger items', async () => { + const mockedTriggerNodes = [...Array(2).keys()].map((n) => + mockSimplifiedNodeType({ + name: `Trigger Node ${n}`, + displayName: `Trigger Node ${n}`, + group: ['trigger'], + }), + ); + const mockedRegularNodes = [...Array(2).keys()].map((n) => + mockSimplifiedNodeType({ + name: `Regular Node ${n}`, + displayName: `Regular Node ${n}`, + group: ['input'], + }), + ); + + const { container } = getWrapperComponent(() => { + const { setMergeNodes } = useNodeCreatorStore(); + + setMergeNodes([...mockedTriggerNodes, ...mockedRegularNodes]); + return {}; + }); + + await Vue.nextTick(); + expect(screen.getByText('Select a trigger')).toBeInTheDocument(); + expect(screen.queryByTestId('node-creator-search-bar')).toBeInTheDocument(); + screen.getByText('On app event').click(); + await Vue.nextTick(); + expect(screen.queryByTestId('node-creator-search-bar')).not.toBeInTheDocument(); + mockedTriggerNodes.forEach((n) => { + expect(screen.queryByText(n.name)).toBeInTheDocument(); + }); + + mockedRegularNodes.forEach((n) => { + expect(screen.queryByText(n.name)).not.toBeInTheDocument(); + }); + + expect(container.querySelector('.backButton')).toBeInTheDocument(); + + fireEvent.click(container.querySelector('.backButton')!); + await Vue.nextTick(); + + expect(screen.queryAllByTestId('item-iterator-item')).toHaveLength(6); + }); + + it('should render regular nodes', async () => { + const mockedNodes = [...Array(8).keys()].map((n) => + mockSimplifiedNodeType({ + name: `Node ${n}`, + displayName: `Node ${n}`, + group: ['input'], + }), + ); + + const wrapperComponent = defineComponent({ + props: { + nodeTypes: { + type: Array as PropType<INodeTypeDescription[]>, + required: true, + }, + selectedView: { + type: String as PropType<NodeFilterType>, + default: REGULAR_NODE_CREATOR_VIEW, + required: false, + }, + }, + components: { + NodesListPanel, + }, + setup(props) { + const { setActions, setMergeNodes, setSelectedView } = useNodeCreatorStore(); + + watch( + () => props.nodeTypes, + (nodeTypes: INodeTypeDescription[]) => { + setMergeNodes([...nodeTypes]); + }, + { immediate: true }, + ); + watch( + () => props.selectedView, + (selectedView: NodeFilterType) => { + setSelectedView(selectedView); + }, + { immediate: true }, + ); + }, + template: '<NodesListPanel @nodeTypeSelected="e => $emit(\'nodeTypeSelected\', e)" />', + }); + + render( + wrapperComponent, + { + pinia: createPinia(), + props: { + nodeTypes: mockedNodes, + selectedView: REGULAR_NODE_CREATOR_VIEW, + }, + }, + (vue) => { + vue.use(PiniaVuePlugin); + vue.use(TelemetryPlugin); + }, + ); + + await Vue.nextTick(); + expect(screen.getByText('What happens next?')).toBeInTheDocument(); + expect(screen.queryAllByTestId('item-iterator-item')).toHaveLength(6); + + screen.getByText('Action in an app').click(); + await Vue.nextTick(); + mockedNodes.forEach((n) => { + expect(screen.queryByText(n.displayName)).toBeInTheDocument(); + }); + }); + }); + + describe('should search nodes', () => { + const mockedNodes = [...Array(8).keys()].map((n) => + mockSimplifiedNodeType({ + name: `Node ${n}`, + displayName: `Node ${n}`, + group: ['trigger'], + }), + ); + + const wrapperComponent = defineComponent({ + props: { + nodeTypes: { + type: Array as PropType<INodeTypeDescription[]>, + required: true, + }, + }, + components: { + NodesListPanel, + }, + setup(props) { + const { setMergeNodes } = useNodeCreatorStore(); + + watch( + () => props.nodeTypes, + (nodeTypes: INodeTypeDescription[]) => { + setMergeNodes([...nodeTypes]); + }, + { immediate: true }, + ); + }, + template: '<NodesListPanel @nodeTypeSelected="e => $emit(\'nodeTypeSelected\', e)" />', + }); + + function renderComponent() { + return render( + wrapperComponent, + { + pinia: createPinia(), + props: { + nodeTypes: mockedNodes, + }, + }, + (vue) => { + vue.use(PiniaVuePlugin); + vue.use(TelemetryPlugin); + }, + ); + } + + it('should be visible in the root view', async () => { + renderComponent(); + await Vue.nextTick(); + + expect(screen.queryByTestId('node-creator-search-bar')).toBeInTheDocument(); + }); + it('should not be visible if subcategory contains less than 9 items', async () => { + renderComponent(); + await Vue.nextTick(); + + screen.getByText('On app event').click(); + await Vue.nextTick(); + expect(screen.queryByTestId('node-creator-search-bar')).not.toBeInTheDocument(); + expect(screen.queryAllByTestId('item-iterator-item')).toHaveLength(8); + }); + it('should be visible if subcategory contains 9 or more items', async () => { + const { updateProps } = renderComponent(); + await Vue.nextTick(); + + mockedNodes.push( + mockSimplifiedNodeType({ + name: 'Ninth node', + displayName: 'Ninth node', + group: ['trigger'], + }), + ); + + await updateProps({ nodeTypes: [...mockedNodes] }); + await Vue.nextTick(); + + screen.getByText('On app event').click(); + await Vue.nextTick(); + + expect(screen.queryAllByTestId('item-iterator-item')).toHaveLength(9); + expect(screen.queryByTestId('node-creator-search-bar')).toBeInTheDocument(); + }); + + it('should correctly handle search', async () => { + const { container } = renderComponent(); + await Vue.nextTick(); + + screen.getByText('On app event').click(); + await Vue.nextTick(); + + fireEvent.input(screen.getByTestId('node-creator-search-bar'), { + target: { value: 'Ninth' }, + }); + await Vue.nextTick(); + expect(screen.queryAllByTestId('item-iterator-item')).toHaveLength(1); + + fireEvent.input(screen.getByTestId('node-creator-search-bar'), { + target: { value: 'Non sense' }, + }); + await Vue.nextTick(); + expect(screen.queryAllByTestId('item-iterator-item')).toHaveLength(0); + expect(screen.queryByText("We didn't make that... yet")).toBeInTheDocument(); + + fireEvent.click(container.querySelector('.clear')!); + await Vue.nextTick(); + expect(screen.queryAllByTestId('item-iterator-item')).toHaveLength(9); + }); + }); +}); diff --git a/packages/editor-ui/src/components/Node/NodeCreator/__tests__/useKeyboardNavigation.test.ts b/packages/editor-ui/src/components/Node/NodeCreator/__tests__/useKeyboardNavigation.test.ts new file mode 100644 index 0000000000000..9bc0557a810f6 --- /dev/null +++ b/packages/editor-ui/src/components/Node/NodeCreator/__tests__/useKeyboardNavigation.test.ts @@ -0,0 +1,90 @@ +import { render } from '@testing-library/vue'; +import userEvent from '@testing-library/user-event'; +import { defineComponent, computed } from 'vue'; +import { useKeyboardNavigation } from '../composables/useKeyboardNavigation'; +import { PiniaVuePlugin, createPinia } from 'pinia'; + +const eventHookSpy = vi.fn(); +describe('useKeyboardNavigation', () => { + const TestComponent = defineComponent({ + setup() { + const { attachKeydownEvent, setActiveItemId, detachKeydownEvent, registerKeyHook } = + useKeyboardNavigation(); + + setActiveItemId('item1'); + const activeItemId = computed(() => useKeyboardNavigation().activeItemId); + + registerKeyHook('testKeys', { + keyboardKeys: ['ArrowDown', 'ArrowUp', 'Enter', 'ArrowRight', 'ArrowLeft', 'Escape'], + handler: eventHookSpy, + }); + + return { attachKeydownEvent, detachKeydownEvent, setActiveItemId, activeItemId }; + }, + template: ` + <span> + <div + v-for="item in 3" + :class="{'active': activeItemId === 'item' + item}" + :key="'item' + item" + v-text="activeItemId" + :data-keyboard-nav-id="'item' + item" + data-keyboard-nav-type="node" + /> + </span> + `, + mounted() { + this.attachKeydownEvent(); + }, + }); + + const renderTestComponent = () => { + return render(TestComponent, { pinia: createPinia() }, (vue) => { + vue.use(PiniaVuePlugin); + }); + }; + + afterAll(() => { + eventHookSpy.mockClear(); + }); + + test('ArrowDown moves to the next item, cycling after last item', async () => { + const { container } = renderTestComponent(); + + expect(container.querySelector('[data-keyboard-nav-id="item1"]')).toHaveClass('active'); + await userEvent.keyboard('{arrowdown}'); + await userEvent.keyboard('{arrowdown}'); + expect(container.querySelector('[data-keyboard-nav-id="item3"]')).toHaveClass('active'); + await userEvent.keyboard('{arrowdown}'); + expect(container.querySelector('[data-keyboard-nav-id="item1"]')).toHaveClass('active'); + }); + + test('ArrowUp moves to the previous item, cycling after firstitem', async () => { + const { container } = renderTestComponent(); + + expect(container.querySelector('[data-keyboard-nav-id="item1"]')).toHaveClass('active'); + await userEvent.keyboard('{arrowup}'); + expect(container.querySelector('[data-keyboard-nav-id="item3"]')).toHaveClass('active'); + await userEvent.keyboard('{arrowup}'); + expect(container.querySelector('[data-keyboard-nav-id="item2"]')).toHaveClass('active'); + await userEvent.keyboard('{arrowup}'); + expect(container.querySelector('[data-keyboard-nav-id="item1"]')).toHaveClass('active'); + }); + + test('Key hooks are executed', async () => { + renderTestComponent(); + + await userEvent.keyboard('{arrowup}'); + expect(eventHookSpy).toHaveBeenCalledWith('item3', 'ArrowUp'); + await userEvent.keyboard('{arrowdown}'); + expect(eventHookSpy).toHaveBeenCalledWith('item1', 'ArrowDown'); + await userEvent.keyboard('{arrowleft}'); + expect(eventHookSpy).toHaveBeenCalledWith('item1', 'ArrowLeft'); + await userEvent.keyboard('{arrowright}'); + expect(eventHookSpy).toHaveBeenCalledWith('item1', 'ArrowRight'); + await userEvent.keyboard('{enter}'); + expect(eventHookSpy).toHaveBeenCalledWith('item1', 'Enter'); + await userEvent.keyboard('{escape}'); + expect(eventHookSpy).toHaveBeenCalledWith('item1', 'Escape'); + }); +}); diff --git a/packages/editor-ui/src/components/Node/NodeCreator/__tests__/utils.ts b/packages/editor-ui/src/components/Node/NodeCreator/__tests__/utils.ts new file mode 100644 index 0000000000000..0acb65fa7df58 --- /dev/null +++ b/packages/editor-ui/src/components/Node/NodeCreator/__tests__/utils.ts @@ -0,0 +1,126 @@ +import type { + SimplifiedNodeType, + ActionTypeDescription, + SubcategoryItemProps, + ViewItemProps, + LabelItemProps, + NodeCreateElement, + SubcategoryCreateElement, + ViewCreateElement, + LabelCreateElement, + ActionCreateElement, +} from '@/Interface'; +import { v4 as uuidv4 } from 'uuid'; + +export const mockSimplifiedNodeType = ( + overrides?: Partial<SimplifiedNodeType>, +): SimplifiedNodeType => ({ + displayName: 'Sample DisplayName', + name: 'sampleName', + icon: 'sampleIcon', + iconUrl: 'https://example.com/icon.png', + group: ['group1', 'group2'], + description: 'Sample description', + codex: { + categories: ['category1', 'category2'], + subcategories: { + category1: ['subcategory1', 'subcategory2'], + category2: ['subcategory3', 'subcategory4'], + }, + alias: ['alias1', 'alias2'], + }, + defaults: { + color: '#ffffff', + }, + ...overrides, +}); + +export const mockActionTypeDescription = ( + overrides?: Partial<ActionTypeDescription>, +): ActionTypeDescription => ({ + ...mockSimplifiedNodeType(), + values: { value1: 'test', value2: 123 }, + actionKey: 'sampleActionKey', + codex: { + label: 'Sample Label', + categories: ['category1', 'category2'], + }, + ...overrides, +}); + +const mockSubcategoryItemProps = ( + overrides?: Partial<SubcategoryItemProps>, +): SubcategoryItemProps => ({ + description: 'Sample description', + iconType: 'sampleIconType', + icon: 'sampleIcon', + title: 'Sample title', + subcategory: 'sampleSubcategory', + defaults: { color: '#ffffff' }, + forceIncludeNodes: ['node1', 'node2'], + ...overrides, +}); + +const mockViewItemProps = (overrides?: Partial<ViewItemProps>): ViewItemProps => ({ + title: 'Sample title', + description: 'Sample description', + icon: 'sampleIcon', + ...overrides, +}); + +const mockLabelItemProps = (overrides?: Partial<LabelItemProps>): LabelItemProps => ({ + key: uuidv4(), + ...overrides, +}); + +export const mockNodeCreateElement = ( + subcategory?: string, + overrides?: Partial<SimplifiedNodeType>, +): NodeCreateElement => ({ + uuid: uuidv4(), + key: uuidv4(), + type: 'node', + subcategory: subcategory || 'sampleSubcategory', + properties: mockSimplifiedNodeType(overrides), +}); + +export const mockSubcategoryCreateElement = ( + overrides?: Partial<SubcategoryItemProps>, +): SubcategoryCreateElement => ({ + uuid: uuidv4(), + key: uuidv4(), + type: 'subcategory', + properties: mockSubcategoryItemProps(overrides), +}); + +export const mockViewCreateElement = ( + overrides?: Partial<ViewCreateElement>, +): ViewCreateElement => ({ + uuid: uuidv4(), + key: uuidv4(), + type: 'view', + properties: mockViewItemProps(), + ...overrides, +}); + +export const mockLabelCreateElement = ( + subcategory?: string, + overrides?: Partial<LabelItemProps>, +): LabelCreateElement => ({ + uuid: uuidv4(), + key: uuidv4(), + type: 'label', + subcategory: subcategory || 'sampleSubcategory', + properties: mockLabelItemProps(overrides), +}); + +export const mockActionCreateElement = ( + subcategory?: string, + overrides?: Partial<ActionTypeDescription>, +): ActionCreateElement => ({ + uuid: uuidv4(), + key: uuidv4(), + type: 'action', + subcategory: subcategory || 'sampleSubcategory', + properties: mockActionTypeDescription(overrides), +}); diff --git a/packages/editor-ui/src/components/Node/NodeCreator/composables/useActions.ts b/packages/editor-ui/src/components/Node/NodeCreator/composables/useActions.ts new file mode 100644 index 0000000000000..a002357e33838 --- /dev/null +++ b/packages/editor-ui/src/components/Node/NodeCreator/composables/useActions.ts @@ -0,0 +1,218 @@ +import { getCurrentInstance, computed } from 'vue'; +import type { IDataObject, INodeParameters } from 'n8n-workflow'; +import type { + ActionTypeDescription, + INodeCreateElement, + IUpdateInformation, + LabelCreateElement, +} from '@/Interface'; +import { + MANUAL_TRIGGER_NODE_TYPE, + NODE_CREATOR_OPEN_SOURCES, + SCHEDULE_TRIGGER_NODE_TYPE, + STICKY_NODE_TYPE, + TRIGGER_NODE_CREATOR_VIEW, + WEBHOOK_NODE_TYPE, +} from '@/constants'; + +import type { BaseTextKey } from '@/plugins/i18n'; +import type { Telemetry } from '@/plugins/telemetry'; +import { useNodeCreatorStore } from '@/stores/nodeCreator'; +import { useWorkflowsStore } from '@/stores/workflows'; +import { useNodeTypesStore } from '@/stores/nodeTypes'; +import { runExternalHook } from '@/utils'; +import { useWebhooksStore } from '@/stores/webhooks'; + +import { sortNodeCreateElements, transformNodeType } from '../utils'; + +export const useActions = () => { + const nodeCreatorStore = useNodeCreatorStore(); + const instance = getCurrentInstance(); + + const actionsCategoryLocales = computed(() => { + return { + actions: instance?.proxy.$locale.baseText('nodeCreator.actionsCategory.actions') ?? '', + triggers: instance?.proxy.$locale.baseText('nodeCreator.actionsCategory.triggers') ?? '', + }; + }); + + function getPlaceholderTriggerActions(subcategory: string) { + const nodes = [WEBHOOK_NODE_TYPE, SCHEDULE_TRIGGER_NODE_TYPE]; + + const matchedNodeTypes = nodeCreatorStore.mergedNodes + .filter((node) => nodes.some((n) => n === node.name)) + .map((node) => { + const transformed = transformNodeType(node, subcategory, 'action'); + + if (transformed.type === 'action') { + const nameBase = node.name.replace('n8n-nodes-base.', ''); + const localeKey = `nodeCreator.actionsPlaceholderNode.${nameBase}` as BaseTextKey; + const overwriteLocale = instance?.proxy.$locale.baseText(localeKey) as string; + + // If the locale key is not the same as the node name, it means it contain a translation + // and we should use it + if (overwriteLocale !== localeKey) { + transformed.properties.displayName = overwriteLocale; + } + } + return transformed; + }); + + return matchedNodeTypes; + } + + function filterActionsCategory(items: INodeCreateElement[], category: string) { + return items.filter( + (item) => item.type === 'action' && item.properties.codex.categories.includes(category), + ); + } + + function injectActionsLabels(items: INodeCreateElement[]): INodeCreateElement[] { + const extendedActions = sortNodeCreateElements([...items]); + const labelsSet = new Set<string>(); + + // Collect unique labels + for (const action of extendedActions) { + if (action.type !== 'action') continue; + const label = action.properties?.codex?.label; + labelsSet.add(label); + } + + if (labelsSet.size <= 1) return extendedActions; + + // Create a map to store the first index of each label + const firstIndexMap = new Map<string, number>(); + + // Iterate through the extendedActions to find the first index of each label + for (let i = 0; i < extendedActions.length; i++) { + const action = extendedActions[i]; + if (action.type !== 'action') continue; + const label = action.properties?.codex?.label; + if (!firstIndexMap.has(label)) { + firstIndexMap.set(label, i); + } + } + + // Keep track of the number of inserted labels + let insertedLabels = 0; + + // Create and insert new label objects at the first index of each label + for (const label of labelsSet) { + const newLabel: LabelCreateElement = { + uuid: label, + type: 'label', + key: label, + subcategory: extendedActions[0].key, + properties: { + key: label, + }, + }; + + const insertIndex = firstIndexMap.get(label)! + insertedLabels; + extendedActions.splice(insertIndex, 0, newLabel); + insertedLabels++; + } + + return extendedActions; + } + + function parseCategoryActions( + actions: INodeCreateElement[], + category: string, + withLabels = true, + ) { + const filteredActions = filterActionsCategory(actions, category); + if (withLabels) return injectActionsLabels(filteredActions); + return filteredActions; + } + + function getActionData(actionItem: ActionTypeDescription): IUpdateInformation { + const displayOptions = actionItem.displayOptions; + + const displayConditions = Object.keys(displayOptions?.show || {}).reduce( + (acc: IDataObject, showCondition: string) => { + acc[showCondition] = displayOptions?.show?.[showCondition]?.[0]; + return acc; + }, + {}, + ); + + return { + name: actionItem.displayName, + key: actionItem.name as string, + value: { ...actionItem.values, ...displayConditions } as INodeParameters, + }; + } + + function getNodeTypesWithManualTrigger(nodeType?: string): string[] { + if (!nodeType) return []; + + const { selectedView, openSource } = useNodeCreatorStore(); + const { workflowTriggerNodes } = useWorkflowsStore(); + const isTrigger = useNodeTypesStore().isTriggerNode(nodeType); + const workflowContainsTrigger = workflowTriggerNodes.length > 0; + const isTriggerPanel = selectedView === TRIGGER_NODE_CREATOR_VIEW; + const isStickyNode = nodeType === STICKY_NODE_TYPE; + const singleNodeOpenSources = [ + NODE_CREATOR_OPEN_SOURCES.PLUS_ENDPOINT, + NODE_CREATOR_OPEN_SOURCES.NODE_CONNECTION_ACTION, + NODE_CREATOR_OPEN_SOURCES.NODE_CONNECTION_DROP, + ]; + + // If the node creator was opened from the plus endpoint, node connection action, or node connection drop + // then we do not want to append the manual trigger + const isSingleNodeOpenSource = singleNodeOpenSources.includes(openSource); + const shouldAppendManualTrigger = + !isSingleNodeOpenSource && + !isTrigger && + !workflowContainsTrigger && + isTriggerPanel && + !isStickyNode; + + const nodeTypes = shouldAppendManualTrigger ? [MANUAL_TRIGGER_NODE_TYPE, nodeType] : [nodeType]; + + return nodeTypes; + } + + // Hook into addNode action to set the last node parameters & track the action selected + function setAddedNodeActionParameters( + action: IUpdateInformation, + telemetry?: Telemetry, + rootView = '', + ) { + const { $onAction: onWorkflowStoreAction } = useWorkflowsStore(); + const storeWatcher = onWorkflowStoreAction( + ({ name, after, store: { setLastNodeParameters }, args }) => { + if (name !== 'addNode' || args[0].type !== action.key) return; + after(() => { + setLastNodeParameters(action); + if (telemetry) trackActionSelected(action, telemetry, rootView); + // Unsubscribe from the store watcher + storeWatcher(); + }); + }, + ); + + return storeWatcher; + } + + function trackActionSelected(action: IUpdateInformation, telemetry: Telemetry, rootView: string) { + const payload = { + node_type: action.key, + action: action.name, + source_mode: rootView.toLowerCase(), + resource: (action.value as INodeParameters).resource || '', + }; + runExternalHook('nodeCreateList.addAction', useWebhooksStore(), payload); + telemetry?.trackNodesPanel('nodeCreateList.addAction', payload); + } + + return { + actionsCategoryLocales, + getPlaceholderTriggerActions, + parseCategoryActions, + getNodeTypesWithManualTrigger, + getActionData, + setAddedNodeActionParameters, + }; +}; diff --git a/packages/editor-ui/src/components/Node/NodeCreator/composables/useActionsGeneration.ts b/packages/editor-ui/src/components/Node/NodeCreator/composables/useActionsGeneration.ts new file mode 100644 index 0000000000000..5876995e7b0f9 --- /dev/null +++ b/packages/editor-ui/src/components/Node/NodeCreator/composables/useActionsGeneration.ts @@ -0,0 +1,279 @@ +import { startCase } from 'lodash-es'; +import type { + INodePropertyCollection, + INodePropertyOptions, + INodeProperties, + INodeTypeDescription, +} from 'n8n-workflow'; +import { deepCopy } from 'n8n-workflow'; +import { CUSTOM_API_CALL_KEY } from '@/constants'; +import type { ActionTypeDescription, SimplifiedNodeType, ActionsRecord } from '@/Interface'; + +import { i18n } from '@/plugins/i18n'; + +const PLACEHOLDER_RECOMMENDED_ACTION_KEY = 'placeholder_recommended'; + +const customNodeActionsParsers: { + [key: string]: ( + matchedProperty: INodeProperties, + nodeTypeDescription: INodeTypeDescription, + ) => ActionTypeDescription[] | undefined; +} = { + ['n8n-nodes-base.hubspotTrigger']: (matchedProperty, nodeTypeDescription) => { + const collection = matchedProperty?.options?.[0] as INodePropertyCollection; + + return (collection?.values[0]?.options as INodePropertyOptions[])?.map( + (categoryItem): ActionTypeDescription => ({ + ...getNodeTypeBase(nodeTypeDescription), + actionKey: categoryItem.value as string, + displayName: i18n.baseText('nodeCreator.actionsCategory.onEvent', { + interpolate: { event: startCase(categoryItem.name) }, + }), + description: categoryItem.description || '', + displayOptions: matchedProperty.displayOptions, + values: { eventsUi: { eventValues: [{ name: categoryItem.value }] } }, + }), + ); + }, +}; + +function getNodeTypeBase(nodeTypeDescription: INodeTypeDescription, label?: string) { + const isTrigger = nodeTypeDescription.group.includes('trigger'); + const category = isTrigger + ? i18n.baseText('nodeCreator.actionsCategory.triggers') + : i18n.baseText('nodeCreator.actionsCategory.actions'); + return { + name: nodeTypeDescription.name, + group: nodeTypeDescription.group, + codex: { + label: label || '', + categories: [category], + }, + iconUrl: nodeTypeDescription.iconUrl, + icon: nodeTypeDescription.icon, + defaults: nodeTypeDescription.defaults, + }; +} + +function operationsCategory(nodeTypeDescription: INodeTypeDescription): ActionTypeDescription[] { + if (!!nodeTypeDescription.properties.find((property) => property.name === 'resource')) return []; + + const matchedProperty = nodeTypeDescription.properties.find( + (property) => property.name?.toLowerCase() === 'operation', + ); + + if (!matchedProperty || !matchedProperty.options) return []; + + const filteredOutItems = (matchedProperty.options as INodePropertyOptions[]).filter( + (categoryItem: INodePropertyOptions) => !['*', '', ' '].includes(categoryItem.name), + ); + + const items = filteredOutItems.map((item: INodePropertyOptions) => ({ + ...getNodeTypeBase(nodeTypeDescription), + actionKey: item.value as string, + displayName: item.action ?? startCase(item.name), + description: item.description ?? '', + displayOptions: matchedProperty.displayOptions, + values: { + [matchedProperty.name]: matchedProperty.type === 'multiOptions' ? [item.value] : item.value, + }, + })); + + // Do not return empty category + if (items.length === 0) return []; + + return items; +} + +function triggersCategory(nodeTypeDescription: INodeTypeDescription): ActionTypeDescription[] { + const matchingKeys = ['event', 'events', 'trigger on']; + const isTrigger = nodeTypeDescription.displayName?.toLowerCase().includes('trigger'); + const matchedProperty = nodeTypeDescription.properties.find((property) => + matchingKeys.includes(property.displayName?.toLowerCase()), + ); + + if (!isTrigger) return []; + + // Inject placeholder action if no events are available + // so user is able to add node to the canvas from the actions panel + if (!matchedProperty || !matchedProperty.options) { + return [ + { + ...getNodeTypeBase(nodeTypeDescription), + actionKey: PLACEHOLDER_RECOMMENDED_ACTION_KEY, + displayName: i18n.baseText('nodeCreator.actionsCategory.onNewEvent', { + interpolate: { event: nodeTypeDescription.displayName.replace('Trigger', '').trimEnd() }, + }), + description: '', + }, + ]; + } + + const filteredOutItems = (matchedProperty.options as INodePropertyOptions[]).filter( + (categoryItem: INodePropertyOptions) => !['*', '', ' '].includes(categoryItem.name), + ); + + const customParsedItem = customNodeActionsParsers[nodeTypeDescription.name]?.( + matchedProperty, + nodeTypeDescription, + ); + + const items = + customParsedItem ?? + filteredOutItems.map((categoryItem: INodePropertyOptions) => ({ + ...getNodeTypeBase(nodeTypeDescription), + actionKey: categoryItem.value as string, + displayName: + categoryItem.action ?? + i18n.baseText('nodeCreator.actionsCategory.onEvent', { + interpolate: { event: startCase(categoryItem.name) }, + }), + description: categoryItem.description || '', + displayOptions: matchedProperty.displayOptions, + values: { + [matchedProperty.name]: + matchedProperty.type === 'multiOptions' ? [categoryItem.value] : categoryItem.value, + }, + })); + + return items; +} + +function resourceCategories(nodeTypeDescription: INodeTypeDescription): ActionTypeDescription[] { + const transformedNodes: ActionTypeDescription[] = []; + const matchedProperties = nodeTypeDescription.properties.filter( + (property) => property.displayName?.toLowerCase() === 'resource', + ); + + matchedProperties.forEach((property) => { + ((property.options as INodePropertyOptions[]) || []) + .filter((option) => option.value !== CUSTOM_API_CALL_KEY) + .forEach((resourceOption, i, options) => { + const isSingleResource = options.length === 1; + + // Match operations for the resource by checking if displayOptions matches or contains the resource name + const operations = nodeTypeDescription.properties.find( + (operation) => + operation.name === 'operation' && + (operation.displayOptions?.show?.resource?.includes(resourceOption.value) || + isSingleResource), + ); + + if (!operations?.options) return; + + const items = ((operations.options as INodePropertyOptions[]) || []).map( + (operationOption) => { + const displayName = + operationOption.action ?? `${resourceOption.name} ${startCase(operationOption.name)}`; + + // We need to manually populate displayOptions as they are not present in the node description + // if the resource has only one option + const displayOptions = isSingleResource + ? { show: { resource: [(options as INodePropertyOptions[])[0]?.value] } } + : operations?.displayOptions; + + return { + ...getNodeTypeBase( + nodeTypeDescription, + `${resourceOption.name} ${i18n.baseText('nodeCreator.actionsCategory.actions')}`, + ), + actionKey: operationOption.value as string, + description: operationOption?.description ?? '', + displayOptions, + values: { + operation: + operations?.type === 'multiOptions' + ? [operationOption.value] + : operationOption.value, + }, + displayName, + group: ['trigger'], + }; + }, + ); + + transformedNodes.push(...items); + }); + }); + + return transformedNodes; +} + +export function useActionsGenerator() { + function generateNodeActions(node: INodeTypeDescription | undefined) { + if (!node) return []; + return [...triggersCategory(node), ...operationsCategory(node), ...resourceCategories(node)]; + } + function filterActions(actions: ActionTypeDescription[]) { + // Do not show single action nodes + if (actions.length <= 1) return []; + return actions.filter( + (action: ActionTypeDescription, _: number, arr: ActionTypeDescription[]) => { + const isApiCall = action.actionKey === CUSTOM_API_CALL_KEY; + if (isApiCall) return false; + + const isPlaceholderTriggerAction = action.actionKey === PLACEHOLDER_RECOMMENDED_ACTION_KEY; + return !isPlaceholderTriggerAction || (isPlaceholderTriggerAction && arr.length > 1); + }, + ); + } + + function getSimplifiedNodeType(node: INodeTypeDescription): SimplifiedNodeType { + const { displayName, defaults, description, name, group, icon, iconUrl, codex } = node; + + return { + displayName, + defaults, + description, + name, + group, + icon, + iconUrl, + codex, + }; + } + + function generateMergedNodesAndActions(nodeTypes: INodeTypeDescription[]) { + const visibleNodeTypes = deepCopy(nodeTypes); + const actions: ActionsRecord<typeof mergedNodes> = {}; + const mergedNodes: SimplifiedNodeType[] = []; + + visibleNodeTypes + .filter((node) => !node.group.includes('trigger')) + .forEach((app) => { + const appActions = generateNodeActions(app); + actions[app.name] = appActions; + + mergedNodes.push(getSimplifiedNodeType(app)); + }); + + visibleNodeTypes + .filter((node) => node.group.includes('trigger')) + .forEach((trigger) => { + const normalizedName = trigger.name.replace('Trigger', ''); + const triggerActions = generateNodeActions(trigger); + const appActions = actions?.[normalizedName] || []; + const app = mergedNodes.find((node) => node.name === normalizedName); + + if (app && appActions?.length > 0) { + // merge triggers into regular nodes that match + const mergedActions = filterActions([...appActions, ...triggerActions]); + actions[normalizedName] = mergedActions; + + app.description = trigger.description; // default to trigger description + } else { + actions[trigger.name] = filterActions(triggerActions); + mergedNodes.push(getSimplifiedNodeType(trigger)); + } + }); + + return { + actions, + mergedNodes, + }; + } + + return { + generateMergedNodesAndActions, + }; +} diff --git a/packages/editor-ui/src/components/Node/NodeCreator/composables/useKeyboardNavigation.ts b/packages/editor-ui/src/components/Node/NodeCreator/composables/useKeyboardNavigation.ts new file mode 100644 index 0000000000000..8e1b81910f7a4 --- /dev/null +++ b/packages/editor-ui/src/components/Node/NodeCreator/composables/useKeyboardNavigation.ts @@ -0,0 +1,156 @@ +import { ref, set } from 'vue'; +import { defineStore } from 'pinia'; + +export type KeyboardKey = (typeof WATCHED_KEYS)[number]; +interface KeyHook { + keyboardKeys: KeyboardKey[]; + condition?: (type: string, activeItemId: string) => boolean; + handler: (activeItemId: string, keyboardKey: KeyboardKey) => void; +} + +export const KEYBOARD_ID_ATTR = 'data-keyboard-nav-id'; +export const WATCHED_KEYS = [ + 'ArrowUp', + 'ArrowDown', + 'ArrowLeft', + 'ArrowRight', + 'Enter', + 'Escape', + 'Tab', +]; + +export const useKeyboardNavigation = defineStore('nodeCreatorKeyboardNavigation', () => { + const selectableItems = ref<Array<WeakRef<Element>>>([]); + const activeItemId = ref<string | null>(null); + // Array of objects that contains key code and handler + const keysHooks = ref<Record<string, KeyHook>>({}); + + function getItemType(element?: Element) { + return element?.getAttribute('data-keyboard-nav-type'); + } + function getElementId(element?: Element) { + return element?.getAttribute(KEYBOARD_ID_ATTR) || undefined; + } + function refreshSelectableItems(): Promise<void> { + return new Promise((resolve) => { + // Wait for DOM to update + cleanupSelectableItems(); + setTimeout(() => { + selectableItems.value = Array.from( + document.querySelectorAll('[data-keyboard-nav-type]'), + ).map((el) => new WeakRef(el)); + resolve(); + }, 0); + }); + } + + function executeKeyHooks(keyboardKey: KeyboardKey, activeItem: Element) { + const flatHooks = Object.values(keysHooks.value); + const hooks = flatHooks.filter((hook) => hook.keyboardKeys.includes(keyboardKey)); + + hooks.forEach((hook) => { + if (!activeItemId.value) return; + + const conditionPassed = + hook.condition === undefined || + hook.condition(getItemType(activeItem) || '', activeItemId.value); + + if (conditionPassed && activeItemId.value) { + hook.handler(activeItemId.value, keyboardKey); + } + }); + } + + async function onKeyDown(e: KeyboardEvent) { + const pressedKey = e.key; + if (!WATCHED_KEYS.includes(pressedKey)) return; + e.preventDefault(); + e.stopPropagation(); + + await refreshSelectableItems(); + const activeItemIndex = selectableItems.value.findIndex( + (item) => getElementId(item?.deref()) === activeItemId.value, + ); + const activeItem = selectableItems.value[activeItemIndex]?.deref(); + + const isArrowDown = pressedKey === 'ArrowDown'; + const isArrowUp = pressedKey === 'ArrowUp'; + + if (!activeItem) return; + + if (isArrowDown) { + const nextItemIndex = + activeItemIndex < selectableItems.value.length - 1 ? activeItemIndex + 1 : 0; + + setActiveItem(selectableItems.value[nextItemIndex]?.deref()); + } + if (isArrowUp) { + const previousIndex = + activeItemIndex > 0 ? activeItemIndex - 1 : selectableItems.value.length - 1; + + setActiveItem(selectableItems.value[previousIndex]?.deref()); + } + executeKeyHooks(pressedKey, activeItem); + } + + function setActiveItemId(id: string) { + activeItemId.value = id; + } + + function setActiveItem(item?: Element) { + const itemId = getElementId(item); + if (!itemId) return; + + setActiveItemId(itemId); + if (item?.scrollIntoView) { + item?.scrollIntoView({ block: 'center' }); + } + } + + async function setActiveItemIndex(index: number) { + await refreshSelectableItems(); + + setActiveItem(selectableItems.value[index]?.deref()); + } + + function attachKeydownEvent() { + document.addEventListener('keydown', onKeyDown, { capture: true }); + } + + function detachKeydownEvent() { + cleanupSelectableItems(); + document.removeEventListener('keydown', onKeyDown, { capture: true }); + } + + function registerKeyHook(name: string, hook: KeyHook) { + hook.keyboardKeys.forEach((keyboardKey) => { + if (WATCHED_KEYS.includes(keyboardKey)) { + set(keysHooks.value, name, hook); + } else { + throw new Error(`Key ${keyboardKey} is not supported`); + } + }); + } + + function cleanupSelectableItems() { + // Cleanup to make sure DOM elements get garbage collected + selectableItems.value = []; + } + + function getActiveItemIndex() { + return selectableItems.value.findIndex( + (item) => getElementId(item?.deref()) === activeItemId.value, + ); + } + + return { + activeItemId, + attachKeydownEvent, + refreshSelectableItems, + detachKeydownEvent, + registerKeyHook, + getActiveItemIndex, + setActiveItemId, + setActiveItemIndex, + }; +}); diff --git a/packages/editor-ui/src/components/Node/NodeCreator/composables/useViewStacks.ts b/packages/editor-ui/src/components/Node/NodeCreator/composables/useViewStacks.ts new file mode 100644 index 0000000000000..dfd65edf2cf61 --- /dev/null +++ b/packages/editor-ui/src/components/Node/NodeCreator/composables/useViewStacks.ts @@ -0,0 +1,188 @@ +import { computed, ref, set } from 'vue'; +import { defineStore } from 'pinia'; +import { v4 as uuid } from 'uuid'; +import type { INodeCreateElement, NodeFilterType, SimplifiedNodeType } from '@/Interface'; +import { DEFAULT_SUBCATEGORY, TRIGGER_NODE_CREATOR_VIEW } from '@/constants'; + +import { useNodeCreatorStore } from '@/stores/nodeCreator'; + +import { useKeyboardNavigation } from './useKeyboardNavigation'; +import { + transformNodeType, + subcategorizeItems, + sortNodeCreateElements, + searchNodes, +} from '../utils'; + +interface ViewStack { + uuid?: string; + title?: string; + subtitle?: string; + search?: string; + subcategory?: string; + nodeIcon?: { + iconType?: string; + icon?: string; + color?: string; + }; + iconUrl?: string; + rootView?: NodeFilterType; + activeIndex?: number; + transitionDirection?: 'in' | 'out'; + hasSearch?: boolean; + items?: INodeCreateElement[]; + baselineItems?: INodeCreateElement[]; + searchItems?: SimplifiedNodeType[]; + forceIncludeNodes?: string[]; + mode?: 'actions' | 'nodes'; + baseFilter?: (item: INodeCreateElement) => boolean; + itemsMapper?: (item: INodeCreateElement) => INodeCreateElement; +} + +export const useViewStacks = defineStore('nodeCreatorViewStacks', () => { + const nodeCreatorStore = useNodeCreatorStore(); + const { getActiveItemIndex } = useKeyboardNavigation(); + + const viewStacks = ref<ViewStack[]>([]); + + const activeStackItems = computed<INodeCreateElement[]>(() => { + const stack = viewStacks.value[viewStacks.value.length - 1]; + + if (!stack?.baselineItems) { + return stack.items ? extendItemsWithUUID(stack.items) : []; + } + + if (stack.search && searchBaseItems.value) { + const searchBase = + searchBaseItems.value.length > 0 ? searchBaseItems.value : stack.baselineItems; + + return extendItemsWithUUID(searchNodes(stack.search || '', searchBase)); + } + return extendItemsWithUUID(stack.baselineItems); + }); + + const activeViewStack = computed<ViewStack>(() => { + const stack = viewStacks.value[viewStacks.value.length - 1]; + if (!stack) return {}; + + return { + ...stack, + items: activeStackItems.value, + hasSearch: (stack.baselineItems || []).length > 8 || stack?.hasSearch, + }; + }); + + const activeViewStackMode = computed( + () => activeViewStack.value.mode || TRIGGER_NODE_CREATOR_VIEW, + ); + + const searchBaseItems = computed<INodeCreateElement[]>(() => { + const stack = viewStacks.value[viewStacks.value.length - 1]; + if (!stack || !stack.searchItems) return []; + + return stack.searchItems.map((item) => transformNodeType(item, stack.subcategory)); + }); + + // Generate a delta between the global search results(all nodes) and the stack search results + const globalSearchItemsDiff = computed<INodeCreateElement[]>(() => { + const stack = viewStacks.value[viewStacks.value.length - 1]; + if (!stack || !stack.search) return []; + + const allNodes = nodeCreatorStore.mergedNodes.map((item) => transformNodeType(item)); + const globalSearchResult = extendItemsWithUUID(searchNodes(stack.search || '', allNodes)); + + return globalSearchResult.filter((item) => { + return !activeStackItems.value.find((activeItem) => activeItem.key === item.key); + }); + }); + + function setStackBaselineItems() { + const stack = viewStacks.value[viewStacks.value.length - 1]; + if (!stack || !activeViewStack.value.uuid) return; + + const subcategorizedItems = subcategorizeItems(nodeCreatorStore.mergedNodes); + let stackItems = + stack?.items ?? subcategorizedItems[stack?.subcategory ?? DEFAULT_SUBCATEGORY] ?? []; + + // Ensure that the nodes specified in `stack.forceIncludeNodes` are always included, + // regardless of whether the subcategory is matched + if ((stack.forceIncludeNodes ?? []).length > 0) { + const matchedNodes = nodeCreatorStore.mergedNodes + .filter((item) => stack.forceIncludeNodes?.includes(item.name)) + .map((item) => transformNodeType(item, stack.subcategory)); + + stackItems.push(...matchedNodes); + } + + if (stack.baseFilter) { + stackItems = stackItems.filter(stack.baseFilter); + } + + if (stack.itemsMapper) { + stackItems = stackItems.map(stack.itemsMapper); + } + + // Sort only if non-root view + if (!stack.items) { + sortNodeCreateElements(stackItems); + } + + updateCurrentViewStack({ baselineItems: stackItems }); + } + + function extendItemsWithUUID(items: INodeCreateElement[]) { + return items.map((item) => ({ + ...item, + uuid: `${item.key}-${uuid()}`, + })); + } + + function pushViewStack(stack: ViewStack) { + if (activeViewStack.value.uuid) { + updateCurrentViewStack({ activeIndex: getActiveItemIndex() }); + } + + const newStackUuid = uuid(); + viewStacks.value.push({ + ...stack, + uuid: newStackUuid, + transitionDirection: 'in', + activeIndex: 0, + }); + setStackBaselineItems(); + } + + function popViewStack() { + if (activeViewStack.value.uuid) { + viewStacks.value.pop(); + updateCurrentViewStack({ transitionDirection: 'out' }); + } + } + + function updateCurrentViewStack(stack: Partial<ViewStack>) { + const currentStack = viewStacks.value[viewStacks.value.length - 1]; + const matchedIndex = viewStacks.value.findIndex((s) => s.uuid === currentStack.uuid); + if (!currentStack) return; + + // For each key in the stack, update the matched stack + Object.keys(stack).forEach((key) => { + const typedKey = key as keyof ViewStack; + set(viewStacks.value[matchedIndex], key, stack[typedKey]); + }); + } + + function resetViewStacks() { + viewStacks.value = []; + } + + return { + viewStacks, + activeViewStack, + activeViewStackMode, + globalSearchItemsDiff, + resetViewStacks, + updateCurrentViewStack, + pushViewStack, + popViewStack, + }; +}); diff --git a/packages/editor-ui/src/components/Node/NodeCreator/useMainPanelView.ts b/packages/editor-ui/src/components/Node/NodeCreator/useMainPanelView.ts deleted file mode 100644 index 05ef090da0432..0000000000000 --- a/packages/editor-ui/src/components/Node/NodeCreator/useMainPanelView.ts +++ /dev/null @@ -1,198 +0,0 @@ -import { getCurrentInstance, computed } from 'vue'; -import { - CORE_NODES_CATEGORY, - WEBHOOK_NODE_TYPE, - OTHER_TRIGGER_NODES_SUBCATEGORY, - EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE, - MANUAL_TRIGGER_NODE_TYPE, - SCHEDULE_TRIGGER_NODE_TYPE, - REGULAR_NODE_FILTER, - TRANSFORM_DATA_SUBCATEGORY, - FILES_SUBCATEGORY, - FLOWS_CONTROL_SUBCATEGORY, - HELPERS_SUBCATEGORY, - TRIGGER_NODE_FILTER, -} from '@/constants'; -import { useNodeCreatorStore } from '@/stores/nodeCreator'; - -export default () => { - const instance = getCurrentInstance(); - const nodeCreatorStore = useNodeCreatorStore(); - - const VIEWS = [ - { - value: REGULAR_NODE_FILTER, - title: instance?.proxy.$locale.baseText('nodeCreator.triggerHelperPanel.whatHappensNext'), - items: [ - { - key: '*', - type: 'subcategory', - properties: { - subcategory: 'App Regular Nodes', - icon: 'globe', - }, - }, - { - type: 'subcategory', - key: TRANSFORM_DATA_SUBCATEGORY, - category: CORE_NODES_CATEGORY, - properties: { - subcategory: TRANSFORM_DATA_SUBCATEGORY, - icon: 'pen', - }, - }, - { - type: 'subcategory', - key: HELPERS_SUBCATEGORY, - category: CORE_NODES_CATEGORY, - properties: { - subcategory: HELPERS_SUBCATEGORY, - icon: 'toolbox', - }, - }, - { - type: 'subcategory', - key: FLOWS_CONTROL_SUBCATEGORY, - category: CORE_NODES_CATEGORY, - properties: { - subcategory: FLOWS_CONTROL_SUBCATEGORY, - icon: 'code-branch', - }, - }, - { - type: 'subcategory', - key: FILES_SUBCATEGORY, - category: CORE_NODES_CATEGORY, - properties: { - subcategory: FILES_SUBCATEGORY, - icon: 'file-alt', - }, - }, - { - key: TRIGGER_NODE_FILTER, - type: 'view', - properties: { - title: instance?.proxy.$locale.baseText( - 'nodeCreator.triggerHelperPanel.addAnotherTrigger', - ), - icon: 'bolt', - withTopBorder: true, - description: instance?.proxy.$locale.baseText( - 'nodeCreator.triggerHelperPanel.addAnotherTriggerDescription', - ), - }, - }, - ], - }, - { - value: TRIGGER_NODE_FILTER, - title: instance?.proxy.$locale.baseText('nodeCreator.triggerHelperPanel.selectATrigger'), - description: instance?.proxy.$locale.baseText( - 'nodeCreator.triggerHelperPanel.selectATriggerDescription', - ), - items: [ - { - key: '*', - type: 'subcategory', - properties: { - subcategory: 'App Trigger Nodes', - icon: 'satellite-dish', - }, - }, - { - key: SCHEDULE_TRIGGER_NODE_TYPE, - type: 'node', - category: [CORE_NODES_CATEGORY], - properties: { - nodeType: { - group: [], - name: SCHEDULE_TRIGGER_NODE_TYPE, - displayName: instance?.proxy.$locale.baseText( - 'nodeCreator.triggerHelperPanel.scheduleTriggerDisplayName', - ), - description: instance?.proxy.$locale.baseText( - 'nodeCreator.triggerHelperPanel.scheduleTriggerDescription', - ), - icon: 'fa:clock', - }, - }, - }, - { - key: WEBHOOK_NODE_TYPE, - type: 'node', - category: [CORE_NODES_CATEGORY], - properties: { - nodeType: { - group: [], - name: WEBHOOK_NODE_TYPE, - displayName: instance?.proxy.$locale.baseText( - 'nodeCreator.triggerHelperPanel.webhookTriggerDisplayName', - ), - description: instance?.proxy.$locale.baseText( - 'nodeCreator.triggerHelperPanel.webhookTriggerDescription', - ), - iconData: { - type: 'file', - icon: 'webhook', - fileBuffer: '/static/webhook-icon.svg', - }, - }, - }, - }, - { - key: MANUAL_TRIGGER_NODE_TYPE, - type: 'node', - category: [CORE_NODES_CATEGORY], - properties: { - nodeType: { - group: [], - name: MANUAL_TRIGGER_NODE_TYPE, - displayName: instance?.proxy.$locale.baseText( - 'nodeCreator.triggerHelperPanel.manualTriggerDisplayName', - ), - description: instance?.proxy.$locale.baseText( - 'nodeCreator.triggerHelperPanel.manualTriggerDescription', - ), - icon: 'fa:mouse-pointer', - }, - }, - }, - { - key: EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE, - type: 'node', - category: [CORE_NODES_CATEGORY], - properties: { - nodeType: { - group: [], - name: EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE, - displayName: instance?.proxy.$locale.baseText( - 'nodeCreator.triggerHelperPanel.workflowTriggerDisplayName', - ), - description: instance?.proxy.$locale.baseText( - 'nodeCreator.triggerHelperPanel.workflowTriggerDescription', - ), - icon: 'fa:sign-out-alt', - }, - }, - }, - { - type: 'subcategory', - key: OTHER_TRIGGER_NODES_SUBCATEGORY, - category: CORE_NODES_CATEGORY, - properties: { - subcategory: OTHER_TRIGGER_NODES_SUBCATEGORY, - icon: 'folder-open', - }, - }, - ], - }, - ]; - - const activeView = computed(() => { - return VIEWS.find((v) => v.value === nodeCreatorStore.selectedView) || VIEWS[0]; - }); - - return { - activeView, - }; -}; diff --git a/packages/editor-ui/src/components/Node/NodeCreator/utils.ts b/packages/editor-ui/src/components/Node/NodeCreator/utils.ts new file mode 100644 index 0000000000000..fc00aef0cc6fd --- /dev/null +++ b/packages/editor-ui/src/components/Node/NodeCreator/utils.ts @@ -0,0 +1,73 @@ +import type { + NodeCreateElement, + ActionCreateElement, + SubcategorizedNodeTypes, + SimplifiedNodeType, + INodeCreateElement, +} from '@/Interface'; +import { CORE_NODES_CATEGORY, DEFAULT_SUBCATEGORY } from '@/constants'; +import { v4 as uuidv4 } from 'uuid'; +import { sublimeSearch } from '@/utils'; + +export function transformNodeType( + node: SimplifiedNodeType, + subcategory?: string, + type: 'node' | 'action' = 'node', +): NodeCreateElement | ActionCreateElement { + const createElement = { + uuid: uuidv4(), + key: node.name, + subcategory: + subcategory ?? node.codex?.subcategories?.[CORE_NODES_CATEGORY]?.[0] ?? DEFAULT_SUBCATEGORY, + properties: { + ...node, + }, + type, + }; + + return type === 'action' + ? (createElement as ActionCreateElement) + : (createElement as NodeCreateElement); +} + +export function subcategorizeItems(items: SimplifiedNodeType[]) { + return items.reduce((acc: SubcategorizedNodeTypes, item) => { + // Only Core Nodes subcategories are valid, others are uncategorized + const isCoreNodesCategory = item.codex?.categories?.includes(CORE_NODES_CATEGORY); + const subcategories = isCoreNodesCategory + ? item?.codex?.subcategories?.[CORE_NODES_CATEGORY] ?? [] + : [DEFAULT_SUBCATEGORY]; + + subcategories.forEach((subcategory: string) => { + if (!acc[subcategory]) { + acc[subcategory] = []; + } + acc[subcategory].push(transformNodeType(item, subcategory)); + }); + + return acc; + }, {}); +} + +export function sortNodeCreateElements(nodes: INodeCreateElement[]) { + return nodes.sort((a, b) => { + if (a.type !== 'node' || b.type !== 'node') return -1; + const displayNameA = a.properties?.displayName?.toLowerCase() || a.key; + const displayNameB = b.properties?.displayName?.toLowerCase() || b.key; + + return displayNameA.localeCompare(displayNameB, undefined, { sensitivity: 'base' }); + }); +} + +export function searchNodes(searchFilter: string, items: INodeCreateElement[]) { + // In order to support the old search we need to remove the 'trigger' part + const trimmedFilter = searchFilter.toLowerCase().replace('trigger', '').trimEnd(); + const result = ( + sublimeSearch<INodeCreateElement>(trimmedFilter, items, [ + { key: 'properties.displayName', weight: 2 }, + { key: 'properties.codex.alias', weight: 1 }, + ]) || [] + ).map(({ item }) => item); + + return result; +} diff --git a/packages/editor-ui/src/components/Node/NodeCreator/viewsData.ts b/packages/editor-ui/src/components/Node/NodeCreator/viewsData.ts new file mode 100644 index 0000000000000..cad4e97a1b01f --- /dev/null +++ b/packages/editor-ui/src/components/Node/NodeCreator/viewsData.ts @@ -0,0 +1,168 @@ +import { + CORE_NODES_CATEGORY, + WEBHOOK_NODE_TYPE, + OTHER_TRIGGER_NODES_SUBCATEGORY, + EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE, + MANUAL_TRIGGER_NODE_TYPE, + SCHEDULE_TRIGGER_NODE_TYPE, + REGULAR_NODE_CREATOR_VIEW, + TRANSFORM_DATA_SUBCATEGORY, + FILES_SUBCATEGORY, + FLOWS_CONTROL_SUBCATEGORY, + HELPERS_SUBCATEGORY, + TRIGGER_NODE_CREATOR_VIEW, + EMAIL_IMAP_NODE_TYPE, + DEFAULT_SUBCATEGORY, +} from '@/constants'; + +export function TriggerView($locale: any) { + return { + value: TRIGGER_NODE_CREATOR_VIEW, + title: $locale.baseText('nodeCreator.triggerHelperPanel.selectATrigger'), + subtitle: $locale.baseText('nodeCreator.triggerHelperPanel.selectATriggerDescription'), + items: [ + { + key: DEFAULT_SUBCATEGORY, + type: 'subcategory', + properties: { + forceIncludeNodes: [WEBHOOK_NODE_TYPE, EMAIL_IMAP_NODE_TYPE], + title: 'App Trigger Nodes', + icon: 'satellite-dish', + }, + }, + { + key: SCHEDULE_TRIGGER_NODE_TYPE, + type: 'node', + category: [CORE_NODES_CATEGORY], + properties: { + group: [], + name: SCHEDULE_TRIGGER_NODE_TYPE, + displayName: $locale.baseText( + 'nodeCreator.triggerHelperPanel.scheduleTriggerDisplayName', + ), + description: $locale.baseText( + 'nodeCreator.triggerHelperPanel.scheduleTriggerDescription', + ), + icon: 'fa:clock', + }, + }, + { + key: WEBHOOK_NODE_TYPE, + type: 'node', + category: [CORE_NODES_CATEGORY], + properties: { + group: [], + name: WEBHOOK_NODE_TYPE, + displayName: $locale.baseText('nodeCreator.triggerHelperPanel.webhookTriggerDisplayName'), + description: $locale.baseText('nodeCreator.triggerHelperPanel.webhookTriggerDescription'), + iconData: { + type: 'file', + icon: 'webhook', + fileBuffer: '/static/webhook-icon.svg', + }, + }, + }, + { + key: MANUAL_TRIGGER_NODE_TYPE, + type: 'node', + category: [CORE_NODES_CATEGORY], + properties: { + group: [], + name: MANUAL_TRIGGER_NODE_TYPE, + displayName: $locale.baseText('nodeCreator.triggerHelperPanel.manualTriggerDisplayName'), + description: $locale.baseText('nodeCreator.triggerHelperPanel.manualTriggerDescription'), + icon: 'fa:mouse-pointer', + }, + }, + { + key: EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE, + type: 'node', + category: [CORE_NODES_CATEGORY], + properties: { + group: [], + name: EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE, + displayName: $locale.baseText( + 'nodeCreator.triggerHelperPanel.workflowTriggerDisplayName', + ), + description: $locale.baseText( + 'nodeCreator.triggerHelperPanel.workflowTriggerDescription', + ), + icon: 'fa:sign-out-alt', + }, + }, + { + type: 'subcategory', + key: OTHER_TRIGGER_NODES_SUBCATEGORY, + category: CORE_NODES_CATEGORY, + properties: { + title: OTHER_TRIGGER_NODES_SUBCATEGORY, + icon: 'folder-open', + }, + }, + ], + }; +} + +export function RegularView($locale: any) { + return { + value: REGULAR_NODE_CREATOR_VIEW, + title: $locale.baseText('nodeCreator.triggerHelperPanel.whatHappensNext'), + items: [ + { + key: DEFAULT_SUBCATEGORY, + type: 'subcategory', + properties: { + title: 'App Regular Nodes', + icon: 'globe', + }, + }, + { + type: 'subcategory', + key: TRANSFORM_DATA_SUBCATEGORY, + category: CORE_NODES_CATEGORY, + properties: { + title: TRANSFORM_DATA_SUBCATEGORY, + icon: 'pen', + }, + }, + { + type: 'subcategory', + key: HELPERS_SUBCATEGORY, + category: CORE_NODES_CATEGORY, + properties: { + title: HELPERS_SUBCATEGORY, + icon: 'toolbox', + }, + }, + { + type: 'subcategory', + key: FLOWS_CONTROL_SUBCATEGORY, + category: CORE_NODES_CATEGORY, + properties: { + title: FLOWS_CONTROL_SUBCATEGORY, + icon: 'code-branch', + }, + }, + { + type: 'subcategory', + key: FILES_SUBCATEGORY, + category: CORE_NODES_CATEGORY, + properties: { + title: FILES_SUBCATEGORY, + icon: 'file-alt', + }, + }, + { + key: TRIGGER_NODE_CREATOR_VIEW, + type: 'view', + properties: { + title: $locale.baseText('nodeCreator.triggerHelperPanel.addAnotherTrigger'), + icon: 'bolt', + description: $locale.baseText( + 'nodeCreator.triggerHelperPanel.addAnotherTriggerDescription', + ), + }, + }, + ], + }; +} diff --git a/packages/editor-ui/src/components/NodeCredentials.vue b/packages/editor-ui/src/components/NodeCredentials.vue index cb1a1621ce805..134a1d64d36d4 100644 --- a/packages/editor-ui/src/components/NodeCredentials.vue +++ b/packages/editor-ui/src/components/NodeCredentials.vue @@ -97,15 +97,14 @@ </template> <script lang="ts"> -import { PropType } from 'vue'; -import { restApi } from '@/mixins/restApi'; -import { +import type { PropType } from 'vue'; +import type { ICredentialsResponse, INodeUi, INodeUpdatePropertiesInformation, IUser, } from '@/Interface'; -import { +import type { ICredentialType, INodeCredentialDescription, INodeCredentialsDetails, @@ -142,7 +141,7 @@ interface CredentialDropdownOption extends ICredentialsResponse { typeDisplayName: string; } -export default mixins(genericHelpers, nodeHelpers, restApi, showMessage).extend({ +export default mixins(genericHelpers, nodeHelpers, showMessage).extend({ name: 'NodeCredentials', props: { readonly: { diff --git a/packages/editor-ui/src/components/NodeDetailsView.vue b/packages/editor-ui/src/components/NodeDetailsView.vue index 5dd3ed32fd800..743a387808647 100644 --- a/packages/editor-ui/src/components/NodeDetailsView.vue +++ b/packages/editor-ui/src/components/NodeDetailsView.vue @@ -41,6 +41,7 @@ :hideInputAndOutput="activeNodeType === null" :position="isTriggerNode && !showTriggerPanel ? 0 : undefined" :isDraggable="!isTriggerNode" + :hasDoubleWidth="activeNodeType?.parameterPane === 'wide'" :nodeType="activeNodeType" @close="close" @init="onPanelsInit" @@ -124,15 +125,15 @@ </template> <script lang="ts"> -import { +import type { INodeConnections, INodeTypeDescription, IRunData, IRunExecutionData, Workflow, - jsonParse, } from 'n8n-workflow'; -import { IExecutionResponse, INodeUi, IUpdateInformation, TargetItem } from '@/Interface'; +import { jsonParse } from 'n8n-workflow'; +import type { IExecutionResponse, INodeUi, IUpdateInformation, TargetItem } from '@/Interface'; import { externalHooks } from '@/mixins/externalHooks'; import { nodeHelpers } from '@/mixins/nodeHelpers'; diff --git a/packages/editor-ui/src/components/NodeExecuteButton.vue b/packages/editor-ui/src/components/NodeExecuteButton.vue index 167872d61f20f..bd43cc2bc889a 100644 --- a/packages/editor-ui/src/components/NodeExecuteButton.vue +++ b/packages/editor-ui/src/components/NodeExecuteButton.vue @@ -19,8 +19,8 @@ <script lang="ts"> import { WEBHOOK_NODE_TYPE, MANUAL_TRIGGER_NODE_TYPE } from '@/constants'; -import { INodeUi } from '@/Interface'; -import { INodeTypeDescription } from 'n8n-workflow'; +import type { INodeUi } from '@/Interface'; +import type { INodeTypeDescription } from 'n8n-workflow'; import mixins from 'vue-typed-mixins'; import { workflowRun } from '@/mixins/workflowRun'; import { pinData } from '@/mixins/pinData'; @@ -171,7 +171,7 @@ export default mixins(workflowRun, pinData).extend({ methods: { async stopWaitingForWebhook() { try { - await this.restApi().removeTestWebhook(this.workflowsStore.workflowId); + await this.workflowsStore.removeTestWebhook(this.workflowsStore.workflowId); } catch (error) { this.$showError(error, this.$locale.baseText('ndv.execute.stopWaitingForWebhook.error')); return; diff --git a/packages/editor-ui/src/components/NodeIcon.vue b/packages/editor-ui/src/components/NodeIcon.vue index 36c1a2b77e630..17428e4430dca 100644 --- a/packages/editor-ui/src/components/NodeIcon.vue +++ b/packages/editor-ui/src/components/NodeIcon.vue @@ -14,11 +14,11 @@ </template> <script lang="ts"> -import { IVersionNode } from '@/Interface'; +import type { IVersionNode } from '@/Interface'; import { useRootStore } from '@/stores/n8nRootStore'; -import { INodeTypeDescription } from 'n8n-workflow'; +import type { INodeTypeDescription } from 'n8n-workflow'; import { mapStores } from 'pinia'; -import Vue from 'vue'; +import { defineComponent } from 'vue'; interface NodeIconSource { path?: string; @@ -26,7 +26,7 @@ interface NodeIconSource { icon?: string; } -export default Vue.extend({ +export default defineComponent({ name: 'NodeIcon', props: { nodeType: {}, diff --git a/packages/editor-ui/src/components/NodeList.vue b/packages/editor-ui/src/components/NodeList.vue index b041f21d525ce..39254c975e4cb 100644 --- a/packages/editor-ui/src/components/NodeList.vue +++ b/packages/editor-ui/src/components/NodeList.vue @@ -15,7 +15,7 @@ <script lang="ts"> import NodeIcon from '@/components/NodeIcon.vue'; import { genericHelpers } from '@/mixins/genericHelpers'; -import { ITemplatesNode } from '@/Interface'; +import type { ITemplatesNode } from '@/Interface'; import mixins from 'vue-typed-mixins'; import { filterTemplateNodes } from '@/utils'; export default mixins(genericHelpers).extend({ diff --git a/packages/editor-ui/src/components/NodeSettings.vue b/packages/editor-ui/src/components/NodeSettings.vue index 65e678568aef9..240f339226342 100644 --- a/packages/editor-ui/src/components/NodeSettings.vue +++ b/packages/editor-ui/src/components/NodeSettings.vue @@ -157,22 +157,20 @@ </template> <script lang="ts"> -import Vue, { PropType } from 'vue'; -import { +import type { PropType } from 'vue'; +import Vue from 'vue'; +import type { INodeTypeDescription, INodeParameters, INodeProperties, - NodeHelpers, NodeParameterValue, - deepCopy, } from 'n8n-workflow'; -import { - ICredentialsResponse, +import { NodeHelpers, deepCopy } from 'n8n-workflow'; +import type { INodeUi, INodeUpdatePropertiesInformation, IUpdateInformation, IUsedCredential, - IUser, } from '@/Interface'; import { @@ -205,7 +203,7 @@ import { useHistoryStore } from '@/stores/history'; import { RenameNodeCommand } from '@/models/history'; import useWorkflowsEEStore from '@/stores/workflows.ee'; import { useCredentialsStore } from '@/stores/credentials'; -import { EventBus } from '@/event-bus'; +import type { EventBus } from '@/event-bus'; export default mixins(externalHooks, nodeHelpers).extend({ name: 'NodeSettings', diff --git a/packages/editor-ui/src/components/NodeSettingsTabs.vue b/packages/editor-ui/src/components/NodeSettingsTabs.vue index 977c9b98e5450..737a4527f0414 100644 --- a/packages/editor-ui/src/components/NodeSettingsTabs.vue +++ b/packages/editor-ui/src/components/NodeSettingsTabs.vue @@ -9,10 +9,10 @@ import { COMMUNITY_NODES_INSTALLATION_DOCS_URL, NPM_PACKAGE_DOCS_BASE_URL, } from '@/constants'; -import { INodeUi, ITab } from '@/Interface'; +import type { INodeUi, ITab } from '@/Interface'; import { useNDVStore } from '@/stores/ndv'; import { useWorkflowsStore } from '@/stores/workflows'; -import { INodeTypeDescription } from 'n8n-workflow'; +import type { INodeTypeDescription } from 'n8n-workflow'; import { mapStores } from 'pinia'; import mixins from 'vue-typed-mixins'; diff --git a/packages/editor-ui/src/components/NodeTitle.vue b/packages/editor-ui/src/components/NodeTitle.vue index a5a045b6be5e2..87b9c352c666e 100644 --- a/packages/editor-ui/src/components/NodeTitle.vue +++ b/packages/editor-ui/src/components/NodeTitle.vue @@ -40,9 +40,9 @@ </template> <script lang="ts"> -import Vue from 'vue'; +import { defineComponent } from 'vue'; -export default Vue.extend({ +export default defineComponent({ name: 'NodeTitle', props: { value: { @@ -70,9 +70,9 @@ export default Vue.extend({ this.newName = this.value; this.editName = true; this.$nextTick(() => { - const input = this.$refs.input; - if (input) { - (input as HTMLInputElement).focus(); + const inputRef = this.$refs.input as HTMLInputElement | undefined; + if (inputRef) { + inputRef.focus(); } }); }, diff --git a/packages/editor-ui/src/components/NodeWebhooks.vue b/packages/editor-ui/src/components/NodeWebhooks.vue index f6d8d34592cdd..9e50397f45529 100644 --- a/packages/editor-ui/src/components/NodeWebhooks.vue +++ b/packages/editor-ui/src/components/NodeWebhooks.vue @@ -58,7 +58,7 @@ </template> <script lang="ts"> -import { INodeTypeDescription, IWebhookDescription } from 'n8n-workflow'; +import type { INodeTypeDescription, IWebhookDescription } from 'n8n-workflow'; import { WEBHOOK_NODE_TYPE } from '@/constants'; import { copyPaste } from '@/mixins/copyPaste'; diff --git a/packages/editor-ui/src/components/OutputPanel.vue b/packages/editor-ui/src/components/OutputPanel.vue index 3810071352a47..e303d1fb51a0e 100644 --- a/packages/editor-ui/src/components/OutputPanel.vue +++ b/packages/editor-ui/src/components/OutputPanel.vue @@ -80,7 +80,7 @@ </n8n-text> </template> - <template #recovered-artifical-output-data> + <template #recovered-artificial-output-data> <div :class="$style.recoveredOutputData"> <n8n-text tag="div" :bold="true" color="text-dark" size="large">{{ $locale.baseText('executionDetails.executionFailed.recoveredNodeTitle') @@ -98,10 +98,9 @@ </template> <script lang="ts"> -import { IExecutionResponse, INodeUi } from '@/Interface'; -import { INodeTypeDescription, IRunData, IRunExecutionData, ITaskData } from 'n8n-workflow'; -import Vue from 'vue'; -import RunData, { EnterEditModeArgs } from './RunData.vue'; +import type { IExecutionResponse, INodeUi } from '@/Interface'; +import type { INodeTypeDescription, IRunData, IRunExecutionData, ITaskData } from 'n8n-workflow'; +import RunData from './RunData.vue'; import RunInfo from './RunInfo.vue'; import { pinData } from '@/mixins/pinData'; import mixins from 'vue-typed-mixins'; @@ -111,7 +110,7 @@ import { useWorkflowsStore } from '@/stores/workflows'; import { useNDVStore } from '@/stores/ndv'; import { useNodeTypesStore } from '@/stores/nodeTypes'; -type RunDataRef = Vue & { enterEditMode: (args: EnterEditModeArgs) => void }; +type RunDataRef = InstanceType<typeof RunData>; export default mixins(pinData).extend({ name: 'OutputPanel', @@ -242,8 +241,9 @@ export default mixins(pinData).extend({ }, methods: { insertTestData() { - if (this.$refs.runData) { - (this.$refs.runData as RunDataRef).enterEditMode({ + const runDataRef = this.$refs.runData as RunDataRef | undefined; + if (runDataRef) { + runDataRef.enterEditMode({ origin: 'insertTestDataLink', }); diff --git a/packages/editor-ui/src/components/PageContentWrapper.vue b/packages/editor-ui/src/components/PageContentWrapper.vue index 7a70103c6cb87..38a7917aafdeb 100644 --- a/packages/editor-ui/src/components/PageContentWrapper.vue +++ b/packages/editor-ui/src/components/PageContentWrapper.vue @@ -9,9 +9,9 @@ </template> <script lang="ts"> -import Vue from 'vue'; +import { defineComponent } from 'vue'; -export default Vue.extend({ +export default defineComponent({ name: 'PageContentWrapper', }); </script> diff --git a/packages/editor-ui/src/components/PanelDragButton.vue b/packages/editor-ui/src/components/PanelDragButton.vue index 6413ff061e9e1..58acc80d9f09f 100644 --- a/packages/editor-ui/src/components/PanelDragButton.vue +++ b/packages/editor-ui/src/components/PanelDragButton.vue @@ -42,10 +42,10 @@ </template> <script lang="ts"> -import Vue from 'vue'; +import { defineComponent } from 'vue'; import Draggable from './Draggable.vue'; -export default Vue.extend({ +export default defineComponent({ components: { Draggable, }, diff --git a/packages/editor-ui/src/components/ParameterInput.vue b/packages/editor-ui/src/components/ParameterInput.vue index 0cd176a07c852..40f98c4e5b0a5 100644 --- a/packages/editor-ui/src/components/ParameterInput.vue +++ b/packages/editor-ui/src/components/ParameterInput.vue @@ -18,6 +18,7 @@ ref="resourceLocator" :parameter="parameter" :value="value" + :dependentParametersValues="dependentParametersValues" :displayTitle="displayTitle" :expressionDisplayValue="expressionDisplayValue" :expressionComputedValue="expressionEvaluated" @@ -51,17 +52,28 @@ remoteParameterOptionsLoadingIssues !== null " > - <code-edit + <el-dialog v-if="codeEditDialogVisible" - :value="value" - :parameter="parameter" - :type="editorType" - :codeAutocomplete="codeAutocomplete" - :path="path" - :readonly="isReadOnly" - @closeDialog="closeCodeEditDialog" - @valueChanged="expressionUpdated" - ></code-edit> + visible + append-to-body + :close-on-click-modal="false" + width="80%" + :title="`${$locale.baseText('codeEdit.edit')} ${$locale + .nodeText() + .inputLabelDisplayName(parameter, path)}`" + :before-close="closeCodeEditDialog" + > + <div class="ignore-key-press"> + <code-node-editor + :value="value" + :defaultValue="parameter.default" + :language="editorLanguage" + :isReadOnly="isReadOnly" + @valueChanged="expressionUpdated" + /> + </div> + </el-dialog> + <text-edit :dialogVisible="textEditDialogVisible" :value="value" @@ -73,15 +85,17 @@ ></text-edit> <code-node-editor - v-if="getArgument('editor') === 'codeNodeEditor' && isCodeNode(node)" + v-if="editorType === 'codeNodeEditor' && isCodeNode(node)" :mode="node.parameters.mode" - :jsCode="node.parameters.jsCode" + :value="node.parameters.jsCode" + :defaultValue="parameter.default" + :language="editorLanguage" :isReadOnly="isReadOnly" @valueChanged="valueChangedDebounced" /> <html-editor - v-else-if="getArgument('editor') === 'htmlEditor'" + v-else-if="editorType === 'htmlEditor'" :html="node.parameters.html" :isReadOnly="isReadOnly" :rows="getArgument('rows')" @@ -90,18 +104,25 @@ @valueChanged="valueChangedDebounced" /> + <sql-editor + v-else-if="editorType === 'sqlEditor'" + :query="node.parameters.query" + :dialect="getArgument('sqlDialect')" + :isReadOnly="isReadOnly" + @valueChanged="valueChangedDebounced" + /> + <div - v-else-if="isEditor === true" - class="code-edit clickable ph-no-capture" + v-else-if="editorType" + class="readonly-code clickable ph-no-capture" @click="displayEditDialog()" > - <prism-editor + <code-node-editor v-if="!codeEditDialogVisible" - :lineNumbers="true" - :readonly="true" - :code="displayValue" - language="js" - ></prism-editor> + :value="value" + :language="editorLanguage" + :isReadOnly="true" + /> </div> <n8n-input @@ -329,10 +350,8 @@ import { get } from 'lodash-es'; -import { INodeUi, INodeUpdatePropertiesInformation } from '@/Interface'; -import { - NodeHelpers, - NodeParameterValue, +import type { INodeUi, INodeUpdatePropertiesInformation } from '@/Interface'; +import type { ILoadOptions, INodeParameters, INodePropertyOptions, @@ -340,9 +359,11 @@ import { INodeProperties, INodePropertyCollection, NodeParameterValueType, + EditorType, + CodeNodeEditorLanguage, } from 'n8n-workflow'; +import { NodeHelpers } from 'n8n-workflow'; -import CodeEdit from '@/components/CodeEdit.vue'; import CredentialsSelect from '@/components/CredentialsSelect.vue'; import ImportParameter from '@/components/ImportParameter.vue'; import ExpressionEdit from '@/components/ExpressionEdit.vue'; @@ -352,11 +373,10 @@ import ParameterOptions from '@/components/ParameterOptions.vue'; import ParameterIssues from '@/components/ParameterIssues.vue'; import ResourceLocator from '@/components/ResourceLocator/ResourceLocator.vue'; import ExpressionParameterInput from '@/components/ExpressionParameterInput.vue'; -// @ts-ignore -import PrismEditor from 'vue-prism-editor'; import TextEdit from '@/components/TextEdit.vue'; import CodeNodeEditor from '@/components/CodeNodeEditor/CodeNodeEditor.vue'; import HtmlEditor from '@/components/HtmlEditor/HtmlEditor.vue'; +import SqlEditor from '@/components/SqlEditor/SqlEditor.vue'; import { externalHooks } from '@/mixins/externalHooks'; import { nodeHelpers } from '@/mixins/nodeHelpers'; import { showMessage } from '@/mixins/showMessage'; @@ -364,9 +384,8 @@ import { workflowHelpers } from '@/mixins/workflowHelpers'; import { hasExpressionMapping, isValueExpression, isResourceLocatorValue } from '@/utils'; import mixins from 'vue-typed-mixins'; -import { CUSTOM_API_CALL_KEY, HTML_NODE_TYPE } from '@/constants'; -import { CODE_NODE_TYPE } from '@/constants'; -import { PropType } from 'vue'; +import { CODE_NODE_TYPE, CUSTOM_API_CALL_KEY, HTML_NODE_TYPE } from '@/constants'; +import type { PropType } from 'vue'; import { debounceHelper } from '@/mixins/debounce'; import { mapStores } from 'pinia'; import { useWorkflowsStore } from '@/stores/workflows'; @@ -374,6 +393,9 @@ import { useNDVStore } from '@/stores/ndv'; import { useNodeTypesStore } from '@/stores/nodeTypes'; import { useCredentialsStore } from '@/stores/credentials'; import { htmlEditorEventBus } from '@/event-bus'; +import Vue from 'vue'; + +type ResourceLocatorRef = InstanceType<typeof ResourceLocator>; export default mixins( externalHooks, @@ -384,14 +406,13 @@ export default mixins( ).extend({ name: 'parameter-input', components: { - CodeEdit, CodeNodeEditor, HtmlEditor, + SqlEditor, ExpressionEdit, ExpressionParameterInput, NodeCredentials, CredentialsSelect, - PrismEditor, ScopesNotice, ParameterOptions, ParameterIssues, @@ -553,8 +574,8 @@ export default mixins( return null; } }, - node(): INodeUi | null { - return this.ndvStore.activeNode; + node(): INodeUi { + return this.ndvStore.activeNode!; }, displayTitle(): string { const interpolation = { interpolate: { shortPath: this.shortPath } }; @@ -635,7 +656,7 @@ export default mixins( return 'textarea'; } - if (this.parameter.typeOptions && this.parameter.typeOptions.editor === 'code') { + if (this.editorType === 'code') { return 'textarea'; } @@ -718,11 +739,12 @@ export default mixins( return []; }, - isEditor(): boolean { - return ['code', 'json'].includes(this.editorType); + editorType(): EditorType { + return this.getArgument('editor') as EditorType; }, - editorType(): string { - return this.getArgument('editor') as string; + editorLanguage(): CodeNodeEditorLanguage { + if (this.editorType === 'json' || this.parameter.type === 'json') return 'json'; + return (this.getArgument('editorLanguage') as CodeNodeEditorLanguage) ?? 'javaScript'; }, parameterOptions(): | Array<INodePropertyOptions | INodeProperties | INodePropertyCollection> @@ -906,22 +928,14 @@ export default mixins( this.textEditDialogVisible = false; }, displayEditDialog() { - if (this.isEditor) { + if (this.editorType) { this.codeEditDialogVisible = true; } else { this.textEditDialogVisible = true; } }, getArgument(argumentName: string): string | number | boolean | undefined { - if (this.parameter.typeOptions === undefined) { - return undefined; - } - - if (this.parameter.typeOptions[argumentName] === undefined) { - return undefined; - } - - return this.parameter.typeOptions[argumentName]; + return this.parameter.typeOptions?.[argumentName]; }, expressionUpdated(value: string) { const val: NodeParameterValueType = this.isResourceLocatorParameter @@ -958,8 +972,7 @@ export default mixins( this.nodeName = this.node.name; } - // Set focus on field - setTimeout(() => { + Vue.nextTick(() => { // @ts-ignore if (this.$refs.inputField?.focus && this.$refs.inputField?.$el) { // @ts-ignore @@ -1099,10 +1112,9 @@ export default mixins( } } else if (command === 'refreshOptions') { if (this.isResourceLocatorParameter) { - const resourceLocator = this.$refs.resourceLocator; - if (resourceLocator) { - (resourceLocator as Vue).$emit('refreshList'); - } + const resourceLocatorRef = this.$refs.resourceLocator as ResourceLocatorRef | undefined; + + resourceLocatorRef?.$emit('refreshList'); } this.loadRemoteParameterOptions(); } else if (command === 'formatHtml') { @@ -1167,32 +1179,6 @@ export default mixins( }, { deep: true, immediate: true }, ); - - // Reload function on change element from - // displayOptions.typeOptions.reloadOnChange parameters - if (this.parameter.typeOptions && this.parameter.typeOptions.reloadOnChange) { - // Get all parameter in reloadOnChange property - // This reload when parameters in reloadOnChange is updated - const parametersOnChange: string[] = this.parameter.typeOptions.reloadOnChange; - for (let i = 0; i < parametersOnChange.length; i++) { - const parameter = parametersOnChange[i] as string; - if (parameter in this.node.parameters) { - this.$watch( - () => { - if (this.node && this.node.parameters && this.node.parameters[parameter]) { - return this.node.parameters![parameter]; - } else { - return null; - } - }, - () => { - this.loadRemoteParameterOptions(); - }, - { deep: true, immediate: true }, - ); - } - } - } } this.$externalHooks().run('parameterInput.mount', { @@ -1204,7 +1190,7 @@ export default mixins( </script> <style scoped lang="scss"> -.code-edit { +.readonly-code { font-size: var(--font-size-xs); } diff --git a/packages/editor-ui/src/components/ParameterInputExpanded.vue b/packages/editor-ui/src/components/ParameterInputExpanded.vue index bba92cbbfde30..8f427d8c56af4 100644 --- a/packages/editor-ui/src/components/ParameterInputExpanded.vue +++ b/packages/editor-ui/src/components/ParameterInputExpanded.vue @@ -56,16 +56,19 @@ </template> <script lang="ts"> -import { IUpdateInformation } from '@/Interface'; +import type { IUpdateInformation } from '@/Interface'; import ParameterOptions from './ParameterOptions.vue'; -import Vue, { PropType } from 'vue'; +import { defineComponent } from 'vue'; +import type { PropType } from 'vue'; import ParameterInputWrapper from './ParameterInputWrapper.vue'; import { isValueExpression } from '@/utils'; -import { INodeParameterResourceLocator, INodeProperties, IParameterLabel } from 'n8n-workflow'; +import type { INodeParameterResourceLocator, INodeProperties, IParameterLabel } from 'n8n-workflow'; import { mapStores } from 'pinia'; import { useWorkflowsStore } from '@/stores/workflows'; -export default Vue.extend({ +type ParamRef = InstanceType<typeof ParameterInputWrapper>; + +export default defineComponent({ name: 'parameter-input-expanded', components: { ParameterOptions, @@ -145,7 +148,7 @@ export default Vue.extend({ }, optionSelected(command: string) { if (this.$refs.param) { - (this.$refs.param as Vue).$emit('optionSelected', command); + (this.$refs.param as ParamRef).$emit('optionSelected', command); } }, valueChanged(parameterData: IUpdateInformation) { diff --git a/packages/editor-ui/src/components/ParameterInputFull.vue b/packages/editor-ui/src/components/ParameterInputFull.vue index b884d76b12808..cc337d5f852f7 100644 --- a/packages/editor-ui/src/components/ParameterInputFull.vue +++ b/packages/editor-ui/src/components/ParameterInputFull.vue @@ -69,9 +69,9 @@ </template> <script lang="ts"> -import Vue, { PropType } from 'vue'; +import type { PropType } from 'vue'; -import { IN8nButton, INodeUi, IRunDataDisplayMode, IUpdateInformation } from '@/Interface'; +import type { IN8nButton, INodeUi, IRunDataDisplayMode, IUpdateInformation } from '@/Interface'; import ParameterOptions from '@/components/ParameterOptions.vue'; import DraggableTarget from '@/components/DraggableTarget.vue'; @@ -84,14 +84,21 @@ import { isValueExpression, } from '@/utils'; import ParameterInputWrapper from '@/components/ParameterInputWrapper.vue'; -import { INodeParameters, INodeProperties, INodePropertyMode, IParameterLabel } from 'n8n-workflow'; -import { BaseTextKey } from '@/plugins/i18n'; +import type { + INodeParameters, + INodeProperties, + INodePropertyMode, + IParameterLabel, +} from 'n8n-workflow'; +import type { BaseTextKey } from '@/plugins/i18n'; import { mapStores } from 'pinia'; import { useNDVStore } from '@/stores/ndv'; import { useSegment } from '@/stores/segment'; import { externalHooks } from '@/mixins/externalHooks'; import { getMappedResult } from '../utils/mappingUtils'; +type ParamterInputWrapperRef = InstanceType<typeof ParameterInputWrapper>; + const DISPLAY_MODES_WITH_DATA_MAPPING = ['table', 'json', 'schema']; export default mixins(showMessage, externalHooks).extend({ @@ -219,18 +226,17 @@ export default mixins(showMessage, externalHooks).extend({ this.menuExpanded = expanded; }, optionSelected(command: string) { - if (this.$refs.param) { - (this.$refs.param as Vue).$emit('optionSelected', command); - } + const paramRef = this.$refs.param as ParamterInputWrapperRef | undefined; + paramRef?.$emit('optionSelected', command); }, valueChanged(parameterData: IUpdateInformation) { this.$emit('valueChanged', parameterData); }, onTextInput(parameterData: IUpdateInformation) { - const param = this.$refs.param as Vue | undefined; + const paramRef = this.$refs.param as ParamterInputWrapperRef | undefined; if (isValueExpression(this.parameter, parameterData.value)) { - param?.$emit('optionSelected', 'addExpression'); + paramRef?.$emit('optionSelected', 'addExpression'); } }, onDrop(newParamValue: string) { diff --git a/packages/editor-ui/src/components/ParameterInputHint.vue b/packages/editor-ui/src/components/ParameterInputHint.vue index f0a39b4e0a1cd..d683e23537f60 100644 --- a/packages/editor-ui/src/components/ParameterInputHint.vue +++ b/packages/editor-ui/src/components/ParameterInputHint.vue @@ -1,11 +1,6 @@ <template> <n8n-text size="small" color="text-base" tag="div" v-if="hint"> - <div - v-if="!renderHTML" - :class="{ [$style.singleline]: singleLine, [$style.highlight]: highlight }" - > - {{ hint }} - </div> + <div v-if="!renderHTML" :class="classes">{{ hint }}</div> <div v-else ref="hint" @@ -17,9 +12,9 @@ <script lang="ts"> import { sanitizeHtml } from '@/utils'; -import Vue from 'vue'; +import { defineComponent } from 'vue'; -export default Vue.extend({ +export default defineComponent({ name: 'InputHint', props: { hint: { @@ -39,6 +34,15 @@ export default Vue.extend({ methods: { sanitizeHtml, }, + computed: { + classes() { + return { + [this.$style.singleline]: this.singleLine, + [this.$style.highlight]: this.highlight, + [this.$style['preserve-whitespace']]: true, + }; + }, + }, mounted() { if (this.$refs.hint) { (this.$refs.hint as Element).querySelectorAll('a').forEach((a) => (a.target = '_blank')); @@ -56,4 +60,7 @@ export default Vue.extend({ .highlight { color: var(--color-secondary); } +.preserve-whitespace { + white-space: pre; +} </style> diff --git a/packages/editor-ui/src/components/ParameterInputList.vue b/packages/editor-ui/src/components/ParameterInputList.vue index 668ed3588a209..ec0f005191828 100644 --- a/packages/editor-ui/src/components/ParameterInputList.vue +++ b/packages/editor-ui/src/components/ParameterInputList.vue @@ -115,9 +115,10 @@ </template> <script lang="ts"> -import { deepCopy, INodeParameters, INodeProperties, NodeParameterValue } from 'n8n-workflow'; +import type { INodeParameters, INodeProperties, NodeParameterValue } from 'n8n-workflow'; +import { deepCopy } from 'n8n-workflow'; -import { INodeUi, IUpdateInformation } from '@/Interface'; +import type { INodeUi, IUpdateInformation } from '@/Interface'; import MultipleParameter from '@/components/MultipleParameter.vue'; import { workflowHelpers } from '@/mixins/workflowHelpers'; @@ -127,7 +128,7 @@ import ImportParameter from '@/components/ImportParameter.vue'; import { get, set } from 'lodash-es'; import mixins from 'vue-typed-mixins'; -import { Component, PropType } from 'vue'; +import type { Component, PropType } from 'vue'; import { mapStores } from 'pinia'; import { useNDVStore } from '@/stores/ndv'; import { useNodeTypesStore } from '@/stores/nodeTypes'; diff --git a/packages/editor-ui/src/components/ParameterInputWrapper.vue b/packages/editor-ui/src/components/ParameterInputWrapper.vue index d1f2c73ebb164..f93970d00eea4 100644 --- a/packages/editor-ui/src/components/ParameterInputWrapper.vue +++ b/packages/editor-ui/src/components/ParameterInputWrapper.vue @@ -28,7 +28,7 @@ :class="$style.hint" data-test-id="parameter-expression-preview" class="ph-no-capture" - :highlight="!!(expressionOutput && targetItem)" + :highlight="!!(expressionOutput && targetItem) && isInputParentOfActiveNode" :hint="expressionOutput" :singleLine="true" /> @@ -42,26 +42,27 @@ </template> <script lang="ts"> -import Vue, { PropType } from 'vue'; +import type { PropType } from 'vue'; import ParameterInput from '@/components/ParameterInput.vue'; import InputHint from './ParameterInputHint.vue'; import mixins from 'vue-typed-mixins'; import { showMessage } from '@/mixins/showMessage'; -import { +import type { INodeProperties, INodePropertyMode, - IRunData, - isResourceLocatorValue, NodeParameterValue, NodeParameterValueType, } from 'n8n-workflow'; -import { INodeUi, IUpdateInformation, TargetItem } from '@/Interface'; +import { isResourceLocatorValue } from 'n8n-workflow'; +import type { INodeUi, IUpdateInformation, TargetItem } from '@/Interface'; import { workflowHelpers } from '@/mixins/workflowHelpers'; import { isValueExpression } from '@/utils'; import { mapStores } from 'pinia'; import { useNDVStore } from '@/stores/ndv'; +type ParamRef = InstanceType<typeof ParameterInput>; + export default mixins(showMessage, workflowHelpers).extend({ name: 'parameter-input-wrapper', components: { @@ -153,31 +154,35 @@ export default mixins(showMessage, workflowHelpers).extend({ targetItem(): TargetItem | null { return this.ndvStore.hoveringItem; }, + isInputParentOfActiveNode(): boolean { + return this.ndvStore.isInputParentOfActiveNode; + }, expressionValueComputed(): string | null { - const inputNodeName: string | undefined = this.ndvStore.ndvInputNodeName; const value = isResourceLocatorValue(this.value) ? this.value.value : this.value; - if (this.activeNode === null || !this.isValueExpression || typeof value !== 'string') { + if (!this.activeNode || !this.isValueExpression || typeof value !== 'string') { return null; } - const inputRunIndex: number | undefined = this.ndvStore.ndvInputRunIndex; - const inputBranchIndex: number | undefined = this.ndvStore.ndvInputBranchIndex; - let computedValue: NodeParameterValue; try { - const targetItem = this.targetItem ?? undefined; - computedValue = this.resolveExpression(value, undefined, { - targetItem, - inputNodeName, - inputRunIndex, - inputBranchIndex, - }); + let opts; + if (this.ndvStore.isInputParentOfActiveNode) { + opts = { + targetItem: this.targetItem ?? undefined, + inputNodeName: this.ndvStore.ndvInputNodeName, + inputRunIndex: this.ndvStore.ndvInputRunIndex, + inputBranchIndex: this.ndvStore.ndvInputBranchIndex, + }; + } + + computedValue = this.resolveExpression(value, undefined, opts); + if (computedValue === null) { return null; } - if (typeof computedValue === 'string' && computedValue.trim().length === 0) { - computedValue = this.$locale.baseText('parameterInput.emptyString'); + if (typeof computedValue === 'string' && computedValue.length === 0) { + return this.$locale.baseText('parameterInput.emptyString'); } } catch (error) { computedValue = `[${this.$locale.baseText('parameterInput.error')}: ${error.message}]`; @@ -204,9 +209,9 @@ export default mixins(showMessage, workflowHelpers).extend({ this.$emit('drop', data); }, optionSelected(command: string) { - if (this.$refs.param) { - (this.$refs.param as Vue).$emit('optionSelected', command); - } + const paramRef = this.$refs.param as ParamRef | undefined; + + paramRef?.$emit('optionSelected', command); }, onValueChanged(parameterData: IUpdateInformation) { this.$emit('valueChanged', parameterData); diff --git a/packages/editor-ui/src/components/ParameterIssues.vue b/packages/editor-ui/src/components/ParameterIssues.vue index 0cf6a7e907b94..96da86f7b771c 100644 --- a/packages/editor-ui/src/components/ParameterIssues.vue +++ b/packages/editor-ui/src/components/ParameterIssues.vue @@ -10,10 +10,10 @@ </template> <script lang="ts"> -import Vue from 'vue'; +import { defineComponent } from 'vue'; import TitledList from '@/components/TitledList.vue'; -export default Vue.extend({ +export default defineComponent({ name: 'ParameterIssues', components: { TitledList, diff --git a/packages/editor-ui/src/components/ParameterOptions.vue b/packages/editor-ui/src/components/ParameterOptions.vue index abf0c2e2d0143..d7f3d24ac40dd 100644 --- a/packages/editor-ui/src/components/ParameterOptions.vue +++ b/packages/editor-ui/src/components/ParameterOptions.vue @@ -25,11 +25,12 @@ </template> <script lang="ts"> -import { NodeParameterValueType } from 'n8n-workflow'; -import Vue, { PropType } from 'vue'; +import type { NodeParameterValueType } from 'n8n-workflow'; +import { defineComponent } from 'vue'; +import type { PropType } from 'vue'; import { isValueExpression, isResourceLocatorValue } from '@/utils'; -export default Vue.extend({ +export default defineComponent({ name: 'parameter-options', props: { parameter: { @@ -69,11 +70,7 @@ export default Vue.extend({ return false; } - if ( - this.parameter.typeOptions && - this.parameter.typeOptions.editor && - this.parameter.typeOptions.editor === 'codeNodeEditor' - ) { + if (this.parameter.typeOptions?.editor === 'codeNodeEditor') { return false; } diff --git a/packages/editor-ui/src/components/PersonalizationModal.vue b/packages/editor-ui/src/components/PersonalizationModal.vue index 7d38d9de97793..1046bdc2cac0b 100644 --- a/packages/editor-ui/src/components/PersonalizationModal.vue +++ b/packages/editor-ui/src/components/PersonalizationModal.vue @@ -127,9 +127,9 @@ import { import { workflowHelpers } from '@/mixins/workflowHelpers'; import { showMessage } from '@/mixins/showMessage'; import Modal from './Modal.vue'; -import { IFormInputs, IPersonalizationLatestVersion, IUser } from '@/Interface'; +import type { IFormInputs, IPersonalizationLatestVersion, IUser } from '@/Interface'; import { getAccountAge } from '@/utils'; -import { GenericValue } from 'n8n-workflow'; +import type { GenericValue } from 'n8n-workflow'; import { mapStores } from 'pinia'; import { useUIStore } from '@/stores/ui'; import { useSettingsStore } from '@/stores/settings'; diff --git a/packages/editor-ui/src/components/PushConnectionTracker.vue b/packages/editor-ui/src/components/PushConnectionTracker.vue index a6cebe70e79ac..6be7906d253da 100644 --- a/packages/editor-ui/src/components/PushConnectionTracker.vue +++ b/packages/editor-ui/src/components/PushConnectionTracker.vue @@ -18,9 +18,9 @@ <script lang="ts"> import { useRootStore } from '@/stores/n8nRootStore'; import { mapStores } from 'pinia'; -import Vue from 'vue'; +import { defineComponent } from 'vue'; -export default Vue.extend({ +export default defineComponent({ name: 'PushConnectionTracker', computed: { ...mapStores(useRootStore), diff --git a/packages/editor-ui/src/components/ResourceLocator/ResourceLocator.vue b/packages/editor-ui/src/components/ResourceLocator/ResourceLocator.vue index 1b4f3f7c7635d..998fe9c3e3480 100644 --- a/packages/editor-ui/src/components/ResourceLocator/ResourceLocator.vue +++ b/packages/editor-ui/src/components/ResourceLocator/ResourceLocator.vue @@ -1,5 +1,9 @@ <template> - <div class="resource-locator" ref="container"> + <div + class="resource-locator" + ref="container" + :data-test-id="`resource-locator-${parameter.name}`" + > <resource-locator-dropdown :value="value ? value.value : ''" :show="showResourceDropdown" @@ -18,14 +22,17 @@ ref="dropdown" > <template #error> - <div :class="$style.error"> + <div :class="$style.error" data-test-id="rlc-error-container"> <n8n-text color="text-dark" align="center" tag="div"> {{ $locale.baseText('resourceLocator.mode.list.error.title') }} </n8n-text> - <n8n-text size="small" color="text-base" v-if="hasCredential"> + <n8n-text size="small" color="text-base" v-if="hasCredential || credentialsNotSet"> {{ $locale.baseText('resourceLocator.mode.list.error.description.part1') }} - <a @click="openCredential">{{ - $locale.baseText('resourceLocator.mode.list.error.description.part2') + <a v-if="credentialsNotSet" @click="createNewCredential">{{ + $locale.baseText('resourceLocator.mode.list.error.description.part2.noCredentials') + }}</a> + <a v-else-if="hasCredential" @click="openCredential">{{ + $locale.baseText('resourceLocator.mode.list.error.description.part2.hasCredentials') }}</a> </n8n-text> </div> @@ -44,6 +51,7 @@ :disabled="isReadOnly" @change="onModeSelected" :placeholder="$locale.baseText('resourceLocator.modeSelector.placeholder')" + data-test-id="rlc-mode-selector" > <n8n-option v-for="mode in parameter.modes" @@ -61,7 +69,7 @@ </n8n-select> </div> - <div :class="$style.inputContainer"> + <div :class="$style.inputContainer" data-test-id="rlc-input-container"> <draggable-target type="mapping" :disabled="hasOnlyListMode" @@ -98,6 +106,7 @@ :placeholder="inputPlaceholder" type="text" ref="input" + data-test-id="rlc-input" @input="onInputChange" @focus="onInputFocus" @blur="onInputBlur" @@ -135,12 +144,11 @@ <script lang="ts"> import mixins from 'vue-typed-mixins'; -import { +import type { ILoadOptions, INode, INodeCredentials, INodeListSearchItems, - INodeListSearchResult, INodeParameterResourceLocator, INodeParameters, INodeProperties, @@ -152,13 +160,18 @@ import DraggableTarget from '@/components/DraggableTarget.vue'; import ExpressionEdit from '@/components/ExpressionEdit.vue'; import ParameterIssues from '@/components/ParameterIssues.vue'; import ResourceLocatorDropdown from './ResourceLocatorDropdown.vue'; -import Vue, { PropType } from 'vue'; -import { INodeUi, IResourceLocatorReqParams, IResourceLocatorResultExpanded } from '@/Interface'; +import type { PropType } from 'vue'; +import type { IResourceLocatorReqParams, IResourceLocatorResultExpanded } from '@/Interface'; import { debounceHelper } from '@/mixins/debounce'; import stringify from 'fast-json-stable-stringify'; import { workflowHelpers } from '@/mixins/workflowHelpers'; import { nodeHelpers } from '@/mixins/nodeHelpers'; -import { getAppNameFromNodeName, isResourceLocatorValue, hasOnlyListMode } from '@/utils'; +import { + getAppNameFromNodeName, + isResourceLocatorValue, + hasOnlyListMode, + getMainAuthField, +} from '@/utils'; import { mapStores } from 'pinia'; import { useUIStore } from '@/stores/ui'; import { useWorkflowsStore } from '@/stores/workflows'; @@ -166,6 +179,8 @@ import { useRootStore } from '@/stores/n8nRootStore'; import { useNDVStore } from '@/stores/ndv'; import { useNodeTypesStore } from '@/stores/nodeTypes'; +type ResourceLocatorDropdownRef = InstanceType<typeof ResourceLocatorDropdown>; + interface IResourceLocatorQuery { results: INodeListSearchItems[]; nextPageToken: unknown; @@ -203,6 +218,10 @@ export default mixins(debounceHelper, workflowHelpers, nodeHelpers).extend({ type: Array as PropType<string[]>, default: () => [], }, + dependentParametersValues: { + type: [String, null] as PropType<string | null>, + default: null, + }, displayTitle: { type: String, default: '', @@ -281,6 +300,17 @@ export default mixins(debounceHelper, workflowHelpers, nodeHelpers).extend({ } return !!(node && node.credentials && Object.keys(node.credentials).length === 1); }, + credentialsNotSet(): boolean { + const nodeType = this.nodeTypesStore.getNodeType(this.node?.type); + if (nodeType) { + const usesCredentials = + nodeType.credentials !== undefined && nodeType.credentials.length > 0; + if (usesCredentials && !this.node?.credentials) { + return true; + } + } + return false; + }, inputPlaceholder(): string { if (this.currentMode.placeholder) { return this.currentMode.placeholder; @@ -407,9 +437,9 @@ export default mixins(debounceHelper, workflowHelpers, nodeHelpers).extend({ watch: { currentQueryError(curr: boolean, prev: boolean) { if (this.showResourceDropdown && curr && !prev) { - const input = this.$refs.input; - if (input) { - (input as HTMLElement).focus(); + const inputRef = this.$refs.input as HTMLInputElement | undefined; + if (inputRef) { + inputRef.focus(); } } }, @@ -428,6 +458,17 @@ export default mixins(debounceHelper, workflowHelpers, nodeHelpers).extend({ this.$emit('input', { ...this.value, __regex: mode.extractValue.regex }); } }, + dependentParametersValues() { + // Reset value if dependent parameters change + if (this.value && isResourceLocatorValue(this.value) && this.value.value !== '') { + this.$emit('input', { + ...this.value, + cachedResultName: '', + cachedResultUrl: '', + value: '', + }); + } + }, }, mounted() { this.$on('refreshList', this.refreshList); @@ -445,7 +486,7 @@ export default mixins(debounceHelper, workflowHelpers, nodeHelpers).extend({ }, methods: { setWidth() { - const containerRef = this.$refs.container as HTMLElement; + const containerRef = this.$refs.container as HTMLElement | undefined; if (containerRef) { this.width = containerRef?.offsetWidth; } @@ -465,9 +506,9 @@ export default mixins(debounceHelper, workflowHelpers, nodeHelpers).extend({ this.trackEvent('User refreshed resource locator list'); }, onKeyDown(e: MouseEvent) { - const dropdown = this.$refs.dropdown; - if (dropdown && this.showResourceDropdown && !this.isSearchable) { - (dropdown as Vue).$emit('keyDown', e); + const dropdownRef = this.$refs.dropdown as ResourceLocatorDropdownRef | undefined; + if (dropdownRef && this.showResourceDropdown && !this.isSearchable) { + dropdownRef.$emit('keyDown', e); } }, openResource(url: string) { @@ -502,6 +543,18 @@ export default mixins(debounceHelper, workflowHelpers, nodeHelpers).extend({ const id = node.credentials[credentialKey].id; this.uiStore.openExistingCredential(id); }, + createNewCredential(): void { + const nodeType = this.nodeTypesStore.getNodeType(this.node?.type); + if (!nodeType) { + return; + } + const mainAuthType = getMainAuthField(nodeType); + const showAuthSelector = + mainAuthType !== null && + Array.isArray(mainAuthType.options) && + mainAuthType.options?.length > 0; + this.uiStore.openNewCredential('', showAuthSelector); + }, findModeByName(name: string): INodePropertyMode | null { if (this.parameter.modes) { return this.parameter.modes.find((mode: INodePropertyMode) => mode.name === name) || null; diff --git a/packages/editor-ui/src/components/ResourceLocator/ResourceLocatorDropdown.vue b/packages/editor-ui/src/components/ResourceLocator/ResourceLocatorDropdown.vue index c26e8cbda8596..dfee08556181a 100644 --- a/packages/editor-ui/src/components/ResourceLocator/ResourceLocatorDropdown.vue +++ b/packages/editor-ui/src/components/ResourceLocator/ResourceLocatorDropdown.vue @@ -5,6 +5,7 @@ :popper-class="$style.popover" :value="show" trigger="manual" + data-test-id="resource-locator-dropdown" v-click-outside="onClickOutside" > <div :class="$style.messageContainer" v-if="errorView"> @@ -78,13 +79,14 @@ </template> <script lang="ts"> -import { IResourceLocatorResultExpanded } from '@/Interface'; -import Vue, { PropType } from 'vue'; +import type { IResourceLocatorResultExpanded } from '@/Interface'; +import { defineComponent } from 'vue'; +import type { PropType } from 'vue'; const SEARCH_BAR_HEIGHT_PX = 40; const SCROLL_MARGIN_PX = 10; -export default Vue.extend({ +export default defineComponent({ name: 'resource-locator-dropdown', props: { value: { @@ -167,18 +169,21 @@ export default Vue.extend({ window.open(url, '_blank'); }, onKeyDown(e: KeyboardEvent) { - const container = this.$refs.resultsContainer as HTMLElement; + const containerRef = this.$refs.resultsContainer as HTMLElement | undefined; if (e.key === 'ArrowDown') { if (this.hoverIndex < this.sortedResources.length - 1) { this.hoverIndex++; - const items = this.$refs[`item-${this.hoverIndex}`] as HTMLElement[]; - if (container && Array.isArray(items) && items.length === 1) { - const item = items[0]; - if (item.offsetTop + item.clientHeight > container.scrollTop + container.offsetHeight) { - const top = item.offsetTop - container.offsetHeight + item.clientHeight; - container.scrollTo({ top }); + const itemRefs = this.$refs[`item-${this.hoverIndex}`] as HTMLElement[] | undefined; + if (containerRef && Array.isArray(itemRefs) && itemRefs.length === 1) { + const item = itemRefs[0]; + if ( + item.offsetTop + item.clientHeight > + containerRef.scrollTop + containerRef.offsetHeight + ) { + const top = item.offsetTop - containerRef.offsetHeight + item.clientHeight; + containerRef.scrollTo({ top }); } } } @@ -187,11 +192,11 @@ export default Vue.extend({ this.hoverIndex--; const searchOffset = this.filterable ? SEARCH_BAR_HEIGHT_PX : 0; - const items = this.$refs[`item-${this.hoverIndex}`] as HTMLElement[]; - if (container && Array.isArray(items) && items.length === 1) { - const item = items[0]; - if (item.offsetTop <= container.scrollTop + searchOffset) { - container.scrollTo({ top: item.offsetTop - searchOffset }); + const itemRefs = this.$refs[`item-${this.hoverIndex}`] as HTMLElement[] | undefined; + if (containerRef && Array.isArray(itemRefs) && itemRefs.length === 1) { + const item = itemRefs[0]; + if (item.offsetTop <= containerRef.scrollTop + searchOffset) { + containerRef.scrollTo({ top: item.offsetTop - searchOffset }); } } } @@ -225,9 +230,10 @@ export default Vue.extend({ return; } - const container = this.$refs.resultsContainer as HTMLElement; - if (container) { - const diff = container.offsetHeight - (container.scrollHeight - container.scrollTop); + const containerRef = this.$refs.resultsContainer as HTMLElement | undefined; + if (containerRef) { + const diff = + containerRef.offsetHeight - (containerRef.scrollHeight - containerRef.scrollTop); if (diff > -SCROLL_MARGIN_PX && diff < SCROLL_MARGIN_PX) { this.$emit('loadMore'); } diff --git a/packages/editor-ui/src/components/RunData.vue b/packages/editor-ui/src/components/RunData.vue index eb31cf17b13f3..8c61488ff4f62 100644 --- a/packages/editor-ui/src/components/RunData.vue +++ b/packages/editor-ui/src/components/RunData.vue @@ -1,5 +1,5 @@ <template> - <div :class="$style.container"> + <div :class="['run-data', $style.container]"> <n8n-callout v-if="canPinData && hasPinData && !editMode.enabled && !isProductionExecutionPreview" theme="secondary" @@ -121,7 +121,12 @@ </div> </div> - <div :class="$style.runSelector" v-if="maxRunIndex > 0" v-show="!editMode.enabled"> + <div + :class="$style.runSelector" + v-if="maxRunIndex > 0" + v-show="!editMode.enabled" + data-test-id="run-selector" + > <n8n-select size="small" :value="runIndex" @@ -157,7 +162,11 @@ <slot name="run-info"></slot> </div> - <div v-if="maxOutputIndex > 0 && branches.length > 1" :class="$style.tabs"> + <div + v-if="maxOutputIndex > 0 && branches.length > 1" + :class="$style.tabs" + data-test-id="branches" + > <n8n-tabs :value="currentOutputIndex" @input="onBranchChange" :options="branches" /> </div> @@ -181,11 +190,10 @@ <div v-else-if="editMode.enabled" :class="$style.editMode"> <div :class="[$style.editModeBody, 'ignore-key-press']"> - <code-editor + <code-node-editor :value="editMode.value" - :options="{ scrollBeyondLastLine: false }" - type="json" - @input="ndvStore.setOutputPanelEditModeValue($event)" + language="json" + @valueChanged="ndvStore.setOutputPanelEditModeValue($event)" /> </div> <div :class="$style.editModeFooter"> @@ -219,7 +227,7 @@ </div> <div v-else-if="hasNodeRun && isArtificialRecoveredEventItem" :class="$style.center"> - <slot name="recovered-artifical-output-data"></slot> + <slot name="recovered-artificial-output-data"></slot> </div> <div v-else-if="hasNodeRun && hasRunError" :class="$style.stretchVertically"> @@ -421,7 +429,13 @@ </div> <div :class="$style.pagination" - v-if="hasNodeRun && !hasRunError && dataCount > pageSize && !isSchemaView" + v-if=" + hasNodeRun && + !hasRunError && + binaryData.length === 0 && + dataCount > pageSize && + !isSchemaView + " v-show="!editMode.enabled" > <el-pagination @@ -450,10 +464,10 @@ </template> <script lang="ts"> -import { PropType } from 'vue'; +import type { PropType } from 'vue'; import mixins from 'vue-typed-mixins'; import { saveAs } from 'file-saver'; -import { +import type { IBinaryData, IBinaryKeyData, IDataObject, @@ -463,7 +477,7 @@ import { IRunExecutionData, } from 'n8n-workflow'; -import { +import type { IExecutionResponse, INodeUi, INodeUpdatePropertiesInformation, @@ -492,7 +506,7 @@ import { externalHooks } from '@/mixins/externalHooks'; import { genericHelpers } from '@/mixins/genericHelpers'; import { nodeHelpers } from '@/mixins/nodeHelpers'; import { pinData } from '@/mixins/pinData'; -import { CodeEditor } from '@/components/forms'; +import CodeNodeEditor from '@/components/CodeNodeEditor/CodeNodeEditor.vue'; import { dataPinningEventBus } from '@/event-bus'; import { clearJsonKey, executionDataToJson, stringSizeInBytes } from '@/utils'; import { isEmpty } from '@/utils'; @@ -516,7 +530,7 @@ export default mixins(externalHooks, genericHelpers, nodeHelpers, pinData).exten BinaryDataDisplay, NodeErrorView, WarningTooltip, - CodeEditor, + CodeNodeEditor, RunDataTable, RunDataJson, RunDataSchema, @@ -1048,16 +1062,14 @@ export default mixins(externalHooks, genericHelpers, nodeHelpers, pinData).exten return; } - const data = executionDataToJson(this.rawInputData) as INodeExecutionData[]; - - if (!this.isValidPinDataSize(data)) { + if (!this.isValidPinDataSize(this.inputData)) { this.onDataPinningError({ errorType: 'data-too-large', source: 'pin-icon-click' }); return; } this.onDataPinningSuccess({ source: 'pin-icon-click' }); - this.workflowsStore.pinData({ node: this.node, data }); + this.workflowsStore.pinData({ node: this.node, data: this.inputData }); if (this.maxRunIndex > 0) { this.$showToast({ @@ -1137,9 +1149,9 @@ export default mixins(externalHooks, genericHelpers, nodeHelpers, pinData).exten const previous = this.displayMode; this.ndvStore.setPanelDisplayMode({ pane: this.paneType, mode: displayMode }); - const dataContainer = this.$refs.dataContainer; - if (dataContainer) { - const dataDisplay = (dataContainer as Element).children[0]; + const dataContainerRef = this.$refs.dataContainer as Element | undefined; + if (dataContainerRef) { + const dataDisplay = dataContainerRef.children[0]; if (dataDisplay) { dataDisplay.scrollTo(0, 0); @@ -1242,7 +1254,7 @@ export default mixins(externalHooks, genericHelpers, nodeHelpers, pinData).exten const { id, data, fileName, fileExtension, mimeType } = this.binaryData[index][key]; if (id) { - const url = this.restApi().getBinaryUrl(id, 'download', fileName, mimeType); + const url = this.workflowsStore.getBinaryUrl(id, 'download', fileName, mimeType); saveAs(url, [fileName, fileExtension].join('.')); return; } else { @@ -1562,28 +1574,30 @@ export default mixins(externalHooks, genericHelpers, nodeHelpers, pinData).exten } .editMode { - height: calc(100% - var(--spacing-s)); + height: 100%; + max-height: calc(100% - var(--spacing-3xl)); display: flex; flex-direction: column; - justify-content: flex-end; - align-items: flex-end; + justify-content: stretch; padding-left: var(--spacing-s); padding-right: var(--spacing-s); } .editModeBody { flex: 1 1 auto; + max-height: 100%; width: 100%; - height: 100%; - overflow: hidden; + overflow: auto; } .editModeFooter { + flex: 0 1 auto; display: flex; width: 100%; justify-content: space-between; align-items: center; padding-top: var(--spacing-s); + padding-bottom: var(--spacing-s); } .editModeFooterInfotip { @@ -1608,3 +1622,11 @@ export default mixins(externalHooks, genericHelpers, nodeHelpers, pinData).exten border-bottom-left-radius: 0; } </style> + +<style lang="scss" scoped> +.run-data { + .code-node-editor { + height: 100%; + } +} +</style> diff --git a/packages/editor-ui/src/components/RunDataJson.vue b/packages/editor-ui/src/components/RunDataJson.vue index ea6718d2b526f..1ecaf4f97532c 100644 --- a/packages/editor-ui/src/components/RunDataJson.vue +++ b/packages/editor-ui/src/components/RunDataJson.vue @@ -70,19 +70,20 @@ </template> <script lang="ts"> -import { PropType } from 'vue'; +import type { PropType } from 'vue'; import mixins from 'vue-typed-mixins'; import VueJsonPretty from 'vue-json-pretty'; -import { IDataObject, INodeExecutionData } from 'n8n-workflow'; +import type { IDataObject, INodeExecutionData } from 'n8n-workflow'; import Draggable from '@/components/Draggable.vue'; import { executionDataToJson, isString, shorten } from '@/utils'; -import { INodeUi } from '@/Interface'; +import type { INodeUi } from '@/Interface'; import { externalHooks } from '@/mixins/externalHooks'; import { mapStores } from 'pinia'; import { useNDVStore } from '@/stores/ndv'; import MappingPill from './MappingPill.vue'; import { getMappedExpression } from '@/utils/mappingUtils'; import { useWorkflowsStore } from '@/stores/workflows'; +import { nonExistingJsonPath } from '@/components/RunDataJsonActions.vue'; const runDataJsonActions = () => import('@/components/RunDataJsonActions.vue'); @@ -125,7 +126,7 @@ export default mixins(externalHooks).extend({ }, data() { return { - selectedJsonPath: null as null | string, + selectedJsonPath: nonExistingJsonPath, draggingPath: null as null | string, displayMode: 'json', }; diff --git a/packages/editor-ui/src/components/RunDataJsonActions.vue b/packages/editor-ui/src/components/RunDataJsonActions.vue index d682349948403..53231481f1846 100644 --- a/packages/editor-ui/src/components/RunDataJsonActions.vue +++ b/packages/editor-ui/src/components/RunDataJsonActions.vue @@ -1,6 +1,14 @@ <template> <div :class="$style.actionsGroup"> - <el-dropdown trigger="click" @command="handleCopyClick"> + <n8n-icon-button + v-if="noSelection" + :title="$locale.baseText('runData.copyToClipboard')" + icon="copy" + type="tertiary" + :circle="false" + @click="handleCopyClick({ command: 'value' })" + /> + <el-dropdown v-else trigger="click" @command="handleCopyClick"> <span class="el-dropdown-link"> <n8n-icon-button :title="$locale.baseText('runData.copyToClipboard')" @@ -27,11 +35,11 @@ </template> <script lang="ts"> -import { PropType } from 'vue'; +import type { PropType } from 'vue'; import mixins from 'vue-typed-mixins'; import jp from 'jsonpath'; -import { INodeUi } from '@/Interface'; -import { IDataObject } from 'n8n-workflow'; +import type { INodeUi } from '@/Interface'; +import type { IDataObject } from 'n8n-workflow'; import { copyPaste } from '@/mixins/copyPaste'; import { pinData } from '@/mixins/pinData'; import { nodeHelpers } from '@/mixins/nodeHelpers'; @@ -47,7 +55,7 @@ type JsonPathData = { }; // A path that does not exist so that nothing is selected by default -const nonExistingJsonPath = '_!^&*'; +export const nonExistingJsonPath = '_!^&*'; export default mixins(genericHelpers, nodeHelpers, pinData, copyPaste).extend({ name: 'run-data-json-actions', @@ -87,15 +95,17 @@ export default mixins(genericHelpers, nodeHelpers, pinData, copyPaste).extend({ activeNode(): INodeUi | null { return this.ndvStore.activeNode; }, + noSelection() { + return this.selectedJsonPath === nonExistingJsonPath; + }, normalisedJsonPath(): string { - const isNotSelected = this.selectedJsonPath === nonExistingJsonPath; - return isNotSelected ? '[""]' : this.selectedJsonPath; + return this.noSelection ? '[""]' : this.selectedJsonPath; }, }, methods: { getJsonValue(): string { let selectedValue = jp.query(this.jsonData, `$${this.normalisedJsonPath}`)[0]; - if (this.selectedJsonPath === nonExistingJsonPath) { + if (this.noSelection) { if (this.hasPinData) { selectedValue = clearJsonKey(this.pinData as object); } else { diff --git a/packages/editor-ui/src/components/RunDataSchema.vue b/packages/editor-ui/src/components/RunDataSchema.vue index 086b9b0bff91f..329e9151e5e9a 100644 --- a/packages/editor-ui/src/components/RunDataSchema.vue +++ b/packages/editor-ui/src/components/RunDataSchema.vue @@ -1,15 +1,14 @@ <script lang="ts" setup> import { computed, ref } from 'vue'; import { merge } from 'lodash-es'; -import { INodeUi, Schema } from '@/Interface'; +import type { INodeUi, Schema } from '@/Interface'; import RunDataSchemaItem from '@/components/RunDataSchemaItem.vue'; import Draggable from '@/components/Draggable.vue'; import { useNDVStore } from '@/stores/ndv'; import { useWebhooksStore } from '@/stores/webhooks'; -import { runExternalHook } from '@/mixins/externalHooks'; import { telemetry } from '@/plugins/telemetry'; -import { IDataObject } from 'n8n-workflow'; -import { getSchema, isEmpty } from '@/utils'; +import type { IDataObject } from 'n8n-workflow'; +import { getSchema, isEmpty, runExternalHook } from '@/utils'; import { i18n } from '@/plugins/i18n'; import MappingPill from './MappingPill.vue'; diff --git a/packages/editor-ui/src/components/RunDataSchemaItem.vue b/packages/editor-ui/src/components/RunDataSchemaItem.vue index 3af9a04e4ef0b..b9d0ae7d39182 100644 --- a/packages/editor-ui/src/components/RunDataSchemaItem.vue +++ b/packages/editor-ui/src/components/RunDataSchemaItem.vue @@ -1,6 +1,6 @@ <script lang="ts" setup> import { computed } from 'vue'; -import { INodeUi, Schema } from '@/Interface'; +import type { INodeUi, Schema } from '@/Interface'; import { checkExhaustive, shorten } from '@/utils'; import { getMappedExpression } from '@/utils/mappingUtils'; diff --git a/packages/editor-ui/src/components/RunDataTable.vue b/packages/editor-ui/src/components/RunDataTable.vue index 07cd4d134d166..269270b18ef2e 100644 --- a/packages/editor-ui/src/components/RunDataTable.vue +++ b/packages/editor-ui/src/components/RunDataTable.vue @@ -107,6 +107,7 @@ v-for="(row, index1) in tableData.data" :key="index1" :class="{ [$style.hoveringRow]: isHoveringRow(index1) }" + :data-test-id="isHoveringRow(index1) ? 'hovering-item' : undefined" > <td v-for="(data, index2) in row" @@ -161,11 +162,11 @@ <script lang="ts"> /* eslint-disable prefer-spread */ -import { INodeUi, ITableData, NDVState } from '@/Interface'; +import type { INodeUi, ITableData, NDVState } from '@/Interface'; import { getPairedItemId } from '@/utils'; -import Vue, { PropType } from 'vue'; +import type { PropType } from 'vue'; import mixins from 'vue-typed-mixins'; -import { GenericValue, IDataObject, INodeExecutionData } from 'n8n-workflow'; +import type { GenericValue, IDataObject, INodeExecutionData } from 'n8n-workflow'; import Draggable from './Draggable.vue'; import { shorten } from '@/utils'; import { externalHooks } from '@/mixins/externalHooks'; @@ -177,6 +178,8 @@ import { getMappedExpression } from '@/utils/mappingUtils'; const MAX_COLUMNS_LIMIT = 40; +type DraggableRef = InstanceType<typeof Draggable>; + export default mixins(externalHooks).extend({ name: 'run-data-table', components: { Draggable, MappingPill }, @@ -224,7 +227,7 @@ export default mixins(externalHooks).extend({ }, mounted() { if (this.tableData && this.tableData.columns && this.$refs.draggable) { - const tbody = (this.$refs.draggable as Vue).$refs.wrapper as HTMLElement; + const tbody = (this.$refs.draggable as DraggableRef).$refs.wrapper; if (tbody) { this.$emit('mounted', { avgRowHeight: tbody.offsetHeight / this.tableData.data.length, diff --git a/packages/editor-ui/src/components/RunInfo.vue b/packages/editor-ui/src/components/RunInfo.vue index b6e779773d666..e6a0003393bd6 100644 --- a/packages/editor-ui/src/components/RunInfo.vue +++ b/packages/editor-ui/src/components/RunInfo.vue @@ -14,10 +14,10 @@ </template> <script lang="ts"> -import { ITaskData } from 'n8n-workflow'; -import Vue from 'vue'; +import type { ITaskData } from 'n8n-workflow'; +import { defineComponent } from 'vue'; -export default Vue.extend({ +export default defineComponent({ props: { taskData: {}, // ITaskData }, diff --git a/packages/editor-ui/src/components/SaveButton.vue b/packages/editor-ui/src/components/SaveButton.vue index 81f5f5e4bde27..4d78e8844d9fa 100644 --- a/packages/editor-ui/src/components/SaveButton.vue +++ b/packages/editor-ui/src/components/SaveButton.vue @@ -14,9 +14,9 @@ </template> <script lang="ts"> -import Vue from 'vue'; +import { defineComponent } from 'vue'; -export default Vue.extend({ +export default defineComponent({ name: 'SaveButton', props: { saved: { diff --git a/packages/editor-ui/src/components/ScopesNotice.vue b/packages/editor-ui/src/components/ScopesNotice.vue index fbe88e2187483..b7a0ddd0f2c71 100644 --- a/packages/editor-ui/src/components/ScopesNotice.vue +++ b/packages/editor-ui/src/components/ScopesNotice.vue @@ -5,9 +5,9 @@ <script lang="ts"> import { useCredentialsStore } from '@/stores/credentials'; import { mapStores } from 'pinia'; -import Vue from 'vue'; +import { defineComponent } from 'vue'; -export default Vue.extend({ +export default defineComponent({ name: 'ScopesNotice', props: ['activeCredentialType', 'scopes'], computed: { diff --git a/packages/editor-ui/src/components/SettingsLogStreaming/EventDestinationCard.ee.vue b/packages/editor-ui/src/components/SettingsLogStreaming/EventDestinationCard.ee.vue index a66f1dcf3bd1a..d2a4326ac2899 100644 --- a/packages/editor-ui/src/components/SettingsLogStreaming/EventDestinationCard.ee.vue +++ b/packages/editor-ui/src/components/SettingsLogStreaming/EventDestinationCard.ee.vue @@ -51,15 +51,12 @@ import mixins from 'vue-typed-mixins'; import { EnterpriseEditionFeature } from '@/constants'; import { showMessage } from '@/mixins/showMessage'; import { useLogStreamingStore } from '../../stores/logStreamingStore'; -import Vue, { PropType } from 'vue'; +import type { PropType } from 'vue'; import { mapStores } from 'pinia'; -import { - deepCopy, - defaultMessageEventBusDestinationOptions, - MessageEventBusDestinationOptions, -} from 'n8n-workflow'; -import { BaseTextKey } from '../../plugins/i18n'; -import { EventBus } from '@/event-bus'; +import type { MessageEventBusDestinationOptions } from 'n8n-workflow'; +import { deepCopy, defaultMessageEventBusDestinationOptions } from 'n8n-workflow'; +import type { BaseTextKey } from '../../plugins/i18n'; +import type { EventBus } from '@/event-bus'; export const DESTINATION_LIST_ITEM_ACTIONS = { OPEN: 'open', diff --git a/packages/editor-ui/src/components/SettingsLogStreaming/EventDestinationSettingsModal.ee.vue b/packages/editor-ui/src/components/SettingsLogStreaming/EventDestinationSettingsModal.ee.vue index 8a1fc3f125aa1..78e00e080a899 100644 --- a/packages/editor-ui/src/components/SettingsLogStreaming/EventDestinationSettingsModal.ee.vue +++ b/packages/editor-ui/src/components/SettingsLogStreaming/EventDestinationSettingsModal.ee.vue @@ -180,20 +180,23 @@ import { useNDVStore } from '../../stores/ndv'; import { useWorkflowsStore } from '../../stores/workflows'; import ParameterInputList from '@/components/ParameterInputList.vue'; import NodeCredentials from '@/components/NodeCredentials.vue'; -import { IMenuItem, INodeUi, ITab, IUpdateInformation } from '../../Interface'; +import type { IMenuItem, INodeUi, ITab, IUpdateInformation } from '../../Interface'; +import type { + IDataObject, + INodeCredentials, + NodeParameterValue, + MessageEventBusDestinationOptions, +} from 'n8n-workflow'; import { deepCopy, defaultMessageEventBusDestinationOptions, defaultMessageEventBusDestinationWebhookOptions, - IDataObject, - INodeCredentials, - NodeParameterValue, MessageEventBusDestinationTypeNames, - MessageEventBusDestinationOptions, defaultMessageEventBusDestinationSyslogOptions, defaultMessageEventBusDestinationSentryOptions, } from 'n8n-workflow'; -import Vue, { PropType } from 'vue'; +import type { PropType } from 'vue'; +import Vue from 'vue'; import { LOG_STREAM_MODAL_KEY } from '../../constants'; import Modal from '@/components/Modal.vue'; import { showMessage } from '@/mixins/showMessage'; @@ -205,12 +208,13 @@ import { sentryModalDescription, syslogModalDescription, } from './descriptions.ee'; -import { BaseTextKey } from '../../plugins/i18n'; +import type { BaseTextKey } from '../../plugins/i18n'; import InlineNameEdit from '../InlineNameEdit.vue'; import SaveButton from '../SaveButton.vue'; import EventSelection from '@/components/SettingsLogStreaming/EventSelection.ee.vue'; import { Checkbox } from 'element-ui'; -import { createEventBus, EventBus } from '@/event-bus'; +import type { EventBus } from '@/event-bus'; +import { createEventBus } from '@/event-bus'; export default mixins(showMessage).extend({ name: 'event-destination-settings-modal', diff --git a/packages/editor-ui/src/components/SettingsLogStreaming/EventSelection.ee.vue b/packages/editor-ui/src/components/SettingsLogStreaming/EventSelection.ee.vue index a7d0f13b41ed4..0ed8883ef3bae 100644 --- a/packages/editor-ui/src/components/SettingsLogStreaming/EventSelection.ee.vue +++ b/packages/editor-ui/src/components/SettingsLogStreaming/EventSelection.ee.vue @@ -71,7 +71,7 @@ <script lang="ts"> import { Checkbox } from 'element-ui'; import { mapStores } from 'pinia'; -import { BaseTextKey } from '../../plugins/i18n'; +import type { BaseTextKey } from '../../plugins/i18n'; import { useLogStreamingStore } from '../../stores/logStreamingStore'; export default { diff --git a/packages/editor-ui/src/components/SettingsLogStreaming/Helpers.ee.ts b/packages/editor-ui/src/components/SettingsLogStreaming/Helpers.ee.ts index 1f367ed9c1971..23984bc8cf4a2 100644 --- a/packages/editor-ui/src/components/SettingsLogStreaming/Helpers.ee.ts +++ b/packages/editor-ui/src/components/SettingsLogStreaming/Helpers.ee.ts @@ -1,5 +1,9 @@ -import { INodeCredentials, INodeParameters, MessageEventBusDestinationOptions } from 'n8n-workflow'; -import { INodeUi } from '../../Interface'; +import type { + INodeCredentials, + INodeParameters, + MessageEventBusDestinationOptions, +} from 'n8n-workflow'; +import type { INodeUi } from '../../Interface'; export function destinationToFakeINodeUi( destination: MessageEventBusDestinationOptions, diff --git a/packages/editor-ui/src/components/SettingsLogStreaming/descriptions.ee.ts b/packages/editor-ui/src/components/SettingsLogStreaming/descriptions.ee.ts index 1cfa03d2d2cf7..421d9f1451a38 100644 --- a/packages/editor-ui/src/components/SettingsLogStreaming/descriptions.ee.ts +++ b/packages/editor-ui/src/components/SettingsLogStreaming/descriptions.ee.ts @@ -1,4 +1,4 @@ -import { INodeProperties } from 'n8n-workflow'; +import type { INodeProperties } from 'n8n-workflow'; export const webhookModalDescription = [ { diff --git a/packages/editor-ui/src/components/SettingsSidebar.vue b/packages/editor-ui/src/components/SettingsSidebar.vue index eb3c3628fb927..e024bd96be2a4 100644 --- a/packages/editor-ui/src/components/SettingsSidebar.vue +++ b/packages/editor-ui/src/components/SettingsSidebar.vue @@ -24,9 +24,9 @@ import mixins from 'vue-typed-mixins'; import { ABOUT_MODAL_KEY, VERSIONS_MODAL_KEY, VIEWS } from '@/constants'; import { userHelpers } from '@/mixins/userHelpers'; -import { IFakeDoor } from '@/Interface'; -import { IMenuItem } from 'n8n-design-system'; -import { BaseTextKey } from '@/plugins/i18n'; +import type { IFakeDoor } from '@/Interface'; +import type { IMenuItem } from 'n8n-design-system'; +import type { BaseTextKey } from '@/plugins/i18n'; import { mapStores } from 'pinia'; import { useUIStore } from '@/stores/ui'; import { useSettingsStore } from '@/stores/settings'; diff --git a/packages/editor-ui/src/components/ShortenName.vue b/packages/editor-ui/src/components/ShortenName.vue index 0d7cf9aa629da..a40b6a7ccfd5f 100644 --- a/packages/editor-ui/src/components/ShortenName.vue +++ b/packages/editor-ui/src/components/ShortenName.vue @@ -5,13 +5,13 @@ </template> <script lang="ts"> -import Vue from 'vue'; +import { defineComponent } from 'vue'; import { shorten } from '@/utils'; const DEFAULT_WORKFLOW_NAME_LIMIT = 25; const WORKFLOW_NAME_END_COUNT_TO_KEEP = 4; -export default Vue.extend({ +export default defineComponent({ name: 'ShortenName', props: ['name', 'limit', 'testId'], computed: { diff --git a/packages/editor-ui/src/components/SqlEditor/SqlEditor.vue b/packages/editor-ui/src/components/SqlEditor/SqlEditor.vue new file mode 100644 index 0000000000000..adac053295961 --- /dev/null +++ b/packages/editor-ui/src/components/SqlEditor/SqlEditor.vue @@ -0,0 +1,95 @@ +<template> + <div ref="sqlEditor" class="ph-no-capture"></div> +</template> + +<script lang="ts"> +import type { PropType } from 'vue'; +import { defineComponent } from 'vue'; +import { autocompletion } from '@codemirror/autocomplete'; +import { indentWithTab, history, redo } from '@codemirror/commands'; +import { foldGutter, indentOnInput } from '@codemirror/language'; +import { lintGutter } from '@codemirror/lint'; +import type { Extension } from '@codemirror/state'; +import { EditorState } from '@codemirror/state'; +import type { ViewUpdate } from '@codemirror/view'; +import { + dropCursor, + EditorView, + highlightActiveLine, + highlightActiveLineGutter, + keymap, + lineNumbers, +} from '@codemirror/view'; +import { MSSQL, MySQL, PostgreSQL, sql, StandardSQL } from '@codemirror/lang-sql'; +import type { SQLDialect } from 'n8n-workflow'; + +import { codeNodeEditorTheme } from '../CodeNodeEditor/theme'; + +const SQL_DIALECTS = { + standard: StandardSQL, + mssql: MSSQL, + mysql: MySQL, + postgres: PostgreSQL, +} as const; + +export default defineComponent({ + name: 'sql-editor', + props: { + query: { + type: String, + required: true, + }, + dialect: { + type: String as PropType<SQLDialect>, + default: 'standard', + }, + isReadOnly: { + type: Boolean, + default: false, + }, + }, + data() { + return { + editor: {} as EditorView, + }; + }, + computed: { + doc(): string { + return this.editor.state.doc.toString(); + }, + }, + + mounted() { + const dialect = SQL_DIALECTS[this.dialect as SQLDialect] ?? SQL_DIALECTS.standard; + const extensions: Extension[] = [ + sql({ dialect, upperCaseKeywords: true }), + codeNodeEditorTheme({ maxHeight: false }), + lineNumbers(), + EditorView.lineWrapping, + lintGutter(), + EditorState.readOnly.of(this.isReadOnly), + ]; + + if (this.isReadOnly) { + extensions.push(EditorView.editable.of(this.isReadOnly)); + } else { + extensions.push( + history(), + keymap.of([indentWithTab, { key: 'Mod-Shift-z', run: redo }]), + autocompletion(), + indentOnInput(), + highlightActiveLine(), + highlightActiveLineGutter(), + foldGutter(), + dropCursor(), + EditorView.updateListener.of((viewUpdate: ViewUpdate) => { + if (!viewUpdate.docChanged) return; + this.$emit('valueChanged', this.doc); + }), + ); + } + const state = EditorState.create({ doc: this.query, extensions }); + this.editor = new EditorView({ parent: this.$refs.sqlEditor as HTMLDivElement, state }); + }, +}); +</script> diff --git a/packages/editor-ui/src/components/Sticky.vue b/packages/editor-ui/src/components/Sticky.vue index 922e9b734cd8b..13e871cb9592c 100644 --- a/packages/editor-ui/src/components/Sticky.vue +++ b/packages/editor-ui/src/components/Sticky.vue @@ -5,6 +5,7 @@ :ref="data.name" :style="stickyPosition" :data-name="data.name" + data-test-id="sticky" > <div :class="{ @@ -41,7 +42,12 @@ </div> <div v-show="showActions" class="sticky-options no-select-on-click"> - <div v-touch:tap="deleteNode" class="option" :title="$locale.baseText('node.deleteNode')"> + <div + v-touch:tap="deleteNode" + class="option" + data-test-id="delete-sticky" + :title="$locale.baseText('node.deleteNode')" + > <font-awesome-icon icon="trash" /> </div> </div> @@ -57,15 +63,15 @@ import { externalHooks } from '@/mixins/externalHooks'; import { nodeBase } from '@/mixins/nodeBase'; import { nodeHelpers } from '@/mixins/nodeHelpers'; import { workflowHelpers } from '@/mixins/workflowHelpers'; -import { getStyleTokenValue, isNumber, isString } from '@/utils'; -import { +import { isNumber, isString } from '@/utils'; +import type { INodeUi, INodeUpdatePropertiesInformation, IUpdateInformation, XYPosition, } from '@/Interface'; -import { IDataObject, INodeTypeDescription } from 'n8n-workflow'; +import type { INodeTypeDescription } from 'n8n-workflow'; import { QUICKSTART_NOTE_NAME } from '@/constants'; import { mapStores } from 'pinia'; import { useUIStore } from '@/stores/ui'; diff --git a/packages/editor-ui/src/components/TagsContainer.vue b/packages/editor-ui/src/components/TagsContainer.vue index 7b615dc7c385c..d46ac3f89801e 100644 --- a/packages/editor-ui/src/components/TagsContainer.vue +++ b/packages/editor-ui/src/components/TagsContainer.vue @@ -39,9 +39,9 @@ </template> <script lang="ts"> -import Vue from 'vue'; +import Vue, { defineComponent } from 'vue'; -import { ITag } from '@/Interface'; +import type { ITag } from '@/Interface'; import IntersectionObserver from './IntersectionObserver.vue'; import IntersectionObserved from './IntersectionObserved.vue'; import { mapStores } from 'pinia'; @@ -56,7 +56,7 @@ interface TagEl extends ITag { isCount?: boolean; } -export default Vue.extend({ +export default defineComponent({ components: { IntersectionObserver, IntersectionObserved }, name: 'TagsContainer', props: ['tagIds', 'limit', 'clickable', 'responsive', 'hoverable'], diff --git a/packages/editor-ui/src/components/TagsDropdown.vue b/packages/editor-ui/src/components/TagsDropdown.vue index a8a9b06d6b3f8..a028d81722e05 100644 --- a/packages/editor-ui/src/components/TagsDropdown.vue +++ b/packages/editor-ui/src/components/TagsDropdown.vue @@ -57,15 +57,20 @@ <script lang="ts"> import mixins from 'vue-typed-mixins'; -import { ITag } from '@/Interface'; +import type { ITag } from '@/Interface'; import { MAX_TAG_NAME_LENGTH, TAGS_MANAGER_MODAL_KEY } from '@/constants'; import { showMessage } from '@/mixins/showMessage'; import { mapStores } from 'pinia'; import { useUIStore } from '@/stores/ui'; import { useTagsStore } from '@/stores/tags'; -import { EventBus } from '@/event-bus'; -import { PropType } from 'vue'; +import type { EventBus } from '@/event-bus'; +import type { PropType } from 'vue'; +import type { N8nOption, N8nSelect } from 'n8n-design-system'; + +type SelectRef = InstanceType<typeof N8nSelect>; +type TagRef = InstanceType<typeof N8nOption>; +type CreateRef = InstanceType<typeof N8nOption>; const MANAGE_KEY = '__manage'; const CREATE_KEY = '__create'; @@ -74,7 +79,10 @@ export default mixins(showMessage).extend({ name: 'TagsDropdown', props: { placeholder: {}, - currentTagIds: {}, + currentTagIds: { + type: Array as PropType<string[]>, + default: () => [], + }, createEnabled: {}, eventBus: { type: Object as PropType<EventBus>, @@ -90,10 +98,8 @@ export default mixins(showMessage).extend({ }; }, mounted() { - // @ts-ignore - const select = (this.$refs.select && - this.$refs.select.$refs && - this.$refs.select.$refs.innerSelect) as Vue | undefined; + const selectRef = this.$refs.select as SelectRef | undefined; + const select = selectRef?.$refs?.innerSelect; if (select) { const input = select.$refs.input as Element | undefined; if (input) { @@ -107,10 +113,8 @@ export default mixins(showMessage).extend({ this.$data.preventUpdate = true; this.$emit('blur'); - // @ts-ignore - if (this.$refs.select && typeof this.$refs.select.blur === 'function') { - // @ts-ignore - this.$refs.select.blur(); + if (typeof selectRef?.blur === 'function') { + selectRef.blur(); } } }); @@ -183,31 +187,27 @@ export default mixins(showMessage).extend({ } }, focusOnTopOption() { - const tags = this.$refs.tag as Vue[] | undefined; - const create = this.$refs.create as Vue | undefined; - //@ts-ignore // focus on create option - if (create && create.hoverItem) { - // @ts-ignore - create.hoverItem(); + const tagRefs = this.$refs.tag as TagRef[] | undefined; + const createRef = this.$refs.create as CreateRef | undefined; + // focus on create option + if (createRef && createRef.hoverItem) { + createRef.hoverItem(); } - //@ts-ignore // focus on top option after filter - else if (tags && tags[0] && tags[0].hoverItem) { - // @ts-ignore - tags[0].hoverItem(); + // focus on top option after filter + else if (tagRefs && tagRefs[0] && tagRefs[0].hoverItem) { + tagRefs[0].hoverItem(); } }, focusOnTag(tagId: string) { - const tagOptions = (this.$refs.tag as Vue[]) || []; + const tagOptions = (this.$refs.tag as TagRef[]) || []; if (tagOptions && tagOptions.length) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const added = tagOptions.find((ref: any) => ref.value === tagId); + const added = tagOptions.find((ref) => ref.value === tagId); } }, focusOnInput() { - const select = this.$refs.select as Vue | undefined; - if (select) { - // @ts-ignore - select.focusOnInput(); + const selectRef = this.$refs.select as SelectRef | undefined; + if (selectRef) { + selectRef.focusOnInput(); this.focused = true; } }, diff --git a/packages/editor-ui/src/components/TagsManager/NoTagsView.vue b/packages/editor-ui/src/components/TagsManager/NoTagsView.vue index ed54497024c3a..e9c64f1bd4b9f 100644 --- a/packages/editor-ui/src/components/TagsManager/NoTagsView.vue +++ b/packages/editor-ui/src/components/TagsManager/NoTagsView.vue @@ -16,9 +16,9 @@ </template> <script lang="ts"> -import Vue from 'vue'; +import { defineComponent } from 'vue'; -export default Vue.extend({ +export default defineComponent({ name: 'NoTagsView', }); </script> diff --git a/packages/editor-ui/src/components/TagsManager/TagsManager.vue b/packages/editor-ui/src/components/TagsManager/TagsManager.vue index da54eb6f3b544..f43e1157c5083 100644 --- a/packages/editor-ui/src/components/TagsManager/TagsManager.vue +++ b/packages/editor-ui/src/components/TagsManager/TagsManager.vue @@ -28,10 +28,9 @@ </template> <script lang="ts"> -import Vue from 'vue'; import mixins from 'vue-typed-mixins'; -import { ITag } from '@/Interface'; +import type { ITag } from '@/Interface'; import { showMessage } from '@/mixins/showMessage'; import TagsView from '@/components/TagsManager/TagsView/TagsView.vue'; diff --git a/packages/editor-ui/src/components/TagsManager/TagsView/TagsTable.vue b/packages/editor-ui/src/components/TagsManager/TagsView/TagsTable.vue index 76af2dd73955f..8b2d43c465c3c 100644 --- a/packages/editor-ui/src/components/TagsManager/TagsView/TagsTable.vue +++ b/packages/editor-ui/src/components/TagsManager/TagsView/TagsTable.vue @@ -107,14 +107,19 @@ </template> <script lang="ts"> +import type { Table as ElTable } from 'element-ui'; import { MAX_TAG_NAME_LENGTH } from '@/constants'; -import { ITagRow } from '@/Interface'; -import Vue from 'vue'; +import type { ITagRow } from '@/Interface'; +import { defineComponent } from 'vue'; +import type { N8nInput } from 'n8n-design-system'; + +type TableRef = InstanceType<typeof ElTable>; +type N8nInputRef = InstanceType<typeof N8nInput>; const INPUT_TRANSITION_TIMEOUT = 350; const DELETE_TRANSITION_TIMEOUT = 100; -export default Vue.extend({ +export default defineComponent({ name: 'TagsTable', props: ['rows', 'isLoading', 'newName', 'isSaving'], data() { @@ -173,26 +178,28 @@ export default Vue.extend({ focusOnInput(): void { setTimeout(() => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const input = this.$refs.nameInput as any; - if (input && input.focus) { - input.focus(); + const inputRef = this.$refs.nameInput as N8nInputRef | undefined; + if (inputRef && inputRef.focus) { + inputRef.focus(); } }, INPUT_TRANSITION_TIMEOUT); }, focusOnDelete(): void { setTimeout(() => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const input = this.$refs.deleteHiddenInput as any; - if (input && input.focus) { - input.focus(); + const inputRef = this.$refs.deleteHiddenInput as N8nInputRef | undefined; + if (inputRef && inputRef.focus) { + inputRef.focus(); } }, DELETE_TRANSITION_TIMEOUT); }, focusOnCreate(): void { - ((this.$refs.table as Vue).$refs.bodyWrapper as Element).scrollTop = 0; + const bodyWrapperRef = (this.$refs.table as TableRef).$refs.bodyWrapper as HTMLElement; + if (bodyWrapperRef) { + bodyWrapperRef.scrollTop = 0; + } + this.focusOnInput(); }, }, diff --git a/packages/editor-ui/src/components/TagsManager/TagsView/TagsTableHeader.vue b/packages/editor-ui/src/components/TagsManager/TagsView/TagsTableHeader.vue index a7079d0232e5e..543fd1b213d3e 100644 --- a/packages/editor-ui/src/components/TagsManager/TagsView/TagsTableHeader.vue +++ b/packages/editor-ui/src/components/TagsManager/TagsView/TagsTableHeader.vue @@ -29,9 +29,9 @@ <script lang="ts"> import { MAX_TAG_NAME_LENGTH } from '@/constants'; -import Vue from 'vue'; +import { defineComponent } from 'vue'; -export default Vue.extend({ +export default defineComponent({ props: { disabled: { default: false, diff --git a/packages/editor-ui/src/components/TagsManager/TagsView/TagsView.vue b/packages/editor-ui/src/components/TagsManager/TagsView/TagsView.vue index d695ecbed1760..39678020d3460 100644 --- a/packages/editor-ui/src/components/TagsManager/TagsView/TagsView.vue +++ b/packages/editor-ui/src/components/TagsManager/TagsView/TagsView.vue @@ -23,9 +23,9 @@ </template> <script lang="ts"> -import Vue from 'vue'; +import { defineComponent } from 'vue'; -import { ITag, ITagRow } from '@/Interface'; +import type { ITag, ITagRow } from '@/Interface'; import TagsTableHeader from '@/components/TagsManager/TagsView/TagsTableHeader.vue'; import TagsTable from '@/components/TagsManager/TagsView/TagsTable.vue'; import { mapStores } from 'pinia'; @@ -34,7 +34,7 @@ import { useUsersStore } from '@/stores/users'; const matches = (name: string, filter: string) => name.toLowerCase().trim().includes(filter.toLowerCase().trim()); -export default Vue.extend({ +export default defineComponent({ components: { TagsTableHeader, TagsTable }, name: 'TagsView', props: ['tags', 'isLoading'], diff --git a/packages/editor-ui/src/components/Telemetry.vue b/packages/editor-ui/src/components/Telemetry.vue index 26b5279832020..f9c40b0d6c2c8 100644 --- a/packages/editor-ui/src/components/Telemetry.vue +++ b/packages/editor-ui/src/components/Telemetry.vue @@ -6,7 +6,7 @@ import { useRootStore } from '@/stores/n8nRootStore'; import { useSettingsStore } from '@/stores/settings'; import { useUsersStore } from '@/stores/users'; -import { ITelemetrySettings } from 'n8n-workflow'; +import type { ITelemetrySettings } from 'n8n-workflow'; import { mapStores } from 'pinia'; import mixins from 'vue-typed-mixins'; import { externalHooks } from '@/mixins/externalHooks'; diff --git a/packages/editor-ui/src/components/TemplateDetails.vue b/packages/editor-ui/src/components/TemplateDetails.vue index 814e8e79783aa..816dd61ff00cc 100644 --- a/packages/editor-ui/src/components/TemplateDetails.vue +++ b/packages/editor-ui/src/components/TemplateDetails.vue @@ -47,14 +47,16 @@ </div> </template> <script lang="ts"> -import Vue, { PropType } from 'vue'; +import { defineComponent } from 'vue'; +import type { PropType } from 'vue'; import TemplateDetailsBlock from '@/components/TemplateDetailsBlock.vue'; import NodeIcon from '@/components/NodeIcon.vue'; import { abbreviateNumber, filterTemplateNodes } from '@/utils'; -import { ITemplatesNode, ITemplatesWorkflow, ITemplatesWorkflowFull } from '@/Interface'; +import type { ITemplatesNode, ITemplatesWorkflow, ITemplatesWorkflowFull } from '@/Interface'; import { mapStores } from 'pinia'; import { useTemplatesStore } from '@/stores/templates'; -export default Vue.extend({ + +export default defineComponent({ name: 'TemplateDetails', props: { blockTitle: { diff --git a/packages/editor-ui/src/components/TemplateDetailsBlock.vue b/packages/editor-ui/src/components/TemplateDetailsBlock.vue index aa6bf787e2b95..d5651f15cfb54 100644 --- a/packages/editor-ui/src/components/TemplateDetailsBlock.vue +++ b/packages/editor-ui/src/components/TemplateDetailsBlock.vue @@ -10,9 +10,9 @@ </template> <script lang="ts"> -import Vue from 'vue'; +import { defineComponent } from 'vue'; -export default Vue.extend({ +export default defineComponent({ name: 'TemplateDetailsBlock', props: { title: { diff --git a/packages/editor-ui/src/components/TemplateFilters.vue b/packages/editor-ui/src/components/TemplateFilters.vue index 3c8debd1e66c1..14a5d13b68f16 100644 --- a/packages/editor-ui/src/components/TemplateFilters.vue +++ b/packages/editor-ui/src/components/TemplateFilters.vue @@ -38,7 +38,7 @@ <script lang="ts"> import { genericHelpers } from '@/mixins/genericHelpers'; -import { ITemplatesCategory } from '@/Interface'; +import type { ITemplatesCategory } from '@/Interface'; import mixins from 'vue-typed-mixins'; export default mixins(genericHelpers).extend({ diff --git a/packages/editor-ui/src/components/TemplateList.vue b/packages/editor-ui/src/components/TemplateList.vue index 8e58021a324ed..62e7fe7d8a626 100644 --- a/packages/editor-ui/src/components/TemplateList.vue +++ b/packages/editor-ui/src/components/TemplateList.vue @@ -76,12 +76,12 @@ export default mixins(genericHelpers).extend({ }, methods: { onScroll() { - const el = this.$refs.loader; - if (!el || this.loading) { + const loaderRef = this.$refs.loader as HTMLElement | undefined; + if (!loaderRef || this.loading) { return; } - const rect = (el as Element).getBoundingClientRect(); + const rect = loaderRef.getBoundingClientRect(); const inView = rect.top >= 0 && rect.left >= 0 && diff --git a/packages/editor-ui/src/components/TextEdit.vue b/packages/editor-ui/src/components/TextEdit.vue index b1ab63592c3a5..c1ebda5687ba2 100644 --- a/packages/editor-ui/src/components/TextEdit.vue +++ b/packages/editor-ui/src/components/TextEdit.vue @@ -30,9 +30,9 @@ </template> <script lang="ts"> -import Vue from 'vue'; +import { nextTick, defineComponent } from 'vue'; -export default Vue.extend({ +export default defineComponent({ name: 'TextEdit', props: ['dialogVisible', 'parameter', 'path', 'value', 'isReadOnly'], data() { @@ -65,7 +65,7 @@ export default Vue.extend({ watch: { dialogVisible() { if (this.dialogVisible === true) { - Vue.nextTick(() => { + nextTick(() => { (this.$refs.inputField as HTMLInputElement).focus(); }); } diff --git a/packages/editor-ui/src/components/TimeAgo.vue b/packages/editor-ui/src/components/TimeAgo.vue index fd2a9869721c3..85e4969bca8a0 100644 --- a/packages/editor-ui/src/components/TimeAgo.vue +++ b/packages/editor-ui/src/components/TimeAgo.vue @@ -5,13 +5,14 @@ </template> <script lang="ts"> -import { format, LocaleFunc, register } from 'timeago.js'; +import type { LocaleFunc } from 'timeago.js'; +import { format, register } from 'timeago.js'; import { convertToHumanReadableDate } from '@/utils'; -import Vue from 'vue'; +import { defineComponent } from 'vue'; import { mapStores } from 'pinia'; import { useRootStore } from '@/stores/n8nRootStore'; -export default Vue.extend({ +export default defineComponent({ name: 'TimeAgo', props: { date: { diff --git a/packages/editor-ui/src/components/TitledList.vue b/packages/editor-ui/src/components/TitledList.vue index 64d1d64f3e77a..f15337f055f31 100644 --- a/packages/editor-ui/src/components/TitledList.vue +++ b/packages/editor-ui/src/components/TitledList.vue @@ -8,9 +8,9 @@ </template> <script lang="ts"> -import Vue from 'vue'; +import { defineComponent } from 'vue'; -export default Vue.extend({ +export default defineComponent({ name: 'TitledList', props: { title: { diff --git a/packages/editor-ui/src/components/TriggerPanel.vue b/packages/editor-ui/src/components/TriggerPanel.vue index 85668b5afbe68..f3ed3b4dcb94c 100644 --- a/packages/editor-ui/src/components/TriggerPanel.vue +++ b/packages/editor-ui/src/components/TriggerPanel.vue @@ -48,6 +48,12 @@ }} </n8n-text> </div> + <NodeExecuteButton + :nodeName="nodeName" + @execute="onNodeExecute" + size="medium" + telemetrySource="inputs" + /> </div> </div> <div key="default" v-else> @@ -97,8 +103,8 @@ <script lang="ts"> import { EXECUTIONS_MODAL_KEY, WEBHOOK_NODE_TYPE, WORKFLOW_SETTINGS_MODAL_KEY } from '@/constants'; -import { INodeUi } from '@/Interface'; -import { INodeTypeDescription } from 'n8n-workflow'; +import type { INodeUi } from '@/Interface'; +import type { INodeTypeDescription } from 'n8n-workflow'; import { getTriggerNodeServiceName } from '@/utils'; import NodeExecuteButton from './NodeExecuteButton.vue'; import { workflowHelpers } from '@/mixins/workflowHelpers'; @@ -107,12 +113,14 @@ import CopyInput from './CopyInput.vue'; import NodeIcon from './NodeIcon.vue'; import { copyPaste } from '@/mixins/copyPaste'; import { showMessage } from '@/mixins/showMessage'; -import Vue from 'vue'; import { mapStores } from 'pinia'; import { useUIStore } from '@/stores/ui'; import { useWorkflowsStore } from '@/stores/workflows'; import { useNDVStore } from '@/stores/ndv'; import { useNodeTypesStore } from '@/stores/nodeTypes'; +import type { N8nInfoAccordion } from 'n8n-design-system'; + +type HelpRef = InstanceType<typeof N8nInfoAccordion>; export default mixins(workflowHelpers, copyPaste, showMessage).extend({ name: 'TriggerPanel', @@ -355,7 +363,7 @@ export default mixins(workflowHelpers, copyPaste, showMessage).extend({ methods: { expandExecutionHelp() { if (this.$refs.help) { - (this.$refs.help as Vue).$emit('expand'); + (this.$refs.help as HelpRef).$emit('expand'); } }, onLinkClick(e: MouseEvent) { diff --git a/packages/editor-ui/src/components/UpdatesPanel.vue b/packages/editor-ui/src/components/UpdatesPanel.vue index 9f0f6ded4f446..76db9ab412976 100644 --- a/packages/editor-ui/src/components/UpdatesPanel.vue +++ b/packages/editor-ui/src/components/UpdatesPanel.vue @@ -43,7 +43,7 @@ </template> <script lang="ts"> -import Vue from 'vue'; +import { defineComponent } from 'vue'; import ModalDrawer from './ModalDrawer.vue'; import TimeAgo from './TimeAgo.vue'; @@ -51,9 +51,9 @@ import VersionCard from './VersionCard.vue'; import { VERSIONS_MODAL_KEY } from '../constants'; import { mapStores } from 'pinia'; import { useVersionsStore } from '@/stores/versions'; -import { IVersion } from '@/Interface'; +import type { IVersion } from '@/Interface'; -export default Vue.extend({ +export default defineComponent({ name: 'UpdatesPanel', components: { ModalDrawer, diff --git a/packages/editor-ui/src/components/UserActivationSurveyModal.vue b/packages/editor-ui/src/components/UserActivationSurveyModal.vue index 3cab5fbf595da..2c1d00a5e9cc9 100644 --- a/packages/editor-ui/src/components/UserActivationSurveyModal.vue +++ b/packages/editor-ui/src/components/UserActivationSurveyModal.vue @@ -47,6 +47,13 @@ </template> <template #footer> <div :class="$style.modalFooter"> + <n8n-button + size="large" + type="secondary" + data-test-id="skip-button" + :label="locale.baseText('userActivationSurveyModal.form.button.skip')" + @click="onSkip" + /> <n8n-button :disabled="!hasAnyChanges" @click="onShareFeedback" @@ -100,6 +107,10 @@ const onShareFeedback = () => { modalBus.emit('close'); }; +const onSkip = () => { + modalBus.emit('close'); +}; + const getCurrentSettings = () => { return userStore.currentUser?.settings; }; diff --git a/packages/editor-ui/src/components/ValueSurvey.vue b/packages/editor-ui/src/components/ValueSurvey.vue index cbc3d22f017c2..39274dd2fb84e 100644 --- a/packages/editor-ui/src/components/ValueSurvey.vue +++ b/packages/editor-ui/src/components/ValueSurvey.vue @@ -57,7 +57,7 @@ <script lang="ts"> import { VALID_EMAIL_REGEX, VALUE_SURVEY_MODAL_KEY } from '@/constants'; -import { IN8nPromptResponse } from '@/Interface'; +import type { IN8nPromptResponse } from '@/Interface'; import ModalDrawer from './ModalDrawer.vue'; diff --git a/packages/editor-ui/src/components/VariableSelector.vue b/packages/editor-ui/src/components/VariableSelector.vue index f6b979e6f128a..168e0ebfa9c83 100644 --- a/packages/editor-ui/src/components/VariableSelector.vue +++ b/packages/editor-ui/src/components/VariableSelector.vue @@ -26,7 +26,7 @@ /* eslint-disable prefer-spread */ import { PLACEHOLDER_FILLED_AT_EXECUTION_TIME, STICKY_NODE_TYPE } from '@/constants'; -import { +import type { GenericValue, IContextObject, IDataObject, @@ -36,16 +36,11 @@ import { IRunExecutionData, IWorkflowDataProxyAdditionalKeys, Workflow, - WorkflowDataProxy, } from 'n8n-workflow'; +import { WorkflowDataProxy } from 'n8n-workflow'; import VariableSelectorItem from '@/components/VariableSelectorItem.vue'; -import { - IExecutionResponse, - INodeUi, - IVariableItemSelected, - IVariableSelectorOption, -} from '@/Interface'; +import type { INodeUi, IVariableItemSelected, IVariableSelectorOption } from '@/Interface'; import { workflowHelpers } from '@/mixins/workflowHelpers'; diff --git a/packages/editor-ui/src/components/VariableSelectorItem.vue b/packages/editor-ui/src/components/VariableSelectorItem.vue index aeb595824d813..138ab476fcd67 100644 --- a/packages/editor-ui/src/components/VariableSelectorItem.vue +++ b/packages/editor-ui/src/components/VariableSelectorItem.vue @@ -59,7 +59,7 @@ </template> <script lang="ts"> -import { IVariableSelectorOption, IVariableItemSelected } from '@/Interface'; +import type { IVariableSelectorOption, IVariableItemSelected } from '@/Interface'; import { externalHooks } from '@/mixins/externalHooks'; import mixins from 'vue-typed-mixins'; diff --git a/packages/editor-ui/src/components/VariablesRow.vue b/packages/editor-ui/src/components/VariablesRow.vue index 7b0861a2d9279..19c59cd5d4682 100644 --- a/packages/editor-ui/src/components/VariablesRow.vue +++ b/packages/editor-ui/src/components/VariablesRow.vue @@ -1,12 +1,13 @@ <script lang="ts" setup> -import { ComponentPublicInstance, computed, nextTick, onMounted, PropType, ref, watch } from 'vue'; -import { EnvironmentVariable, IValidator, Rule, RuleGroup, Validatable } from '@/Interface'; +import type { ComponentPublicInstance, PropType } from 'vue'; +import { computed, nextTick, onMounted, ref, watch } from 'vue'; +import type { EnvironmentVariable, Rule, RuleGroup } from '@/Interface'; import { useI18n, useToast, useCopyToClipboard } from '@/composables'; import { EnterpriseEditionFeature } from '@/constants'; import { useSettingsStore, useUsersStore } from '@/stores'; import { getVariablesPermissions } from '@/permissions'; -const i18n = useI18n(); +const { i18n } = useI18n(); const copyToClipboard = useCopyToClipboard(); const { showMessage } = useToast(); const settingsStore = useSettingsStore(); diff --git a/packages/editor-ui/src/components/VersionCard.vue b/packages/editor-ui/src/components/VersionCard.vue index d3e52bc91fcab..a444786e3bb3b 100644 --- a/packages/editor-ui/src/components/VersionCard.vue +++ b/packages/editor-ui/src/components/VersionCard.vue @@ -55,19 +55,19 @@ </template> <script lang="ts"> -import Vue from 'vue'; +import Vue, { defineComponent } from 'vue'; import NodeIcon from './NodeIcon.vue'; import TimeAgo from './TimeAgo.vue'; import Badge from './Badge.vue'; import WarningTooltip from './WarningTooltip.vue'; -import { IVersionNode } from '@/Interface'; +import type { IVersionNode } from '@/Interface'; Vue.component('NodeIcon', NodeIcon); Vue.component('TimeAgo', TimeAgo); Vue.component('Badge', Badge); Vue.component('WarningTooltip', WarningTooltip); -export default Vue.extend({ +export default defineComponent({ components: { NodeIcon, TimeAgo, Badge, WarningTooltip }, name: 'VersionCard', props: ['version'], diff --git a/packages/editor-ui/src/components/WorkflowActivator.vue b/packages/editor-ui/src/components/WorkflowActivator.vue index 143bbf41b7104..c4e19c966c1f9 100644 --- a/packages/editor-ui/src/components/WorkflowActivator.vue +++ b/packages/editor-ui/src/components/WorkflowActivator.vue @@ -105,7 +105,7 @@ export default mixins(showMessage, workflowActivate).extend({ async displayActivationError() { let errorMessage: string; try { - const errorData = await this.restApi().getActivationError(this.workflowId); + const errorData = await this.workflowsStore.getActivationError(this.workflowId); if (errorData === undefined) { errorMessage = this.$locale.baseText( diff --git a/packages/editor-ui/src/components/WorkflowCard.vue b/packages/editor-ui/src/components/WorkflowCard.vue index c4d76d7e066ab..f49d336713dfd 100644 --- a/packages/editor-ui/src/components/WorkflowCard.vue +++ b/packages/editor-ui/src/components/WorkflowCard.vue @@ -63,7 +63,7 @@ <script lang="ts"> import mixins from 'vue-typed-mixins'; -import { IWorkflowDb, IUser, ITag } from '@/Interface'; +import type { IWorkflowDb, IUser, ITag } from '@/Interface'; import { DUPLICATE_MODAL_KEY, EnterpriseEditionFeature, @@ -71,17 +71,18 @@ import { WORKFLOW_SHARE_MODAL_KEY, } from '@/constants'; import { showMessage } from '@/mixins/showMessage'; -import { getWorkflowPermissions, IPermissions } from '@/permissions'; +import type { IPermissions } from '@/permissions'; +import { getWorkflowPermissions } from '@/permissions'; import dateformat from 'dateformat'; -import { restApi } from '@/mixins/restApi'; import WorkflowActivator from '@/components/WorkflowActivator.vue'; -import Vue from 'vue'; import { mapStores } from 'pinia'; import { useUIStore } from '@/stores/ui'; import { useSettingsStore } from '@/stores/settings'; import { useUsersStore } from '@/stores/users'; import { useWorkflowsStore } from '@/stores/workflows'; +type ActivatorRef = InstanceType<typeof WorkflowActivator>; + export const WORKFLOW_LIST_ITEM_ACTIONS = { OPEN: 'open', SHARE: 'share', @@ -89,7 +90,7 @@ export const WORKFLOW_LIST_ITEM_ACTIONS = { DELETE: 'delete', }; -export default mixins(showMessage, restApi).extend({ +export default mixins(showMessage).extend({ data() { return { EnterpriseEditionFeature, @@ -165,7 +166,7 @@ export default mixins(showMessage, restApi).extend({ methods: { async onClick(event?: PointerEvent) { if (event) { - if ((this.$refs.activator as Vue)?.$el.contains(event.target as HTMLElement)) { + if ((this.$refs.activator as ActivatorRef)?.$el.contains(event.target as HTMLElement)) { return; } @@ -232,13 +233,9 @@ export default mixins(showMessage, restApi).extend({ } try { - await this.restApi().deleteWorkflow(this.data.id); - this.workflowsStore.deleteWorkflow(this.data.id); + await this.workflowsStore.deleteWorkflow(this.data.id); } catch (error) { - this.$showError( - error, - this.$locale.baseText('mainSidebar.showError.stopExecution.title'), - ); + this.$showError(error, this.$locale.baseText('generic.deleteWorkflowError')); return; } diff --git a/packages/editor-ui/src/components/WorkflowPreview.vue b/packages/editor-ui/src/components/WorkflowPreview.vue index baa1b8653b032..5ea1a06342e45 100644 --- a/packages/editor-ui/src/components/WorkflowPreview.vue +++ b/packages/editor-ui/src/components/WorkflowPreview.vue @@ -24,7 +24,7 @@ <script lang="ts"> import mixins from 'vue-typed-mixins'; import { showMessage } from '@/mixins/showMessage'; -import { IWorkflowDb } from '../Interface'; +import type { IWorkflowDb } from '../Interface'; import { mapStores } from 'pinia'; import { useRootStore } from '@/stores/n8nRootStore'; @@ -96,9 +96,9 @@ export default mixins(showMessage).extend({ throw new Error(this.$locale.baseText('workflowPreview.showError.arrayEmpty')); } - const iframe = this.$refs.preview_iframe as HTMLIFrameElement; - if (iframe.contentWindow) { - iframe.contentWindow.postMessage( + const iframeRef = this.$refs.preview_iframe as HTMLIFrameElement | undefined; + if (iframeRef?.contentWindow) { + iframeRef.contentWindow.postMessage( JSON.stringify({ command: 'openWorkflow', workflow: this.workflow, @@ -119,9 +119,9 @@ export default mixins(showMessage).extend({ if (!this.executionId) { throw new Error(this.$locale.baseText('workflowPreview.showError.missingExecution')); } - const iframe = this.$refs.preview_iframe as HTMLIFrameElement; - if (iframe.contentWindow) { - iframe.contentWindow.postMessage( + const iframeRef = this.$refs.preview_iframe as HTMLIFrameElement | undefined; + if (iframeRef?.contentWindow) { + iframeRef.contentWindow.postMessage( JSON.stringify({ command: 'openExecution', executionId: this.executionId, diff --git a/packages/editor-ui/src/components/WorkflowSettings.vue b/packages/editor-ui/src/components/WorkflowSettings.vue index a2fd1bcdb18fb..23ad961ab55ee 100644 --- a/packages/editor-ui/src/components/WorkflowSettings.vue +++ b/packages/editor-ui/src/components/WorkflowSettings.vue @@ -327,17 +327,15 @@ import Vue from 'vue'; import { externalHooks } from '@/mixins/externalHooks'; -import { restApi } from '@/mixins/restApi'; import { genericHelpers } from '@/mixins/genericHelpers'; import { showMessage } from '@/mixins/showMessage'; -import { +import type { ITimeoutHMS, IUser, IWorkflowDataUpdate, IWorkflowDb, IWorkflowSettings, IWorkflowShortResponse, - WorkflowCallerPolicyDefaultOption, } from '@/Interface'; import Modal from './Modal.vue'; import { @@ -348,6 +346,7 @@ import { import mixins from 'vue-typed-mixins'; +import type { WorkflowSettings } from 'n8n-workflow'; import { deepCopy } from 'n8n-workflow'; import { mapStores } from 'pinia'; import { useWorkflowsStore } from '@/stores/workflows'; @@ -357,7 +356,7 @@ import useWorkflowsEEStore from '@/stores/workflows.ee'; import { useUsersStore } from '@/stores/users'; import { createEventBus } from '@/event-bus'; -export default mixins(externalHooks, genericHelpers, restApi, showMessage).extend({ +export default mixins(externalHooks, genericHelpers, showMessage).extend({ name: 'WorkflowSettings', components: { Modal, @@ -504,7 +503,7 @@ export default mixins(externalHooks, genericHelpers, restApi, showMessage).exten } if (workflowSettings.callerPolicy === undefined) { workflowSettings.callerPolicy = this.defaultValues - .workflowCallerPolicy as WorkflowCallerPolicyDefaultOption; + .workflowCallerPolicy as WorkflowSettings.CallerPolicy; } if (workflowSettings.executionTimeout === undefined) { workflowSettings.executionTimeout = this.rootStore.executionTimeout; @@ -703,7 +702,7 @@ export default mixins(externalHooks, genericHelpers, restApi, showMessage).exten return; } - const timezones = await this.restApi().getTimezones(); + const timezones = await this.settingsStore.getTimezones(); let defaultTimezoneValue = timezones[this.defaultValues.timezone] as string | undefined; if (defaultTimezoneValue === undefined) { @@ -724,7 +723,7 @@ export default mixins(externalHooks, genericHelpers, restApi, showMessage).exten } }, async loadWorkflows() { - const workflows = await this.restApi().getWorkflows(); + const workflows = await this.workflowsStore.fetchAllWorkflows(); workflows.sort((a, b) => { if (a.name.toLowerCase() < b.name.toLowerCase()) { return -1; @@ -789,7 +788,7 @@ export default mixins(externalHooks, genericHelpers, restApi, showMessage).exten data.versionId = this.workflowsStore.workflowVersionId; try { - const workflow = await this.restApi().updateWorkflow(this.$route.params.name, data); + const workflow = await this.workflowsStore.updateWorkflow(this.$route.params.name, data); this.workflowsStore.setWorkflowVersionId(workflow.versionId); } catch (error) { this.$showError( diff --git a/packages/editor-ui/src/components/WorkflowShareModal.ee.vue b/packages/editor-ui/src/components/WorkflowShareModal.ee.vue index e78a996b747db..8dbf603982bd1 100644 --- a/packages/editor-ui/src/components/WorkflowShareModal.ee.vue +++ b/packages/editor-ui/src/components/WorkflowShareModal.ee.vue @@ -130,8 +130,9 @@ import { VIEWS, WORKFLOW_SHARE_MODAL_KEY, } from '../constants'; -import { IUser, IWorkflowDb, UIState } from '@/Interface'; -import { getWorkflowPermissions, IPermissions } from '@/permissions'; +import type { IUser, IWorkflowDb } from '@/Interface'; +import type { IPermissions } from '@/permissions'; +import { getWorkflowPermissions } from '@/permissions'; import mixins from 'vue-typed-mixins'; import { showMessage } from '@/mixins/showMessage'; import { createEventBus, nodeViewEventBus } from '@/event-bus'; @@ -141,9 +142,9 @@ import { useUIStore } from '@/stores/ui'; import { useUsersStore } from '@/stores/users'; import { useWorkflowsStore } from '@/stores/workflows'; import { useWorkflowsEEStore } from '@/stores/workflows.ee'; -import { ITelemetryTrackProperties } from 'n8n-workflow'; +import type { ITelemetryTrackProperties } from 'n8n-workflow'; import { useUsageStore } from '@/stores/usage'; -import { BaseTextKey } from '@/plugins/i18n'; +import type { BaseTextKey } from '@/plugins/i18n'; import { isNavigationFailure } from 'vue-router'; export default mixins(showMessage).extend({ diff --git a/packages/editor-ui/src/components/__tests__/ExecutionFilter.test.ts b/packages/editor-ui/src/components/__tests__/ExecutionFilter.test.ts index b209b0e6198b3..8d4ccaf000e6f 100644 --- a/packages/editor-ui/src/components/__tests__/ExecutionFilter.test.ts +++ b/packages/editor-ui/src/components/__tests__/ExecutionFilter.test.ts @@ -2,13 +2,26 @@ import { describe, test, expect } from 'vitest'; import Vue from 'vue'; import { PiniaVuePlugin } from 'pinia'; import { createTestingPinia } from '@pinia/testing'; -import { render, RenderOptions } from '@testing-library/vue'; +import type { RenderOptions } from '@testing-library/vue'; +import { render } from '@testing-library/vue'; import userEvent from '@testing-library/user-event'; import { faker } from '@faker-js/faker'; import ExecutionFilter from '@/components/ExecutionFilter.vue'; import { STORES } from '@/constants'; import { i18nInstance } from '@/plugins/i18n'; import type { IWorkflowShortResponse, ExecutionFilterType } from '@/Interface'; +import { useTelemetry } from '@/composables'; + +vi.mock('@/composables', () => { + const track = vi.fn(); + return { + useTelemetry: () => ({ + track, + }), + }; +}); + +let telemetry: ReturnType<typeof useTelemetry>; Vue.use(PiniaVuePlugin); @@ -57,6 +70,10 @@ const renderOptions: RenderOptions<ExecutionFilter> = { }; describe('ExecutionFilter', () => { + beforeEach(() => { + telemetry = useTelemetry(); + }); + test.each([ ['development', 'default', false, workflowsData], ['development', 'default', true, workflowsData], @@ -119,4 +136,14 @@ describe('ExecutionFilter', () => { expect(queryByTestId('executions-filter-reset-button')).not.toBeInTheDocument(); expect(queryByTestId('execution-filter-badge')).not.toBeInTheDocument(); }); + + test('telemetry sent only once after component is mounted', async () => { + const { getByTestId } = render(ExecutionFilter, renderOptions); + const customDataKeyInput = getByTestId('execution-filter-saved-data-key-input'); + + await userEvent.type(customDataKeyInput, 'test'); + await userEvent.type(customDataKeyInput, 'key'); + + expect(telemetry.track).toHaveBeenCalledTimes(1); + }); }); diff --git a/packages/editor-ui/src/components/__tests__/ExecutionsList.test.ts b/packages/editor-ui/src/components/__tests__/ExecutionsList.test.ts index 10faa739409bc..2075750da4faf 100644 --- a/packages/editor-ui/src/components/__tests__/ExecutionsList.test.ts +++ b/packages/editor-ui/src/components/__tests__/ExecutionsList.test.ts @@ -12,17 +12,21 @@ import { genericHelpers } from '@/mixins/genericHelpers'; import { executionHelpers } from '@/mixins/executionsHelpers'; import { showMessage } from '@/mixins/showMessage'; import { i18nInstance } from '@/plugins/i18n'; -import type { IWorkflowShortResponse } from '@/Interface'; +import type { IWorkflowDb } from '@/Interface'; import type { IExecutionsSummary } from 'n8n-workflow'; import { waitAllPromises } from '@/__tests__/utils'; +import { useWorkflowsStore } from '@/stores'; -const workflowDataFactory = (): IWorkflowShortResponse => ({ +const workflowDataFactory = (): IWorkflowDb => ({ createdAt: faker.date.past().toDateString(), updatedAt: faker.date.past().toDateString(), id: faker.datatype.uuid(), name: faker.datatype.string(), active: faker.datatype.boolean(), tags: [], + nodes: [], + connections: {}, + versionId: faker.datatype.number().toString(), }); const executionDataFactory = (): IExecutionsSummary => ({ @@ -45,20 +49,6 @@ const executionsData = Array.from({ length: 2 }, () => ({ estimated: false, })); -let getPastExecutionsSpy = vi.fn().mockResolvedValue({ count: 0, results: [], estimated: false }); - -const mockRestApiMixin = Vue.extend({ - methods: { - restApi() { - return { - getWorkflows: vi.fn().mockResolvedValue(workflowsData), - getCurrentExecutions: vi.fn().mockResolvedValue([]), - getPastExecutions: getPastExecutionsSpy, - }; - }, - }, -}); - const renderOptions = { pinia: createTestingPinia({ initialState: { @@ -83,7 +73,7 @@ const renderOptions = { }), i18n: i18nInstance, stubs: ['font-awesome-icon'], - mixins: [externalHooks, genericHelpers, executionHelpers, showMessage, mockRestApiMixin], + mixins: [externalHooks, genericHelpers, executionHelpers, showMessage], }; function TelemetryPlugin(vue: typeof Vue): void { @@ -113,7 +103,22 @@ Vue.use(TelemetryPlugin); Vue.use(PiniaVuePlugin); describe('ExecutionsList.vue', () => { + const workflowsStore: ReturnType<typeof useWorkflowsStore> = useWorkflowsStore(); + beforeEach(() => { + vi.spyOn(workflowsStore, 'fetchAllWorkflows').mockResolvedValue(workflowsData); + vi.spyOn(workflowsStore, 'getCurrentExecutions').mockResolvedValue([]); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + it('should render empty list', async () => { + vi.spyOn(workflowsStore, 'getPastExecutions').mockResolvedValueOnce({ + count: 0, + results: [], + estimated: false, + }); const { queryAllByTestId, queryByTestId, getByTestId } = await renderComponent(); await userEvent.click(getByTestId('execution-auto-refresh-checkbox')); @@ -124,8 +129,8 @@ describe('ExecutionsList.vue', () => { }); it('should handle selection flow when loading more items', async () => { - getPastExecutionsSpy = vi - .fn() + const storeSpy = vi + .spyOn(workflowsStore, 'getPastExecutions') .mockResolvedValueOnce(executionsData[0]) .mockResolvedValueOnce(executionsData[1]); @@ -134,7 +139,7 @@ describe('ExecutionsList.vue', () => { await userEvent.click(getByTestId('select-visible-executions-checkbox')); - expect(getPastExecutionsSpy).toHaveBeenCalledTimes(1); + expect(storeSpy).toHaveBeenCalledTimes(1); expect( getAllByTestId('select-execution-checkbox').filter((el) => el.contains(el.querySelector(':checked')), @@ -145,7 +150,7 @@ describe('ExecutionsList.vue', () => { await userEvent.click(getByTestId('load-more-button')); - expect(getPastExecutionsSpy).toHaveBeenCalledTimes(2); + expect(storeSpy).toHaveBeenCalledTimes(2); expect(getAllByTestId('select-execution-checkbox').length).toBe(20); expect( getAllByTestId('select-execution-checkbox').filter((el) => diff --git a/packages/editor-ui/src/components/__tests__/PersonalizationModal.spec.ts b/packages/editor-ui/src/components/__tests__/PersonalizationModal.spec.ts index 145426923275f..4f2c653dc3198 100644 --- a/packages/editor-ui/src/components/__tests__/PersonalizationModal.spec.ts +++ b/packages/editor-ui/src/components/__tests__/PersonalizationModal.spec.ts @@ -2,7 +2,7 @@ import { PiniaVuePlugin } from 'pinia'; import { createLocalVue, mount } from '@vue/test-utils'; import PersonalizationModal from '@/components/PersonalizationModal.vue'; import { createTestingPinia } from '@pinia/testing'; -import { PERSONALIZATION_MODAL_KEY, ROLE_SALES_AND_MARKETING } from '@/constants'; +import { PERSONALIZATION_MODAL_KEY } from '@/constants'; import { retry } from '@/__tests__/utils'; describe('PersonalizationModal.vue', () => { diff --git a/packages/editor-ui/src/components/__tests__/RunData.test.ts b/packages/editor-ui/src/components/__tests__/RunData.test.ts new file mode 100644 index 0000000000000..f6e16fb004750 --- /dev/null +++ b/packages/editor-ui/src/components/__tests__/RunData.test.ts @@ -0,0 +1,164 @@ +import type Vue from 'vue'; +import { defineComponent } from 'vue'; +import { PiniaVuePlugin } from 'pinia'; +import { render, waitFor } from '@testing-library/vue'; +import userEvent from '@testing-library/user-event'; +import { createTestingPinia } from '@pinia/testing'; +import { merge } from 'lodash-es'; +import RunData from '@/components/RunData.vue'; +import { STORES, VIEWS } from '@/constants'; +import { useSSOStore } from '@/stores/sso'; +import { SETTINGS_STORE_DEFAULT_STATE } from '@/__tests__/utils'; +import { externalHooks } from '@/mixins/externalHooks'; +import { genericHelpers } from '@/mixins/genericHelpers'; +import { pinData } from '@/mixins/pinData'; +import { useNDVStore, useWorkflowsStore } from '@/stores'; + +let pinia: ReturnType<typeof createTestingPinia>; +let ssoStore: ReturnType<typeof useSSOStore>; +let workflowsStore: ReturnType<typeof useWorkflowsStore>; +let ndvStore: ReturnType<typeof useNDVStore>; + +function TelemetryPlugin(vue: typeof Vue): void { + Object.defineProperty(vue, '$telemetry', { + get() { + return { + track: () => {}, + }; + }, + }); + Object.defineProperty(vue.prototype, '$telemetry', { + get() { + return { + track: () => {}, + }; + }, + }); +} + +const nodeHelpers = defineComponent({ + methods: { + getNodeInputData: vi.fn().mockReturnValue([ + { + json: { + id: 1, + name: 'Test 1', + json: { + data: 'Json data 1', + }, + }, + }, + { + json: { + id: 2, + name: 'Test 2', + json: { + data: 'Json data 2', + }, + }, + }, + ]), + }, +}); + +const renderComponent = (renderOptions: Parameters<typeof render>[1] = {}) => + render( + RunData, + merge( + { + pinia, + mocks: { + $route: { + name: VIEWS.WORKFLOW, + }, + }, + mixins: [externalHooks, genericHelpers, nodeHelpers, pinData], + }, + renderOptions, + ), + (vue) => { + vue.use(TelemetryPlugin); + vue.use(PiniaVuePlugin); + }, + ); + +describe('RunData', () => { + beforeEach(() => { + pinia = createTestingPinia({ + initialState: { + [STORES.SETTINGS]: { + settings: merge({}, SETTINGS_STORE_DEFAULT_STATE.settings), + }, + }, + }); + ssoStore = useSSOStore(); + workflowsStore = useWorkflowsStore(); + ndvStore = useNDVStore(); + + vi.spyOn(workflowsStore, 'getWorkflowExecution', 'get').mockReturnValue({ + id: '1', + finished: true, + mode: 'trigger', + startedAt: new Date(), + workflowData: { + id: '1', + name: 'Test Workflow', + versionId: '1', + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + active: false, + nodes: [], + connections: {}, + }, + data: { + resultData: { + runData: { + 'Test Node': [ + { + startTime: new Date().getTime(), + executionTime: new Date().getTime(), + data: {}, + source: [null], + }, + ], + }, + }, + }, + }); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should render data correctly even when "item.json" has another "json" key', async () => { + vi.spyOn(ndvStore, 'getPanelDisplayMode').mockReturnValue('schema'); + vi.spyOn(ndvStore, 'activeNode', 'get').mockReturnValue({ + id: '1', + typeVersion: 1, + name: 'Test Node', + position: [0, 0], + type: 'test', + parameters: {}, + }); + + const { getByText, getAllByTestId, getByTestId } = renderComponent({ + props: { + nodeUi: { + name: 'Test Node', + position: [0, 0], + }, + runIndex: 0, + paneType: 'output', + isExecuting: false, + mappingEnabled: true, + distanceFromActive: 0, + }, + }); + + await userEvent.click(getByTestId('ndv-pin-data')); + await waitFor(() => getAllByTestId('run-data-schema-item')); + expect(getByText('Test 1')).toBeInTheDocument(); + expect(getByText('Json data 1')).toBeInTheDocument(); + }); +}); diff --git a/packages/editor-ui/src/components/__tests__/VariablesRow.spec.ts b/packages/editor-ui/src/components/__tests__/VariablesRow.spec.ts index ac87e260b0eed..2a68e87a2a0a9 100644 --- a/packages/editor-ui/src/components/__tests__/VariablesRow.spec.ts +++ b/packages/editor-ui/src/components/__tests__/VariablesRow.spec.ts @@ -1,20 +1,23 @@ import VariablesRow from '../VariablesRow.vue'; -import { EnvironmentVariable } from '@/Interface'; -import { fireEvent, render } from '@testing-library/vue'; +import type { EnvironmentVariable } from '@/Interface'; +import { fireEvent } from '@testing-library/vue'; import { createPinia, setActivePinia } from 'pinia'; import { setupServer } from '@/__tests__/server'; import { afterAll, beforeAll } from 'vitest'; import { useSettingsStore, useUsersStore } from '@/stores'; +import { renderComponent } from '@/__tests__/utils'; describe('VariablesRow', () => { let server: ReturnType<typeof setupServer>; + let pinia: ReturnType<typeof createPinia>; beforeAll(() => { server = setupServer(); }); beforeEach(async () => { - setActivePinia(createPinia()); + pinia = createPinia(); + setActivePinia(pinia); await useSettingsStore().getSettings(); await useUsersStore().loginWithCookie(); @@ -33,11 +36,12 @@ describe('VariablesRow', () => { }; it('should render correctly', () => { - const wrapper = render(VariablesRow, { + const wrapper = renderComponent(VariablesRow, { props: { data: environmentVariable, }, stubs, + pinia, }); expect(wrapper.html()).toMatchSnapshot(); @@ -45,11 +49,12 @@ describe('VariablesRow', () => { }); it('should show edit and delete buttons on hover', async () => { - const wrapper = render(VariablesRow, { + const wrapper = renderComponent(VariablesRow, { props: { data: environmentVariable, }, stubs, + pinia, }); await fireEvent.mouseEnter(wrapper.container); @@ -59,12 +64,13 @@ describe('VariablesRow', () => { }); it('should show key and value inputs in edit mode', async () => { - const wrapper = render(VariablesRow, { + const wrapper = renderComponent(VariablesRow, { props: { data: environmentVariable, editing: true, }, stubs, + pinia, }); await fireEvent.mouseEnter(wrapper.container); @@ -82,12 +88,13 @@ describe('VariablesRow', () => { }); it('should show cancel and save buttons in edit mode', async () => { - const wrapper = render(VariablesRow, { + const wrapper = renderComponent(VariablesRow, { props: { data: environmentVariable, editing: true, }, stubs, + pinia, }); await fireEvent.mouseEnter(wrapper.container); diff --git a/packages/editor-ui/src/components/forms/CodeEditor.vue b/packages/editor-ui/src/components/forms/CodeEditor.vue deleted file mode 100644 index bd105f3809998..0000000000000 --- a/packages/editor-ui/src/components/forms/CodeEditor.vue +++ /dev/null @@ -1,137 +0,0 @@ -<template> - <div ref="code" class="text-editor ph-no-capture" @keydown.stop /> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import * as monaco from 'monaco-editor'; - -export default Vue.extend({ - props: { - type: { - type: String, - default: 'code', - }, - readonly: { - type: Boolean, - default: false, - }, - value: { - type: String, - default: '', - }, - autocomplete: { - type: Function, - }, - options: { - type: Object, - default: () => ({}), - }, - }, - data() { - return { - monacoInstance: null as monaco.editor.IStandaloneCodeEditor | null, - monacoLibrary: null as monaco.IDisposable | null, - }; - }, - methods: { - loadEditor() { - if (!this.$refs.code) return; - - this.monacoInstance = monaco.editor.create(this.$refs.code as HTMLElement, { - automaticLayout: true, - value: this.value, - language: this.type === 'code' ? 'javascript' : 'json', - tabSize: 2, - wordBasedSuggestions: false, - readOnly: this.readonly, - padding: { - top: 16, - }, - minimap: { - enabled: false, - }, - ...this.options, - }); - - this.monacoInstance.onDidChangeModelContent(() => { - const model = this.monacoInstance!.getModel(); - if (model) { - this.$emit('input', model.getValue()); - } - }); - - const darkModeBetaEnabled = - document.body.classList.contains('theme-dark-beta') && - window.matchMedia('(prefers-color-scheme: dark)').matches; - - monaco.editor.defineTheme('n8nCustomTheme', { - base: darkModeBetaEnabled ? 'vs-dark' : 'vs', - inherit: true, - rules: [], - colors: {}, - }); - monaco.editor.setTheme('n8nCustomTheme'); - - if (this.type === 'code') { - // As wordBasedSuggestions: false does not have any effect does it however seem - // to remove all all suggestions from the editor if I do this - monaco.languages.typescript.javascriptDefaults.setCompilerOptions({ - allowNonTsExtensions: true, - }); - - if (this.autocomplete) { - this.monacoLibrary = monaco.languages.typescript.javascriptDefaults.addExtraLib( - this.autocomplete().join('\n'), - ); - } - } else if (this.type === 'json') { - monaco.languages.json.jsonDefaults.setDiagnosticsOptions({ - validate: true, - }); - } - }, - handleResize() { - if (this.monacoInstance) { - // Workaround to force Monaco to recompute its boundaries - this.monacoInstance.layout({} as unknown as undefined); - } - }, - }, - mounted() { - setTimeout(this.loadEditor); - window.addEventListener('resize', this.handleResize); - }, - destroyed() { - if (this.monacoLibrary) { - this.monacoLibrary.dispose(); - } - window.removeEventListener('resize', this.handleResize); - }, -}); -</script> - -<style lang="scss" scoped> -.text-editor { - width: 100%; - height: 100%; - flex: 1 1 auto; - border: 1px solid var(--color-foreground-base); - border-radius: var(--border-radius-base); -} - -::v-deep { - .monaco-editor { - .slider { - border-radius: var(--border-radius-base); - } - - &, - &-background, - .inputarea.ime-input, - .margin { - border-radius: var(--border-radius-base); - } - } -} -</style> diff --git a/packages/editor-ui/src/components/forms/ResourceFiltersDropdown.vue b/packages/editor-ui/src/components/forms/ResourceFiltersDropdown.vue index be77568a94209..0aa5f6b35535f 100644 --- a/packages/editor-ui/src/components/forms/ResourceFiltersDropdown.vue +++ b/packages/editor-ui/src/components/forms/ResourceFiltersDropdown.vue @@ -63,15 +63,16 @@ </template> <script lang="ts"> -import Vue, { PropType } from 'vue'; +import { defineComponent } from 'vue'; import { EnterpriseEditionFeature } from '@/constants'; -import { IUser } from '@/Interface'; import { mapStores } from 'pinia'; import { useUsersStore } from '@/stores/users'; +import type { PropType } from 'vue'; +import type { IUser } from '@/Interface'; export type IResourceFiltersType = Record<string, boolean | string | string[]>; -export default Vue.extend({ +export default defineComponent({ props: { value: { type: Object as PropType<IResourceFiltersType>, diff --git a/packages/editor-ui/src/components/forms/ResourceOwnershipSelect.ee.vue b/packages/editor-ui/src/components/forms/ResourceOwnershipSelect.ee.vue index 8f68a890a318d..1c5ccc865782d 100644 --- a/packages/editor-ui/src/components/forms/ResourceOwnershipSelect.ee.vue +++ b/packages/editor-ui/src/components/forms/ResourceOwnershipSelect.ee.vue @@ -8,10 +8,10 @@ </template> <script lang="ts"> -import Vue from 'vue'; -import { IMenuItem } from 'n8n-design-system'; +import { defineComponent } from 'vue'; +import type { IMenuItem } from 'n8n-design-system'; -export default Vue.extend({ +export default defineComponent({ props: { value: { type: Boolean, diff --git a/packages/editor-ui/src/components/forms/index.ts b/packages/editor-ui/src/components/forms/index.ts deleted file mode 100644 index 345b928ff05d8..0000000000000 --- a/packages/editor-ui/src/components/forms/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as CodeEditor } from './CodeEditor.vue'; diff --git a/packages/editor-ui/src/components/layouts/PageViewLayout.vue b/packages/editor-ui/src/components/layouts/PageViewLayout.vue index bc670f2b51222..f3340c5983728 100644 --- a/packages/editor-ui/src/components/layouts/PageViewLayout.vue +++ b/packages/editor-ui/src/components/layouts/PageViewLayout.vue @@ -14,9 +14,9 @@ <script lang="ts"> import { useUIStore } from '@/stores/ui'; import { mapStores } from 'pinia'; -import Vue from 'vue'; +import { defineComponent } from 'vue'; -export default Vue.extend({ +export default defineComponent({ name: 'PageViewLayout', data() { return { diff --git a/packages/editor-ui/src/components/layouts/ResourcesListLayout.vue b/packages/editor-ui/src/components/layouts/ResourcesListLayout.vue index af66d9b24b485..5a9d28ba599b8 100644 --- a/packages/editor-ui/src/components/layouts/ResourcesListLayout.vue +++ b/packages/editor-ui/src/components/layouts/ResourcesListLayout.vue @@ -182,21 +182,22 @@ <script lang="ts"> import { showMessage } from '@/mixins/showMessage'; -import { IUser } from '@/Interface'; +import type { IUser } from '@/Interface'; import mixins from 'vue-typed-mixins'; import PageViewLayout from '@/components/layouts/PageViewLayout.vue'; import PageViewLayoutList from '@/components/layouts/PageViewLayoutList.vue'; import { EnterpriseEditionFeature } from '@/constants'; import TemplateCard from '@/components/TemplateCard.vue'; -import Vue, { PropType } from 'vue'; +import type { PropType } from 'vue'; import { debounceHelper } from '@/mixins/debounce'; import ResourceOwnershipSelect from '@/components/forms/ResourceOwnershipSelect.ee.vue'; import ResourceFiltersDropdown from '@/components/forms/ResourceFiltersDropdown.vue'; import { mapStores } from 'pinia'; import { useSettingsStore } from '@/stores/settings'; import { useUsersStore } from '@/stores/users'; -import { DatatableColumn } from 'n8n-design-system'; +import type { N8nInput } from 'n8n-design-system'; +import type { DatatableColumn } from 'n8n-design-system'; export interface IResource { id: string; @@ -216,6 +217,7 @@ interface IFilters { } type IResourceKeyType = 'credentials' | 'workflows'; +type SearchRef = InstanceType<typeof N8nInput>; const filterKeys = ['ownedBy', 'sharedWith']; @@ -247,7 +249,7 @@ export default mixins(showMessage, debounceHelper).extend({ }, initialize: { type: Function as PropType<() => Promise<void>>, - default: () => () => Promise.resolve(), + default: () => async () => {}, }, filters: { type: Object, @@ -405,7 +407,7 @@ export default mixins(showMessage, debounceHelper).extend({ }, focusSearchInput() { if (this.$refs.search) { - (this.$refs.search as Vue & { focus: () => void }).focus(); + (this.$refs.search as SearchRef).focus(); } }, setOwnerSubview(active: boolean) { diff --git a/packages/editor-ui/src/components/transitions/SlideTransition.vue b/packages/editor-ui/src/components/transitions/SlideTransition.vue index e4fa497a20f54..872e1f43ea274 100644 --- a/packages/editor-ui/src/components/transitions/SlideTransition.vue +++ b/packages/editor-ui/src/components/transitions/SlideTransition.vue @@ -5,9 +5,9 @@ </template> <script lang="ts"> -import Vue from 'vue'; +import { defineComponent } from 'vue'; -export default Vue.extend({ +export default defineComponent({ name: 'SlideTransition', }); </script> @@ -15,7 +15,7 @@ export default Vue.extend({ <style lang="scss" scoped> .slide-leave-active, .slide-enter-active { - transition: 0.3s ease; + transition: 200ms ease; } .slide-leave-to, .slide-enter { diff --git a/packages/editor-ui/src/composables/__tests__/useHistoryHelper.test.ts b/packages/editor-ui/src/composables/__tests__/useHistoryHelper.test.ts index e31753736886c..069b2f30b7196 100644 --- a/packages/editor-ui/src/composables/__tests__/useHistoryHelper.test.ts +++ b/packages/editor-ui/src/composables/__tests__/useHistoryHelper.test.ts @@ -4,7 +4,7 @@ import { render } from '@testing-library/vue'; import userEvent from '@testing-library/user-event'; import { useHistoryHelper } from '../useHistoryHelper'; import { defineComponent } from 'vue'; -import { Route } from 'vue-router'; +import type { Route } from 'vue-router'; const undoMock = vi.fn(); const redoMock = vi.fn(); diff --git a/packages/editor-ui/src/composables/__tests__/useUniqueNodeName.test.ts b/packages/editor-ui/src/composables/__tests__/useUniqueNodeName.test.ts new file mode 100644 index 0000000000000..3bdf653243d2c --- /dev/null +++ b/packages/editor-ui/src/composables/__tests__/useUniqueNodeName.test.ts @@ -0,0 +1,79 @@ +import { setActivePinia } from 'pinia'; +import { createTestingPinia } from '@pinia/testing'; +import { useUniqueNodeName } from '@/composables/useUniqueNodeName'; +import { useNodeTypesStore } from '@/stores/nodeTypes'; +import { useWorkflowsStore } from '@/stores/workflows'; + +describe('useUniqueNodeName', () => { + beforeAll(() => { + setActivePinia(createTestingPinia()); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + test('should return a unique node name for an alphabetic node name', () => { + const workflowsStore = useWorkflowsStore(); + + const mockCanvasNames = new Set(['Hello']); + + vi.spyOn(workflowsStore, 'canvasNames', 'get').mockReturnValue(mockCanvasNames); + + const { uniqueNodeName } = useUniqueNodeName(); + + expect(uniqueNodeName('Hello')).toBe('Hello1'); + + mockCanvasNames.add('Hello1'); + + expect(uniqueNodeName('Hello')).toBe('Hello2'); + }); + + test('should return a unique node name for a numeric node name', () => { + const workflowsStore = useWorkflowsStore(); + + const mockCanvasNames = new Set(['123']); + + vi.spyOn(workflowsStore, 'canvasNames', 'get').mockReturnValue(mockCanvasNames); + + const { uniqueNodeName } = useUniqueNodeName(); + + expect(uniqueNodeName('123')).toBe('123-1'); + + mockCanvasNames.add('123-1'); + + expect(uniqueNodeName('123')).toBe('123-2'); + }); + + test('should return a unique node name for a number-suffixed node name', () => { + const workflowsStore = useWorkflowsStore(); + const nodeTypesStore = useNodeTypesStore(); + + const mockCanvasNames = new Set(['S3']); + + vi.spyOn(workflowsStore, 'canvasNames', 'get').mockReturnValue(mockCanvasNames); + vi.spyOn(nodeTypesStore, 'allNodeTypes', 'get').mockReturnValue([ + { + displayName: 'S3', + name: 'S3', + description: '', + version: 1, + inputs: [''], + outputs: [''], + group: [''], + properties: [], + defaults: { + name: 'S3', + }, + }, + ]); + + const { uniqueNodeName } = useUniqueNodeName(); + + expect(uniqueNodeName('S3')).toBe('S31'); + + mockCanvasNames.add('S31'); + + expect(uniqueNodeName('S3')).toBe('S32'); + }); +}); diff --git a/packages/editor-ui/src/composables/useCanvasMouseSelect.ts b/packages/editor-ui/src/composables/useCanvasMouseSelect.ts index 51b1eac9d8c06..02a10a0c0d7f8 100644 --- a/packages/editor-ui/src/composables/useCanvasMouseSelect.ts +++ b/packages/editor-ui/src/composables/useCanvasMouseSelect.ts @@ -1,4 +1,4 @@ -import { INodeUi, XYPosition } from '@/Interface'; +import type { INodeUi, XYPosition } from '@/Interface'; import useDeviceSupport from './useDeviceSupport'; import { useUIStore } from '@/stores/ui'; @@ -10,7 +10,7 @@ import { SIDEBAR_WIDTH, SIDEBAR_WIDTH_EXPANDED, } from '@/utils/nodeViewUtils'; -import { ref, watchEffect, onMounted, computed, onUnmounted } from 'vue'; +import { ref, onMounted, computed } from 'vue'; import { useCanvasStore } from '@/stores/canvas'; interface ExtendedHTMLSpanElement extends HTMLSpanElement { diff --git a/packages/editor-ui/src/composables/useExternalHooks.ts b/packages/editor-ui/src/composables/useExternalHooks.ts index ff3e3ab48123c..cd9b1049b5060 100644 --- a/packages/editor-ui/src/composables/useExternalHooks.ts +++ b/packages/editor-ui/src/composables/useExternalHooks.ts @@ -1,12 +1,12 @@ -import { IExternalHooks } from '@/Interface'; -import { IDataObject } from 'n8n-workflow'; +import type { IExternalHooks } from '@/Interface'; +import type { IDataObject } from 'n8n-workflow'; import { useWebhooksStore } from '@/stores'; -import { runExternalHook } from '@/mixins/externalHooks'; +import { runExternalHook } from '@/utils'; export function useExternalHooks(): IExternalHooks { return { async run(eventName: string, metadata?: IDataObject): Promise<void> { - return await runExternalHook.call(this, eventName, useWebhooksStore(), metadata); + return await runExternalHook(eventName, useWebhooksStore(), metadata); }, }; } diff --git a/packages/editor-ui/src/composables/useHistoryHelper.ts b/packages/editor-ui/src/composables/useHistoryHelper.ts index 79fc5f6d72049..4f9b23bfb0614 100644 --- a/packages/editor-ui/src/composables/useHistoryHelper.ts +++ b/packages/editor-ui/src/composables/useHistoryHelper.ts @@ -1,15 +1,16 @@ import { MAIN_HEADER_TABS } from '@/constants'; import { useNDVStore } from '@/stores/ndv'; -import { BulkCommand, Undoable } from '@/models/history'; +import type { Undoable } from '@/models/history'; +import { BulkCommand } from '@/models/history'; import { useHistoryStore } from '@/stores/history'; import { useUIStore } from '@/stores/ui'; -import { ref, onMounted, onUnmounted, Ref, nextTick, getCurrentInstance } from 'vue'; +import { ref, onMounted, onUnmounted, nextTick, getCurrentInstance } from 'vue'; import { Command } from '@/models/history'; import { useDebounceHelper } from './useDebounce'; import useDeviceSupportHelpers from './useDeviceSupport'; import { getNodeViewTab } from '@/utils'; -import { Route } from 'vue-router'; +import type { Route } from 'vue-router'; const UNDO_REDO_DEBOUNCE_INTERVAL = 100; diff --git a/packages/editor-ui/src/composables/useI18n.ts b/packages/editor-ui/src/composables/useI18n.ts index 61d7bf807a50d..f8b73cfd2a650 100644 --- a/packages/editor-ui/src/composables/useI18n.ts +++ b/packages/editor-ui/src/composables/useI18n.ts @@ -1,5 +1,19 @@ import { i18n } from '@/plugins/i18n'; +import { useRootStore } from '@/stores/n8nRootStore'; export function useI18n() { - return i18n; + const isEnglishLocale = useRootStore().defaultLocale === 'en'; + + function localizeNodeName(nodeName: string, type: string) { + if (isEnglishLocale) return nodeName; + + const nodeTypeName = i18n.shortNodeType(type); + + return i18n.headerText({ + key: `headers.${nodeTypeName}.displayName`, + fallback: nodeName, + }); + } + + return { i18n, localizeNodeName }; } diff --git a/packages/editor-ui/src/composables/useMessage.ts b/packages/editor-ui/src/composables/useMessage.ts index 2a5dd6e8e7ac8..3fbecc5807996 100644 --- a/packages/editor-ui/src/composables/useMessage.ts +++ b/packages/editor-ui/src/composables/useMessage.ts @@ -4,8 +4,8 @@ import { Message, MessageBox } from 'element-ui'; export function useMessage() { async function alert( message: string, - configOrTitle: string | ElMessageBoxOptions | undefined, - config: ElMessageBoxOptions | undefined, + configOrTitle?: string | ElMessageBoxOptions, + config?: ElMessageBoxOptions, ) { const resolvedConfig = { ...(config || (typeof configOrTitle === 'object' ? configOrTitle : {})), @@ -21,8 +21,8 @@ export function useMessage() { async function confirm( message: string, - configOrTitle: string | ElMessageBoxOptions | undefined, - config: ElMessageBoxOptions | undefined, + configOrTitle?: string | ElMessageBoxOptions, + config?: ElMessageBoxOptions, ) { const resolvedConfig = { ...(config || (typeof configOrTitle === 'object' ? configOrTitle : {})), @@ -41,8 +41,8 @@ export function useMessage() { async function prompt( message: string, - configOrTitle: string | ElMessageBoxOptions | undefined, - config: ElMessageBoxOptions | undefined, + configOrTitle?: string | ElMessageBoxOptions, + config?: ElMessageBoxOptions, ) { const resolvedConfig = { ...(config || (typeof configOrTitle === 'object' ? configOrTitle : {})), diff --git a/packages/editor-ui/src/composables/useTelemetry.ts b/packages/editor-ui/src/composables/useTelemetry.ts index 856b15477b84e..9c88b3e57d729 100644 --- a/packages/editor-ui/src/composables/useTelemetry.ts +++ b/packages/editor-ui/src/composables/useTelemetry.ts @@ -1,4 +1,5 @@ -import { Telemetry, telemetry } from '@/plugins/telemetry'; +import type { Telemetry } from '@/plugins/telemetry'; +import { telemetry } from '@/plugins/telemetry'; export function useTelemetry(): Telemetry { return telemetry; diff --git a/packages/editor-ui/src/composables/useTitleChange.ts b/packages/editor-ui/src/composables/useTitleChange.ts new file mode 100644 index 0000000000000..55cd41988fbae --- /dev/null +++ b/packages/editor-ui/src/composables/useTitleChange.ts @@ -0,0 +1,19 @@ +import type { WorkflowTitleStatus } from '@/Interface'; + +export function useTitleChange() { + return { + titleSet(workflow: string, status: WorkflowTitleStatus) { + let icon = 'ā ļø'; + if (status === 'EXECUTING') { + icon = 'š'; + } else if (status === 'IDLE') { + icon = 'ā¶ļø'; + } + + window.document.title = `n8n - ${icon} ${workflow}`; + }, + titleReset() { + document.title = 'n8n - Workflow Automation'; + }, + }; +} diff --git a/packages/editor-ui/src/composables/useToast.ts b/packages/editor-ui/src/composables/useToast.ts index 97fff04d7030a..996de86e7c544 100644 --- a/packages/editor-ui/src/composables/useToast.ts +++ b/packages/editor-ui/src/composables/useToast.ts @@ -18,7 +18,7 @@ export function useToast() { const telemetry = useTelemetry(); const workflowsStore = useWorkflowsStore(); const externalHooks = useExternalHooks(); - const i18n = useI18n(); + const { i18n } = useI18n(); function showMessage( messageData: Omit<ElNotificationOptions, 'message'> & { message?: string }, diff --git a/packages/editor-ui/src/composables/useUniqueNodeName.ts b/packages/editor-ui/src/composables/useUniqueNodeName.ts new file mode 100644 index 0000000000000..23eba9a4f7dc2 --- /dev/null +++ b/packages/editor-ui/src/composables/useUniqueNodeName.ts @@ -0,0 +1,108 @@ +import { useWorkflowsStore } from '@/stores/workflows'; +import { useNodeTypesStore } from '@/stores/nodeTypes'; + +export function useUniqueNodeName() { + /** + * All in-store node name defaults ending with a number, e.g. + * `AWS S3`, `Magento 2`, `MSG91`, `S3`, `SIGNL4`, `sms77` + */ + function numberSuffixedNames() { + return useNodeTypesStore().allNodeTypes.reduce<string[]>((acc, nodeType) => { + if (typeof nodeType.defaults.name !== 'string') { + throw new Error('Expected node name default to be a string'); + } + + if (/\d$/.test(nodeType.defaults.name)) acc.push(nodeType.defaults.name); + + return acc; + }, []); + } + + /** + * Create a unique node name from an original name, based on the names of + * all nodes on canvas and any extra names that cannot be used. + */ + function uniqueNodeName(originalName: string, extraNames: string[] = []) { + const { canvasNames } = useWorkflowsStore(); + + const isUnique = !canvasNames.has(originalName) && !extraNames.includes(originalName); + + if (isUnique) return originalName; + + const nsn = numberSuffixedNames().find((nsn) => originalName.startsWith(nsn)); + + // edge case, number suffix as part of name: S3 -> S31 -> S32 + + if (nsn) { + let unique = ''; + let index = 1; + + const remainder = originalName.split(nsn).pop(); + + const lastChar = remainder?.[remainder.length - 1]; + + if (lastChar && Number.isInteger(Number(lastChar))) { + index = parseInt(lastChar, 10); + originalName = originalName.slice(0, -1); + } + + unique = originalName; + + while (canvasNames.has(unique) || extraNames.includes(unique)) { + unique = originalName + index++; + } + + return unique; + } + + // edge case, all-number name: 123 -> 123-1 -> 123-2 + + if (/^\d+-?\d*$/.test(originalName)) { + let unique = ''; + let index = 1; + + const match = originalName.match(/(?<base>\d+)-?(?<suffix>\d*)/); + + if (!match?.groups) { + throw new Error('Failed to find match for unique name'); + } + + if (match?.groups?.suffix !== '') { + index = parseInt(match.groups.suffix, 10); + } + + unique = match.groups.base; + + while (canvasNames.has(unique) || extraNames.includes(unique)) { + unique = match.groups.base + '-' + index++; + } + + return unique; + } + + // normal case: A -> A1 -> A2 + + let unique = ''; + let index = 1; + + const match = originalName.match(/(?<base>.*\D+)(?<suffix>\d*)/); + + if (!match?.groups) { + throw new Error('Failed to find match for unique name'); + } + + if (match?.groups?.suffix !== '') { + index = parseInt(match.groups.suffix, 10); + } + + unique = match.groups.base; + + while (canvasNames.has(unique) || extraNames.includes(unique)) { + unique = match.groups.base + index++; + } + + return unique; + } + + return { uniqueNodeName }; +} diff --git a/packages/editor-ui/src/composables/useUpgradeLink.ts b/packages/editor-ui/src/composables/useUpgradeLink.ts index 89cbaeeb4aca5..cbe6fb6d83f39 100644 --- a/packages/editor-ui/src/composables/useUpgradeLink.ts +++ b/packages/editor-ui/src/composables/useUpgradeLink.ts @@ -1,4 +1,4 @@ -import { BaseTextKey } from '@/plugins/i18n'; +import type { BaseTextKey } from '@/plugins/i18n'; import { useUIStore, useUsageStore } from '@/stores'; import { useI18n } from '@/composables'; import { computed } from 'vue'; @@ -6,7 +6,7 @@ import { computed } from 'vue'; export function useUpgradeLink(queryParams = { default: '', desktop: '' }) { const uiStore = useUIStore(); const usageStore = useUsageStore(); - const i18n = useI18n(); + const { i18n } = useI18n(); const upgradeLinkUrl = computed(() => { const linkUrlTranslationKey = uiStore.contextBasedTranslationKeys.upgradeLinkUrl as BaseTextKey; diff --git a/packages/editor-ui/src/constants.ts b/packages/editor-ui/src/constants.ts index f600da467ac3a..501fbbcdebb6f 100644 --- a/packages/editor-ui/src/constants.ts +++ b/packages/editor-ui/src/constants.ts @@ -1,4 +1,4 @@ -import { NodeCreatorOpenSource } from './Interface'; +import type { NodeCreatorOpenSource } from './Interface'; export const MAX_WORKFLOW_SIZE = 16777216; // Workflow size limit in bytes export const MAX_WORKFLOW_PINNED_DATA_SIZE = 12582912; // Workflow pinned data size limit in bytes @@ -165,28 +165,12 @@ export const NODE_CREATOR_OPEN_SOURCES: Record< NODE_CONNECTION_DROP: 'node_connection_drop', '': '', }; - export const CORE_NODES_CATEGORY = 'Core Nodes'; -export const COMMUNICATION_CATEGORY = 'Communication'; export const CUSTOM_NODES_CATEGORY = 'Custom Nodes'; -export const RECOMMENDED_CATEGORY = 'Recommended'; -export const SUBCATEGORY_DESCRIPTIONS: { - [category: string]: { [subcategory: string]: string }; -} = { - 'Core Nodes': { - // this - all subkeys are set from codex - Flow: 'Branches, core triggers, merge data', - Files: 'Work with CSV, XML, text, images etc.', - 'Data Transformation': 'Manipulate data fields, run code', - Helpers: 'HTTP Requests (API calls), date and time, scrape HTML', - }, -}; -export const REGULAR_NODE_FILTER = 'Regular'; -export const TRIGGER_NODE_FILTER = 'Trigger'; -export const ALL_NODE_FILTER = 'All'; +export const DEFAULT_SUBCATEGORY = '*'; +export const REGULAR_NODE_CREATOR_VIEW = 'Regular'; +export const TRIGGER_NODE_CREATOR_VIEW = 'Trigger'; export const UNCATEGORIZED_CATEGORY = 'Miscellaneous'; -export const UNCATEGORIZED_SUBCATEGORY = 'Helpers'; -export const PERSONALIZED_CATEGORY = 'Suggested Nodes'; export const OTHER_TRIGGER_NODES_SUBCATEGORY = 'Other Trigger Nodes'; export const TRANSFORM_DATA_SUBCATEGORY = 'Data Transformation'; export const FILES_SUBCATEGORY = 'Files'; @@ -355,14 +339,9 @@ export const HIRING_BANNER = ` Love n8n? Help us build the future of automation! https://n8n.io/careers?utm_source=n8n_user&utm_medium=console_output `; -export const NODE_TYPE_COUNT_MAPPER = { - [REGULAR_NODE_FILTER]: ['regularCount'], - [TRIGGER_NODE_FILTER]: ['triggerCount'], - [ALL_NODE_FILTER]: ['triggerCount', 'regularCount'], -}; export const TEMPLATES_NODES_FILTER = ['n8n-nodes-base.start', 'n8n-nodes-base.respondToWebhook']; -export enum VIEWS { +export const enum VIEWS { HOMEPAGE = 'Homepage', COLLECTION = 'TemplatesCollectionView', EXECUTIONS = 'Executions', @@ -398,7 +377,7 @@ export enum VIEWS { VERSION_CONTROL = 'VersionControl', } -export enum FAKE_DOOR_FEATURES { +export const enum FAKE_DOOR_FEATURES { ENVIRONMENTS = 'environments', LOGGING = 'logging', SSO = 'sso', @@ -443,7 +422,7 @@ export const MAPPING_PARAMS = [ export const DEFAULT_STICKY_HEIGHT = 160; export const DEFAULT_STICKY_WIDTH = 240; -export enum WORKFLOW_MENU_ACTIONS { +export const enum WORKFLOW_MENU_ACTIONS { DUPLICATE = 'duplicate', DOWNLOAD = 'download', IMPORT_FROM_URL = 'import-from-url', @@ -455,7 +434,7 @@ export enum WORKFLOW_MENU_ACTIONS { /** * Enterprise edition */ -export enum EnterpriseEditionFeature { +export const enum EnterpriseEditionFeature { AdvancedExecutionFilters = 'advancedExecutionFilters', Sharing = 'sharing', Ldap = 'ldap', @@ -466,7 +445,7 @@ export enum EnterpriseEditionFeature { } export const MAIN_NODE_PANEL_WIDTH = 360; -export enum MAIN_HEADER_TABS { +export const enum MAIN_HEADER_TABS { WORKFLOW = 'workflow', EXECUTIONS = 'executions', SETTINGS = 'settings', @@ -504,7 +483,7 @@ export const CURL_IMPORT_NODES_PROTOCOLS: { [key: string]: string } = { imaps: 'IMAP', }; -export enum STORES { +export const enum STORES { COMMUNITY_NODES = 'communityNodes', ROOT = 'root', SETTINGS = 'settings', @@ -523,7 +502,7 @@ export enum STORES { HISTORY = 'history', } -export enum SignInType { +export const enum SignInType { LDAP = 'ldap', EMAIL = 'email', } @@ -538,12 +517,34 @@ export const KEEP_AUTH_IN_NDV_FOR_NODES = [HTTP_REQUEST_NODE_TYPE, WEBHOOK_NODE_ export const MAIN_AUTH_FIELD_NAME = 'authentication'; export const NODE_RESOURCE_FIELD_NAME = 'resource'; +export const AUTO_INSERT_ACTION_EXPERIMENT = { + name: '003_auto_insert_action', + control: 'control', + variant: 'variant', +}; + export const TEMPLATE_EXPERIMENT = { name: '002_remove_templates', control: 'control', variant: 'variant', }; -export const EXPERIMENTS_TO_TRACK = [TEMPLATE_EXPERIMENT.name]; +export const EXPERIMENTS_TO_TRACK = [TEMPLATE_EXPERIMENT.name, AUTO_INSERT_ACTION_EXPERIMENT.name]; export const NODE_TYPES_EXCLUDED_FROM_OUTPUT_NAME_APPEND = [FILTER_NODE_TYPE]; + +export const ALLOWED_HTML_ATTRIBUTES = ['href', 'name', 'target', 'title', 'class', 'id', 'style']; + +export const ALLOWED_HTML_TAGS = [ + 'p', + 'strong', + 'b', + 'code', + 'a', + 'br', + 'i', + 'em', + 'small', + 'details', + 'summary', +]; diff --git a/packages/editor-ui/src/main.ts b/packages/editor-ui/src/main.ts index b95beda7f2876..9359589d17edd 100644 --- a/packages/editor-ui/src/main.ts +++ b/packages/editor-ui/src/main.ts @@ -3,13 +3,11 @@ import Vue from 'vue'; import './plugins'; -import 'prismjs'; -import 'prismjs/themes/prism.css'; -import 'vue-prism-editor/dist/VuePrismEditor.css'; import 'vue-json-pretty/lib/styles.css'; import '@jsplumb/browser-ui/css/jsplumbtoolkit.css'; import 'n8n-design-system/css/index.scss'; import './n8n-theme.scss'; + import './styles/autocomplete-theme.scss'; import '@fontsource/open-sans/latin-400.css'; @@ -19,16 +17,14 @@ import '@fontsource/open-sans/latin-700.css'; import App from '@/App.vue'; import router from './router'; -import { runExternalHook } from '@/mixins/externalHooks'; +import { runExternalHook } from '@/utils'; import { TelemetryPlugin } from './plugins/telemetry'; import { I18nPlugin, i18nInstance } from './plugins/i18n'; import { createPinia, PiniaVuePlugin } from 'pinia'; -import { useWebhooksStore } from './stores/webhooks'; -import { useUsersStore } from './stores/users'; +import { useWebhooksStore, useUsersStore } from '@/stores'; import { VIEWS } from '@/constants'; -import { useUIStore } from './stores/ui'; Vue.config.productionTip = false; diff --git a/packages/editor-ui/src/mixins/completionManager.ts b/packages/editor-ui/src/mixins/completionManager.ts index ba4c39dbfd4b8..6b73ea645b296 100644 --- a/packages/editor-ui/src/mixins/completionManager.ts +++ b/packages/editor-ui/src/mixins/completionManager.ts @@ -1,6 +1,6 @@ import mixins from 'vue-typed-mixins'; import { ExpressionExtensions } from 'n8n-workflow'; -import { EditorView, ViewUpdate } from '@codemirror/view'; +import type { EditorView, ViewUpdate } from '@codemirror/view'; import { expressionManager } from './expressionManager'; diff --git a/packages/editor-ui/src/mixins/copyPaste.ts b/packages/editor-ui/src/mixins/copyPaste.ts index eee52394d99ac..49c4ae830d233 100644 --- a/packages/editor-ui/src/mixins/copyPaste.ts +++ b/packages/editor-ui/src/mixins/copyPaste.ts @@ -2,10 +2,10 @@ * Captures any pasted data and sends it to method "receivedCopyPasteData" which has to be * defined on the component which uses this mixin */ -import Vue from 'vue'; +import { defineComponent } from 'vue'; import { debounce } from 'lodash-es'; -export const copyPaste = Vue.extend({ +export const copyPaste = defineComponent({ data() { return { copyPasteElementsGotCreated: false, diff --git a/packages/editor-ui/src/mixins/debounce.ts b/packages/editor-ui/src/mixins/debounce.ts index 7c491780aaf3d..217db45d7a66e 100644 --- a/packages/editor-ui/src/mixins/debounce.ts +++ b/packages/editor-ui/src/mixins/debounce.ts @@ -1,7 +1,7 @@ import { debounce } from 'lodash-es'; -import Vue from 'vue'; +import { defineComponent } from 'vue'; -export const debounceHelper = Vue.extend({ +export const debounceHelper = defineComponent({ data() { return { debouncedFunctions: [] as any[], diff --git a/packages/editor-ui/src/mixins/deviceSupportHelpers.ts b/packages/editor-ui/src/mixins/deviceSupportHelpers.ts index 6b316bf87d8f1..a8f87f8820aee 100644 --- a/packages/editor-ui/src/mixins/deviceSupportHelpers.ts +++ b/packages/editor-ui/src/mixins/deviceSupportHelpers.ts @@ -1,6 +1,6 @@ -import Vue from 'vue'; +import { defineComponent } from 'vue'; -export const deviceSupportHelpers = Vue.extend({ +export const deviceSupportHelpers = defineComponent({ data() { return { // @ts-ignore msMaxTouchPoints is deprecated but must fix tablet bugs before fixing this.. otherwise breaks touchscreen computers diff --git a/packages/editor-ui/src/mixins/emitter.ts b/packages/editor-ui/src/mixins/emitter.ts index ab645d6e10c9a..913b9c295b7dc 100644 --- a/packages/editor-ui/src/mixins/emitter.ts +++ b/packages/editor-ui/src/mixins/emitter.ts @@ -1,39 +1,43 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import Vue from 'vue'; +import { defineComponent } from 'vue'; -function broadcast(componentName: string, eventName: string, params: any) { - // @ts-ignore - (this as Vue).$children.forEach((child) => { +function broadcast( + this: InstanceType<typeof EmitterMixin>, + componentName: string, + eventName: string, + params: any, +) { + this.$children.forEach((child) => { const name = child.$options.name; if (name === componentName) { - // @ts-ignore // eslint-disable-next-line prefer-spread - child.$emit.apply(child, [eventName].concat(params)); + child.$emit.apply(child, [eventName].concat(params) as Parameters<typeof child.$emit>); } else { - // @ts-ignore - broadcast.apply(child, [componentName, eventName].concat([params])); + broadcast.apply( + child as InstanceType<typeof EmitterMixin>, + [componentName, eventName].concat([params]) as Parameters<typeof broadcast>, + ); } }); } -export default Vue.extend({ +const EmitterMixin = defineComponent({ methods: { $dispatch(componentName: string, eventName: string, params: any) { let parent = this.$parent || this.$root; let name = parent.$options.name; while (parent && (!name || name !== componentName)) { - parent = parent.$parent; + parent = parent.$parent as InstanceType<typeof EmitterMixin>; if (parent) { name = parent.$options.name; } } if (parent) { - // @ts-ignore // eslint-disable-next-line prefer-spread - parent.$emit.apply(parent, [eventName].concat(params)); + parent.$emit.apply(parent, [eventName].concat(params) as Parameters<typeof parent.$emit>); } }, @@ -42,3 +46,5 @@ export default Vue.extend({ }, }, }); + +export default EmitterMixin; diff --git a/packages/editor-ui/src/mixins/executionsHelpers.ts b/packages/editor-ui/src/mixins/executionsHelpers.ts index e4a8f2237f2d5..1fea3ebb0133b 100644 --- a/packages/editor-ui/src/mixins/executionsHelpers.ts +++ b/packages/editor-ui/src/mixins/executionsHelpers.ts @@ -3,7 +3,7 @@ import { i18n as locale } from '@/plugins/i18n'; import { mapStores } from 'pinia'; import mixins from 'vue-typed-mixins'; import { genericHelpers } from './genericHelpers'; -import { IExecutionsSummary } from 'n8n-workflow'; +import type { IExecutionsSummary } from 'n8n-workflow'; export interface IExecutionUIData { name: string; diff --git a/packages/editor-ui/src/mixins/expressionManager.ts b/packages/editor-ui/src/mixins/expressionManager.ts index 845afc7f878e2..fe2a4fe4e5ebb 100644 --- a/packages/editor-ui/src/mixins/expressionManager.ts +++ b/packages/editor-ui/src/mixins/expressionManager.ts @@ -170,16 +170,21 @@ export const expressionManager = mixins(workflowHelpers).extend({ }; try { - if (!useNDVStore().activeNode) { + const ndvStore = useNDVStore(); + if (!ndvStore.activeNode) { // e.g. credential modal result.resolved = Expression.resolveWithoutWorkflow(resolvable); } else { - result.resolved = this.resolveExpression('=' + resolvable, undefined, { - targetItem: targetItem ?? undefined, - inputNodeName: this.ndvStore.ndvInputNodeName, - inputRunIndex: this.ndvStore.ndvInputRunIndex, - inputBranchIndex: this.ndvStore.ndvInputBranchIndex, - }); + let opts; + if (ndvStore.isInputParentOfActiveNode) { + opts = { + targetItem: targetItem ?? undefined, + inputNodeName: this.ndvStore.ndvInputNodeName, + inputRunIndex: this.ndvStore.ndvInputRunIndex, + inputBranchIndex: this.ndvStore.ndvInputBranchIndex, + }; + } + result.resolved = this.resolveExpression('=' + resolvable, undefined, opts); } } catch (error) { result.resolved = `[${error.message}]`; diff --git a/packages/editor-ui/src/mixins/externalHooks.ts b/packages/editor-ui/src/mixins/externalHooks.ts index 5a336ae125ac8..09b180b039641 100644 --- a/packages/editor-ui/src/mixins/externalHooks.ts +++ b/packages/editor-ui/src/mixins/externalHooks.ts @@ -1,35 +1,10 @@ -import { IExternalHooks } from '@/Interface'; +import type { IExternalHooks } from '@/Interface'; +import type { IDataObject } from 'n8n-workflow'; import { useWebhooksStore } from '@/stores/webhooks'; -import { IDataObject } from 'n8n-workflow'; -import { Store } from 'pinia'; -import Vue from 'vue'; +import { defineComponent } from 'vue'; +import { runExternalHook } from '@/utils'; -declare global { - interface Window { - n8nExternalHooks?: Record< - string, - Record<string, Array<(store: Store, metadata?: IDataObject) => Promise<void>>> - >; - } -} - -export async function runExternalHook(eventName: string, store: Store, metadata?: IDataObject) { - if (!window.n8nExternalHooks) { - return; - } - - const [resource, operator] = eventName.split('.'); - - if (window.n8nExternalHooks[resource]?.[operator]) { - const hookMethods = window.n8nExternalHooks[resource][operator]; - - for (const hookMethod of hookMethods) { - await hookMethod(store, metadata); - } - } -} - -export const externalHooks = Vue.extend({ +export const externalHooks = defineComponent({ methods: { $externalHooks(): IExternalHooks { return { diff --git a/packages/editor-ui/src/mixins/nodeBase.ts b/packages/editor-ui/src/mixins/nodeBase.ts index 10e9f3b0d8c34..79f95814ff426 100644 --- a/packages/editor-ui/src/mixins/nodeBase.ts +++ b/packages/editor-ui/src/mixins/nodeBase.ts @@ -1,16 +1,16 @@ -import { PropType } from 'vue'; +import type { PropType } from 'vue'; import mixins from 'vue-typed-mixins'; -import { INodeUi } from '@/Interface'; +import type { INodeUi } from '@/Interface'; import { deviceSupportHelpers } from '@/mixins/deviceSupportHelpers'; import { NO_OP_NODE_TYPE } from '@/constants'; -import { INodeTypeDescription } from 'n8n-workflow'; +import type { INodeTypeDescription } from 'n8n-workflow'; import { mapStores } from 'pinia'; import { useUIStore } from '@/stores/ui'; import { useWorkflowsStore } from '@/stores/workflows'; import { useNodeTypesStore } from '@/stores/nodeTypes'; -import { BrowserJsPlumbInstance } from '@jsplumb/browser-ui'; -import { Endpoint, EndpointOptions } from '@jsplumb/core'; +import type { BrowserJsPlumbInstance } from '@jsplumb/browser-ui'; +import type { Endpoint, EndpointOptions } from '@jsplumb/core'; import * as NodeViewUtils from '@/utils/nodeViewUtils'; import { useHistoryStore } from '@/stores/history'; import { useCanvasStore } from '@/stores/canvas'; diff --git a/packages/editor-ui/src/mixins/nodeHelpers.ts b/packages/editor-ui/src/mixins/nodeHelpers.ts index e859295107dfa..742804cb793bc 100644 --- a/packages/editor-ui/src/mixins/nodeHelpers.ts +++ b/packages/editor-ui/src/mixins/nodeHelpers.ts @@ -1,16 +1,11 @@ import { EnableNodeToggleCommand } from './../models/history'; import { useHistoryStore } from '@/stores/history'; -import { - PLACEHOLDER_FILLED_AT_EXECUTION_TIME, - CUSTOM_API_CALL_KEY, - EnterpriseEditionFeature, -} from '@/constants'; +import { PLACEHOLDER_FILLED_AT_EXECUTION_TIME, CUSTOM_API_CALL_KEY } from '@/constants'; -import { +import type { IBinaryKeyData, ICredentialType, INodeCredentialDescription, - NodeHelpers, INodeCredentialsDetails, INodeExecutionData, INodeIssues, @@ -24,19 +19,17 @@ import { INodePropertyOptions, IDataObject, } from 'n8n-workflow'; +import { NodeHelpers } from 'n8n-workflow'; -import { +import type { ICredentialsResponse, INodeUi, INodeUpdatePropertiesInformation, IUser, } from '@/Interface'; -import { restApi } from '@/mixins/restApi'; - import { get } from 'lodash-es'; -import mixins from 'vue-typed-mixins'; import { isObjectLiteral } from '@/utils'; import { getCredentialPermissions } from '@/permissions'; import { mapStores } from 'pinia'; @@ -45,8 +38,9 @@ import { useUsersStore } from '@/stores/users'; import { useWorkflowsStore } from '@/stores/workflows'; import { useNodeTypesStore } from '@/stores/nodeTypes'; import { useCredentialsStore } from '@/stores/credentials'; +import Vue from 'vue'; -export const nodeHelpers = mixins(restApi).extend({ +export const nodeHelpers = Vue.extend({ computed: { ...mapStores( useCredentialsStore, diff --git a/packages/editor-ui/src/mixins/pinData.ts b/packages/editor-ui/src/mixins/pinData.ts index dbaa4e79361bb..990d8a2b903bf 100644 --- a/packages/editor-ui/src/mixins/pinData.ts +++ b/packages/editor-ui/src/mixins/pinData.ts @@ -1,6 +1,6 @@ import Vue from 'vue'; -import { INodeUi } from '@/Interface'; -import { INodeTypeDescription, IPinData } from 'n8n-workflow'; +import type { INodeUi } from '@/Interface'; +import type { INodeTypeDescription, IPinData } from 'n8n-workflow'; import { stringSizeInBytes } from '@/utils'; import { MAX_WORKFLOW_PINNED_DATA_SIZE, PIN_DATA_NODE_TYPES_DENYLIST } from '@/constants'; import { mapStores } from 'pinia'; diff --git a/packages/editor-ui/src/mixins/pushConnection.ts b/packages/editor-ui/src/mixins/pushConnection.ts index a43c5a5c35bc9..3c33586b59581 100644 --- a/packages/editor-ui/src/mixins/pushConnection.ts +++ b/packages/editor-ui/src/mixins/pushConnection.ts @@ -1,4 +1,4 @@ -import { +import type { IExecutionResponse, IExecutionsCurrentSummaryExtended, IPushData, @@ -8,10 +8,10 @@ import { import { externalHooks } from '@/mixins/externalHooks'; import { nodeHelpers } from '@/mixins/nodeHelpers'; import { showMessage } from '@/mixins/showMessage'; -import { titleChange } from '@/mixins/titleChange'; +import { useTitleChange } from '@/composables/useTitleChange'; import { workflowHelpers } from '@/mixins/workflowHelpers'; -import { +import type { ExpressionError, IDataObject, INodeTypeNameVersion, @@ -19,8 +19,8 @@ import { IRunExecutionData, IWorkflowBase, SubworkflowOperationError, - TelemetryHelpers, } from 'n8n-workflow'; +import { TelemetryHelpers } from 'n8n-workflow'; import mixins from 'vue-typed-mixins'; import { WORKFLOW_SETTINGS_MODAL_KEY } from '@/constants'; @@ -39,9 +39,13 @@ export const pushConnection = mixins( externalHooks, nodeHelpers, showMessage, - titleChange, workflowHelpers, ).extend({ + setup() { + return { + ...useTitleChange(), + }; + }, data() { return { pushSource: null as WebSocket | EventSource | null, @@ -119,7 +123,7 @@ export const pushConnection = mixins( this.connectRetries++; this.reconnectTimeout = setTimeout( this.attemptReconnect, - Math.min(this.connectRetries * 3000, 30000), // maximum 30 seconds backoff + Math.min(this.connectRetries * 2000, 8000), // maximum 8 seconds backoff ); }, @@ -362,7 +366,7 @@ export const pushConnection = mixins( } // Workflow did start but had been put to wait - this.$titleSet(workflow.name as string, 'IDLE'); + this.titleSet(workflow.name as string, 'IDLE'); this.$showToast({ title: 'Workflow started waiting', message: `${action} <a href="https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.wait/" target="_blank">More info</a>`, @@ -370,7 +374,7 @@ export const pushConnection = mixins( duration: 0, }); } else if (runDataExecuted.finished !== true) { - this.$titleSet(workflow.name as string, 'ERROR'); + this.titleSet(workflow.name as string, 'ERROR'); if ( runDataExecuted.data.resultData.error?.name === 'ExpressionError' && @@ -441,7 +445,7 @@ export const pushConnection = mixins( } } else { // Workflow did execute without a problem - this.$titleSet(workflow.name as string, 'IDLE'); + this.titleSet(workflow.name as string, 'IDLE'); const execution = this.workflowsStore.getWorkflowExecution; if (execution && execution.executedNode) { diff --git a/packages/editor-ui/src/mixins/restApi.ts b/packages/editor-ui/src/mixins/restApi.ts deleted file mode 100644 index f14d197815cc4..0000000000000 --- a/packages/editor-ui/src/mixins/restApi.ts +++ /dev/null @@ -1,225 +0,0 @@ -import Vue from 'vue'; -import { parse } from 'flatted'; - -import { Method } from 'axios'; -import { - IActivationError, - IExecutionsCurrentSummaryExtended, - IExecutionDeleteFilter, - IExecutionPushResponse, - IExecutionResponse, - IExecutionFlattedResponse, - IExecutionsListResponse, - IExecutionsStopData, - IStartRunData, - IWorkflowDb, - IWorkflowShortResponse, - IRestApi, - IWorkflowDataUpdate, - INodeTranslationHeaders, -} from '@/Interface'; -import { - IAbstractEventMessage, - IDataObject, - ILoadOptions, - INodeCredentials, - INodeParameters, - INodePropertyOptions, - INodeTypeDescription, - INodeTypeNameVersion, - IRunExecutionData, -} from 'n8n-workflow'; -import { makeRestApiRequest } from '@/utils'; -import { mapStores } from 'pinia'; -import { useRootStore } from '@/stores/n8nRootStore'; - -/** - * Unflattens the Execution data. - * - * @param {IExecutionFlattedResponse} fullExecutionData The data to unflatten - */ -function unflattenExecutionData(fullExecutionData: IExecutionFlattedResponse): IExecutionResponse { - // Unflatten the data - const returnData: IExecutionResponse = { - ...fullExecutionData, - workflowData: fullExecutionData.workflowData as IWorkflowDb, - data: parse(fullExecutionData.data), - }; - - returnData.finished = returnData.finished ? returnData.finished : false; - - if (fullExecutionData.id) { - returnData.id = fullExecutionData.id; - } - - return returnData; -} - -export const restApi = Vue.extend({ - computed: { - ...mapStores(useRootStore), - }, - methods: { - restApi(): IRestApi { - const self = this; - return { - async makeRestApiRequest( - method: Method, - endpoint: string, - data?: IDataObject, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ): Promise<any> { - return makeRestApiRequest(self.rootStore.getRestApiContext, method, endpoint, data); - }, - getActiveWorkflows: (): Promise<string[]> => { - return self.restApi().makeRestApiRequest('GET', '/active'); - }, - getActivationError: (id: string): Promise<IActivationError | undefined> => { - return self.restApi().makeRestApiRequest('GET', `/active/error/${id}`); - }, - getCurrentExecutions: ( - filter: IDataObject, - ): Promise<IExecutionsCurrentSummaryExtended[]> => { - let sendData = {}; - if (filter) { - sendData = { - filter, - }; - } - return self.restApi().makeRestApiRequest('GET', '/executions-current', sendData); - }, - stopCurrentExecution: (executionId: string): Promise<IExecutionsStopData> => { - return self - .restApi() - .makeRestApiRequest('POST', `/executions-current/${executionId}/stop`); - }, - - getCredentialTranslation: (credentialType): Promise<object> => { - return self - .restApi() - .makeRestApiRequest('GET', '/credential-translation', { credentialType }); - }, - - // Removes a test webhook - removeTestWebhook: (workflowId: string): Promise<boolean> => { - return self.restApi().makeRestApiRequest('DELETE', `/test-webhook/${workflowId}`); - }, - - // Execute a workflow - runWorkflow: async (startRunData: IStartRunData): Promise<IExecutionPushResponse> => { - return self.restApi().makeRestApiRequest('POST', '/workflows/run', startRunData); - }, - - // Creates a new workflow - createNewWorkflow: (sendData: IWorkflowDataUpdate): Promise<IWorkflowDb> => { - return self.restApi().makeRestApiRequest('POST', '/workflows', sendData); - }, - - // Updates an existing workflow - updateWorkflow: ( - id: string, - data: IWorkflowDataUpdate, - forceSave = false, - ): Promise<IWorkflowDb> => { - return self - .restApi() - .makeRestApiRequest( - 'PATCH', - `/workflows/${id}${forceSave ? '?forceSave=true' : ''}`, - data, - ); - }, - - // Deletes a workflow - deleteWorkflow: (name: string): Promise<void> => { - return self.restApi().makeRestApiRequest('DELETE', `/workflows/${name}`); - }, - - // Returns the workflow with the given name - getWorkflow: (id: string): Promise<IWorkflowDb> => { - return self.restApi().makeRestApiRequest('GET', `/workflows/${id}`); - }, - - // Returns all saved workflows - getWorkflows: (filter?: object): Promise<IWorkflowShortResponse[]> => { - let sendData; - if (filter) { - sendData = { - filter, - }; - } - return self.restApi().makeRestApiRequest('GET', '/workflows', sendData); - }, - - // Returns a workflow from a given URL - getWorkflowFromUrl: (url: string): Promise<IWorkflowDb> => { - return self.restApi().makeRestApiRequest('GET', '/workflows/from-url', { url }); - }, - - // Returns the execution with the given name - getExecution: async (id: string): Promise<IExecutionResponse | undefined> => { - const response = await self.restApi().makeRestApiRequest('GET', `/executions/${id}`); - return response && unflattenExecutionData(response); - }, - - // Deletes executions - deleteExecutions: (sendData: IExecutionDeleteFilter): Promise<void> => { - return self.restApi().makeRestApiRequest('POST', '/executions/delete', sendData); - }, - - // Returns the execution with the given name - retryExecution: (id: string, loadWorkflow?: boolean): Promise<boolean> => { - let sendData; - if (loadWorkflow === true) { - sendData = { - loadWorkflow: true, - }; - } - return self.restApi().makeRestApiRequest('POST', `/executions/${id}/retry`, sendData); - }, - - // Returns all saved executions - // TODO: For sure needs some kind of default filter like last day, with max 10 results, ... - getPastExecutions: ( - filter: IDataObject, - limit: number, - lastId?: string, - firstId?: string, - ): Promise<IExecutionsListResponse> => { - let sendData = {}; - if (filter) { - sendData = { - filter, - firstId, - lastId, - limit, - }; - } - - return self.restApi().makeRestApiRequest('GET', '/executions', sendData); - }, - - // Returns all the available timezones - getTimezones: (): Promise<IDataObject> => { - return self.restApi().makeRestApiRequest('GET', '/options/timezones'); - }, - - // Binary data - getBinaryUrl: (dataPath, mode, fileName, mimeType): string => { - let restUrl = self.rootStore.getRestUrl; - if (restUrl.startsWith('/')) restUrl = window.location.origin + restUrl; - const url = new URL(`${restUrl}/data/${dataPath}`); - url.searchParams.append('mode', mode); - if (fileName) url.searchParams.append('fileName', fileName); - if (mimeType) url.searchParams.append('mimeType', mimeType); - return url.toString(); - }, - - // Returns all the available timezones - getExecutionEvents: (id: string): Promise<IAbstractEventMessage[]> => { - return self.restApi().makeRestApiRequest('GET', '/eventbus/execution/' + id); - }, - }; - }, - }, -}); diff --git a/packages/editor-ui/src/mixins/showMessage.ts b/packages/editor-ui/src/mixins/showMessage.ts index 6edb2b99eb933..1158cc187f1a0 100644 --- a/packages/editor-ui/src/mixins/showMessage.ts +++ b/packages/editor-ui/src/mixins/showMessage.ts @@ -1,9 +1,9 @@ // @ts-ignore -import { ElNotificationComponent, ElNotificationOptions } from 'element-ui/types/notification'; +import type { ElNotificationComponent, ElNotificationOptions } from 'element-ui/types/notification'; import mixins from 'vue-typed-mixins'; import { externalHooks } from '@/mixins/externalHooks'; -import { IExecuteContextData, IRunExecutionData } from 'n8n-workflow'; +import type { IExecuteContextData, IRunExecutionData } from 'n8n-workflow'; import type { ElMessageBoxOptions } from 'element-ui/types/message-box'; import type { ElMessageComponent, ElMessageOptions, MessageType } from 'element-ui/types/message'; import { sanitizeHtml } from '@/utils'; diff --git a/packages/editor-ui/src/mixins/titleChange.ts b/packages/editor-ui/src/mixins/titleChange.ts deleted file mode 100644 index 63cde777c44e3..0000000000000 --- a/packages/editor-ui/src/mixins/titleChange.ts +++ /dev/null @@ -1,28 +0,0 @@ -import Vue from 'vue'; - -import { WorkflowTitleStatus } from '@/Interface'; - -export const titleChange = Vue.extend({ - methods: { - /** - * Change title of n8n tab - * - * @param {string} workflow Name of workflow - * @param {WorkflowTitleStatus} status Status of workflow - */ - $titleSet(workflow: string, status: WorkflowTitleStatus) { - let icon = 'ā ļø'; - if (status === 'EXECUTING') { - icon = 'š'; - } else if (status === 'IDLE') { - icon = 'ā¶ļø'; - } - - window.document.title = `n8n - ${icon} ${workflow}`; - }, - - $titleReset() { - document.title = 'n8n - Workflow Automation'; - }, - }, -}); diff --git a/packages/editor-ui/src/mixins/userHelpers.ts b/packages/editor-ui/src/mixins/userHelpers.ts index 8ac4d45e0341f..ecf4bd6d9da8a 100644 --- a/packages/editor-ui/src/mixins/userHelpers.ts +++ b/packages/editor-ui/src/mixins/userHelpers.ts @@ -1,10 +1,10 @@ -import { IPermissions, IUser } from '@/Interface'; +import type { IPermissions } from '@/Interface'; import { isAuthorized } from '@/utils'; import { useUsersStore } from '@/stores/users'; -import Vue from 'vue'; -import { Route } from 'vue-router'; +import { defineComponent } from 'vue'; +import type { Route } from 'vue-router'; -export const userHelpers = Vue.extend({ +export const userHelpers = defineComponent({ methods: { canUserAccessRouteByName(name: string): boolean { const { route } = this.$router.resolve({ name }); diff --git a/packages/editor-ui/src/mixins/workflowActivate.ts b/packages/editor-ui/src/mixins/workflowActivate.ts index 8b1024c75b838..ff3a77cc8b16f 100644 --- a/packages/editor-ui/src/mixins/workflowActivate.ts +++ b/packages/editor-ui/src/mixins/workflowActivate.ts @@ -69,7 +69,7 @@ export const workflowActivate = mixins(externalHooks, workflowHelpers, showMessa return; } - if (isCurrentWorkflow && nodesIssuesExist) { + if (isCurrentWorkflow && nodesIssuesExist && newActiveState === true) { this.$showMessage({ title: this.$locale.baseText( 'workflowActivator.showMessage.activeChangedNodesIssuesExistTrue.title', diff --git a/packages/editor-ui/src/mixins/workflowHelpers.ts b/packages/editor-ui/src/mixins/workflowHelpers.ts index 1327391d7a54e..9dc1b044c68a0 100644 --- a/packages/editor-ui/src/mixins/workflowHelpers.ts +++ b/packages/editor-ui/src/mixins/workflowHelpers.ts @@ -1,14 +1,12 @@ import { - ERROR_TRIGGER_NODE_TYPE, PLACEHOLDER_FILLED_AT_EXECUTION_TIME, PLACEHOLDER_EMPTY_WORKFLOW_ID, - START_NODE_TYPE, WEBHOOK_NODE_TYPE, VIEWS, EnterpriseEditionFeature, } from '@/constants'; -import { +import type { IConnections, IDataObject, INode, @@ -19,20 +17,17 @@ import { INodeCredentials, INodeType, INodeTypes, - INodeTypeData, - IPinData, IRunExecutionData, IWorkflowIssues, IWorkflowDataProxyAdditionalKeys, Workflow, - NodeHelpers, IExecuteData, INodeConnection, IWebhookDescription, - deepCopy, } from 'n8n-workflow'; +import { NodeHelpers } from 'n8n-workflow'; -import { +import type { INodeTypesMaxCount, INodeUi, IWorkflowData, @@ -44,7 +39,6 @@ import { } from '../Interface'; import { externalHooks } from '@/mixins/externalHooks'; -import { restApi } from '@/mixins/restApi'; import { nodeHelpers } from '@/mixins/nodeHelpers'; import { showMessage } from '@/mixins/showMessage'; @@ -57,19 +51,17 @@ import { mapStores } from 'pinia'; import { useUIStore } from '@/stores/ui'; import { useWorkflowsStore } from '@/stores/workflows'; import { useRootStore } from '@/stores/n8nRootStore'; -import { IWorkflowSettings } from 'n8n-workflow'; +import type { IWorkflowSettings } from 'n8n-workflow'; import { useNDVStore } from '@/stores/ndv'; import { useTemplatesStore } from '@/stores/templates'; import { useNodeTypesStore } from '@/stores/nodeTypes'; import { useWorkflowsEEStore } from '@/stores/workflows.ee'; import { useUsersStore } from '@/stores/users'; -import { getWorkflowPermissions, IPermissions } from '@/permissions'; -import { ICredentialsResponse } from '@/Interface'; +import type { IPermissions } from '@/permissions'; +import { getWorkflowPermissions } from '@/permissions'; +import type { ICredentialsResponse } from '@/Interface'; import { useEnvironmentsStore } from '@/stores'; -let cachedWorkflowKey: string | null = ''; -let cachedWorkflow: Workflow | null = null; - export function resolveParameter( parameter: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[], opts: { @@ -89,10 +81,6 @@ export function resolveParameter( let parentNode = workflow.getParentNodes(activeNode!.name, inputName, 1); const executionData = useWorkflowsStore().getWorkflowExecution; - if (opts?.inputNodeName && !parentNode.includes(opts.inputNodeName)) { - return null; - } - let runIndexParent = opts?.inputRunIndex ?? 0; const nodeConnection = workflow.getNodeConnectionIndexes(activeNode!.name, parentNode[0]); if (opts.targetItem && opts?.targetItem?.nodeName === activeNode!.name && executionData) { @@ -184,84 +172,20 @@ export function resolveParameter( } function getCurrentWorkflow(copyData?: boolean): Workflow { - const nodes = getNodes(); - const connections = useWorkflowsStore().allConnections; - const cacheKey = JSON.stringify({ nodes, connections }); - if (!copyData && cachedWorkflow && cacheKey === cachedWorkflowKey) { - return cachedWorkflow; - } - cachedWorkflowKey = cacheKey; - - return getWorkflow(nodes, connections, copyData); + return useWorkflowsStore().getCurrentWorkflow(copyData); } -// Returns a shallow copy of the nodes which means that all the data on the lower -// levels still only gets referenced but the top level object is a different one. -// This has the advantage that it is very fast and does not cause problems with vuex -// when the workflow replaces the node-parameters. function getNodes(): INodeUi[] { - const nodes = useWorkflowsStore().allNodes; - const returnNodes: INodeUi[] = []; - - for (const node of nodes) { - returnNodes.push(Object.assign({}, node)); - } - - return returnNodes; + return useWorkflowsStore().getNodes(); } // Returns a workflow instance. function getWorkflow(nodes: INodeUi[], connections: IConnections, copyData?: boolean): Workflow { - const nodeTypes = getNodeTypes(); - let workflowId: string | undefined = useWorkflowsStore().workflowId; - if (workflowId && workflowId === PLACEHOLDER_EMPTY_WORKFLOW_ID) { - workflowId = undefined; - } - - const workflowName = useWorkflowsStore().workflow.name; - - cachedWorkflow = new Workflow({ - id: workflowId, - name: workflowName, - nodes: copyData ? deepCopy(nodes) : nodes, - connections: copyData ? deepCopy(connections) : connections, - active: false, - nodeTypes, - settings: useWorkflowsStore().workflowSettings, - // @ts-ignore - pinData: useWorkflowsStore().getPinData, - }); - - return cachedWorkflow; + return useWorkflowsStore().getWorkflow(nodes, connections, copyData); } function getNodeTypes(): INodeTypes { - const nodeTypes: INodeTypes = { - nodeTypes: {}, - init: async (nodeTypes?: INodeTypeData): Promise<void> => {}, - // @ts-ignore - getByNameAndVersion: (nodeType: string, version?: number): INodeType | undefined => { - const nodeTypeDescription = useNodeTypesStore().getNodeType(nodeType, version); - - if (nodeTypeDescription === null) { - return undefined; - } - - return { - description: nodeTypeDescription, - // As we do not have the trigger/poll functions available in the frontend - // we use the information available to figure out what are trigger nodes - // @ts-ignore - trigger: - (![ERROR_TRIGGER_NODE_TYPE, START_NODE_TYPE].includes(nodeType) && - nodeTypeDescription.inputs.length === 0 && - !nodeTypeDescription.webhooks) || - undefined, - }; - }, - }; - - return nodeTypes; + return useWorkflowsStore().getNodeTypes(); } // Returns connectionInputData to be able to execute an expression. @@ -396,7 +320,7 @@ function executeData( return executeData; } -export const workflowHelpers = mixins(externalHooks, nodeHelpers, restApi, showMessage).extend({ +export const workflowHelpers = mixins(externalHooks, nodeHelpers, showMessage).extend({ computed: { ...mapStores( useNodeTypesStore, @@ -534,7 +458,7 @@ export const workflowHelpers = mixins(externalHooks, nodeHelpers, restApi, showM }, // Returns the currently loaded workflow as JSON. - getWorkflowDataToSave(): Promise<IWorkflowData> { + async getWorkflowDataToSave(): Promise<IWorkflowData> { const workflowNodes = this.workflowsStore.allNodes; const workflowConnections = this.workflowsStore.allConnections; @@ -542,12 +466,8 @@ export const workflowHelpers = mixins(externalHooks, nodeHelpers, restApi, showM const nodes = []; for (let nodeIndex = 0; nodeIndex < workflowNodes.length; nodeIndex++) { - try { - // @ts-ignore - nodeData = this.getNodeDataToSave(workflowNodes[nodeIndex]); - } catch (e) { - return Promise.reject(e); - } + // @ts-ignore + nodeData = this.getNodeDataToSave(workflowNodes[nodeIndex]); nodes.push(nodeData); } @@ -568,7 +488,7 @@ export const workflowHelpers = mixins(externalHooks, nodeHelpers, restApi, showM data.id = workflowId; } - return Promise.resolve(data); + return data; }, // Returns all node-types @@ -735,7 +655,7 @@ export const workflowHelpers = mixins(externalHooks, nodeHelpers, restApi, showM if (isCurrentWorkflow) { data = await this.getWorkflowDataToSave(); } else { - const { versionId } = await this.restApi().getWorkflow(workflowId); + const { versionId } = await this.workflowsStore.fetchWorkflow(workflowId); data.versionId = versionId; } @@ -743,7 +663,7 @@ export const workflowHelpers = mixins(externalHooks, nodeHelpers, restApi, showM data.active = active; } - const workflow = await this.restApi().updateWorkflow(workflowId, data); + const workflow = await this.workflowsStore.updateWorkflow(workflowId, data); this.workflowsStore.setWorkflowVersionId(workflow.versionId); if (isCurrentWorkflow) { @@ -785,7 +705,7 @@ export const workflowHelpers = mixins(externalHooks, nodeHelpers, restApi, showM workflowDataRequest.versionId = this.workflowsStore.workflowVersionId; - const workflowData = await this.restApi().updateWorkflow( + const workflowData = await this.workflowsStore.updateWorkflow( currentWorkflow, workflowDataRequest, forceSave, @@ -902,7 +822,7 @@ export const workflowHelpers = mixins(externalHooks, nodeHelpers, restApi, showM if (tags) { workflowDataRequest.tags = tags; } - const workflowData = await this.restApi().createNewWorkflow(workflowDataRequest); + const workflowData = await this.workflowsStore.createNewWorkflow(workflowDataRequest); this.workflowsStore.addWorkflow(workflowData); @@ -1012,7 +932,7 @@ export const workflowHelpers = mixins(externalHooks, nodeHelpers, restApi, showM async dataHasChanged(id: string) { const currentData = await this.getWorkflowDataToSave(); - const data: IWorkflowDb = await this.restApi().getWorkflow(id); + const data: IWorkflowDb = await this.workflowsStore.fetchWorkflow(id); if (data !== undefined) { const x = { diff --git a/packages/editor-ui/src/mixins/workflowRun.ts b/packages/editor-ui/src/mixins/workflowRun.ts index 0529aedee354b..a1cc0232d9c16 100644 --- a/packages/editor-ui/src/mixins/workflowRun.ts +++ b/packages/editor-ui/src/mixins/workflowRun.ts @@ -1,32 +1,25 @@ -import { IExecutionPushResponse, IExecutionResponse, IStartRunData } from '@/Interface'; +import type { IExecutionPushResponse, IExecutionResponse, IStartRunData } from '@/Interface'; -import { - IRunData, - IRunExecutionData, - IWorkflowBase, - NodeHelpers, - TelemetryHelpers, -} from 'n8n-workflow'; +import type { IRunData, IRunExecutionData, IWorkflowBase } from 'n8n-workflow'; +import { NodeHelpers, TelemetryHelpers } from 'n8n-workflow'; import { externalHooks } from '@/mixins/externalHooks'; -import { restApi } from '@/mixins/restApi'; import { workflowHelpers } from '@/mixins/workflowHelpers'; import { showMessage } from '@/mixins/showMessage'; import mixins from 'vue-typed-mixins'; -import { titleChange } from './titleChange'; +import { useTitleChange } from '@/composables/useTitleChange'; import { mapStores } from 'pinia'; import { useUIStore } from '@/stores/ui'; import { useWorkflowsStore } from '@/stores/workflows'; import { useRootStore } from '@/stores/n8nRootStore'; -export const workflowRun = mixins( - externalHooks, - restApi, - workflowHelpers, - showMessage, - titleChange, -).extend({ +export const workflowRun = mixins(externalHooks, workflowHelpers, showMessage).extend({ + setup() { + return { + ...useTitleChange(), + }; + }, computed: { ...mapStores(useRootStore, useUIStore, useWorkflowsStore), }, @@ -46,7 +39,7 @@ export const workflowRun = mixins( let response: IExecutionPushResponse; try { - response = await this.restApi().runWorkflow(runData); + response = await this.workflowsStore.runWorkflow(runData); } catch (error) { this.uiStore.removeActiveAction('workflowRunning'); throw error; @@ -72,7 +65,7 @@ export const workflowRun = mixins( return; } - this.$titleSet(workflow.name as string, 'EXECUTING'); + this.titleSet(workflow.name as string, 'EXECUTING'); this.clearAllStickyNotifications(); @@ -119,7 +112,7 @@ export const workflowRun = mixins( type: 'error', duration: 0, }); - this.$titleSet(workflow.name as string, 'ERROR'); + this.titleSet(workflow.name as string, 'ERROR'); this.$externalHooks().run('workflowRun.runError', { errorMessages, nodeName }); this.getWorkflowDataToSave().then((workflowData) => { @@ -245,7 +238,7 @@ export const workflowRun = mixins( return runWorkflowApiResponse; } catch (error) { - this.$titleSet(workflow.name as string, 'ERROR'); + this.titleSet(workflow.name as string, 'ERROR'); this.$showError(error, this.$locale.baseText('workflowRun.showError.title')); return undefined; } diff --git a/packages/editor-ui/src/models/history.ts b/packages/editor-ui/src/models/history.ts index 5a404b27f3f2b..f907af1a615c1 100644 --- a/packages/editor-ui/src/models/history.ts +++ b/packages/editor-ui/src/models/history.ts @@ -1,12 +1,12 @@ -import { INodeUi } from '@/Interface'; -import { IConnection } from 'n8n-workflow'; -import { XYPosition } from '../Interface'; +import type { INodeUi } from '@/Interface'; +import type { IConnection } from 'n8n-workflow'; +import type { XYPosition } from '../Interface'; import { createEventBus } from '@/event-bus'; // Command names don't serve any particular purpose in the app // but they make it easier to identify each command on stack // when debugging -export enum COMMANDS { +export const enum COMMANDS { MOVE_NODE = 'moveNode', ADD_NODE = 'addNode', REMOVE_NODE = 'removeNode', diff --git a/packages/editor-ui/src/permissions.ts b/packages/editor-ui/src/permissions.ts index a2300d6399b7f..e2e9b740018f6 100644 --- a/packages/editor-ui/src/permissions.ts +++ b/packages/editor-ui/src/permissions.ts @@ -4,17 +4,11 @@ * @usage getCredentialPermissions(user, credential).isOwner; */ -import { - IUser, - ICredentialsResponse, - IRootState, - IWorkflowDb, - EnvironmentVariable, -} from '@/Interface'; +import type { IUser, ICredentialsResponse, IWorkflowDb } from '@/Interface'; import { EnterpriseEditionFeature, PLACEHOLDER_EMPTY_WORKFLOW_ID } from '@/constants'; import { useSettingsStore } from './stores/settings'; -export enum UserRole { +export const enum UserRole { InstanceOwner = 'isInstanceOwner', ResourceOwner = 'isOwner', ResourceEditor = 'isEditor', diff --git a/packages/editor-ui/src/plugins/codemirror/completions/__tests__/completions.test.ts b/packages/editor-ui/src/plugins/codemirror/completions/__tests__/completions.test.ts index d844359c669e6..4d0a36c95ca39 100644 --- a/packages/editor-ui/src/plugins/codemirror/completions/__tests__/completions.test.ts +++ b/packages/editor-ui/src/plugins/codemirror/completions/__tests__/completions.test.ts @@ -13,7 +13,8 @@ import { } from '@/plugins/codemirror/completions/datatype.completions'; import { mockNodes, mockProxy } from './mock'; -import { CompletionContext, CompletionSource, CompletionResult } from '@codemirror/autocomplete'; +import type { CompletionSource, CompletionResult } from '@codemirror/autocomplete'; +import { CompletionContext } from '@codemirror/autocomplete'; import { EditorState } from '@codemirror/state'; import { n8nLang } from '@/plugins/codemirror/n8nLang'; @@ -285,6 +286,14 @@ describe('Resolution-based completions', () => { ); }); + test('should return completions for: {{ "hello"+input.| }}', () => { + resolveParameterSpy.mockReturnValue($input); + + expect(completions('{{ "hello"+$input.| }}')).toHaveLength( + Reflect.ownKeys($input).length + natives('object').length, + ); + }); + test("should return completions for: {{ $('nodeName').| }}", () => { resolveParameterSpy.mockReturnValue($('Rename')); diff --git a/packages/editor-ui/src/plugins/codemirror/completions/__tests__/mock.ts b/packages/editor-ui/src/plugins/codemirror/completions/__tests__/mock.ts index 811e4276af4ff..9616deb087b58 100644 --- a/packages/editor-ui/src/plugins/codemirror/completions/__tests__/mock.ts +++ b/packages/editor-ui/src/plugins/codemirror/completions/__tests__/mock.ts @@ -1,17 +1,15 @@ import { v4 as uuidv4 } from 'uuid'; -import { +import type { INode, IConnections, IRunExecutionData, - Workflow, IExecuteData, - WorkflowDataProxy, INodeType, INodeTypeData, INodeTypes, IVersionedNodeType, - NodeHelpers, } from 'n8n-workflow'; +import { Workflow, WorkflowDataProxy, NodeHelpers } from 'n8n-workflow'; class NodeTypesClass implements INodeTypes { nodeTypes: INodeTypeData = { diff --git a/packages/editor-ui/src/plugins/codemirror/completions/datatype.completions.ts b/packages/editor-ui/src/plugins/codemirror/completions/datatype.completions.ts index 060a9604fc584..1e7b12914f968 100644 --- a/packages/editor-ui/src/plugins/codemirror/completions/datatype.completions.ts +++ b/packages/editor-ui/src/plugins/codemirror/completions/datatype.completions.ts @@ -1,4 +1,5 @@ -import { ExpressionExtensions, NativeMethods, IDataObject, DocMetadata } from 'n8n-workflow'; +import type { IDataObject, DocMetadata } from 'n8n-workflow'; +import { ExpressionExtensions, NativeMethods } from 'n8n-workflow'; import { DateTime } from 'luxon'; import { i18n } from '@/plugins/i18n'; import { resolveParameter } from '@/mixins/workflowHelpers'; @@ -16,7 +17,7 @@ import { import type { Completion, CompletionContext, CompletionResult } from '@codemirror/autocomplete'; import type { AutocompleteOptionType, ExtensionTypeName, FnToDoc, Resolved } from './types'; import { sanitizeHtml } from '@/utils'; -import { NativeDoc } from 'n8n-workflow/src/Extensions/Extensions'; +import type { NativeDoc } from 'n8n-workflow/src/Extensions/Extensions'; import { isFunctionOption } from './typeGuards'; import { luxonInstanceDocs } from './nativesAutocompleteDocs/luxon.instance.docs'; import { luxonStaticDocs } from './nativesAutocompleteDocs/luxon.static.docs'; @@ -439,11 +440,12 @@ export const objectGlobalOptions = () => { }; const regexes = { - generalRef: /\$[^$]+\.([^{\s])*/, // $input. or $json. or similar ones + generalRef: /\$[^$'"]+\.([^{\s])*/, // $input. or $json. or similar ones selectorRef: /\$\(['"][\S\s]+['"]\)\.([^{\s])*/, // $('nodeName'). numberLiteral: /\((\d+)\.?(\d*)\)\.([^{\s])*/, // (123). or (123.4). - stringLiteral: /(".+"|('.+'))\.([^{\s])*/, // 'abc'. or "abc". + singleQuoteStringLiteral: /('.+')\.([^'{\s])*/, // 'abc'. + doubleQuoteStringLiteral: /(".+")\.([^"{\s])*/, // "abc". dateLiteral: /\(?new Date\(\(?.*?\)\)?\.([^{\s])*/, // new Date(). or (new Date()). arrayLiteral: /(\[.+\])\.([^{\s])*/, // [1, 2, 3]. objectLiteral: /\(\{.*\}\)\.([^{\s])*/, // ({}). diff --git a/packages/editor-ui/src/plugins/codemirror/completions/nativesAutocompleteDocs/luxon.instance.docs.ts b/packages/editor-ui/src/plugins/codemirror/completions/nativesAutocompleteDocs/luxon.instance.docs.ts index 61e7f3e27cde6..9df1f5b6c86cb 100644 --- a/packages/editor-ui/src/plugins/codemirror/completions/nativesAutocompleteDocs/luxon.instance.docs.ts +++ b/packages/editor-ui/src/plugins/codemirror/completions/nativesAutocompleteDocs/luxon.instance.docs.ts @@ -1,4 +1,4 @@ -import { NativeDoc } from 'n8n-workflow/src/Extensions/Extensions'; +import type { NativeDoc } from 'n8n-workflow/src/Extensions/Extensions'; // Autocomplete documentation definition for DateTime instance props and methods // Descriptions are added dynamically so they can be localized diff --git a/packages/editor-ui/src/plugins/codemirror/completions/nativesAutocompleteDocs/luxon.static.docs.ts b/packages/editor-ui/src/plugins/codemirror/completions/nativesAutocompleteDocs/luxon.static.docs.ts index db5fa9f3019dd..8d1ed738c2964 100644 --- a/packages/editor-ui/src/plugins/codemirror/completions/nativesAutocompleteDocs/luxon.static.docs.ts +++ b/packages/editor-ui/src/plugins/codemirror/completions/nativesAutocompleteDocs/luxon.static.docs.ts @@ -1,4 +1,4 @@ -import { NativeDoc } from 'n8n-workflow/src/Extensions/Extensions'; +import type { NativeDoc } from 'n8n-workflow/src/Extensions/Extensions'; // Autocomplete documentation definition for DateTime class static props and methods // Descriptions are added dynamically so they can be localized diff --git a/packages/editor-ui/src/plugins/codemirror/completions/typeGuards.ts b/packages/editor-ui/src/plugins/codemirror/completions/typeGuards.ts index a27b9970111e2..74cbe9ec47a6e 100644 --- a/packages/editor-ui/src/plugins/codemirror/completions/typeGuards.ts +++ b/packages/editor-ui/src/plugins/codemirror/completions/typeGuards.ts @@ -1,4 +1,4 @@ -import { AutocompleteOptionType, FunctionOptionType } from './types'; +import type { AutocompleteOptionType, FunctionOptionType } from './types'; export const isFunctionOption = (value: AutocompleteOptionType): value is FunctionOptionType => { return value === 'native-function' || value === 'extension-function'; diff --git a/packages/editor-ui/src/plugins/codemirror/completions/types.ts b/packages/editor-ui/src/plugins/codemirror/completions/types.ts index 4e12a5db6ff20..e85f05196e9bc 100644 --- a/packages/editor-ui/src/plugins/codemirror/completions/types.ts +++ b/packages/editor-ui/src/plugins/codemirror/completions/types.ts @@ -1,4 +1,4 @@ -import { resolveParameter } from '@/mixins/workflowHelpers'; +import type { resolveParameter } from '@/mixins/workflowHelpers'; import type { DocMetadata } from 'n8n-workflow'; export type Resolved = ReturnType<typeof resolveParameter>; diff --git a/packages/editor-ui/src/plugins/codemirror/inputHandlers/code.inputHandler.ts b/packages/editor-ui/src/plugins/codemirror/inputHandlers/code.inputHandler.ts index 74e026b321032..13bb282908652 100644 --- a/packages/editor-ui/src/plugins/codemirror/inputHandlers/code.inputHandler.ts +++ b/packages/editor-ui/src/plugins/codemirror/inputHandlers/code.inputHandler.ts @@ -1,5 +1,6 @@ import { closeBrackets, completionStatus, insertBracket } from '@codemirror/autocomplete'; -import { codePointAt, codePointSize, Extension } from '@codemirror/state'; +import type { Extension } from '@codemirror/state'; +import { codePointAt, codePointSize } from '@codemirror/state'; import { EditorView } from '@codemirror/view'; const handler = EditorView.inputHandler.of((view, from, to, insert) => { diff --git a/packages/editor-ui/src/plugins/codemirror/inputHandlers/expression.inputHandler.ts b/packages/editor-ui/src/plugins/codemirror/inputHandlers/expression.inputHandler.ts index 01f07b5bfec5d..5337e847c6e7b 100644 --- a/packages/editor-ui/src/plugins/codemirror/inputHandlers/expression.inputHandler.ts +++ b/packages/editor-ui/src/plugins/codemirror/inputHandlers/expression.inputHandler.ts @@ -4,7 +4,8 @@ import { insertBracket, startCompletion, } from '@codemirror/autocomplete'; -import { codePointAt, codePointSize, Extension } from '@codemirror/state'; +import type { Extension } from '@codemirror/state'; +import { codePointAt, codePointSize } from '@codemirror/state'; import { EditorView } from '@codemirror/view'; const handler = EditorView.inputHandler.of((view, from, to, insert) => { diff --git a/packages/editor-ui/src/plugins/codemirror/resolvableHighlighter.ts b/packages/editor-ui/src/plugins/codemirror/resolvableHighlighter.ts index 489d637453a2f..8b5baa51fa082 100644 --- a/packages/editor-ui/src/plugins/codemirror/resolvableHighlighter.ts +++ b/packages/editor-ui/src/plugins/codemirror/resolvableHighlighter.ts @@ -1,4 +1,5 @@ -import { EditorView, Decoration, DecorationSet } from '@codemirror/view'; +import type { DecorationSet } from '@codemirror/view'; +import { EditorView, Decoration } from '@codemirror/view'; import { StateField, StateEffect } from '@codemirror/state'; import { tags } from '@lezer/highlight'; import { syntaxHighlighting, HighlightStyle } from '@codemirror/language'; diff --git a/packages/editor-ui/src/plugins/components.ts b/packages/editor-ui/src/plugins/components.ts index 075742d8451d4..32100d08c087a 100644 --- a/packages/editor-ui/src/plugins/components.ts +++ b/packages/editor-ui/src/plugins/components.ts @@ -1,5 +1,3 @@ -// @ts-nocheck - import Vue from 'vue'; import Fragment from 'vue-fragment'; import VueAgile from 'vue-agile'; @@ -8,7 +6,7 @@ import 'regenerator-runtime/runtime'; import ElementUI from 'element-ui'; import { Loading, MessageBox, Notification } from 'element-ui'; -import { designSystemComponents } from 'n8n-design-system'; +import { N8nPlugin } from 'n8n-design-system'; import EnterpriseEdition from '@/components/EnterpriseEdition.ee.vue'; import { useMessage } from '@/composables/useMessage'; @@ -16,7 +14,7 @@ Vue.use(Fragment.Plugin); Vue.use(VueAgile); Vue.use(ElementUI); -Vue.use(designSystemComponents); +Vue.use(N8nPlugin); Vue.component('enterprise-edition', EnterpriseEdition); diff --git a/packages/editor-ui/src/plugins/connectors/N8nCustomConnector.ts b/packages/editor-ui/src/plugins/connectors/N8nCustomConnector.ts index 75ec0a6ec5831..56d7f1fdb3e6b 100644 --- a/packages/editor-ui/src/plugins/connectors/N8nCustomConnector.ts +++ b/packages/editor-ui/src/plugins/connectors/N8nCustomConnector.ts @@ -1,16 +1,15 @@ -import { PointXY, log, extend, quadrant } from '@jsplumb/util'; +import type { PointXY } from '@jsplumb/util'; +import { quadrant } from '@jsplumb/util'; -import { +import type { Connection, - ArcSegment, - AbstractConnector, ConnectorComputeParams, PaintGeometry, Endpoint, - StraightSegment, Orientation, } from '@jsplumb/core'; -import { AnchorPlacement, ConnectorOptions, Geometry, PaintAxis } from '@jsplumb/common'; +import { ArcSegment, AbstractConnector, StraightSegment } from '@jsplumb/core'; +import type { AnchorPlacement, ConnectorOptions, Geometry, PaintAxis } from '@jsplumb/common'; import { BezierSegment } from '@jsplumb/connector-bezier'; import { isArray } from 'lodash-es'; import { deepCopy } from 'n8n-workflow'; diff --git a/packages/editor-ui/src/plugins/endpoints/N8nPlusEndpointType.ts b/packages/editor-ui/src/plugins/endpoints/N8nPlusEndpointType.ts index 377bba677001f..8481a2e350faf 100644 --- a/packages/editor-ui/src/plugins/endpoints/N8nPlusEndpointType.ts +++ b/packages/editor-ui/src/plugins/endpoints/N8nPlusEndpointType.ts @@ -1,5 +1,6 @@ -import { EndpointHandler, Endpoint, EndpointRepresentation, Overlay } from '@jsplumb/core'; -import { AnchorPlacement, EndpointRepresentationParams } from '@jsplumb/common'; +import type { EndpointHandler, Endpoint, Overlay } from '@jsplumb/core'; +import { EndpointRepresentation } from '@jsplumb/core'; +import type { AnchorPlacement, EndpointRepresentationParams } from '@jsplumb/common'; import { createElement, EVENT_ENDPOINT_MOUSEOVER, diff --git a/packages/editor-ui/src/plugins/i18n/index.ts b/packages/editor-ui/src/plugins/i18n/index.ts index bb3daaf5f70fa..350f4b4688a79 100644 --- a/packages/editor-ui/src/plugins/i18n/index.ts +++ b/packages/editor-ui/src/plugins/i18n/index.ts @@ -1,7 +1,7 @@ import Vue from 'vue'; import axios from 'axios'; import VueI18n from 'vue-i18n'; -import { INodeTranslationHeaders, IRootState } from '@/Interface'; +import type { INodeTranslationHeaders } from '@/Interface'; import { deriveMiddleKey, isNestedInCollectionLike, @@ -13,7 +13,7 @@ import { locale } from 'n8n-design-system'; import englishBaseText from './locales/en.json'; import { useUIStore } from '@/stores/ui'; import { useNDVStore } from '@/stores/ndv'; -import { INodeProperties, INodePropertyCollection, INodePropertyOptions } from 'n8n-workflow'; +import type { INodeProperties, INodePropertyCollection, INodePropertyOptions } from 'n8n-workflow'; Vue.use(VueI18n); locale.use('en'); @@ -534,14 +534,16 @@ function setLanguage(language: string) { } export async function loadLanguage(language?: string) { - if (!language) return Promise.resolve(); + if (!language) return; if (i18nInstance.locale === language) { - return Promise.resolve(setLanguage(language)); + setLanguage(language); + return; } if (loadedLanguages.includes(language)) { - return Promise.resolve(setLanguage(language)); + setLanguage(language); + return; } const { numberFormats, ...rest } = (await import(`./locales/${language}.json`)).default; diff --git a/packages/editor-ui/src/plugins/i18n/locales/en.json b/packages/editor-ui/src/plugins/i18n/locales/en.json index e40f8fe355ba7..fa89ec4ca1955 100644 --- a/packages/editor-ui/src/plugins/i18n/locales/en.json +++ b/packages/editor-ui/src/plugins/i18n/locales/en.json @@ -19,6 +19,7 @@ "generic.any": "Any", "generic.cancel": "Cancel", "generic.confirm": "Confirm", + "generic.deleteWorkflowError": "Problem deleting workflow", "generic.filtersApplied": "Filters are currently applied.", "generic.learnMore": "Learn more", "generic.reset": "Reset", @@ -509,6 +510,7 @@ "executionsList.status": "Status", "executionsList.statusCanceled": "Canceled", "executionsList.statusText": "{status} in {time}", + "executionsList.statusTextWithoutTime": "{status}", "executionsList.statusRunning": "{status} for {time}", "executionsList.statusWaiting": "{status} until {time}", "executionsList.statusUnknown": "Could not complete", @@ -719,29 +721,22 @@ "node.discovery.pinData.canvas": "You can pin this output instead of waiting for a test event. Open node to do so.", "node.discovery.pinData.ndv": "You can pin this output instead of waiting for a test event.", "nodeBase.clickToAddNodeOrDragToConnect": "Click to add node<br />or drag to connect", + "nodeCreator.actionsPlaceholderNode.scheduleTrigger": "On a Schedule", + "nodeCreator.actionsPlaceholderNode.webhook": "On a Webhook call", "nodeCreator.actionsCategory.actions": "Actions", "nodeCreator.actionsCategory.onNewEvent": "On new {event} event", "nodeCreator.actionsCategory.onEvent": "On {event}", "nodeCreator.actionsCategory.triggers": "Triggers", - "nodeCreator.actionsCategory.searchActions": "Search {nodeNameTitle} Actions...", - "nodeCreator.actionsList.apiCall": "Didn't find the right event? Make a <a data-action='addHttpNode'>custom {nodeNameTitle} API call</a>", - "nodeCreator.actionsList.apiCallNoResult": "Nothing found ā try making a <a data-action='addHttpNode'>custom {nodeNameTitle} API call</a>", - "nodeCreator.categoryNames.analytics": "Analytics", - "nodeCreator.categoryNames.communication": "Communication", - "nodeCreator.categoryNames.coreNodes": "Core Nodes", - "nodeCreator.categoryNames.customNodes": "Custom Nodes", - "nodeCreator.categoryNames.dataStorage": "Data & Storage", - "nodeCreator.categoryNames.development": "Development", - "nodeCreator.categoryNames.financeAccounting": "Finance & Accounting", - "nodeCreator.categoryNames.marketingContent": "Marketing & Content", - "nodeCreator.categoryNames.miscellaneous": "Miscellaneous", - "nodeCreator.categoryNames.productivity": "Productivity", - "nodeCreator.categoryNames.sales": "Sales", - "nodeCreator.categoryNames.suggestedNodes": "Suggested Nodes āØ", - "nodeCreator.categoryNames.utility": "Utility", - "nodeCreator.mainPanel.all": "All", - "nodeCreator.mainPanel.regular": "Regular", - "nodeCreator.mainPanel.trigger": "Trigger", + "nodeCreator.actionsCategory.searchActions": "Search {node} Actions...", + "nodeCreator.actionsCategory.noMatchingActions": "No matching Actions. <i>Reset search</i>", + "nodeCreator.actionsCategory.noMatchingTriggers": "No matching Triggers. <i>Reset search</i>", + "nodeCreator.actionsList.apiCall": "Didn't find the right event? Make a <a data-action='addHttpNode'>custom {node} API call</a>", + "nodeCreator.actionsCallout.noActionItems": "We don't have <strong>{nodeName}</strong> actions yet. Have one in mind? Make a <a target=\"_blank\" href=\"https://community.n8n.io/c/feature-requests/5\"> request in our community</a>", + "nodeCreator.actionsCallout.triggersStartWorkflow": "Actions need to be triggered by another node, e.g. at regular intervals with the <strong>Schedule</strong> node. <a target=\"_blank\" href=\"https://docs.n8n.io/integrations/builtin/\"> Learn more</a>", + "nodeCreator.actionsTooltip.triggersStartWorkflow": "A trigger is a step that starts your workflow. <a target=\"_blank\" href=\"https://docs.n8n.io/integrations/builtin/\"> Learn more</a>", + "nodeCreator.actionsTooltip.actionsPerformStep": "Actions perform a step once your workflow has already started. <a target=\"_blank\" href=\"https://docs.n8n.io/integrations/builtin/\"> Learn more</a>", + "nodeCreator.actionsCallout.noTriggerItems": "No <strong>{nodeName}</strong> Triggers available. Users often combine the following Triggers with <strong>{nodeName}</strong> Actions.", + "nodeCreator.categoryNames.otherCategories": "Results in other categories", "nodeCreator.noResults.dontWorryYouCanProbablyDoItWithThe": "Donāt worry, you can probably do it with the", "nodeCreator.noResults.httpRequest": "HTTP Request", "nodeCreator.noResults.node": "node", @@ -749,8 +744,6 @@ "nodeCreator.noResults.requestTheNode": "Request the node", "nodeCreator.noResults.wantUsToMakeItFaster": "Want us to make it faster?", "nodeCreator.noResults.weDidntMakeThatYet": "We didn't make that... yet", - "nodeCreator.noResults.noMatchingActions": "No actions matching your results", - "nodeCreator.noResults.clickToSeeResults": "To see all results, <a data-action='showAllNodeCreatorNodes'>click here</a>", "nodeCreator.noResults.webhook": "Webhook", "nodeCreator.searchBar.searchNodes": "Search nodes...", "nodeCreator.subcategoryDescriptions.appTriggerNodes": "Runs the flow when something happens in an app like Telegram, Notion or Airtable", @@ -767,7 +760,6 @@ "nodeCreator.subcategoryNames.flow": "Flow", "nodeCreator.subcategoryNames.helpers": "Helpers", "nodeCreator.subcategoryNames.otherTriggerNodes": "Other ways...", - "nodeCreator.subcategoryTitles.otherTriggerNodes": "Other Trigger Nodes", "nodeCreator.triggerHelperPanel.addAnotherTrigger": "Add another trigger", "nodeCreator.triggerHelperPanel.addAnotherTriggerDescription": "Triggers start your workflow. Workflows can have multiple triggers.", "nodeCreator.triggerHelperPanel.title": "When should this workflow run?", @@ -1047,7 +1039,8 @@ "resourceLocator.mode.list.disabled.title": "Change to Fixed mode to choose From List", "resourceLocator.mode.list.error.title": "Could not load list", "resourceLocator.mode.list.error.description.part1": "Please", - "resourceLocator.mode.list.error.description.part2": "check your credential", + "resourceLocator.mode.list.error.description.part2.hasCredentials": "check your credential", + "resourceLocator.mode.list.error.description.part2.noCredentials": "add your credential", "resourceLocator.mode.list.noResults": "No results", "resourceLocator.mode.list.openUrl": "Open URL", "resourceLocator.mode.list.placeholder": "Choose...", @@ -1216,9 +1209,9 @@ "settings.log-streaming": "Log Streaming", "settings.log-streaming.heading": "Log Streaming", "settings.log-streaming.add": "Add new destination", - "settings.log-streaming.actionBox.title": "Available on custom plans", - "settings.log-streaming.actionBox.description": "Log Streaming is available as a paid feature. Get in touch to learn more about it.", - "settings.log-streaming.actionBox.button": "Contact us", + "settings.log-streaming.actionBox.title": "Available on Enterprise plan", + "settings.log-streaming.actionBox.description": "Log Streaming is available as a paid feature. Learn more about it.", + "settings.log-streaming.actionBox.button": "See plans", "settings.log-streaming.infoText": "Send logs to external endpoints of your choice. You can also write logs to a file or the console using environment variables. <a href=\"https://docs.n8n.io/hosting/logging/\" target=\"_blank\">More info</a>", "settings.log-streaming.addFirstTitle": "Set up a destination to get started", "settings.log-streaming.addFirst": "Add your first destination by clicking on the button and selecting a destination type.", @@ -1292,6 +1285,27 @@ "settings.usageAndPlan.desktop.title": "Upgrade to n8n Cloud for the full experience", "settings.usageAndPlan.desktop.description": "Cloud plans allow you to collaborate with teammates. Plus you donāt need to leave this app open all the time for your workflows to run.", "settings.versionControl.title": "Version Control", + "settings.versionControl.actionBox.title": "Available on Enterprise plan", + "settings.versionControl.actionBox.description": "Use Version Control to connect your instance to an external Git repository to backup and track changes made to your workflows, variables, and credentials. With Version Control you can also sync instances across multiple environments (development, production...).", + "settings.versionControl.actionBox.buttonText": "See plans", + "settings.versionControl.description": "Versioning allows you to connect your n8n instance to a Git branch of a repository. You can connect your branches to multiples n8n instances to create a multi environments setup. Learn how to set up versioning and environments in n8n.", + "settings.versionControl.repoUrl": "Git repository URL", + "settings.versionControl.repoUrlPlaceholder": "e.g. git@github.com:my-team/my-repository", + "settings.versionControl.repoUrlDescription": "The SSH url of your Git repository", + "settings.versionControl.authorName": "Author name", + "settings.versionControl.authorEmail": "Author email", + "settings.versionControl.sshKey": "SSH Key", + "settings.versionControl.sshKeyDescription": "Paste the SSH key in yout git repository settings. {link}.", + "settings.versionControl.sshKeyDescriptionLink": "More info.", + "settings.versionControl.button.continue": "Continue", + "settings.versionControl.button.connect": "Connect", + "settings.versionControl.branches": "Select branch", + "settings.versionControl.switchBranch.title": "Switch to {branch} branch", + "settings.versionControl.switchBranch.description": "Please confirm you want to switch the current n8n instance to the branch: {branch}", + "settings.versionControl.sync.prompt.title": "Sync changes in {branch} branch", + "settings.versionControl.sync.prompt.description": "All the changes on your n8n instances will be synced with branch {branch} on the remote git repository. The following git sequence will be executed: pull > commit > push.", + "settings.versionControl.sync.prompt.placeholder": "Commit message", + "settings.versionControl.sync.prompt.error": "Please enter a commit message", "showMessage.cancel": "@:_reusableBaseText.cancel", "showMessage.ok": "OK", "showMessage.showDetails": "Show Details", @@ -1660,7 +1674,7 @@ "contextual.workflows.sharing.unavailable.button.desktop": "View plans", "contextual.variables.unavailable.title": "Available on Enterprise plan", - "contextual.variables.unavailable.title.cloud": "Available on Power plan", + "contextual.variables.unavailable.title.cloud": "Available on Pro plan", "contextual.variables.unavailable.title.desktop": "Upgrade to n8n Cloud to collaborate", "contextual.variables.unavailable.description": "Variables can be used to store and access data across workflows. Reference them in n8n using the prefix <code>$vars</code> (e.g. <code>$vars.myVariable</code>). Variables are immutable and cannot be modified within your workflows.<br/><a href=\"https://docs.n8n.io/environments/variables/\" target=\"_blank\">Learn more in the docs.</a>", "contextual.variables.unavailable.button": "View plans", @@ -1681,10 +1695,11 @@ "contextual.communityNodes.unavailable.button.desktop": "View plans", "contextual.upgradeLinkUrl": "https://subscription.n8n.io/", - "contextual.upgradeLinkUrl.cloud": "https://app.n8n.cloud/manage?edition=cloud", + "contextual.upgradeLinkUrl.cloud": "https://app.n8n.cloud/account/change-plan", "contextual.upgradeLinkUrl.desktop": "https://n8n.io/pricing/?utm_source=n8n-internal&utm_medium=desktop", "settings.ldap": "LDAP", + "settings.ldap.note": "LDAP allows users to authenticate with their centralized account. It's compatible with services that provide an LDAP interface like Active Directory, Okta and Jumpcloud.", "settings.ldap.infoTip": "Learn more about <a href='https://docs.n8n.io/user-management/ldap/' target='_blank'>LDAP in the Docs</a>", "settings.ldap.save": "Save connection", "settings.ldap.connectionTestError": "Problem testing LDAP connection", @@ -1709,9 +1724,9 @@ "settings.ldap.confirmMessage.beforeSaveForm.confirmButtonText": "No", "settings.ldap.confirmMessage.beforeSaveForm.headline": "Are you sure you want to disable LDAP login?", "settings.ldap.confirmMessage.beforeSaveForm.message": "If you do so, all LDAP users will be converted to email users.", - "settings.ldap.disabled.title": "Available in custom plans", - "settings.ldap.disabled.description": "LDAP is available as a paid feature. Get in touch to learn more about it.", - "settings.ldap.disabled.buttonText": "Contact us", + "settings.ldap.disabled.title": "Available on Enterprise plan", + "settings.ldap.disabled.description": "LDAP is available as a paid feature. Learn more about it.", + "settings.ldap.disabled.buttonText": "See plans", "settings.ldap.toast.sync.success": "Synchronization succeeded", "settings.ldap.toast.connection.success": "Connection succeeded", "settings.ldap.form.loginEnabled.label": "Enable LDAP Login", @@ -1792,6 +1807,7 @@ "userActivationSurveyModal.description.savedTime": "for the first time. Looks like n8n just saved you some time! Can you help us make n8n even better and answer the following question?", "userActivationSurveyModal.form.label": "What almost stopped you from creating this workflow?", "userActivationSurveyModal.form.button.shareFeedback": "Share feedback", + "userActivationSurveyModal.form.button.skip": "Skip", "userActivationSurveyModal.sharedFeedback.success": "Thanks for your feedback", "userActivationSurveyModal.sharedFeedback.error": "Problem sharing feedback, try again", "sso.login.divider": "or", diff --git a/packages/editor-ui/src/plugins/telemetry/index.ts b/packages/editor-ui/src/plugins/telemetry/index.ts index f4fcbdfb3c760..6d94ed45ea56a 100644 --- a/packages/editor-ui/src/plugins/telemetry/index.ts +++ b/packages/editor-ui/src/plugins/telemetry/index.ts @@ -1,6 +1,6 @@ -import _Vue from 'vue'; -import { ITelemetrySettings, ITelemetryTrackProperties, IDataObject } from 'n8n-workflow'; -import { Route } from 'vue-router'; +import type _Vue from 'vue'; +import type { ITelemetrySettings, ITelemetryTrackProperties, IDataObject } from 'n8n-workflow'; +import type { Route } from 'vue-router'; import type { INodeCreateElement } from '@/Interface'; import type { IUserNodesPanelSession } from './telemetry.types'; @@ -136,10 +136,6 @@ export class Telemetry { this.track('User opened nodes panel', properties); } break; - case 'nodeCreateList.selectedTypeChanged': - this.userNodesPanelSession.data.filterMode = properties.new_filter as string; - this.track('User changed nodes panel filter', properties); - break; case 'nodeCreateList.destroyed': if ( this.userNodesPanelSession.data.nodeFilter.length > 0 && @@ -183,10 +179,7 @@ export class Telemetry { this.track('User added action', properties); break; case 'nodeCreateList.onSubcategorySelected': - const selectedProperties = (properties.selected as IDataObject).properties as IDataObject; - if (selectedProperties && selectedProperties.subcategory) { - properties.category_name = selectedProperties.subcategory; - } + properties.category_name = properties.subcategory; properties.is_subcategory = true; properties.nodes_panel_session_id = this.userNodesPanelSession.sessionId; delete properties.selected; diff --git a/packages/editor-ui/src/router.ts b/packages/editor-ui/src/router.ts index 1fb6f0059a650..ed00df273235f 100644 --- a/packages/editor-ui/src/router.ts +++ b/packages/editor-ui/src/router.ts @@ -20,7 +20,8 @@ import SettingsFakeDoorView from './views/SettingsFakeDoorView.vue'; import SetupView from './views/SetupView.vue'; import SigninView from './views/SigninView.vue'; import SignupView from './views/SignupView.vue'; -import Router, { Route } from 'vue-router'; +import type { Route } from 'vue-router'; +import Router from 'vue-router'; import TemplatesCollectionView from '@/views/TemplatesCollectionView.vue'; import TemplatesWorkflowView from '@/views/TemplatesWorkflowView.vue'; @@ -29,9 +30,9 @@ import CredentialsView from '@/views/CredentialsView.vue'; import ExecutionsView from '@/views/ExecutionsView.vue'; import WorkflowsView from '@/views/WorkflowsView.vue'; import VariablesView from '@/views/VariablesView.vue'; -import { IPermissions } from './Interface'; +import type { IPermissions } from './Interface'; import { LOGIN_STATUS, ROLE } from '@/utils'; -import { RouteConfigSingleView } from 'vue-router/types/router'; +import type { RouteConfigSingleView } from 'vue-router/types/router'; import { TEMPLATE_EXPERIMENT, VIEWS } from './constants'; import { useSettingsStore } from './stores/settings'; import { useTemplatesStore } from './stores/templates'; @@ -625,7 +626,7 @@ export const routes = [ deny: { shouldDeny: () => { const settingsStore = useSettingsStore(); - return settingsStore.isCloudDeployment || settingsStore.isDesktopDeployment; + return settingsStore.isDesktopDeployment; }, }, }, diff --git a/packages/editor-ui/src/shims.d.ts b/packages/editor-ui/src/shims.d.ts index c231305c43c1c..fc95a6f17da9c 100644 --- a/packages/editor-ui/src/shims.d.ts +++ b/packages/editor-ui/src/shims.d.ts @@ -1,4 +1,6 @@ import Vue, { VNode } from 'vue'; +import type { Store } from 'pinia'; +import type { IDataObject } from 'n8n-workflow'; declare module 'markdown-it-link-attributes'; declare module 'markdown-it-emoji'; @@ -17,6 +19,10 @@ declare global { interface Window { BASE_PATH: string; REST_ENDPOINT: string; + n8nExternalHooks?: Record< + string, + Record<string, Array<(store: Store, metadata?: IDataObject) => Promise<void>>> + >; } namespace JSX { diff --git a/packages/editor-ui/src/stores/__tests__/environments.spec.ts b/packages/editor-ui/src/stores/__tests__/environments.spec.ts index 3df3f6b28d165..c6fcd61b83a80 100644 --- a/packages/editor-ui/src/stores/__tests__/environments.spec.ts +++ b/packages/editor-ui/src/stores/__tests__/environments.spec.ts @@ -2,7 +2,7 @@ import { afterAll, beforeAll } from 'vitest'; import { setActivePinia, createPinia } from 'pinia'; import { setupServer } from '@/__tests__/server'; import { useEnvironmentsStore } from '@/stores/environments.ee'; -import { EnvironmentVariable } from '@/Interface'; +import type { EnvironmentVariable } from '@/Interface'; describe('store', () => { let server: ReturnType<typeof setupServer>; diff --git a/packages/editor-ui/src/stores/__tests__/posthog.test.ts b/packages/editor-ui/src/stores/__tests__/posthog.test.ts index ed31f1a252e39..b7054b232fb3f 100644 --- a/packages/editor-ui/src/stores/__tests__/posthog.test.ts +++ b/packages/editor-ui/src/stores/__tests__/posthog.test.ts @@ -2,9 +2,9 @@ import { createPinia, setActivePinia } from 'pinia'; import { usePostHog } from '@/stores/posthog'; import { useUsersStore } from '@/stores/users'; import { useSettingsStore } from '@/stores/settings'; -import { IN8nUISettings } from '@/Interface'; import { useRootStore } from '@/stores/n8nRootStore'; import { useTelemetryStore } from '@/stores/telemetry'; +import type { IN8nUISettings } from 'n8n-workflow'; const DEFAULT_POSTHOG_SETTINGS: IN8nUISettings['posthog'] = { enabled: true, diff --git a/packages/editor-ui/src/stores/__tests__/sso.test.ts b/packages/editor-ui/src/stores/__tests__/sso.test.ts index eea1d3684e6e8..d07cf3747ef8f 100644 --- a/packages/editor-ui/src/stores/__tests__/sso.test.ts +++ b/packages/editor-ui/src/stores/__tests__/sso.test.ts @@ -2,8 +2,8 @@ import { createPinia, setActivePinia } from 'pinia'; import { useSettingsStore } from '@/stores/settings'; import { useSSOStore } from '@/stores/sso'; import { merge } from 'lodash-es'; -import { IN8nUISettings } from '@/Interface'; import { SETTINGS_STORE_DEFAULT_STATE } from '@/__tests__/utils'; +import type { IN8nUISettings } from 'n8n-workflow'; let ssoStore: ReturnType<typeof useSSOStore>; let settingsStore: ReturnType<typeof useSettingsStore>; diff --git a/packages/editor-ui/src/stores/__tests__/ui.test.ts b/packages/editor-ui/src/stores/__tests__/ui.test.ts index d1bcb831df225..8b076252168dd 100644 --- a/packages/editor-ui/src/stores/__tests__/ui.test.ts +++ b/packages/editor-ui/src/stores/__tests__/ui.test.ts @@ -30,7 +30,7 @@ describe('UI store', () => { 'production', 'https://n8n.io/pricing/?utm_source=n8n-internal&utm_medium=desktop&utm_campaign=utm-test-campaign', ], - ['cloud', 'production', 'https://app.n8n.cloud/manage?edition=cloud'], + ['cloud', 'production', 'https://app.n8n.cloud/account/change-plan'], ])( '"upgradeLinkUrl" should generate the correct URL for "%s" deployment and "%s" license environment', (type, environment, expectation) => { diff --git a/packages/editor-ui/src/stores/canvas.ts b/packages/editor-ui/src/stores/canvas.ts index 7fa8329cb7500..8643c9261610b 100644 --- a/packages/editor-ui/src/stores/canvas.ts +++ b/packages/editor-ui/src/stores/canvas.ts @@ -6,7 +6,7 @@ import { useWorkflowsStore } from '@/stores/workflows'; import { useNodeTypesStore } from '@/stores/nodeTypes'; import { useUIStore } from '@/stores/ui'; import { useHistoryStore } from '@/stores/history'; -import { INodeUi, XYPosition } from '@/Interface'; +import type { INodeUi, XYPosition } from '@/Interface'; import { scaleBigger, scaleReset, scaleSmaller } from '@/utils'; import { START_NODE_TYPE } from '@/constants'; import type { @@ -18,7 +18,8 @@ import { newInstance } from '@jsplumb/browser-ui'; import { N8nPlusEndpointHandler } from '@/plugins/endpoints/N8nPlusEndpointType'; import * as N8nPlusEndpointRenderer from '@/plugins/endpoints/N8nPlusEndpointRenderer'; import { N8nConnector } from '@/plugins/connectors/N8nCustomConnector'; -import { EndpointFactory, Connectors, Connection } from '@jsplumb/core'; +import type { Connection } from '@jsplumb/core'; +import { EndpointFactory, Connectors } from '@jsplumb/core'; import { MoveNodeCommand } from '@/models/history'; import { DEFAULT_PLACEHOLDER_TRIGGER_BUTTON, @@ -32,7 +33,7 @@ import { CONNECTOR_PAINT_STYLE_PRIMARY, CONNECTOR_ARROW_OVERLAYS, } from '@/utils/nodeViewUtils'; -import { PointXY } from '@jsplumb/util'; +import type { PointXY } from '@jsplumb/util'; export const useCanvasStore = defineStore('canvas', () => { const workflowStore = useWorkflowsStore(); diff --git a/packages/editor-ui/src/stores/communityNodes.ts b/packages/editor-ui/src/stores/communityNodes.ts index 4caa452a8f141..e8702b3ad8714 100644 --- a/packages/editor-ui/src/stores/communityNodes.ts +++ b/packages/editor-ui/src/stores/communityNodes.ts @@ -7,9 +7,9 @@ import { import { getAvailableCommunityPackageCount } from '@/api/settings'; import { defineStore } from 'pinia'; import { useRootStore } from './n8nRootStore'; -import { PublicInstalledPackage } from 'n8n-workflow'; +import type { PublicInstalledPackage } from 'n8n-workflow'; import Vue from 'vue'; -import { CommunityNodesState, CommunityPackageMap } from '@/Interface'; +import type { CommunityNodesState, CommunityPackageMap } from '@/Interface'; import { STORES } from '@/constants'; const LOADER_DELAY = 300; diff --git a/packages/editor-ui/src/stores/credentials.ts b/packages/editor-ui/src/stores/credentials.ts index b3c071e2b882b..d96fc2063ee87 100644 --- a/packages/editor-ui/src/stores/credentials.ts +++ b/packages/editor-ui/src/stores/credentials.ts @@ -1,4 +1,4 @@ -import { INodeUi, IUsedCredential } from './../Interface'; +import type { INodeUi, IUsedCredential } from './../Interface'; import { createNewCredential, deleteCredential, @@ -12,9 +12,9 @@ import { updateCredential, } from '@/api/credentials'; import { setCredentialSharedWith } from '@/api/credentials.ee'; -import { getAppNameFromCredType } from '@/utils'; +import { getAppNameFromCredType, makeRestApiRequest } from '@/utils'; import { EnterpriseEditionFeature, STORES } from '@/constants'; -import { +import type { ICredentialMap, ICredentialsDecryptedResponse, ICredentialsResponse, @@ -22,7 +22,7 @@ import { ICredentialTypeMap, } from '@/Interface'; import { i18n } from '@/plugins/i18n'; -import { +import type { ICredentialsDecrypted, ICredentialType, INodeCredentialTestResult, @@ -376,5 +376,15 @@ export const useCredentialsStore = defineStore(STORES.CREDENTIALS, { ), ); }, + + async getCredentialTranslation(credentialType: string): Promise<object> { + const rootStore = useRootStore(); + return await makeRestApiRequest( + rootStore.getRestApiContext, + 'GET', + '/credential-translation', + { credentialType }, + ); + }, }, }); diff --git a/packages/editor-ui/src/stores/environments.ee.ts b/packages/editor-ui/src/stores/environments.ee.ts index ac28effe9a737..d93bfff5c9863 100644 --- a/packages/editor-ui/src/stores/environments.ee.ts +++ b/packages/editor-ui/src/stores/environments.ee.ts @@ -1,10 +1,8 @@ import { defineStore } from 'pinia'; -import { useSettingsStore } from '@/stores/settings'; import { computed, ref } from 'vue'; -import { EnvironmentVariable } from '@/Interface'; +import type { EnvironmentVariable } from '@/Interface'; import * as environmentsApi from '@/api/environments.ee'; import { useRootStore } from '@/stores/n8nRootStore'; -import { createVariable } from '@/api/environments.ee'; export const useEnvironmentsStore = defineStore('environments', () => { const rootStore = useRootStore(); diff --git a/packages/editor-ui/src/stores/history.ts b/packages/editor-ui/src/stores/history.ts index 5f79df10c9c79..d4bd04227e6b8 100644 --- a/packages/editor-ui/src/stores/history.ts +++ b/packages/editor-ui/src/stores/history.ts @@ -1,7 +1,7 @@ -import { AddConnectionCommand, COMMANDS, RemoveConnectionCommand } from './../models/history'; -import { BulkCommand, Command, Undoable, MoveNodeCommand } from '@/models/history'; +import type { Command, Undoable } from '@/models/history'; +import { BulkCommand } from '@/models/history'; import { STORES } from '@/constants'; -import { HistoryState } from '@/Interface'; +import type { HistoryState } from '@/Interface'; import { defineStore } from 'pinia'; const STACK_LIMIT = 100; diff --git a/packages/editor-ui/src/stores/logStreamingStore.ts b/packages/editor-ui/src/stores/logStreamingStore.ts index 3c1794cbe53a2..e1b3a33661072 100644 --- a/packages/editor-ui/src/stores/logStreamingStore.ts +++ b/packages/editor-ui/src/stores/logStreamingStore.ts @@ -1,4 +1,5 @@ -import { deepCopy, MessageEventBusDestinationOptions } from 'n8n-workflow'; +import type { MessageEventBusDestinationOptions } from 'n8n-workflow'; +import { deepCopy } from 'n8n-workflow'; import { defineStore } from 'pinia'; import { deleteDestinationFromDb, diff --git a/packages/editor-ui/src/stores/n8nRootStore.ts b/packages/editor-ui/src/stores/n8nRootStore.ts index 8abd800004797..8f071afe6fdff 100644 --- a/packages/editor-ui/src/stores/n8nRootStore.ts +++ b/packages/editor-ui/src/stores/n8nRootStore.ts @@ -1,6 +1,6 @@ import { STORES } from '@/constants'; -import { IRestApiContext, RootState } from '@/Interface'; -import { IDataObject } from 'n8n-workflow'; +import type { IRestApiContext, RootState } from '@/Interface'; +import type { IDataObject } from 'n8n-workflow'; import { defineStore } from 'pinia'; import Vue from 'vue'; import { useNodeTypesStore } from './nodeTypes'; diff --git a/packages/editor-ui/src/stores/ndv.ts b/packages/editor-ui/src/stores/ndv.ts index fed4fc96ea045..937c8b07b620f 100644 --- a/packages/editor-ui/src/stores/ndv.ts +++ b/packages/editor-ui/src/stores/ndv.ts @@ -1,6 +1,12 @@ import { LOCAL_STORAGE_MAPPING_IS_ONBOARDED, STORES } from '@/constants'; -import { INodeUi, IRunDataDisplayMode, NDVState, NodePanelType, XYPosition } from '@/Interface'; -import { IRunData } from 'n8n-workflow'; +import type { + INodeUi, + IRunDataDisplayMode, + NDVState, + NodePanelType, + XYPosition, +} from '@/Interface'; +import type { IRunData } from 'n8n-workflow'; import { defineStore } from 'pinia'; import Vue from 'vue'; import { useWorkflowsStore } from './workflows'; @@ -111,6 +117,15 @@ export const useNDVStore = defineStore(STORES.NDV, { isDNVDataEmpty() { return (panel: 'input' | 'output'): boolean => this[panel].data.isEmpty; }, + isInputParentOfActiveNode(): boolean { + const inputNodeName = this.ndvInputNodeName; + if (!this.activeNode || !inputNodeName) { + return false; + } + const workflow = useWorkflowsStore().getCurrentWorkflow(); + const parentNodes = workflow.getParentNodes(this.activeNode.name, 'main', 1); + return parentNodes.includes(inputNodeName); + }, }, actions: { setInputNodeName(name: string | undefined): void { diff --git a/packages/editor-ui/src/stores/nodeCreator.ts b/packages/editor-ui/src/stores/nodeCreator.ts index 49bf4e0a587dd..849e51d8e2e3c 100644 --- a/packages/editor-ui/src/stores/nodeCreator.ts +++ b/packages/editor-ui/src/stores/nodeCreator.ts @@ -1,403 +1,52 @@ -import { startCase } from 'lodash-es'; import { defineStore } from 'pinia'; -import { - INodePropertyCollection, - INodePropertyOptions, - IDataObject, - INodeProperties, - INodeTypeDescription, - deepCopy, - INodeParameters, - INodeActionTypeDescription, -} from 'n8n-workflow'; -import { - STORES, - MANUAL_TRIGGER_NODE_TYPE, - CORE_NODES_CATEGORY, - TRIGGER_NODE_FILTER, - STICKY_NODE_TYPE, - NODE_CREATOR_OPEN_SOURCES, -} from '@/constants'; -import { useNodeTypesStore } from '@/stores/nodeTypes'; -import { useWorkflowsStore } from './workflows'; -import { CUSTOM_API_CALL_KEY, ALL_NODE_FILTER } from '@/constants'; -import { INodeCreatorState, INodeFilterType, IUpdateInformation } from '@/Interface'; -import { BaseTextKey, i18n } from '@/plugins/i18n'; -import { externalHooks } from '@/mixins/externalHooks'; -import { Telemetry } from '@/plugins/telemetry'; - -const PLACEHOLDER_RECOMMENDED_ACTION_KEY = 'placeholder_recommended'; +import { STORES, TRIGGER_NODE_CREATOR_VIEW } from '@/constants'; +import type { + NodeFilterType, + NodeCreatorOpenSource, + SimplifiedNodeType, + ActionsRecord, +} from '@/Interface'; + +import { ref } from 'vue'; + +export const useNodeCreatorStore = defineStore(STORES.NODE_CREATOR, () => { + const selectedView = ref<NodeFilterType>(TRIGGER_NODE_CREATOR_VIEW); + const mergedNodes = ref<SimplifiedNodeType[]>([]); + const actions = ref<ActionsRecord<typeof mergedNodes.value>>({}); + + const showScrim = ref(false); + const openSource = ref<NodeCreatorOpenSource>(''); + + function setMergeNodes(nodes: SimplifiedNodeType[]) { + mergedNodes.value = nodes; + } -const customNodeActionsParsers: { - [key: string]: ( - matchedProperty: INodeProperties, - nodeTypeDescription: INodeTypeDescription, - ) => INodeActionTypeDescription[] | undefined; -} = { - ['n8n-nodes-base.hubspotTrigger']: (matchedProperty, nodeTypeDescription) => { - const collection = matchedProperty?.options?.[0] as INodePropertyCollection; + function setActions(nodes: ActionsRecord<typeof mergedNodes.value>) { + actions.value = nodes; + } - return (collection?.values[0]?.options as INodePropertyOptions[])?.map( - (categoryItem): INodeActionTypeDescription => ({ - ...getNodeTypeBase( - nodeTypeDescription, - i18n.baseText('nodeCreator.actionsCategory.triggers'), - ), - actionKey: categoryItem.value as string, - displayName: i18n.baseText('nodeCreator.actionsCategory.onEvent', { - interpolate: { event: startCase(categoryItem.name) }, - }), - description: categoryItem.description || '', - displayOptions: matchedProperty.displayOptions, - values: { eventsUi: { eventValues: [{ name: categoryItem.value }] } }, - }), - ); - }, -}; + function setShowScrim(isVisible: boolean) { + showScrim.value = isVisible; + } -function filterActions(actions: INodeActionTypeDescription[]) { - // Do not show single action nodes - if (actions.length <= 1) return []; - return actions.filter( - (action: INodeActionTypeDescription, _: number, arr: INodeActionTypeDescription[]) => { - const isApiCall = action.actionKey === CUSTOM_API_CALL_KEY; - if (isApiCall) return false; + function setSelectedView(view: NodeFilterType) { + selectedView.value = view; + } - const isPlaceholderTriggerAction = action.actionKey === PLACEHOLDER_RECOMMENDED_ACTION_KEY; - return !isPlaceholderTriggerAction || (isPlaceholderTriggerAction && arr.length > 1); - }, - ); -} + function setOpenSource(view: NodeCreatorOpenSource) { + openSource.value = view; + } -function getNodeTypeBase(nodeTypeDescription: INodeTypeDescription, category: string) { return { - name: nodeTypeDescription.name, - group: ['trigger'], - codex: { - categories: [category], - subcategories: { - [nodeTypeDescription.displayName]: [category], - }, - }, - iconUrl: nodeTypeDescription.iconUrl, - icon: nodeTypeDescription.icon, - version: [1], - defaults: { - ...nodeTypeDescription.defaults, - }, - inputs: [], - outputs: [], - properties: [], + openSource, + selectedView, + showScrim, + mergedNodes, + actions, + setShowScrim, + setSelectedView, + setOpenSource, + setActions, + setMergeNodes, }; -} - -function operationsCategory( - nodeTypeDescription: INodeTypeDescription, -): INodeActionTypeDescription[] { - if (!!nodeTypeDescription.properties.find((property) => property.name === 'resource')) return []; - - const matchedProperty = nodeTypeDescription.properties.find( - (property) => property.name?.toLowerCase() === 'operation', - ); - - if (!matchedProperty || !matchedProperty.options) return []; - - const filteredOutItems = (matchedProperty.options as INodePropertyOptions[]).filter( - (categoryItem: INodePropertyOptions) => !['*', '', ' '].includes(categoryItem.name), - ); - - const items = filteredOutItems.map((item: INodePropertyOptions) => ({ - ...getNodeTypeBase(nodeTypeDescription, i18n.baseText('nodeCreator.actionsCategory.actions')), - actionKey: item.value as string, - displayName: item.action ?? startCase(item.name), - description: item.description ?? '', - displayOptions: matchedProperty.displayOptions, - values: { - [matchedProperty.name]: matchedProperty.type === 'multiOptions' ? [item.value] : item.value, - }, - })); - - // Do not return empty category - if (items.length === 0) return []; - - return items; -} - -function triggersCategory(nodeTypeDescription: INodeTypeDescription): INodeActionTypeDescription[] { - const matchingKeys = ['event', 'events', 'trigger on']; - const isTrigger = nodeTypeDescription.displayName?.toLowerCase().includes('trigger'); - const matchedProperty = nodeTypeDescription.properties.find((property) => - matchingKeys.includes(property.displayName?.toLowerCase()), - ); - - if (!isTrigger) return []; - - // Inject placeholder action if no events are available - // so user is able to add node to the canvas from the actions panel - if (!matchedProperty || !matchedProperty.options) { - return [ - { - ...getNodeTypeBase( - nodeTypeDescription, - i18n.baseText('nodeCreator.actionsCategory.triggers'), - ), - actionKey: PLACEHOLDER_RECOMMENDED_ACTION_KEY, - displayName: i18n.baseText('nodeCreator.actionsCategory.onNewEvent', { - interpolate: { event: nodeTypeDescription.displayName.replace('Trigger', '').trimEnd() }, - }), - description: '', - }, - ]; - } - - const filteredOutItems = (matchedProperty.options as INodePropertyOptions[]).filter( - (categoryItem: INodePropertyOptions) => !['*', '', ' '].includes(categoryItem.name), - ); - - const customParsedItem = customNodeActionsParsers[nodeTypeDescription.name]?.( - matchedProperty, - nodeTypeDescription, - ); - - const items = - customParsedItem ?? - filteredOutItems.map((categoryItem: INodePropertyOptions) => ({ - ...getNodeTypeBase( - nodeTypeDescription, - i18n.baseText('nodeCreator.actionsCategory.triggers'), - ), - actionKey: categoryItem.value as string, - displayName: - categoryItem.action ?? - i18n.baseText('nodeCreator.actionsCategory.onEvent', { - interpolate: { event: startCase(categoryItem.name) }, - }), - description: categoryItem.description || '', - displayOptions: matchedProperty.displayOptions, - values: { - [matchedProperty.name]: - matchedProperty.type === 'multiOptions' ? [categoryItem.value] : categoryItem.value, - }, - })); - - return items; -} - -function resourceCategories( - nodeTypeDescription: INodeTypeDescription, -): INodeActionTypeDescription[] { - const transformedNodes: INodeActionTypeDescription[] = []; - const matchedProperties = nodeTypeDescription.properties.filter( - (property) => property.displayName?.toLowerCase() === 'resource', - ); - - matchedProperties.forEach((property) => { - ((property.options as INodePropertyOptions[]) || []) - .filter((option) => option.value !== CUSTOM_API_CALL_KEY) - .forEach((resourceOption, i, options) => { - const isSingleResource = options.length === 1; - - // Match operations for the resource by checking if displayOptions matches or contains the resource name - const operations = nodeTypeDescription.properties.find( - (operation) => - operation.name === 'operation' && - (operation.displayOptions?.show?.resource?.includes(resourceOption.value) || - isSingleResource), - ); - - if (!operations?.options) return; - - const items = ((operations.options as INodePropertyOptions[]) || []).map( - (operationOption) => { - const displayName = - operationOption.action ?? `${resourceOption.name} ${startCase(operationOption.name)}`; - - // We need to manually populate displayOptions as they are not present in the node description - // if the resource has only one option - const displayOptions = isSingleResource - ? { show: { resource: [(options as INodePropertyOptions[])[0]?.value] } } - : operations?.displayOptions; - - return { - ...getNodeTypeBase(nodeTypeDescription, resourceOption.name), - actionKey: operationOption.value as string, - description: operationOption?.description ?? '', - displayOptions, - values: { - operation: - operations?.type === 'multiOptions' - ? [operationOption.value] - : operationOption.value, - }, - displayName, - group: ['trigger'], - }; - }, - ); - - transformedNodes.push(...items); - }); - }); - - return transformedNodes; -} - -export const useNodeCreatorStore = defineStore(STORES.NODE_CREATOR, { - state: (): INodeCreatorState => ({ - itemsFilter: '', - showScrim: false, - selectedView: TRIGGER_NODE_FILTER, - rootViewHistory: [], - openSource: '', - }), - actions: { - setShowScrim(isVisible: boolean) { - this.showScrim = isVisible; - }, - setSelectedView(selectedNodeType: INodeFilterType) { - this.selectedView = selectedNodeType; - if (!this.rootViewHistory.includes(selectedNodeType)) { - this.rootViewHistory.push(selectedNodeType); - } - }, - closeCurrentView() { - this.rootViewHistory.pop(); - this.selectedView = this.rootViewHistory[this.rootViewHistory.length - 1]; - }, - resetRootViewHistory() { - this.rootViewHistory = []; - }, - setFilter(search: string) { - this.itemsFilter = search; - }, - setAddedNodeActionParameters(action: IUpdateInformation, telemetry?: Telemetry, track = true) { - const { $onAction: onWorkflowStoreAction } = useWorkflowsStore(); - const storeWatcher = onWorkflowStoreAction( - ({ name, after, store: { setLastNodeParameters }, args }) => { - if (name !== 'addNode' || args[0].type !== action.key) return; - after(() => { - setLastNodeParameters(action); - if (track) this.trackActionSelected(action, telemetry); - storeWatcher(); - }); - }, - ); - - return storeWatcher; - }, - trackActionSelected(action: IUpdateInformation, telemetry?: Telemetry) { - const { $externalHooks } = new externalHooks(); - - const payload = { - node_type: action.key, - action: action.name, - resource: (action.value as INodeParameters).resource || '', - }; - $externalHooks().run('nodeCreateList.addAction', payload); - telemetry?.trackNodesPanel('nodeCreateList.addAction', payload); - }, - }, - getters: { - visibleNodesWithActions(): INodeTypeDescription[] { - const nodes = deepCopy(useNodeTypesStore().visibleNodeTypes); - const nodesWithActions = nodes.map((node) => { - node.actions = [ - ...triggersCategory(node), - ...operationsCategory(node), - ...resourceCategories(node), - ]; - - return node; - }); - return nodesWithActions; - }, - mergedAppNodes(): INodeTypeDescription[] { - const triggers = this.visibleNodesWithActions.filter((node) => - node.group.includes('trigger'), - ); - const apps = this.visibleNodesWithActions - .filter((node) => !node.group.includes('trigger')) - .map((node) => { - const newNode = deepCopy(node); - newNode.actions = newNode.actions || []; - return newNode; - }); - - triggers.forEach((node) => { - const normalizedName = node.name.toLowerCase().replace('trigger', ''); - const app = apps.find((node) => node.name.toLowerCase() === normalizedName); - const newNode = deepCopy(node); - if (app && app.actions?.length) { - // merge triggers into regular nodes that match - app?.actions?.push(...(newNode.actions || [])); - app.description = newNode.description; // default to trigger description - } else { - newNode.actions = newNode.actions || []; - apps.push(newNode); - } - }); - - const filteredNodes = apps.map((node) => ({ - ...node, - actions: filterActions(node.actions || []), - })); - - return filteredNodes; - }, - getNodeTypesWithManualTrigger: - () => - (nodeType?: string): string[] => { - if (!nodeType) return []; - - const { workflowTriggerNodes } = useWorkflowsStore(); - const isTrigger = useNodeTypesStore().isTriggerNode(nodeType); - const workflowContainsTrigger = workflowTriggerNodes.length > 0; - const isTriggerPanel = useNodeCreatorStore().selectedView === TRIGGER_NODE_FILTER; - const isStickyNode = nodeType === STICKY_NODE_TYPE; - const singleNodeOpenSources = [ - NODE_CREATOR_OPEN_SOURCES.PLUS_ENDPOINT, - NODE_CREATOR_OPEN_SOURCES.NODE_CONNECTION_ACTION, - NODE_CREATOR_OPEN_SOURCES.NODE_CONNECTION_DROP, - ]; - - // If the node creator was opened from the plus endpoint, node connection action, or node connection drop - // then we do not want to append the manual trigger - const isSingleNodeOpenSource = singleNodeOpenSources.includes( - useNodeCreatorStore().openSource, - ); - - const shouldAppendManualTrigger = - !isSingleNodeOpenSource && - !isTrigger && - !workflowContainsTrigger && - isTriggerPanel && - !isStickyNode; - - const nodeTypes = shouldAppendManualTrigger - ? [MANUAL_TRIGGER_NODE_TYPE, nodeType] - : [nodeType]; - - return nodeTypes; - }, - - getActionData: - () => - (actionItem: INodeActionTypeDescription): IUpdateInformation => { - const displayOptions = actionItem.displayOptions; - - const displayConditions = Object.keys(displayOptions?.show || {}).reduce( - (acc: IDataObject, showCondition: string) => { - acc[showCondition] = displayOptions?.show?.[showCondition]?.[0]; - return acc; - }, - {}, - ); - - return { - name: actionItem.displayName, - key: actionItem.name as string, - value: { ...actionItem.values, ...displayConditions } as INodeParameters, - }; - }, - }, }); diff --git a/packages/editor-ui/src/stores/nodeTypes.ts b/packages/editor-ui/src/stores/nodeTypes.ts index d8d63e13d1c98..5f91bbb80231a 100644 --- a/packages/editor-ui/src/stores/nodeTypes.ts +++ b/packages/editor-ui/src/stores/nodeTypes.ts @@ -6,15 +6,10 @@ import { getResourceLocatorResults, } from '@/api/nodeTypes'; import { DEFAULT_NODETYPE_VERSION, STORES } from '@/constants'; -import { - ICategoriesWithNodes, - INodeCreateElement, - INodeTypesState, - IResourceLocatorReqParams, -} from '@/Interface'; +import type { INodeTypesState, IResourceLocatorReqParams } from '@/Interface'; import { addHeaders, addNodeTranslation } from '@/plugins/i18n'; -import { omit, getCategoriesWithNodes, getCategorizedList } from '@/utils'; -import { +import { omit } from '@/utils'; +import type { ILoadOptions, INodeCredentials, INodeListSearchResult, @@ -27,8 +22,7 @@ import { defineStore } from 'pinia'; import Vue from 'vue'; import { useCredentialsStore } from './credentials'; import { useRootStore } from './n8nRootStore'; -import { useUsersStore } from './users'; -import { useNodeCreatorStore } from './nodeCreator'; + function getNodeVersions(nodeType: INodeTypeDescription) { return Array.isArray(nodeType.version) ? nodeType.version : [nodeType.version]; } @@ -88,13 +82,6 @@ export const useNodeTypesStore = defineStore(STORES.NODE_TYPES, { visibleNodeTypes(): INodeTypeDescription[] { return this.allLatestNodeTypes.filter((nodeType: INodeTypeDescription) => !nodeType.hidden); }, - categoriesWithNodes(): ICategoriesWithNodes { - const usersStore = useUsersStore(); - return getCategoriesWithNodes(this.visibleNodeTypes, usersStore.personalizedNodeTypes); - }, - categorizedItems(): INodeCreateElement[] { - return getCategorizedList(this.categoriesWithNodes); - }, }, actions: { setNodeTypes(newNodeTypes: INodeTypeDescription[] = []): void { @@ -128,9 +115,6 @@ export const useNodeTypesStore = defineStore(STORES.NODE_TYPES, { { ...this.nodeTypes }, ); Vue.set(this, 'nodeTypes', nodeTypes); - - // Trigger compute of mergedAppNodes getter so it's ready when user opens the node creator - useNodeCreatorStore().mergedAppNodes; }, removeNodeTypes(nodeTypesToRemove: INodeTypeDescription[]): void { this.nodeTypes = nodeTypesToRemove.reduce( diff --git a/packages/editor-ui/src/stores/posthog.ts b/packages/editor-ui/src/stores/posthog.ts index 8c9d088b465a6..1e627eb287b5b 100644 --- a/packages/editor-ui/src/stores/posthog.ts +++ b/packages/editor-ui/src/stores/posthog.ts @@ -1,9 +1,10 @@ -import { ref, Ref } from 'vue'; +import type { Ref } from 'vue'; +import { ref } from 'vue'; import { defineStore } from 'pinia'; import { useUsersStore } from '@/stores/users'; import { useRootStore } from '@/stores/n8nRootStore'; import { useSettingsStore } from '@/stores/settings'; -import { FeatureFlags } from 'n8n-workflow'; +import type { FeatureFlags } from 'n8n-workflow'; import { EXPERIMENTS_TO_TRACK, LOCAL_STORAGE_EXPERIMENT_OVERRIDES, diff --git a/packages/editor-ui/src/stores/segment.ts b/packages/editor-ui/src/stores/segment.ts index e51987c787eaf..2191dd58bcc97 100644 --- a/packages/editor-ui/src/stores/segment.ts +++ b/packages/editor-ui/src/stores/segment.ts @@ -6,10 +6,9 @@ import { SET_NODE_TYPE, WEBHOOK_NODE_TYPE, } from '@/constants'; -import { ITelemetryTrackProperties } from 'n8n-workflow'; import { defineStore } from 'pinia'; import { useSettingsStore } from '@/stores/settings'; -import { INodeTypeDescription, IRun } from 'n8n-workflow'; +import type { INodeTypeDescription, IRun, ITelemetryTrackProperties } from 'n8n-workflow'; import { useWorkflowsStore } from '@/stores/workflows'; import { useNodeTypesStore } from '@/stores/nodeTypes'; diff --git a/packages/editor-ui/src/stores/settings.ts b/packages/editor-ui/src/stores/settings.ts index 24e48f5a1fbae..65fb38b137a1f 100644 --- a/packages/editor-ui/src/stores/settings.ts +++ b/packages/editor-ui/src/stores/settings.ts @@ -8,30 +8,30 @@ import { } from '@/api/ldap'; import { getPromptsData, getSettings, submitContactInfo, submitValueSurvey } from '@/api/settings'; import { testHealthEndpoint } from '@/api/templates'; -import { - CONTACT_PROMPT_MODAL_KEY, - EnterpriseEditionFeature, - STORES, - VALUE_SURVEY_MODAL_KEY, -} from '@/constants'; -import { +import type { EnterpriseEditionFeature } from '@/constants'; +import { CONTACT_PROMPT_MODAL_KEY, STORES, VALUE_SURVEY_MODAL_KEY } from '@/constants'; +import type { ILdapConfig, - ILogLevel, IN8nPromptResponse, IN8nPrompts, - IN8nUISettings, IN8nValueSurveyData, ISettingsState, - UserManagementAuthenticationMethod, - WorkflowCallerPolicyDefaultOption, } from '@/Interface'; -import { IDataObject, ITelemetrySettings } from 'n8n-workflow'; +import { UserManagementAuthenticationMethod } from '@/Interface'; +import type { + IDataObject, + ILogLevel, + IN8nUISettings, + ITelemetrySettings, + WorkflowSettings, +} from 'n8n-workflow'; import { defineStore } from 'pinia'; import Vue from 'vue'; import { useRootStore } from './n8nRootStore'; import { useUIStore } from './ui'; import { useUsersStore } from './users'; import { useVersionsStore } from './versions'; +import { makeRestApiRequest } from '@/utils'; export const useSettingsStore = defineStore(STORES.SETTINGS, { state: (): ISettingsState => ({ @@ -175,7 +175,7 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, { isQueueModeEnabled(): boolean { return this.settings.executionMode === 'queue'; }, - workflowCallerPolicyDefaultOption(): WorkflowCallerPolicyDefaultOption { + workflowCallerPolicyDefaultOption(): WorkflowSettings.CallerPolicy { return this.settings.workflowCallerPolicyDefaultOption; }, isDefaultAuthenticationSaml(): boolean { @@ -235,35 +235,28 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, { setPromptsData(promptsData: IN8nPrompts): void { Vue.set(this, 'promptsData', promptsData); }, - setAllowedModules(allowedModules: { builtIn?: string; external?: string }): void { - this.settings.allowedModules = { - ...(allowedModules.builtIn && { builtIn: allowedModules.builtIn.split(',') }), - ...(allowedModules.external && { external: allowedModules.external.split(',') }), - }; + setAllowedModules(allowedModules: { builtIn?: string[]; external?: string[] }): void { + this.settings.allowedModules = allowedModules; }, async fetchPromptsData(): Promise<void> { if (!this.isTelemetryEnabled) { - Promise.resolve(); + return; } - try { - const uiStore = useUIStore(); - const usersStore = useUsersStore(); - const promptsData: IN8nPrompts = await getPromptsData( - this.settings.instanceId, - usersStore.currentUserId || '', - ); - if (promptsData && promptsData.showContactPrompt) { - uiStore.openModal(CONTACT_PROMPT_MODAL_KEY); - } else if (promptsData && promptsData.showValueSurvey) { - uiStore.openModal(VALUE_SURVEY_MODAL_KEY); - } + const uiStore = useUIStore(); + const usersStore = useUsersStore(); + const promptsData: IN8nPrompts = await getPromptsData( + this.settings.instanceId, + usersStore.currentUserId || '', + ); - this.setPromptsData(promptsData); - Promise.resolve(); - } catch (error) { - Promise.reject(error); + if (promptsData && promptsData.showContactPrompt) { + uiStore.openModal(CONTACT_PROMPT_MODAL_KEY); + } else if (promptsData && promptsData.showValueSurvey) { + uiStore.openModal(VALUE_SURVEY_MODAL_KEY); } + + this.setPromptsData(promptsData); }, async submitContactInfo(email: string): Promise<IN8nPromptResponse | undefined> { try { @@ -337,5 +330,9 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, { setSaveManualExecutions(saveManualExecutions: boolean) { Vue.set(this, 'saveManualExecutions', saveManualExecutions); }, + async getTimezones(): Promise<IDataObject> { + const rootStore = useRootStore(); + return makeRestApiRequest(rootStore.getRestApiContext, 'GET', '/options/timezones'); + }, }, }); diff --git a/packages/editor-ui/src/stores/sso.ts b/packages/editor-ui/src/stores/sso.ts index 3de019a01a1ad..6fedcddbfc728 100644 --- a/packages/editor-ui/src/stores/sso.ts +++ b/packages/editor-ui/src/stores/sso.ts @@ -1,10 +1,10 @@ -import { computed, reactive, ref } from 'vue'; +import { computed, reactive } from 'vue'; import { defineStore } from 'pinia'; import { EnterpriseEditionFeature } from '@/constants'; import { useRootStore } from '@/stores/n8nRootStore'; import { useSettingsStore } from '@/stores/settings'; import * as ssoApi from '@/api/sso'; -import { SamlPreferences } from '@/Interface'; +import type { SamlPreferences } from '@/Interface'; import { updateCurrentUser } from '@/api/users'; import { useUsersStore } from '@/stores/users'; diff --git a/packages/editor-ui/src/stores/tags.ts b/packages/editor-ui/src/stores/tags.ts index 870b01a573189..70e37bc5f0a3d 100644 --- a/packages/editor-ui/src/stores/tags.ts +++ b/packages/editor-ui/src/stores/tags.ts @@ -1,6 +1,6 @@ import { createTag, deleteTag, getTags, updateTag } from '@/api/tags'; import { STORES } from '@/constants'; -import { ITag, ITagsState } from '@/Interface'; +import type { ITag, ITagsState } from '@/Interface'; import { defineStore } from 'pinia'; import Vue from 'vue'; import { useRootStore } from './n8nRootStore'; diff --git a/packages/editor-ui/src/stores/telemetry.ts b/packages/editor-ui/src/stores/telemetry.ts index 26338651a439e..ed3b68bbf01f4 100644 --- a/packages/editor-ui/src/stores/telemetry.ts +++ b/packages/editor-ui/src/stores/telemetry.ts @@ -1,7 +1,8 @@ import type { Telemetry } from '@/plugins/telemetry'; -import { ITelemetryTrackProperties } from 'n8n-workflow'; +import type { ITelemetryTrackProperties } from 'n8n-workflow'; import { defineStore } from 'pinia'; -import { ref, Ref } from 'vue'; +import type { Ref } from 'vue'; +import { ref } from 'vue'; export const useTelemetryStore = defineStore('telemetry', () => { const telemetry: Ref<Telemetry | undefined> = ref(); diff --git a/packages/editor-ui/src/stores/templates.ts b/packages/editor-ui/src/stores/templates.ts index 9b59e30cd6514..694a5ca284879 100644 --- a/packages/editor-ui/src/stores/templates.ts +++ b/packages/editor-ui/src/stores/templates.ts @@ -1,6 +1,6 @@ import { defineStore } from 'pinia'; import { STORES } from '@/constants'; -import { +import type { ITemplatesCategory, ITemplatesCollection, ITemplatesCollectionFull, diff --git a/packages/editor-ui/src/stores/ui.ts b/packages/editor-ui/src/stores/ui.ts index e9b18cef260ba..cc9ce72dccca3 100644 --- a/packages/editor-ui/src/stores/ui.ts +++ b/packages/editor-ui/src/stores/ui.ts @@ -32,7 +32,7 @@ import { WORKFLOW_SHARE_MODAL_KEY, USER_ACTIVATION_SURVEY_MODAL, } from '@/constants'; -import { +import type { CurlToJSONResponse, IFakeDoorLocation, IMenuItem, @@ -49,7 +49,8 @@ import { getCurlToJson } from '@/api/curlHelper'; import { useWorkflowsStore } from './workflows'; import { useSettingsStore } from './settings'; import { useUsageStore } from './usage'; -import { i18n as locale, BaseTextKey } from '@/plugins/i18n'; +import type { BaseTextKey } from '@/plugins/i18n'; +import { i18n as locale } from '@/plugins/i18n'; export const useUIStore = defineStore(STORES.UI, { state: (): UIState => ({ diff --git a/packages/editor-ui/src/stores/usage.ts b/packages/editor-ui/src/stores/usage.ts index e9e1b7dc77df2..49df9cb2f1bda 100644 --- a/packages/editor-ui/src/stores/usage.ts +++ b/packages/editor-ui/src/stores/usage.ts @@ -1,6 +1,6 @@ import { computed, reactive } from 'vue'; import { defineStore } from 'pinia'; -import { UsageState } from '@/Interface'; +import type { UsageState } from '@/Interface'; import { activateLicenseKey, getLicense, renewLicense } from '@/api/usage'; import { useRootStore } from '@/stores/n8nRootStore'; import { useSettingsStore } from '@/stores/settings'; diff --git a/packages/editor-ui/src/stores/users.ts b/packages/editor-ui/src/stores/users.ts index 330046557deec..29dfa0ceb04e6 100644 --- a/packages/editor-ui/src/stores/users.ts +++ b/packages/editor-ui/src/stores/users.ts @@ -20,12 +20,7 @@ import { validatePasswordToken, validateSignupToken, } from '@/api/users'; -import { - PERSONALIZATION_MODAL_KEY, - USER_ACTIVATION_SURVEY_MODAL, - STORES, - LOCAL_STORAGE_ACTIVE_MODAL, -} from '@/constants'; +import { PERSONALIZATION_MODAL_KEY, USER_ACTIVATION_SURVEY_MODAL, STORES } from '@/constants'; import type { ICredentialsResponse, IInviteResponse, diff --git a/packages/editor-ui/src/stores/versionControl.ts b/packages/editor-ui/src/stores/versionControl.ts new file mode 100644 index 0000000000000..9c0d0abfa0a38 --- /dev/null +++ b/packages/editor-ui/src/stores/versionControl.ts @@ -0,0 +1,88 @@ +import { computed, reactive } from 'vue'; +import { defineStore } from 'pinia'; +import type { IDataObject } from 'n8n-workflow'; +import { EnterpriseEditionFeature } from '@/constants'; +import { useSettingsStore } from '@/stores/settings'; +import * as vcApi from '@/api/versionControl'; +import { useRootStore } from '@/stores/n8nRootStore'; +import type { VersionControlPreferences } from '@/Interface'; + +export const useVersionControlStore = defineStore('versionControl', () => { + const rootStore = useRootStore(); + const settingsStore = useSettingsStore(); + + const isEnterpriseVersionControlEnabled = computed(() => + settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.VersionControl), + ); + + const preferences = reactive<VersionControlPreferences>({ + branchName: '', + authorName: '', + authorEmail: '', + repositoryUrl: '', + branchReadOnly: false, + branchColor: '#000000', + connected: false, + publicKey: '', + }); + + const state = reactive({ + branches: [] as string[], + currentBranch: '', + authorName: '', + authorEmail: '', + repositoryUrl: '', + sshKey: '', + commitMessage: 'commit message', + }); + + const initSsh = async (data: IDataObject) => { + state.sshKey = await vcApi.initSsh(rootStore.getRestApiContext, data); + }; + + const initRepository = async () => { + const { branches, currentBranch } = await vcApi.initRepository(rootStore.getRestApiContext); + state.branches = branches; + state.currentBranch = currentBranch; + }; + + const sync = async (data: { commitMessage: string }) => { + state.commitMessage = data.commitMessage; + return vcApi.sync(rootStore.getRestApiContext, { message: data.commitMessage }); + }; + const getConfig = async () => { + const { remoteRepository, name, email, currentBranch } = await vcApi.getConfig( + rootStore.getRestApiContext, + ); + state.repositoryUrl = remoteRepository; + state.authorName = name; + state.authorEmail = email; + state.currentBranch = currentBranch; + }; + + const setPreferences = (data: Partial<VersionControlPreferences>) => { + Object.assign(preferences, data); + }; + + const getPreferences = async () => { + const data = await vcApi.getPreferences(rootStore.getRestApiContext); + setPreferences(data); + }; + + const savePreferences = async (preferences: Partial<VersionControlPreferences>) => { + const data = await vcApi.setPreferences(rootStore.getRestApiContext, preferences); + setPreferences(data); + }; + + return { + isEnterpriseVersionControlEnabled, + state, + initSsh, + initRepository, + sync, + getConfig, + getPreferences, + setPreferences, + savePreferences, + }; +}); diff --git a/packages/editor-ui/src/stores/versions.ts b/packages/editor-ui/src/stores/versions.ts index cc16d0c2da145..6642e71dad498 100644 --- a/packages/editor-ui/src/stores/versions.ts +++ b/packages/editor-ui/src/stores/versions.ts @@ -1,6 +1,6 @@ import { getNextVersions } from '@/api/versions'; import { STORES } from '@/constants'; -import { IVersion, IVersionNotificationSettings, IVersionsState } from '@/Interface'; +import type { IVersion, IVersionNotificationSettings, IVersionsState } from '@/Interface'; import { defineStore } from 'pinia'; import { useRootStore } from './n8nRootStore'; diff --git a/packages/editor-ui/src/stores/webhooks.ts b/packages/editor-ui/src/stores/webhooks.ts index ab03396afa88b..4b510686a2c18 100644 --- a/packages/editor-ui/src/stores/webhooks.ts +++ b/packages/editor-ui/src/stores/webhooks.ts @@ -1,7 +1,7 @@ import { STORES } from '@/constants'; -import { IFakeDoor, INodeUi, IRootState, NestedRecord } from '@/Interface'; -import { IMenuItem } from 'n8n-design-system'; -import { IWorkflowSettings } from 'n8n-workflow'; +import type { IFakeDoor, INodeUi, IRootState, NestedRecord } from '@/Interface'; +import type { IMenuItem } from 'n8n-design-system'; +import type { IWorkflowSettings } from 'n8n-workflow'; import { defineStore } from 'pinia'; import { useRootStore } from './n8nRootStore'; import { useNDVStore } from './ndv'; diff --git a/packages/editor-ui/src/stores/workflows.ee.ts b/packages/editor-ui/src/stores/workflows.ee.ts index 78f95173db9b9..0f9fcb83b971b 100644 --- a/packages/editor-ui/src/stores/workflows.ee.ts +++ b/packages/editor-ui/src/stores/workflows.ee.ts @@ -1,5 +1,5 @@ import Vue from 'vue'; -import { IUser } from '../Interface'; +import type { IUser } from '../Interface'; import { setWorkflowSharedWith } from '@/api/workflows.ee'; import { EnterpriseEditionFeature, STORES } from '@/constants'; import { useRootStore } from '@/stores/n8nRootStore'; diff --git a/packages/editor-ui/src/stores/workflows.ts b/packages/editor-ui/src/stores/workflows.ts index 6a8f2efea54a7..9ddd9300ee4b1 100644 --- a/packages/editor-ui/src/stores/workflows.ts +++ b/packages/editor-ui/src/stores/workflows.ts @@ -2,29 +2,38 @@ import { DEFAULT_NEW_WORKFLOW_NAME, DUPLICATE_POSTFFIX, EnterpriseEditionFeature, + ERROR_TRIGGER_NODE_TYPE, MAX_WORKFLOW_NAME_LENGTH, PLACEHOLDER_EMPTY_WORKFLOW_ID, + START_NODE_TYPE, STORES, } from '@/constants'; -import { +import type { ExecutionsQueryFilter, + IActivationError, + IExecutionDeleteFilter, + IExecutionPushResponse, IExecutionResponse, IExecutionsCurrentSummaryExtended, + IExecutionsListResponse, + IExecutionsStopData, INewWorkflowData, INodeUi, INodeUpdatePropertiesInformation, IPushDataExecutionFinished, IPushDataNodeExecuteAfter, IPushDataUnsavedExecutionFinished, + IStartRunData, IUpdateInformation, IUsedCredential, + IWorkflowDataUpdate, IWorkflowDb, IWorkflowsMap, WorkflowsState, } from '@/Interface'; import { defineStore } from 'pinia'; -import { - deepCopy, +import type { + IAbstractEventMessage, IConnection, IConnections, IDataObject, @@ -36,14 +45,16 @@ import { INodeExecutionData, INodeIssueData, INodeParameters, + INodeTypeData, + INodeTypes, IPinData, IRun, IRunData, IRunExecutionData, ITaskData, IWorkflowSettings, - NodeHelpers, } from 'n8n-workflow'; +import { deepCopy, NodeHelpers, Workflow } from 'n8n-workflow'; import Vue from 'vue'; import { useRootStore } from './n8nRootStore'; @@ -64,6 +75,8 @@ import { stringSizeInBytes, isObjectLiteral, isEmpty, + makeRestApiRequest, + unflattenExecutionData, } from '@/utils'; import { useNDVStore } from './ndv'; import { useNodeTypesStore } from './nodeTypes'; @@ -86,6 +99,9 @@ const createEmptyWorkflow = (): IWorkflowDb => ({ usedCredentials: [], }); +let cachedWorkflowKey: string | null = ''; +let cachedWorkflow: Workflow | null = null; + export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, { state: (): WorkflowsState => ({ currentWorkflowId: PLACEHOLDER_EMPTY_WORKFLOW_ID, @@ -190,6 +206,12 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, { allNodes(): INodeUi[] { return this.workflow.nodes || []; }, + /** + * Names of all nodes currently on canvas. + */ + canvasNames(): Set<string> { + return new Set(this.allNodes.map((n) => n.name)); + }, nodesByName(): { [name: string]: INodeUi } { if (!this.workflow.nodes) { return {}; @@ -267,7 +289,99 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, { }, }, actions: { - // Workflow actions + getNodeTypes(): INodeTypes { + const nodeTypes: INodeTypes = { + nodeTypes: {}, + init: async (nodeTypes?: INodeTypeData): Promise<void> => {}, + // @ts-ignore + getByNameAndVersion: (nodeType: string, version?: number): INodeType | undefined => { + const nodeTypeDescription = useNodeTypesStore().getNodeType(nodeType, version); + + if (nodeTypeDescription === null) { + return undefined; + } + + return { + description: nodeTypeDescription, + // As we do not have the trigger/poll functions available in the frontend + // we use the information available to figure out what are trigger nodes + // @ts-ignore + trigger: + (![ERROR_TRIGGER_NODE_TYPE, START_NODE_TYPE].includes(nodeType) && + nodeTypeDescription.inputs.length === 0 && + !nodeTypeDescription.webhooks) || + undefined, + }; + }, + }; + + return nodeTypes; + }, + + // Returns a shallow copy of the nodes which means that all the data on the lower + // levels still only gets referenced but the top level object is a different one. + // This has the advantage that it is very fast and does not cause problems with vuex + // when the workflow replaces the node-parameters. + getNodes(): INodeUi[] { + const nodes = useWorkflowsStore().allNodes; + const returnNodes: INodeUi[] = []; + + for (const node of nodes) { + returnNodes.push(Object.assign({}, node)); + } + + return returnNodes; + }, + + // Returns a workflow instance. + getWorkflow(nodes: INodeUi[], connections: IConnections, copyData?: boolean): Workflow { + const nodeTypes = this.getNodeTypes(); + let workflowId: string | undefined = useWorkflowsStore().workflowId; + if (workflowId && workflowId === PLACEHOLDER_EMPTY_WORKFLOW_ID) { + workflowId = undefined; + } + + const workflowName = useWorkflowsStore().workflowName; + + cachedWorkflow = new Workflow({ + id: workflowId, + name: workflowName, + nodes: copyData ? deepCopy(nodes) : nodes, + connections: copyData ? deepCopy(connections) : connections, + active: false, + nodeTypes, + settings: useWorkflowsStore().workflowSettings, + // @ts-ignore + pinData: useWorkflowsStore().getPinData, + }); + + return cachedWorkflow; + }, + + getCurrentWorkflow(copyData?: boolean): Workflow { + const nodes = this.getNodes(); + const connections = this.allConnections; + const cacheKey = JSON.stringify({ nodes, connections }); + if (!copyData && cachedWorkflow && cacheKey === cachedWorkflowKey) { + return cachedWorkflow; + } + cachedWorkflowKey = cacheKey; + + return this.getWorkflow(nodes, connections, copyData); + }, + + // Returns a workflow from a given URL + async getWorkflowFromUrl(url: string): Promise<IWorkflowDb> { + const rootStore = useRootStore(); + return await makeRestApiRequest(rootStore.getRestApiContext, 'GET', '/workflows/from-url', { + url, + }); + }, + + async getActivationError(id: string): Promise<IActivationError | undefined> { + const rootStore = useRootStore(); + return makeRestApiRequest(rootStore.getRestApiContext, 'GET', `/active/error/${id}`); + }, async fetchAllWorkflows(): Promise<IWorkflowDb[]> { const rootStore = useRootStore(); @@ -393,7 +507,9 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, { }, {}); }, - deleteWorkflow(id: string): void { + async deleteWorkflow(id: string): Promise<void> { + const rootStore = useRootStore(); + await makeRestApiRequest(rootStore.getRestApiContext, 'DELETE', `/workflows/${id}`); const { [id]: deletedWorkflow, ...workflows } = this.workflowsById; this.workflowsById = workflows; }, @@ -929,6 +1045,133 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, { Vue.set(this, 'activeExecutions', newActiveExecutions); }, + async retryExecution(id: string, loadWorkflow?: boolean): Promise<boolean> { + let sendData; + if (loadWorkflow === true) { + sendData = { + loadWorkflow: true, + }; + } + const rootStore = useRootStore(); + return await makeRestApiRequest( + rootStore.getRestApiContext, + 'POST', + `/executions/${id}/retry`, + sendData, + ); + }, + + // Deletes executions + async deleteExecutions(sendData: IExecutionDeleteFilter): Promise<void> { + const rootStore = useRootStore(); + return await makeRestApiRequest( + rootStore.getRestApiContext, + 'POST', + '/executions/delete', + sendData as unknown as IDataObject, + ); + }, + + // TODO: For sure needs some kind of default filter like last day, with max 10 results, ... + async getPastExecutions( + filter: IDataObject, + limit: number, + lastId?: string, + firstId?: string, + ): Promise<IExecutionsListResponse> { + let sendData = {}; + if (filter) { + sendData = { + filter, + firstId, + lastId, + limit, + }; + } + const rootStore = useRootStore(); + return makeRestApiRequest(rootStore.getRestApiContext, 'GET', '/executions', sendData); + }, + + async getCurrentExecutions(filter: IDataObject): Promise<IExecutionsCurrentSummaryExtended[]> { + let sendData = {}; + if (filter) { + sendData = { + filter, + }; + } + const rootStore = useRootStore(); + return await makeRestApiRequest( + rootStore.getRestApiContext, + 'GET', + '/executions-current', + sendData, + ); + }, + + async getExecution(id: string): Promise<IExecutionResponse | undefined> { + const rootStore = useRootStore(); + const response = await makeRestApiRequest( + rootStore.getRestApiContext, + 'GET', + `/executions/${id}`, + ); + return response && unflattenExecutionData(response); + }, + + // Creates a new workflow + async createNewWorkflow(sendData: IWorkflowDataUpdate): Promise<IWorkflowDb> { + const rootStore = useRootStore(); + return makeRestApiRequest( + rootStore.getRestApiContext, + 'POST', + '/workflows', + sendData as unknown as IDataObject, + ); + }, + + // Updates an existing workflow + async updateWorkflow( + id: string, + data: IWorkflowDataUpdate, + forceSave = false, + ): Promise<IWorkflowDb> { + const rootStore = useRootStore(); + return makeRestApiRequest( + rootStore.getRestApiContext, + 'PATCH', + `/workflows/${id}${forceSave ? '?forceSave=true' : ''}`, + data as unknown as IDataObject, + ); + }, + + async runWorkflow(startRunData: IStartRunData): Promise<IExecutionPushResponse> { + const rootStore = useRootStore(); + return await makeRestApiRequest( + rootStore.getRestApiContext, + 'POST', + '/workflows/run', + startRunData as unknown as IDataObject, + ); + }, + + async removeTestWebhook(workflowId: string): Promise<boolean> { + const rootStore = useRootStore(); + return await makeRestApiRequest( + rootStore.getRestApiContext, + 'DELETE', + `/test-webhook/${workflowId}`, + ); + }, + + async stopCurrentExecution(executionId: string): Promise<IExecutionsStopData> { + const rootStore = useRootStore(); + return await makeRestApiRequest( + rootStore.getRestApiContext, + 'POST', + `/executions-current/${executionId}/stop`, + ); + }, + async loadCurrentWorkflowExecutions( requestFilter: ExecutionsQueryFilter, ): Promise<IExecutionsSummary[]> { @@ -951,13 +1194,16 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, { throw error; } }, + async fetchExecutionDataById(executionId: string): Promise<IExecutionResponse | null> { const rootStore = useRootStore(); return await getExecutionData(rootStore.getRestApiContext, executionId); }, + deleteExecution(execution: IExecutionsSummary): void { this.currentWorkflowExecutions.splice(this.currentWorkflowExecutions.indexOf(execution), 1); }, + addToCurrentExecutions(executions: IExecutionsSummary[]): void { executions.forEach((execution) => { const exists = this.currentWorkflowExecutions.find((ex) => ex.id === execution.id); @@ -966,6 +1212,23 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, { } }); }, + // Returns all the available timezones + async getExecutionEvents(id: string): Promise<IAbstractEventMessage[]> { + const rootStore = useRootStore(); + return makeRestApiRequest(rootStore.getRestApiContext, 'GET', '/eventbus/execution/' + id); + }, + // Binary data + async getBinaryUrl(dataPath, mode, fileName, mimeType): string { + const rootStore = useRootStore(); + let restUrl = rootStore.getRestUrl; + if (restUrl.startsWith('/')) restUrl = window.location.origin + restUrl; + const url = new URL(`${restUrl}/data/${dataPath}`); + url.searchParams.append('mode', mode); + if (fileName) url.searchParams.append('fileName', fileName); + if (mimeType) url.searchParams.append('mimeType', mimeType); + return url.toString(); + }, + setNodePristine(nodeName: string, isPristine: boolean): void { Vue.set(this.nodeMetadata[nodeName], 'pristine', isPristine); }, diff --git a/packages/editor-ui/src/styles/autocomplete-theme.scss b/packages/editor-ui/src/styles/autocomplete-theme.scss index f67c42975936e..5cdf4f7442ffd 100644 --- a/packages/editor-ui/src/styles/autocomplete-theme.scss +++ b/packages/editor-ui/src/styles/autocomplete-theme.scss @@ -17,7 +17,6 @@ background-color: var(--color-background-xlight) !important; .cm-tooltip { - white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } diff --git a/packages/editor-ui/src/utils/__tests__/mappingUtils.test.ts b/packages/editor-ui/src/utils/__tests__/mappingUtils.test.ts index d8246a150fc04..51862efecab61 100644 --- a/packages/editor-ui/src/utils/__tests__/mappingUtils.test.ts +++ b/packages/editor-ui/src/utils/__tests__/mappingUtils.test.ts @@ -1,4 +1,4 @@ -import { INodeProperties } from 'n8n-workflow'; +import type { INodeProperties } from 'n8n-workflow'; import { getMappedResult, getMappedExpression } from '../mappingUtils'; const RLC_PARAM: INodeProperties = { diff --git a/packages/editor-ui/src/utils/__tests__/typesUtils.test.ts b/packages/editor-ui/src/utils/__tests__/typesUtils.test.ts index 5c87c1a5315d8..d914146b0d3cb 100644 --- a/packages/editor-ui/src/utils/__tests__/typesUtils.test.ts +++ b/packages/editor-ui/src/utils/__tests__/typesUtils.test.ts @@ -1,6 +1,6 @@ import jp from 'jsonpath'; import { isEmpty, intersection, getSchema, isValidDate } from '@/utils'; -import { Schema } from '@/Interface'; +import type { Schema } from '@/Interface'; describe('Types Utils', () => { describe('isEmpty', () => { diff --git a/packages/editor-ui/src/utils/__tests__/userUtils.test.ts b/packages/editor-ui/src/utils/__tests__/userUtils.test.ts index 0a9f98e64d9f3..b5ed96815724d 100644 --- a/packages/editor-ui/src/utils/__tests__/userUtils.test.ts +++ b/packages/editor-ui/src/utils/__tests__/userUtils.test.ts @@ -4,10 +4,11 @@ import { merge } from 'lodash-es'; import { isAuthorized } from '@/utils'; import { useSettingsStore } from '@/stores/settings'; import { useSSOStore } from '@/stores/sso'; -import { IN8nUISettings, IUser } from '@/Interface'; +import type { IUser } from '@/Interface'; import { routes } from '@/router'; import { VIEWS } from '@/constants'; import { SETTINGS_STORE_DEFAULT_STATE } from '@/__tests__/utils'; +import type { IN8nUISettings } from 'n8n-workflow'; const DEFAULT_SETTINGS: IN8nUISettings = SETTINGS_STORE_DEFAULT_STATE.settings; diff --git a/packages/editor-ui/src/utils/apiUtils.ts b/packages/editor-ui/src/utils/apiUtils.ts index bd107ae78f35e..6d7b809036366 100644 --- a/packages/editor-ui/src/utils/apiUtils.ts +++ b/packages/editor-ui/src/utils/apiUtils.ts @@ -1,6 +1,13 @@ -import axios, { AxiosRequestConfig, Method } from 'axios'; -import { IDataObject } from 'n8n-workflow'; -import type { IRestApiContext } from '@/Interface'; +import type { AxiosRequestConfig, Method } from 'axios'; +import axios from 'axios'; +import type { IDataObject } from 'n8n-workflow'; +import type { + IExecutionFlattedResponse, + IExecutionResponse, + IRestApiContext, + IWorkflowDb, +} from '@/Interface'; +import { parse } from 'flatted'; export const NO_NETWORK_ERROR_CODE = 999; @@ -127,3 +134,27 @@ export async function post( ) { return await request({ method: 'POST', baseURL, endpoint, headers, data: params }); } + +/** + * Unflattens the Execution data. + * + * @param {IExecutionFlattedResponse} fullExecutionData The data to unflatten + */ +export function unflattenExecutionData( + fullExecutionData: IExecutionFlattedResponse, +): IExecutionResponse { + // Unflatten the data + const returnData: IExecutionResponse = { + ...fullExecutionData, + workflowData: fullExecutionData.workflowData as IWorkflowDb, + data: parse(fullExecutionData.data), + }; + + returnData.finished = returnData.finished ? returnData.finished : false; + + if (fullExecutionData.id) { + returnData.id = fullExecutionData.id; + } + + return returnData; +} diff --git a/packages/editor-ui/src/utils/canvasUtils.ts b/packages/editor-ui/src/utils/canvasUtils.ts index 258a0b3e9cdf4..5996802a8880c 100644 --- a/packages/editor-ui/src/utils/canvasUtils.ts +++ b/packages/editor-ui/src/utils/canvasUtils.ts @@ -1,9 +1,9 @@ import { MAIN_HEADER_TABS, VIEWS } from '@/constants'; -import { IZoomConfig } from '@/Interface'; +import type { IZoomConfig } from '@/Interface'; import { useWorkflowsStore } from '@/stores/workflows'; -import { ConnectionDetachedParams } from '@jsplumb/core'; -import { IConnection } from 'n8n-workflow'; -import { Route } from 'vue-router'; +import type { ConnectionDetachedParams } from '@jsplumb/core'; +import type { IConnection } from 'n8n-workflow'; +import type { Route } from 'vue-router'; /* Constants and utility functions mainly used by canvas store diff --git a/packages/editor-ui/src/utils/executionUtils.ts b/packages/editor-ui/src/utils/executionUtils.ts index 5c9babc306d4d..06a64b60d6a7a 100644 --- a/packages/editor-ui/src/utils/executionUtils.ts +++ b/packages/editor-ui/src/utils/executionUtils.ts @@ -1,5 +1,5 @@ -import { ExecutionStatus, IDataObject } from 'n8n-workflow'; -import { ExecutionFilterType, ExecutionsQueryFilter } from '@/Interface'; +import type { ExecutionStatus, IDataObject } from 'n8n-workflow'; +import type { ExecutionFilterType, ExecutionsQueryFilter } from '@/Interface'; import { isEmpty } from '@/utils/typesUtils'; export const executionFilterToQueryFilter = ( diff --git a/packages/editor-ui/src/utils/externalHooks.ts b/packages/editor-ui/src/utils/externalHooks.ts new file mode 100644 index 0000000000000..2cb48bc76988d --- /dev/null +++ b/packages/editor-ui/src/utils/externalHooks.ts @@ -0,0 +1,18 @@ +import type { IDataObject } from 'n8n-workflow'; +import type { Store } from 'pinia'; + +export async function runExternalHook(eventName: string, store: Store, metadata?: IDataObject) { + if (!window.n8nExternalHooks) { + return; + } + + const [resource, operator] = eventName.split('.'); + + if (window.n8nExternalHooks[resource]?.[operator]) { + const hookMethods = window.n8nExternalHooks[resource][operator]; + + for (const hookMethod of hookMethods) { + await hookMethod(store, metadata); + } + } +} diff --git a/packages/editor-ui/src/utils/forceParse.ts b/packages/editor-ui/src/utils/forceParse.ts index 427babd4ce76b..1b0136f0a4ebd 100644 --- a/packages/editor-ui/src/utils/forceParse.ts +++ b/packages/editor-ui/src/utils/forceParse.ts @@ -1,4 +1,4 @@ -import { EditorView } from '@codemirror/view'; +import type { EditorView } from '@codemirror/view'; /** * Simulate user action to force parser to catch up during scroll. diff --git a/packages/editor-ui/src/utils/htmlUtils.ts b/packages/editor-ui/src/utils/htmlUtils.ts index 45ea9fcc1d780..4fb059697d9bd 100644 --- a/packages/editor-ui/src/utils/htmlUtils.ts +++ b/packages/editor-ui/src/utils/htmlUtils.ts @@ -1,13 +1,11 @@ import xss, { friendlyAttrValue } from 'xss'; +import { ALLOWED_HTML_ATTRIBUTES, ALLOWED_HTML_TAGS } from '@/constants'; /* Constants and utility functions that help in HTML, CSS and DOM manipulation */ export function sanitizeHtml(dirtyHtml: string) { - const allowedAttributes = ['href', 'name', 'target', 'title', 'class', 'id']; - const allowedTags = ['p', 'strong', 'b', 'code', 'a', 'br', 'i', 'em', 'small']; - const sanitizedHtml = xss(dirtyHtml, { onTagAttr: (tag, name, value) => { if (tag === 'img' && name === 'src') { @@ -19,8 +17,7 @@ export function sanitizeHtml(dirtyHtml: string) { } } - // Allow `allowedAttributes` and all `data-*` attributes - if (allowedAttributes.includes(name) || name.startsWith('data-')) { + if (ALLOWED_HTML_ATTRIBUTES.includes(name) || name.startsWith('data-')) { return `${name}="${friendlyAttrValue(value)}"`; } @@ -28,7 +25,7 @@ export function sanitizeHtml(dirtyHtml: string) { // Return nothing, means keep the default handling measure }, onTag: (tag) => { - if (!allowedTags.includes(tag)) return ''; + if (!ALLOWED_HTML_TAGS.includes(tag)) return ''; return; }, }); diff --git a/packages/editor-ui/src/utils/index.ts b/packages/editor-ui/src/utils/index.ts index 8cb8b0281681a..f5b1bdb5c1ad6 100644 --- a/packages/editor-ui/src/utils/index.ts +++ b/packages/editor-ui/src/utils/index.ts @@ -1,5 +1,6 @@ export * from './apiUtils'; export * from './canvasUtils'; +export * from './externalHooks'; export * from './htmlUtils'; export * from './nodeTypesUtils'; export * from './sortUtils'; diff --git a/packages/editor-ui/src/utils/mappingUtils.ts b/packages/editor-ui/src/utils/mappingUtils.ts index bf07289b25141..47adeffdec228 100644 --- a/packages/editor-ui/src/utils/mappingUtils.ts +++ b/packages/editor-ui/src/utils/mappingUtils.ts @@ -1,4 +1,5 @@ -import { INodeProperties, isResourceLocatorValue, NodeParameterValueType } from 'n8n-workflow'; +import type { INodeProperties, NodeParameterValueType } from 'n8n-workflow'; +import { isResourceLocatorValue } from 'n8n-workflow'; export function generatePath(root: string, path: Array<string | number>): string { return path.reduce((accu: string, part: string | number) => { diff --git a/packages/editor-ui/src/utils/nodeTypesUtils.ts b/packages/editor-ui/src/utils/nodeTypesUtils.ts index 2d07bf93e95e4..796e727e6bbee 100644 --- a/packages/editor-ui/src/utils/nodeTypesUtils.ts +++ b/packages/editor-ui/src/utils/nodeTypesUtils.ts @@ -1,36 +1,24 @@ -import { MAIN_AUTH_FIELD_NAME, NODE_RESOURCE_FIELD_NAME } from './../constants'; +import { MAIN_AUTH_FIELD_NAME } from './../constants'; import { useWorkflowsStore } from '@/stores/workflows'; import { useNodeTypesStore } from './../stores/nodeTypes'; -import { INodeCredentialDescription } from './../../../workflow/src/Interfaces'; +import type { INodeCredentialDescription } from './../../../workflow/src/Interfaces'; import { CORE_NODES_CATEGORY, - CUSTOM_NODES_CATEGORY, - SUBCATEGORY_DESCRIPTIONS, - UNCATEGORIZED_CATEGORY, - UNCATEGORIZED_SUBCATEGORY, - PERSONALIZED_CATEGORY, NON_ACTIVATABLE_TRIGGER_NODE_TYPES, TEMPLATES_NODES_FILTER, - REGULAR_NODE_FILTER, - TRIGGER_NODE_FILTER, - ALL_NODE_FILTER, MAPPING_PARAMS, } from '@/constants'; -import { - INodeCreateElement, - ICategoriesWithNodes, +import type { INodeUi, ITemplatesNode, - INodeItemProps, NodeAuthenticationOption, INodeUpdatePropertiesInformation, } from '@/Interface'; -import { +import type { IDataObject, INodeExecutionData, INodeProperties, INodeTypeDescription, - INodeActionTypeDescription, NodeParameterValueType, INodePropertyOptions, INodePropertyCollection, @@ -38,7 +26,6 @@ import { import { isResourceLocatorValue, isJsonKeyObject } from '@/utils'; import { useCredentialsStore } from '@/stores/credentials'; import { i18n as locale } from '@/plugins/i18n'; -import { useSettingsStore } from '@/stores/settings'; /* Constants and utility functions mainly used to get information about @@ -49,158 +36,6 @@ const CRED_KEYWORDS_TO_FILTER = ['API', 'OAuth1', 'OAuth2']; const NODE_KEYWORDS_TO_FILTER = ['Trigger']; const COMMUNITY_PACKAGE_NAME_REGEX = /(@\w+\/)?n8n-nodes-(?!base\b)\b\w+/g; -const addNodeToCategory = ( - accu: ICategoriesWithNodes, - nodeType: INodeTypeDescription | INodeActionTypeDescription, - category: string, - subcategory: string, -) => { - if (!accu[category]) { - accu[category] = {}; - } - if (!accu[category][subcategory]) { - accu[category][subcategory] = { - triggerCount: 0, - regularCount: 0, - nodes: [], - }; - } - const isTrigger = nodeType.group.includes('trigger'); - if (isTrigger) { - accu[category][subcategory].triggerCount++; - } - if (!isTrigger) { - accu[category][subcategory].regularCount++; - } - accu[category][subcategory].nodes.push({ - type: nodeType.actionKey ? 'action' : 'node', - key: `${nodeType.name}`, - category, - properties: { - nodeType, - subcategory, - }, - includedByTrigger: isTrigger, - includedByRegular: !isTrigger, - }); -}; - -export const getCategoriesWithNodes = ( - nodeTypes: INodeTypeDescription[], - uncategorizedSubcategory = UNCATEGORIZED_SUBCATEGORY, -): ICategoriesWithNodes => { - const sorted = [...nodeTypes].sort((a: INodeTypeDescription, b: INodeTypeDescription) => - a.displayName > b.displayName ? 1 : -1, - ); - const result = sorted.reduce((accu: ICategoriesWithNodes, nodeType: INodeTypeDescription) => { - if (!nodeType.codex || !nodeType.codex.categories) { - addNodeToCategory(accu, nodeType, UNCATEGORIZED_CATEGORY, uncategorizedSubcategory); - return accu; - } - - nodeType.codex.categories.forEach((_category: string) => { - const category = _category.trim(); - const subcategories = nodeType?.codex?.subcategories?.[category] ?? null; - - if (subcategories === null || subcategories.length === 0) { - addNodeToCategory(accu, nodeType, category, uncategorizedSubcategory); - return; - } - - subcategories.forEach((subcategory) => { - addNodeToCategory(accu, nodeType, category, subcategory); - }); - }); - return accu; - }, {}); - return result; -}; - -const getCategories = (categoriesWithNodes: ICategoriesWithNodes): string[] => { - const excludeFromSort = [ - CORE_NODES_CATEGORY, - CUSTOM_NODES_CATEGORY, - UNCATEGORIZED_CATEGORY, - PERSONALIZED_CATEGORY, - ]; - const categories = Object.keys(categoriesWithNodes); - const sorted = categories.filter((category: string) => !excludeFromSort.includes(category)); - sorted.sort(); - - return [ - CORE_NODES_CATEGORY, - CUSTOM_NODES_CATEGORY, - PERSONALIZED_CATEGORY, - ...sorted, - UNCATEGORIZED_CATEGORY, - ]; -}; - -export const getCategorizedList = ( - categoriesWithNodes: ICategoriesWithNodes, - categoryIsExpanded = false, -): INodeCreateElement[] => { - const categories = getCategories(categoriesWithNodes); - - const result = categories.reduce((accu: INodeCreateElement[], category: string) => { - if (!categoriesWithNodes[category]) { - return accu; - } - - const categoryEl: INodeCreateElement = { - type: 'category', - key: category, - properties: { - category, - name: category, - expanded: categoryIsExpanded, - }, - }; - - const subcategories = Object.keys(categoriesWithNodes[category]); - if (subcategories.length === 1) { - const subcategory = categoriesWithNodes[category][subcategories[0]]; - if (subcategory.triggerCount > 0) { - categoryEl.includedByTrigger = subcategory.triggerCount > 0; - } - if (subcategory.regularCount > 0) { - categoryEl.includedByRegular = subcategory.regularCount > 0; - } - return [...accu, categoryEl, ...subcategory.nodes]; - } - - subcategories.sort(); - const subcategorized = subcategories.reduce( - (accu: INodeCreateElement[], subcategory: string) => { - const subcategoryEl: INodeCreateElement = { - type: 'subcategory', - key: `${category}_${subcategory}`, - properties: { - subcategory, - description: SUBCATEGORY_DESCRIPTIONS[category][subcategory], - }, - includedByTrigger: categoriesWithNodes[category][subcategory].triggerCount > 0, - includedByRegular: categoriesWithNodes[category][subcategory].regularCount > 0, - }; - - if (subcategoryEl.includedByTrigger) { - categoryEl.includedByTrigger = true; - } - if (subcategoryEl.includedByRegular) { - categoryEl.includedByRegular = true; - } - - accu.push(subcategoryEl); - return accu; - }, - [], - ); - - return [...accu, categoryEl, ...subcategorized]; - }, []); - return result; -}; - export function getAppNameFromCredType(name: string) { return name .split(' ') @@ -272,35 +107,6 @@ export const executionDataToJson = (inputData: INodeExecutionData[]): IDataObjec [], ); -export const matchesSelectType = (el: INodeCreateElement, selectedView: string) => { - if (selectedView === REGULAR_NODE_FILTER && el.includedByRegular) { - return true; - } - if (selectedView === TRIGGER_NODE_FILTER && el.includedByTrigger) { - return true; - } - - return selectedView === ALL_NODE_FILTER; -}; - -const matchesAlias = (nodeType: INodeTypeDescription, filter: string): boolean => { - if (!nodeType.codex || !nodeType.codex.alias) { - return false; - } - - return nodeType.codex.alias.reduce((accu: boolean, alias: string) => { - return accu || alias.toLowerCase().indexOf(filter) > -1; - }, false); -}; - -export const matchesNodeType = (el: INodeCreateElement, filter: string) => { - const nodeType = (el.properties as INodeItemProps).nodeType; - - return ( - nodeType.displayName.toLowerCase().indexOf(filter) !== -1 || matchesAlias(nodeType, filter) - ); -}; - export const hasOnlyListMode = (parameter: INodeProperties): boolean => { return ( parameter.modes !== undefined && diff --git a/packages/editor-ui/src/utils/nodeViewUtils.ts b/packages/editor-ui/src/utils/nodeViewUtils.ts index 6884924a38ac9..cfbd415cc6c2d 100644 --- a/packages/editor-ui/src/utils/nodeViewUtils.ts +++ b/packages/editor-ui/src/utils/nodeViewUtils.ts @@ -1,12 +1,12 @@ import { getStyleTokenValue } from '@/utils/htmlUtils'; import { isNumber } from '@/utils'; -import { NODE_OUTPUT_DEFAULT_KEY, STICKY_NODE_TYPE, QUICKSTART_NOTE_NAME } from '@/constants'; -import { EndpointStyle, IBounds, INodeUi, XYPosition } from '@/Interface'; -import { ArrayAnchorSpec, ConnectorSpec, OverlaySpec, PaintStyle } from '@jsplumb/common'; -import { Endpoint, Connection, ConnectionEstablishedParams } from '@jsplumb/core'; +import { NODE_OUTPUT_DEFAULT_KEY, STICKY_NODE_TYPE } from '@/constants'; +import type { EndpointStyle, IBounds, INodeUi, XYPosition } from '@/Interface'; +import type { ArrayAnchorSpec, ConnectorSpec, OverlaySpec, PaintStyle } from '@jsplumb/common'; +import type { Endpoint, Connection } from '@jsplumb/core'; import { N8nConnector } from '@/plugins/connectors/N8nCustomConnector'; import { closestNumberDivisibleBy } from '@/utils'; -import { +import type { IConnection, INode, ITaskData, diff --git a/packages/editor-ui/src/utils/pairedItemUtils.ts b/packages/editor-ui/src/utils/pairedItemUtils.ts index 3f1d2efc41846..0f5c0fc95ad94 100644 --- a/packages/editor-ui/src/utils/pairedItemUtils.ts +++ b/packages/editor-ui/src/utils/pairedItemUtils.ts @@ -1,5 +1,5 @@ -import { IPairedItemData, IRunData, ITaskData } from 'n8n-workflow'; -import { IExecutionResponse, TargetItem } from '../Interface'; +import type { IPairedItemData, IRunData, ITaskData } from 'n8n-workflow'; +import type { IExecutionResponse, TargetItem } from '../Interface'; import { isNotNull } from '@/utils'; /* diff --git a/packages/editor-ui/src/utils/typeGuards.ts b/packages/editor-ui/src/utils/typeGuards.ts index 0ccfa9b4613df..e38edb441fa7b 100644 --- a/packages/editor-ui/src/utils/typeGuards.ts +++ b/packages/editor-ui/src/utils/typeGuards.ts @@ -1,6 +1,5 @@ -import { NewCredentialsModal } from './../Interface'; -import { INodeParameterResourceLocator } from 'n8n-workflow'; -import { ICredentialsResponse } from '@/Interface'; +import type { INodeParameterResourceLocator } from 'n8n-workflow'; +import type { ICredentialsResponse, NewCredentialsModal } from '@/Interface'; /* Type guards used in editor-ui project diff --git a/packages/editor-ui/src/utils/typesUtils.ts b/packages/editor-ui/src/utils/typesUtils.ts index 4a4f5d28b48f4..2135d869c9f82 100644 --- a/packages/editor-ui/src/utils/typesUtils.ts +++ b/packages/editor-ui/src/utils/typesUtils.ts @@ -1,10 +1,9 @@ import dateformat from 'dateformat'; -import { IDataObject, jsonParse } from 'n8n-workflow'; -import { Schema, Optional, Primitives } from '@/Interface'; +import type { IDataObject } from 'n8n-workflow'; +import { jsonParse } from 'n8n-workflow'; +import type { Schema, Optional, Primitives } from '@/Interface'; import { isObj } from '@/utils/typeGuards'; import { generatePath } from '@/utils/mappingUtils'; -import { DateTime } from 'luxon'; -import { useWorkflowsStore } from '@/stores/workflows'; /* Constants and utility functions than can be used to manipulate different data types and objects diff --git a/packages/editor-ui/src/utils/userUtils.ts b/packages/editor-ui/src/utils/userUtils.ts index 2d7b1ce01b9e2..b9797f0a19911 100644 --- a/packages/editor-ui/src/utils/userUtils.ts +++ b/packages/editor-ui/src/utils/userUtils.ts @@ -60,7 +60,7 @@ import { GOOGLE_SHEETS_NODE_TYPE, CODE_NODE_TYPE, } from '@/constants'; -import { +import type { IPermissions, IPersonalizationSurveyAnswersV1, IPersonalizationSurveyAnswersV2, @@ -68,8 +68,10 @@ import { IPersonalizationSurveyAnswersV4, IPersonalizationSurveyVersions, IUser, + ILogInStatus, + IRole, + IUserPermissions, } from '@/Interface'; -import { ILogInStatus, IRole, IUserPermissions } from '@/Interface'; /* Utility functions used to handle users in n8n diff --git a/packages/editor-ui/src/views/AuthView.vue b/packages/editor-ui/src/views/AuthView.vue index 611090cbd4cba..7a3fa27ac7f7d 100644 --- a/packages/editor-ui/src/views/AuthView.vue +++ b/packages/editor-ui/src/views/AuthView.vue @@ -22,12 +22,12 @@ </template> <script lang="ts"> -import Vue from 'vue'; +import { defineComponent } from 'vue'; import Logo from '@/components/Logo.vue'; import SSOLogin from '@/components/SSOLogin.vue'; -export default Vue.extend({ +export default defineComponent({ name: 'AuthView', components: { Logo, diff --git a/packages/editor-ui/src/views/CanvasAddButton.vue b/packages/editor-ui/src/views/CanvasAddButton.vue index 945f8151b457d..5fa33c79c41fb 100644 --- a/packages/editor-ui/src/views/CanvasAddButton.vue +++ b/packages/editor-ui/src/views/CanvasAddButton.vue @@ -26,7 +26,7 @@ <script setup lang="ts"> import { computed } from 'vue'; -import { XYPosition } from '@/Interface'; +import type { XYPosition } from '@/Interface'; import { useNodeCreatorStore } from '@/stores/nodeCreator'; export interface Props { diff --git a/packages/editor-ui/src/views/ChangePasswordView.vue b/packages/editor-ui/src/views/ChangePasswordView.vue index a40bfefbe27ac..e88df40ca4467 100644 --- a/packages/editor-ui/src/views/ChangePasswordView.vue +++ b/packages/editor-ui/src/views/ChangePasswordView.vue @@ -13,7 +13,7 @@ import AuthView from './AuthView.vue'; import { showMessage } from '@/mixins/showMessage'; import mixins from 'vue-typed-mixins'; -import { IFormBoxConfig } from '@/Interface'; +import type { IFormBoxConfig } from '@/Interface'; import { VIEWS } from '@/constants'; import { mapStores } from 'pinia'; import { useUsersStore } from '@/stores/users'; diff --git a/packages/editor-ui/src/views/CredentialsView.vue b/packages/editor-ui/src/views/CredentialsView.vue index 7c037bce62da4..7ba21ea323d4a 100644 --- a/packages/editor-ui/src/views/CredentialsView.vue +++ b/packages/editor-ui/src/views/CredentialsView.vue @@ -45,7 +45,7 @@ <script lang="ts"> import { showMessage } from '@/mixins/showMessage'; -import { ICredentialsResponse, ICredentialTypeMap, IUser } from '@/Interface'; +import type { ICredentialsResponse, ICredentialTypeMap } from '@/Interface'; import mixins from 'vue-typed-mixins'; import SettingsView from './SettingsView.vue'; @@ -53,13 +53,13 @@ import ResourcesListLayout from '@/components/layouts/ResourcesListLayout.vue'; import PageViewLayout from '@/components/layouts/PageViewLayout.vue'; import PageViewLayoutList from '@/components/layouts/PageViewLayoutList.vue'; import CredentialCard from '@/components/CredentialCard.vue'; -import { ICredentialType } from 'n8n-workflow'; +import type { ICredentialType } from 'n8n-workflow'; import TemplateCard from '@/components/TemplateCard.vue'; import { debounceHelper } from '@/mixins/debounce'; import ResourceOwnershipSelect from '@/components/forms/ResourceOwnershipSelect.ee.vue'; import ResourceFiltersDropdown from '@/components/forms/ResourceFiltersDropdown.vue'; import { CREDENTIAL_SELECT_MODAL_KEY } from '@/constants'; -import Vue from 'vue'; +import type Vue from 'vue'; import { mapStores } from 'pinia'; import { useUIStore } from '@/stores/ui'; import { useUsersStore } from '@/stores/users'; diff --git a/packages/editor-ui/src/views/ErrorView.vue b/packages/editor-ui/src/views/ErrorView.vue index 6da6cab01a395..80a337d73ab86 100644 --- a/packages/editor-ui/src/views/ErrorView.vue +++ b/packages/editor-ui/src/views/ErrorView.vue @@ -18,9 +18,9 @@ </template> <script lang="ts"> -import Vue from 'vue'; +import { defineComponent } from 'vue'; -export default Vue.extend({ +export default defineComponent({ name: 'ErrorView', props: { messageKey: { diff --git a/packages/editor-ui/src/views/ExecutionsView.vue b/packages/editor-ui/src/views/ExecutionsView.vue index 21bea4def6679..4530be2933e23 100644 --- a/packages/editor-ui/src/views/ExecutionsView.vue +++ b/packages/editor-ui/src/views/ExecutionsView.vue @@ -3,10 +3,10 @@ </template> <script lang="ts"> -import Vue from 'vue'; +import { defineComponent } from 'vue'; import ExecutionsList from '@/components/ExecutionsList.vue'; -export default Vue.extend({ +export default defineComponent({ name: 'ExecutionsView', components: { ExecutionsList, diff --git a/packages/editor-ui/src/views/ForgotMyPasswordView.vue b/packages/editor-ui/src/views/ForgotMyPasswordView.vue index e930f6285eb3d..4ae66a0200470 100644 --- a/packages/editor-ui/src/views/ForgotMyPasswordView.vue +++ b/packages/editor-ui/src/views/ForgotMyPasswordView.vue @@ -7,7 +7,7 @@ import AuthView from './AuthView.vue'; import { showMessage } from '@/mixins/showMessage'; import mixins from 'vue-typed-mixins'; -import { IFormBoxConfig } from '@/Interface'; +import type { IFormBoxConfig } from '@/Interface'; import { mapStores } from 'pinia'; import { useSettingsStore } from '@/stores/settings'; import { useUsersStore } from '@/stores/users'; diff --git a/packages/editor-ui/src/views/NodeView.vue b/packages/editor-ui/src/views/NodeView.vue index 6005f9d7f06ea..2e330d31a2ca0 100644 --- a/packages/editor-ui/src/views/NodeView.vue +++ b/packages/editor-ui/src/views/NodeView.vue @@ -165,18 +165,20 @@ import Vue from 'vue'; import { mapStores } from 'pinia'; -import { +import type { Endpoint, Connection, - EVENT_CONNECTION, ConnectionEstablishedParams, - EVENT_CONNECTION_DETACHED, - EVENT_CONNECTION_MOVED, - INTERCEPT_BEFORE_DROP, BeforeDropParams, ConnectionDetachedParams, ConnectionMovedParams, } from '@jsplumb/core'; +import { + EVENT_CONNECTION, + EVENT_CONNECTION_DETACHED, + EVENT_CONNECTION_MOVED, + INTERCEPT_BEFORE_DROP, +} from '@jsplumb/core'; import type { MessageBoxInputData } from 'element-ui/types/message-box'; import { @@ -194,10 +196,9 @@ import { STICKY_NODE_TYPE, VIEWS, WEBHOOK_NODE_TYPE, - TRIGGER_NODE_FILTER, + TRIGGER_NODE_CREATOR_VIEW, EnterpriseEditionFeature, - ASSUMPTION_EXPERIMENT, - REGULAR_NODE_FILTER, + REGULAR_NODE_CREATOR_VIEW, MANUAL_TRIGGER_NODE_TYPE, NODE_CREATOR_OPEN_SOURCES, } from '@/constants'; @@ -205,11 +206,12 @@ import { copyPaste } from '@/mixins/copyPaste'; import { externalHooks } from '@/mixins/externalHooks'; import { genericHelpers } from '@/mixins/genericHelpers'; import { moveNodeWorkflow } from '@/mixins/moveNodeWorkflow'; -import { restApi } from '@/mixins/restApi'; import useGlobalLinkActions from '@/composables/useGlobalLinkActions'; import useCanvasMouseSelect from '@/composables/useCanvasMouseSelect'; import { showMessage } from '@/mixins/showMessage'; -import { titleChange } from '@/mixins/titleChange'; +import { useTitleChange } from '@/composables/useTitleChange'; +import { useUniqueNodeName } from '@/composables/useUniqueNodeName'; +import { useI18n } from '@/composables/useI18n'; import { workflowHelpers } from '@/mixins/workflowHelpers'; import { workflowRun } from '@/mixins/workflowRun'; @@ -221,11 +223,11 @@ import Sticky from '@/components/Sticky.vue'; import CanvasAddButton from './CanvasAddButton.vue'; import mixins from 'vue-typed-mixins'; import { v4 as uuid } from 'uuid'; -import { - deepCopy, +import type { IConnection, IConnections, IDataObject, + IExecutionsSummary, INode, INodeConnections, INodeCredentialsDetails, @@ -237,10 +239,9 @@ import { ITaskData, ITelemetryTrackProperties, IWorkflowBase, - NodeHelpers, - TelemetryHelpers, Workflow, } from 'n8n-workflow'; +import { deepCopy, NodeHelpers, TelemetryHelpers } from 'n8n-workflow'; import type { ICredentialsResponse, IExecutionResponse, @@ -254,7 +255,6 @@ import type { ITag, INewWorkflowData, IWorkflowTemplate, - IExecutionsSummary, IWorkflowToShare, IUser, INodeUpdatePropertiesInformation, @@ -265,7 +265,7 @@ import { debounceHelper } from '@/mixins/debounce'; import { useUIStore } from '@/stores/ui'; import { useSettingsStore } from '@/stores/settings'; import { useUsersStore } from '@/stores/users'; -import { Route, RawLocation } from 'vue-router'; +import type { Route, RawLocation } from 'vue-router'; import { dataPinningEventBus, nodeViewEventBus } from '@/event-bus'; import { useWorkflowsStore } from '@/stores/workflows'; import { useRootStore } from '@/stores/n8nRootStore'; @@ -291,6 +291,7 @@ import { RenameNodeCommand, historyBus, } from '@/models/history'; +import type { BrowserJsPlumbInstance } from '@jsplumb/browser-ui'; import { EVENT_ENDPOINT_MOUSEOVER, EVENT_ENDPOINT_MOUSEOUT, @@ -299,15 +300,13 @@ import { EVENT_CONNECTION_ABORT, EVENT_CONNECTION_MOUSEOUT, EVENT_CONNECTION_MOUSEOVER, - BrowserJsPlumbInstance, ready, } from '@jsplumb/browser-ui'; +import type { N8nPlusEndpoint } from '@/plugins/endpoints/N8nPlusEndpointType'; import { - N8nPlusEndpoint, N8nPlusEndpointType, EVENT_PLUS_ENDPOINT_CLICK, } from '@/plugins/endpoints/N8nPlusEndpointType'; -import { usePostHog } from '@/stores/posthog'; interface AddNodeOptions { position?: XYPosition; @@ -323,9 +322,7 @@ export default mixins( externalHooks, genericHelpers, moveNodeWorkflow, - restApi, showMessage, - titleChange, workflowHelpers, workflowRun, debounceHelper, @@ -345,6 +342,9 @@ export default mixins( return { ...useCanvasMouseSelect(), ...useGlobalLinkActions(), + ...useTitleChange(), + ...useUniqueNodeName(), + ...useI18n(), }; }, errorCaptured: (err, vm, info) => { @@ -403,9 +403,9 @@ export default mixins( this.canvasStore.setRecenteredCanvasAddButtonPosition(this.getNodeViewOffsetPosition); }, nodeViewScale(newScale) { - const element = this.$refs.nodeView as HTMLDivElement; - if (element) { - element.style.transform = `scale(${newScale})`; + const elementRef = this.$refs.nodeView as HTMLDivElement | undefined; + if (elementRef) { + elementRef.style.transform = `scale(${newScale})`; } }, }, @@ -446,7 +446,7 @@ export default mixins( } } else if (confirmModal === MODAL_CANCEL) { this.workflowsStore.setWorkflowId(PLACEHOLDER_EMPTY_WORKFLOW_ID); - await this.resetWorkspace(); + this.resetWorkspace(); this.uiStore.stateIsDirty = false; next(); } @@ -479,12 +479,6 @@ export default mixins( currentUser(): IUser | null { return this.usersStore.currentUser; }, - defaultLocale(): string { - return this.rootStore.defaultLocale; - }, - isEnglishLocale(): boolean { - return this.defaultLocale === 'en'; - }, activeNode(): INodeUi | null { return this.ndvStore.activeNode; }, @@ -680,78 +674,6 @@ export default mixins( this.workflowsStore.workflowExecutionData = null; this.updateNodesExecutionIssues(); }, - translateName(type: string, originalName: string) { - return this.$locale.headerText({ - key: `headers.${this.$locale.shortNodeType(type)}.displayName`, - fallback: originalName, - }); - }, - getUniqueNodeName({ - originalName, - additionalUsedNames = [], - type = '', - }: { - originalName: string; - additionalUsedNames?: string[]; - type?: string; - }) { - const allNodeNamesOnCanvas = this.workflowsStore.allNodes.map((n: INodeUi) => n.name); - originalName = this.isEnglishLocale ? originalName : this.translateName(type, originalName); - - if ( - !allNodeNamesOnCanvas.includes(originalName) && - !additionalUsedNames.includes(originalName) - ) { - return originalName; // already unique - } - - let natives: string[] = this.nativelyNumberSuffixedDefaults; - natives = this.isEnglishLocale - ? natives - : natives.map((name) => { - const type = name.toLowerCase().replace('_', ''); - return this.translateName(type, name); - }); - - const found = natives.find((n) => originalName.startsWith(n)); - - let ignore, baseName, nameIndex, uniqueName; - let index = 1; - - if (found) { - // name natively ends with number - nameIndex = originalName.split(found).pop(); - if (nameIndex) { - index = parseInt(nameIndex, 10); - } - baseName = uniqueName = found; - } else { - const nameMatch = originalName.match(/(.*\D+)(\d*)/); - - if (nameMatch === null) { - // name is only a number - index = parseInt(originalName, 10); - baseName = ''; - uniqueName = baseName + index; - } else { - // name is string or string/number combination - [ignore, baseName, nameIndex] = nameMatch; - if (nameIndex !== '') { - index = parseInt(nameIndex, 10); - } - uniqueName = baseName; - } - } - - while ( - allNodeNamesOnCanvas.includes(uniqueName) || - additionalUsedNames.includes(uniqueName) - ) { - uniqueName = baseName + index++; - } - - return uniqueName; - }, async onSaveKeyboardShortcut(e: KeyboardEvent) { let saved = await this.saveCurrentWorkflow(); if (saved) await this.settingsStore.fetchPromptsData(); @@ -773,7 +695,7 @@ export default mixins( }, showTriggerCreator(source: NodeCreatorOpenSource) { if (this.createNodeActive) return; - this.nodeCreatorStore.setSelectedView(TRIGGER_NODE_FILTER); + this.nodeCreatorStore.setSelectedView(TRIGGER_NODE_CREATOR_VIEW); this.nodeCreatorStore.setShowScrim(true); this.onToggleNodeCreator({ source, createNodeActive: true }); }, @@ -782,7 +704,7 @@ export default mixins( this.resetWorkspace(); let data: IExecutionResponse | undefined; try { - data = await this.restApi().getExecution(executionId); + data = await this.workflowsStore.getExecution(executionId); } catch (error) { this.$showError(error, this.$locale.baseText('nodeView.showError.openExecution.title')); return; @@ -1402,14 +1324,14 @@ export default mixins( try { this.stopExecutionInProgress = true; - await this.restApi().stopCurrentExecution(executionId); + await this.workflowsStore.stopCurrentExecution(executionId); this.$showMessage({ title: this.$locale.baseText('nodeView.showMessage.stopExecutionTry.title'), type: 'success', }); } catch (error) { // Execution stop might fail when the execution has already finished. Let's treat this here. - const execution = await this.restApi().getExecution(executionId); + const execution = await this.workflowsStore.getExecution(executionId); if (execution === undefined) { // execution finished but was not saved (e.g. due to low connectivity) @@ -1445,7 +1367,7 @@ export default mixins( retryOf: execution.retryOf, } as IPushDataExecutionFinished; this.workflowsStore.finishActiveExecution(pushData); - this.$titleSet(execution.workflowData.name, 'IDLE'); + this.titleSet(execution.workflowData.name, 'IDLE'); this.workflowsStore.executingNode = null; this.workflowsStore.setWorkflowExecutionData(executedData as IExecutionResponse); this.uiStore.removeActiveAction('workflowRunning'); @@ -1475,7 +1397,7 @@ export default mixins( async stopWaitingForWebhook() { try { - await this.restApi().removeTestWebhook(this.workflowsStore.workflowId); + await this.workflowsStore.removeTestWebhook(this.workflowsStore.workflowId); } catch (error) { this.$showError( error, @@ -1548,7 +1470,7 @@ export default mixins( this.startLoading(); try { - workflowData = await this.restApi().getWorkflowFromUrl(url); + workflowData = await this.workflowsStore.getWorkflowFromUrl(url); } catch (error) { this.stopLoading(); this.$showError( @@ -1748,13 +1670,19 @@ export default mixins( }, async getNewNodeWithDefaultCredential(nodeTypeData: INodeTypeDescription) { + let nodeVersion = nodeTypeData.defaultVersion; + + if (nodeVersion === undefined) { + nodeVersion = Array.isArray(nodeTypeData.version) + ? nodeTypeData.version.slice(-1)[0] + : nodeTypeData.version; + } + const newNodeData: INodeUi = { id: uuid(), name: nodeTypeData.defaults.name as string, type: nodeTypeData.name, - typeVersion: Array.isArray(nodeTypeData.version) - ? nodeTypeData.version.slice(-1)[0] - : nodeTypeData.version, + typeVersion: nodeVersion, position: [0, 0], parameters: {}, }; @@ -1933,11 +1861,9 @@ export default mixins( newNodeData.position = NodeViewUtils.getNewNodePosition(this.nodes, position); } - // Check if node-name is unique else find one that is - newNodeData.name = this.getUniqueNodeName({ - originalName: newNodeData.name, - type: newNodeData.type, - }); + const localizedName = this.localizeNodeName(newNodeData.name, newNodeData.type); + + newNodeData.name = this.uniqueNodeName(localizedName); if (nodeTypeData.webhooks && nodeTypeData.webhooks.length) { newNodeData.webhookId = uuid(); @@ -2552,7 +2478,7 @@ export default mixins( // In case the workflow got saved we do not have to run init // as only the route changed but all the needed data is already loaded this.uiStore.stateIsDirty = false; - return Promise.resolve(); + return; } if (this.blankRedirect) { this.blankRedirect = false; @@ -2574,7 +2500,7 @@ export default mixins( const saved = await this.saveCurrentWorkflow(); if (saved) await this.settingsStore.fetchPromptsData(); } else if (confirmModal === MODAL_CLOSE) { - return Promise.resolve(); + return; } } // Load a workflow @@ -2585,7 +2511,7 @@ export default mixins( if (workflowId !== null) { let workflow: IWorkflowDb | undefined = undefined; try { - workflow = await this.restApi().getWorkflow(workflowId); + workflow = await this.workflowsStore.fetchWorkflow(workflowId); } catch (error) { this.$showError(error, this.$locale.baseText('openWorkflow.workflowNotFoundError')); @@ -2595,7 +2521,7 @@ export default mixins( } if (workflow) { - this.$titleSet(workflow.name, 'IDLE'); + this.titleSet(workflow.name, 'IDLE'); // Open existing workflow await this.openWorkflow(workflow); } @@ -2734,11 +2660,9 @@ export default mixins( const newNodeData = deepCopy(this.getNodeDataToSave(node)); newNodeData.id = uuid(); - // Check if node-name is unique else find one that is - newNodeData.name = this.getUniqueNodeName({ - originalName: newNodeData.name, - type: newNodeData.type, - }); + const localizedName = this.localizeNodeName(newNodeData.name, newNodeData.type); + + newNodeData.name = this.uniqueNodeName(localizedName); newNodeData.position = NodeViewUtils.getNewNodePosition( this.nodes, @@ -3118,10 +3042,7 @@ export default mixins( this.renamingActive = true; } - // Check if node-name is unique else find one that is - newName = this.getUniqueNodeName({ - originalName: newName, - }); + newName = this.uniqueNodeName(newName); // Rename the node and update the connections const workflow = this.getCurrentWorkflow(true); @@ -3388,11 +3309,10 @@ export default mixins( } oldName = node.name; - newName = this.getUniqueNodeName({ - originalName: node.name, - additionalUsedNames: newNodeNames, - type: node.type, - }); + + const localized = this.localizeNodeName(node.name, node.type); + + newName = this.uniqueNodeName(localized, newNodeNames); newNodeNames.push(newName); nodeNameTable[oldName] = newName; @@ -3474,7 +3394,7 @@ export default mixins( connections: tempWorkflow.connectionsBySourceNode, }; }, - getSelectedNodesToSave(): Promise<IWorkflowData> { + async getSelectedNodesToSave(): Promise<IWorkflowData> { const data: IWorkflowData = { nodes: [], connections: {}, @@ -3485,12 +3405,8 @@ export default mixins( const exportNodeNames: string[] = []; for (const node of this.uiStore.getSelectedNodes) { - try { - nodeData = this.getNodeDataToSave(node); - exportNodeNames.push(node.name); - } catch (e) { - return Promise.reject(e); - } + nodeData = this.getNodeDataToSave(node); + exportNodeNames.push(node.name); data.nodes.push(nodeData); } @@ -3540,7 +3456,7 @@ export default mixins( } }); - return Promise.resolve(data); + return data; }, resetWorkspace() { this.workflowsStore.resetWorkflow(); @@ -3582,11 +3498,9 @@ export default mixins( this.uiStore.nodeViewOffsetPosition = [0, 0]; this.credentialsUpdated = false; - return Promise.resolve(); }, async loadActiveWorkflows(): Promise<void> { - const activeWorkflows = await this.restApi().getActiveWorkflows(); - this.workflowsStore.activeWorkflows = activeWorkflows; + await this.workflowsStore.fetchActiveWorkflows(); }, async loadNodeTypes(): Promise<void> { await this.nodeTypesStore.getNodeTypes(); @@ -3731,15 +3645,15 @@ export default mixins( // Default to the trigger tab in node creator if there's no trigger node yet this.nodeCreatorStore.setSelectedView( - this.containsTrigger ? REGULAR_NODE_FILTER : TRIGGER_NODE_FILTER, + this.containsTrigger ? REGULAR_NODE_CREATOR_VIEW : TRIGGER_NODE_CREATOR_VIEW, ); this.createNodeActive = createNodeActive; const mode = - this.nodeCreatorStore.selectedView === TRIGGER_NODE_FILTER ? 'trigger' : 'regular'; + this.nodeCreatorStore.selectedView === TRIGGER_NODE_CREATOR_VIEW ? 'trigger' : 'regular'; - this.nodeCreatorStore.openSource = source || ''; + if (createNodeActive === true) this.nodeCreatorStore.setOpenSource(source); this.$externalHooks().run('nodeView.createNodeActiveChanged', { source, mode, @@ -3848,7 +3762,7 @@ export default mixins( async mounted() { this.resetWorkspace(); this.canvasStore.initInstance(this.$refs.nodeView as HTMLElement); - this.$titleReset(); + this.titleReset(); window.addEventListener('message', this.onPostMessageReceived); this.startLoading(); diff --git a/packages/editor-ui/src/views/SamlOnboarding.vue b/packages/editor-ui/src/views/SamlOnboarding.vue index 4a33f6a988678..a57fd8cbc0f22 100644 --- a/packages/editor-ui/src/views/SamlOnboarding.vue +++ b/packages/editor-ui/src/views/SamlOnboarding.vue @@ -2,7 +2,7 @@ import { reactive, ref } from 'vue'; import { useRouter } from 'vue-router/composables'; import { Notification } from 'element-ui'; -import { IFormBoxConfig } from 'n8n-design-system'; +import type { IFormBoxConfig } from 'n8n-design-system'; import AuthView from '@/views/AuthView.vue'; import { i18n as locale } from '@/plugins/i18n'; import { useSSOStore } from '@/stores/sso'; diff --git a/packages/editor-ui/src/views/SettingsApiView.vue b/packages/editor-ui/src/views/SettingsApiView.vue index 21c7ce3abaf50..6321440d3b0c9 100644 --- a/packages/editor-ui/src/views/SettingsApiView.vue +++ b/packages/editor-ui/src/views/SettingsApiView.vue @@ -76,7 +76,7 @@ <script lang="ts"> import { showMessage } from '@/mixins/showMessage'; -import { IUser } from '@/Interface'; +import type { IUser } from '@/Interface'; import mixins from 'vue-typed-mixins'; import CopyInput from '@/components/CopyInput.vue'; diff --git a/packages/editor-ui/src/views/SettingsCommunityNodesView.vue b/packages/editor-ui/src/views/SettingsCommunityNodesView.vue index c6e4dbfd6d9c4..ede7b37e11d07 100644 --- a/packages/editor-ui/src/views/SettingsCommunityNodesView.vue +++ b/packages/editor-ui/src/views/SettingsCommunityNodesView.vue @@ -61,13 +61,12 @@ import CommunityPackageCard from '@/components/CommunityPackageCard.vue'; import { showMessage } from '@/mixins/showMessage'; import { pushConnection } from '@/mixins/pushConnection'; import mixins from 'vue-typed-mixins'; -import { PublicInstalledPackage } from 'n8n-workflow'; +import type { PublicInstalledPackage } from 'n8n-workflow'; import { useCommunityNodesStore } from '@/stores/communityNodes'; import { useUIStore } from '@/stores/ui'; import { mapStores } from 'pinia'; import { useSettingsStore } from '@/stores/settings'; -import { BaseTextKey } from '@/plugins/i18n'; const PACKAGE_COUNT_THRESHOLD = 31; diff --git a/packages/editor-ui/src/views/SettingsFakeDoorView.vue b/packages/editor-ui/src/views/SettingsFakeDoorView.vue index 2df26ff258d3a..2a15637b8a630 100644 --- a/packages/editor-ui/src/views/SettingsFakeDoorView.vue +++ b/packages/editor-ui/src/views/SettingsFakeDoorView.vue @@ -3,13 +3,13 @@ </template> <script lang="ts"> -import { IFakeDoor } from '@/Interface'; -import Vue from 'vue'; +import type { IFakeDoor } from '@/Interface'; +import { defineComponent } from 'vue'; import FeatureComingSoon from '@/components/FeatureComingSoon.vue'; import { mapStores } from 'pinia'; import { useUIStore } from '@/stores/ui'; -export default Vue.extend({ +export default defineComponent({ name: 'SettingsFakeDoorView', components: { FeatureComingSoon, diff --git a/packages/editor-ui/src/views/SettingsLdapView.vue b/packages/editor-ui/src/views/SettingsLdapView.vue index 3c38f2a65632e..7f94ddcfacccb 100644 --- a/packages/editor-ui/src/views/SettingsLdapView.vue +++ b/packages/editor-ui/src/views/SettingsLdapView.vue @@ -6,17 +6,13 @@ </n8n-heading> </div> - <n8n-info-tip type="note" theme="info-light" tooltipPlacement="right"> - <div> - LDAP allows users to authenticate with their centralized account. It's compatible with - services that provide an LDAP interface like Active Directory, Okta and Jumpcloud. - </div> - <br /> + <n8n-info-tip type="note" theme="info-light" tooltipPlacement="right" class="mb-l"> + {{ $locale.baseText('settings.ldap.note') }} </n8n-info-tip> <n8n-action-box :description="$locale.baseText('settings.ldap.disabled.description')" :buttonText="$locale.baseText('settings.ldap.disabled.buttonText')" - @click="onContactUsClick" + @click="goToUpgrade" > <template #heading> <span>{{ $locale.baseText('settings.ldap.disabled.title') }}</span> @@ -149,7 +145,7 @@ <script lang="ts"> import { convertToDisplayDate } from '@/utils'; import { showMessage } from '@/mixins/showMessage'; -import { +import type { ILdapConfig, ILdapSyncData, ILdapSyncTable, @@ -157,7 +153,6 @@ import { IFormInputs, IUser, } from '@/Interface'; -import Vue from 'vue'; import mixins from 'vue-typed-mixins'; import humanizeDuration from 'humanize-duration'; @@ -167,32 +162,11 @@ import InfiniteLoading from 'vue-infinite-loading'; import { mapStores } from 'pinia'; import { useUsersStore } from '@/stores/users'; import { useSettingsStore } from '@/stores/settings'; -import { getLdapSynchronizations } from '@/api/ldap'; -import { N8N_CONTACT_EMAIL, N8N_SALES_EMAIL } from '@/constants'; +import { useUIStore } from '@/stores'; import { createEventBus } from '@/event-bus'; +import type { N8nFormInputs } from 'n8n-design-system'; -type FormValues = { - loginEnabled: boolean; - loginLabel: string; - serverAddress: string; - baseDn: string; - bindingType: string; - adminDn: string; - adminPassword: string; - loginId: string; - email: string; - lastName: string; - firstName: string; - ldapId: string; - synchronizationEnabled: boolean; - allowUnauthorizedCerts: boolean; - synchronizationInterval: number; - userFilter: string; - pageSize: number; - searchTimeout: number; - port: number; - connectionSecurity: string; -}; +type N8nFormInputsRef = InstanceType<typeof N8nFormInputs>; type tableRow = { status: string; @@ -234,7 +208,7 @@ export default mixins(showMessage).extend({ await this.getLdapConfig(); }, computed: { - ...mapStores(useUsersStore, useSettingsStore), + ...mapStores(useUsersStore, useSettingsStore, useUIStore), currentUser(): null | IUser { return this.usersStore.currentUser; }, @@ -243,9 +217,8 @@ export default mixins(showMessage).extend({ }, }, methods: { - onContactUsClick(event: MouseEvent): void { - const email = this.settingsStore.isCloudDeployment ? N8N_CONTACT_EMAIL : N8N_SALES_EMAIL; - location.href = `mailto:${email}`; + goToUpgrade() { + this.uiStore.goToUpgrade('ldap', 'upgrade-ldap'); }, cellClassStyle({ row, column }: { row: rowType; column: cellType }) { if (column.property === 'status') { @@ -295,32 +268,33 @@ export default mixins(showMessage).extend({ async onSubmit(): Promise<void> { // We want to save all form values (incl. the hidden onces), so we are using // `values` data prop of the `FormInputs` child component since they are all preserved there - const formInputs = this.$refs.ldapConfigForm as (Vue & { values: FormValues }) | undefined; - if (!this.hasAnyChanges || !formInputs) { + const formInputsRef = this.$refs.ldapConfigForm as N8nFormInputsRef | undefined; + if (!this.hasAnyChanges || !formInputsRef) { return; } const newConfiguration: ILdapConfig = { - loginEnabled: formInputs.values.loginEnabled, - loginLabel: formInputs.values.loginLabel, - connectionUrl: formInputs.values.serverAddress, - allowUnauthorizedCerts: formInputs.values.allowUnauthorizedCerts, - connectionPort: +formInputs.values.port, - connectionSecurity: formInputs.values.connectionSecurity, - baseDn: formInputs.values.baseDn, - bindingAdminDn: formInputs.values.bindingType === 'admin' ? formInputs.values.adminDn : '', + loginEnabled: formInputsRef.values.loginEnabled, + loginLabel: formInputsRef.values.loginLabel, + connectionUrl: formInputsRef.values.serverAddress, + allowUnauthorizedCerts: formInputsRef.values.allowUnauthorizedCerts, + connectionPort: +formInputsRef.values.port, + connectionSecurity: formInputsRef.values.connectionSecurity, + baseDn: formInputsRef.values.baseDn, + bindingAdminDn: + formInputsRef.values.bindingType === 'admin' ? formInputsRef.values.adminDn : '', bindingAdminPassword: - formInputs.values.bindingType === 'admin' ? formInputs.values.adminPassword : '', - emailAttribute: formInputs.values.email, - firstNameAttribute: formInputs.values.firstName, - lastNameAttribute: formInputs.values.lastName, - loginIdAttribute: formInputs.values.loginId, - ldapIdAttribute: formInputs.values.ldapId, - userFilter: formInputs.values.userFilter, - synchronizationEnabled: formInputs.values.synchronizationEnabled, - synchronizationInterval: +formInputs.values.synchronizationInterval, - searchPageSize: +formInputs.values.pageSize, - searchTimeout: +formInputs.values.searchTimeout, + formInputsRef.values.bindingType === 'admin' ? formInputsRef.values.adminPassword : '', + emailAttribute: formInputsRef.values.email, + firstNameAttribute: formInputsRef.values.firstName, + lastNameAttribute: formInputsRef.values.lastName, + loginIdAttribute: formInputsRef.values.loginId, + ldapIdAttribute: formInputsRef.values.ldapId, + userFilter: formInputsRef.values.userFilter, + synchronizationEnabled: formInputsRef.values.synchronizationEnabled, + synchronizationInterval: +formInputsRef.values.synchronizationInterval, + searchPageSize: +formInputsRef.values.pageSize, + searchTimeout: +formInputsRef.values.searchTimeout, }; let saveForm = true; diff --git a/packages/editor-ui/src/views/SettingsLogStreamingView.vue b/packages/editor-ui/src/views/SettingsLogStreamingView.vue index 0508462ed786b..afdf934f4ecfb 100644 --- a/packages/editor-ui/src/views/SettingsLogStreamingView.vue +++ b/packages/editor-ui/src/views/SettingsLogStreamingView.vue @@ -6,7 +6,7 @@ {{ $locale.baseText(`settings.log-streaming.heading`) }} </n8n-heading> <template v-if="environment !== 'production'"> - <strong> Disable License ({{ environment }}) </strong> + <strong class="ml-m">Disable License ({{ environment }}) </strong> <el-switch v-model="disableLicense" size="large" data-test-id="disable-license-toggle" /> </template> </div> @@ -43,7 +43,7 @@ </div> </template> <template v-else> - <div :class="$style.actionBoxContainer" data-test-id="action-box-licensed"> + <div data-test-id="action-box-licensed"> <n8n-action-box :buttonText="$locale.baseText(`settings.log-streaming.add`)" @click="addDestination" @@ -63,11 +63,11 @@ </template> </n8n-info-tip> </div> - <div :class="$style.actionBoxContainer" data-test-id="action-box-unlicensed"> + <div data-test-id="action-box-unlicensed"> <n8n-action-box :description="$locale.baseText('settings.log-streaming.actionBox.description')" :buttonText="$locale.baseText('settings.log-streaming.actionBox.button')" - @click="onContactUsClicked" + @click="goToUpgrade" > <template #heading> <span v-html="$locale.baseText('settings.log-streaming.actionBox.title')" /> @@ -89,11 +89,8 @@ import { useLogStreamingStore } from '../stores/logStreamingStore'; import { useSettingsStore } from '../stores/settings'; import { useUIStore } from '../stores/ui'; import { LOG_STREAM_MODAL_KEY, EnterpriseEditionFeature } from '../constants'; -import { - deepCopy, - defaultMessageEventBusDestinationOptions, - MessageEventBusDestinationOptions, -} from 'n8n-workflow'; +import type { MessageEventBusDestinationOptions } from 'n8n-workflow'; +import { deepCopy, defaultMessageEventBusDestinationOptions } from 'n8n-workflow'; import PageViewLayout from '@/components/layouts/PageViewLayout.vue'; import EventDestinationCard from '@/components/SettingsLogStreaming/EventDestinationCard.ee.vue'; import { createEventBus } from '@/event-bus'; @@ -198,11 +195,8 @@ export default mixins().extend({ } this.$forceUpdate(); }, - onContactUsClicked() { - window.open('mailto:sales@n8n.io', '_blank'); - this.$telemetry.track('user clicked contact us button', { - feature: EnterpriseEditionFeature.LogStreaming, - }); + goToUpgrade() { + this.uiStore.goToUpgrade('log-streaming', 'upgrade-log-streaming'); }, storeHasItems(): boolean { return this.logStreamingStore.items && Object.keys(this.logStreamingStore.items).length > 0; diff --git a/packages/editor-ui/src/views/SettingsPersonalView.vue b/packages/editor-ui/src/views/SettingsPersonalView.vue index 91263038cc675..e0a32a2c14989 100644 --- a/packages/editor-ui/src/views/SettingsPersonalView.vue +++ b/packages/editor-ui/src/views/SettingsPersonalView.vue @@ -32,7 +32,7 @@ /> </div> </div> - <div v-if="!signInWithLdap"> + <div v-if="!signInWithLdap && !signInWithSaml"> <div :class="$style.sectionHeader"> <n8n-heading size="large">{{ $locale.baseText('settings.personal.security') }}</n8n-heading> </div> @@ -59,8 +59,8 @@ <script lang="ts"> import { showMessage } from '@/mixins/showMessage'; -import { CHANGE_PASSWORD_MODAL_KEY, SignInType } from '@/constants'; -import { IFormInputs, IUser } from '@/Interface'; +import { CHANGE_PASSWORD_MODAL_KEY } from '@/constants'; +import type { IFormInputs, IUser } from '@/Interface'; import { useUIStore } from '@/stores/ui'; import { useUsersStore } from '@/stores/users'; import { useSettingsStore } from '@/stores/settings'; @@ -114,7 +114,7 @@ export default mixins(showMessage).extend({ validationRules: [{ name: 'VALID_EMAIL' }], autocomplete: 'email', capitalize: true, - disabled: this.isLDAPFeatureEnabled && this.signInWithLdap, + disabled: (this.isLDAPFeatureEnabled && this.signInWithLdap) || this.signInWithSaml, }, }, ]; @@ -130,6 +130,11 @@ export default mixins(showMessage).extend({ isLDAPFeatureEnabled(): boolean { return this.settingsStore.settings.enterprise.ldap === true; }, + signInWithSaml(): boolean { + return ( + this.settingsStore.isSamlLoginEnabled && this.settingsStore.isDefaultAuthenticationSaml + ); + }, }, methods: { onInput() { diff --git a/packages/editor-ui/src/views/SettingsSso.vue b/packages/editor-ui/src/views/SettingsSso.vue index 4208d6d33648c..0b9af1285caaf 100644 --- a/packages/editor-ui/src/views/SettingsSso.vue +++ b/packages/editor-ui/src/views/SettingsSso.vue @@ -93,7 +93,7 @@ onBeforeMount(async () => { </n8n-tooltip> </div> <n8n-info-tip> - <i18n :class="$style.count" path="settings.sso.info"> + <i18n path="settings.sso.info"> <template #link> <a href="https://docs.n8n.io/user-management/saml/" target="_blank"> {{ locale.baseText('settings.sso.info.link') }} @@ -105,7 +105,6 @@ onBeforeMount(async () => { <div :class="$style.group"> <label>{{ locale.baseText('settings.sso.settings.redirectUrl.label') }}</label> <CopyInput - :class="$style.copyInput" :value="redirectUrl" :copy-button-text="locale.baseText('generic.clickToCopy')" :toast-title="locale.baseText('settings.sso.settings.redirectUrl.copied')" @@ -115,7 +114,6 @@ onBeforeMount(async () => { <div :class="$style.group"> <label>{{ locale.baseText('settings.sso.settings.entityId.label') }}</label> <CopyInput - :class="$style.copyInput" :value="entityId" :copy-button-text="locale.baseText('generic.clickToCopy')" :toast-title="locale.baseText('settings.sso.settings.entityId.copied')" diff --git a/packages/editor-ui/src/views/SettingsUsageAndPlan.vue b/packages/editor-ui/src/views/SettingsUsageAndPlan.vue index e9e8803768200..5623e48fa4c4f 100644 --- a/packages/editor-ui/src/views/SettingsUsageAndPlan.vue +++ b/packages/editor-ui/src/views/SettingsUsageAndPlan.vue @@ -2,7 +2,8 @@ import { computed, onMounted, ref } from 'vue'; import { useRoute, useRouter } from 'vue-router/composables'; import { Notification } from 'element-ui'; -import { UsageTelemetry, useUsageStore } from '@/stores/usage'; +import type { UsageTelemetry } from '@/stores/usage'; +import { useUsageStore } from '@/stores/usage'; import { telemetry } from '@/plugins/telemetry'; import { i18n as locale } from '@/plugins/i18n'; import { N8N_PRICING_PAGE_URL } from '@/constants'; diff --git a/packages/editor-ui/src/views/SettingsUsersView.vue b/packages/editor-ui/src/views/SettingsUsersView.vue index 56d724a2c3475..c542ded7ec81e 100644 --- a/packages/editor-ui/src/views/SettingsUsersView.vue +++ b/packages/editor-ui/src/views/SettingsUsersView.vue @@ -62,7 +62,7 @@ import { EnterpriseEditionFeature, INVITE_USER_MODAL_KEY, VIEWS } from '@/constants'; import PageAlert from '../components/PageAlert.vue'; -import { IUser, IUserListAction } from '@/Interface'; +import type { IUser, IUserListAction } from '@/Interface'; import mixins from 'vue-typed-mixins'; import { showMessage } from '@/mixins/showMessage'; import { copyPaste } from '@/mixins/copyPaste'; @@ -70,7 +70,6 @@ import { mapStores } from 'pinia'; import { useUIStore } from '@/stores/ui'; import { useSettingsStore } from '@/stores/settings'; import { useUsersStore } from '@/stores/users'; -import { BaseTextKey } from '@/plugins/i18n'; import { useUsageStore } from '@/stores/usage'; import { useSSOStore } from '@/stores/sso'; diff --git a/packages/editor-ui/src/views/SettingsVersionControl.vue b/packages/editor-ui/src/views/SettingsVersionControl.vue index ca0a897e6c399..917e3b870b56c 100644 --- a/packages/editor-ui/src/views/SettingsVersionControl.vue +++ b/packages/editor-ui/src/views/SettingsVersionControl.vue @@ -1,11 +1,161 @@ <script lang="ts" setup> +import { computed, ref } from 'vue'; import { i18n as locale } from '@/plugins/i18n'; +import { useVersionControlStore } from '@/stores/versionControl'; +import { useUIStore } from '@/stores/ui'; +import { useMessage } from '@/composables'; + +const versionControlStore = useVersionControlStore(); +const uiStore = useUIStore(); +const message = useMessage(); + +const sshKey = computed(() => versionControlStore.state.sshKey); +const branch = computed(() => versionControlStore.state.currentBranch); +const branches = ref<string[]>([]); +const selectElement = ref<HTMLSelectElement | null>(null); + +const onContinue = () => { + versionControlStore.initSsh({ + name: versionControlStore.state.authorName, + email: versionControlStore.state.authorEmail, + remoteRepository: versionControlStore.state.repositoryUrl, + }); +}; + +const onConnect = () => { + versionControlStore.initRepository(); +}; + +const onSelect = async (b: string) => { + if (b === branch.value) { + return; + } + const switchBranch = await message + .confirm( + locale.baseText('settings.versionControl.switchBranch.description', { + interpolate: { branch: b }, + }), + locale.baseText('settings.versionControl.switchBranch.title', { interpolate: { branch: b } }), + ) + .catch(() => {}); + if (switchBranch === 'confirm') { + versionControlStore.state.currentBranch = b; + selectElement.value?.blur(); + } +}; + +const goToUpgrade = () => { + uiStore.goToUpgrade('version-control', 'upgrade-version-control'); +}; </script> <template> <div> <n8n-heading size="2xlarge">{{ locale.baseText('settings.versionControl.title') }}</n8n-heading> + <div + v-if="versionControlStore.isEnterpriseVersionControlEnabled" + data-test-id="version-control-content-licensed" + > + <n8n-callout theme="secondary" icon="info-circle" class="mt-2xl mb-l">{{ + locale.baseText('settings.versionControl.description') + }}</n8n-callout> + <div :class="$style.group"> + <label for="repoUrl">{{ locale.baseText('settings.versionControl.repoUrl') }}</label> + <n8n-input + id="repoUrl" + :placeholder="locale.baseText('settings.versionControl.repoUrlPlaceholder')" + v-model="versionControlStore.state.repositoryUrl" + /> + <small>{{ locale.baseText('settings.versionControl.repoUrlDescription') }}</small> + </div> + <div :class="$style.group"> + <label for="authorName">{{ locale.baseText('settings.versionControl.authorName') }}</label> + <n8n-input id="authorName" v-model="versionControlStore.state.authorName" /> + </div> + <div :class="$style.group"> + <label for="authorEmail">{{ + locale.baseText('settings.versionControl.authorEmail') + }}</label> + <n8n-input id="authorEmail" v-model="versionControlStore.state.authorEmail" /> + </div> + <n8n-button v-if="!sshKey" @click="onContinue" size="large" class="mt-2xs">{{ + locale.baseText('settings.versionControl.button.continue') + }}</n8n-button> + <div v-if="sshKey" :class="$style.group"> + <label>{{ locale.baseText('settings.versionControl.sshKey') }}</label> + <CopyInput + :value="versionControlStore.state.sshKey" + :copy-button-text="locale.baseText('generic.clickToCopy')" + /> + <n8n-notice type="info" class="mt-s"> + <i18n path="settings.versionControl.sshKeyDescription"> + <template #link> + <a href="#" target="_blank"> + {{ locale.baseText('settings.versionControl.sshKeyDescriptionLink') }} + </a> + </template> + </i18n> + </n8n-notice> + </div> + <n8n-button v-if="sshKey" @click="onConnect" size="large" :class="$style.connect">{{ + locale.baseText('settings.versionControl.button.connect') + }}</n8n-button> + <div v-if="versionControlStore.state.branches.length" :class="$style.group"> + <label>{{ locale.baseText('settings.versionControl.branches') }}</label> + <n8n-select + ref="selectElement" + :value="versionControlStore.state.currentBranch" + size="medium" + filterable + @input="onSelect" + > + <n8n-option + v-for="b in versionControlStore.state.branches" + :key="b" + :value="b" + :label="b" + /> + </n8n-select> + </div> + </div> + <n8n-action-box + v-else + data-test-id="version-control-content-unlicensed" + :class="$style.actionBox" + :description="locale.baseText('settings.versionControl.actionBox.description')" + :buttonText="locale.baseText('settings.versionControl.actionBox.buttonText')" + @click="goToUpgrade" + > + <template #heading> + <span>{{ locale.baseText('settings.versionControl.actionBox.title') }}</span> + </template> + </n8n-action-box> </div> </template> -<style lang="scss" module></style> +<style lang="scss" module> +.group { + padding: 0 0 var(--spacing-2xs); + + label { + display: inline-block; + padding: 0 0 var(--spacing-2xs); + font-size: var(--font-size-s); + } + + small { + display: inline-block; + padding: var(--spacing-2xs) 0 0; + font-size: var(--font-size-2xs); + color: var(--color-text-light); + } +} + +.connect { + margin: calc(var(--spacing-2xs) * -1) 0 var(--spacing-2xs); +} + +.actionBox { + margin: var(--spacing-2xl) 0 0; +} +</style> diff --git a/packages/editor-ui/src/views/SettingsView.vue b/packages/editor-ui/src/views/SettingsView.vue index a5016ce6a9cd8..37adbe9c609fc 100644 --- a/packages/editor-ui/src/views/SettingsView.vue +++ b/packages/editor-ui/src/views/SettingsView.vue @@ -15,7 +15,7 @@ <script lang="ts"> import { defineComponent } from 'vue'; -import { Route } from 'vue-router'; +import type { Route } from 'vue-router'; import { VIEWS } from '@/constants'; import SettingsSidebar from '@/components/SettingsSidebar.vue'; diff --git a/packages/editor-ui/src/views/SetupView.vue b/packages/editor-ui/src/views/SetupView.vue index 87b46c2b9401c..264c9c22ee76d 100644 --- a/packages/editor-ui/src/views/SetupView.vue +++ b/packages/editor-ui/src/views/SetupView.vue @@ -13,17 +13,15 @@ import AuthView from './AuthView.vue'; import { showMessage } from '@/mixins/showMessage'; import mixins from 'vue-typed-mixins'; -import { IFormBoxConfig } from '@/Interface'; -import { VIEWS, ASSUMPTION_EXPERIMENT } from '@/constants'; -import { restApi } from '@/mixins/restApi'; +import type { IFormBoxConfig } from '@/Interface'; +import { VIEWS } from '@/constants'; import { mapStores } from 'pinia'; import { useUIStore } from '@/stores/ui'; import { useSettingsStore } from '@/stores/settings'; import { useUsersStore } from '@/stores/users'; import { useCredentialsStore } from '@/stores/credentials'; -import { usePostHog } from '@/stores/posthog'; -export default mixins(showMessage, restApi).extend({ +export default mixins(showMessage).extend({ name: 'SetupView', components: { AuthView, diff --git a/packages/editor-ui/src/views/SigninView.vue b/packages/editor-ui/src/views/SigninView.vue index 33eed26eec0bf..e758f79218c75 100644 --- a/packages/editor-ui/src/views/SigninView.vue +++ b/packages/editor-ui/src/views/SigninView.vue @@ -13,7 +13,7 @@ import AuthView from './AuthView.vue'; import { showMessage } from '@/mixins/showMessage'; import mixins from 'vue-typed-mixins'; -import { IFormBoxConfig } from '@/Interface'; +import type { IFormBoxConfig } from '@/Interface'; import { VIEWS } from '@/constants'; import { mapStores } from 'pinia'; import { useUsersStore } from '@/stores/users'; diff --git a/packages/editor-ui/src/views/SignupView.vue b/packages/editor-ui/src/views/SignupView.vue index f46e26a5b191d..012f0920182d9 100644 --- a/packages/editor-ui/src/views/SignupView.vue +++ b/packages/editor-ui/src/views/SignupView.vue @@ -12,7 +12,7 @@ import AuthView from './AuthView.vue'; import { showMessage } from '@/mixins/showMessage'; import mixins from 'vue-typed-mixins'; -import { IFormBoxConfig } from '@/Interface'; +import type { IFormBoxConfig } from '@/Interface'; import { VIEWS } from '@/constants'; import { mapStores } from 'pinia'; import { useUIStore } from '@/stores/ui'; diff --git a/packages/editor-ui/src/views/TemplatesCollectionView.vue b/packages/editor-ui/src/views/TemplatesCollectionView.vue index 08e5dc90bebca..720a7929b4ff6 100644 --- a/packages/editor-ui/src/views/TemplatesCollectionView.vue +++ b/packages/editor-ui/src/views/TemplatesCollectionView.vue @@ -55,7 +55,7 @@ import TemplateList from '@/components/TemplateList.vue'; import TemplatesView from './TemplatesView.vue'; import { workflowHelpers } from '@/mixins/workflowHelpers'; -import { +import type { ITemplatesCollection, ITemplatesCollectionFull, ITemplatesWorkflow, diff --git a/packages/editor-ui/src/views/TemplatesSearchView.vue b/packages/editor-ui/src/views/TemplatesSearchView.vue index 4fb65f08215e2..6a4670af49d4f 100644 --- a/packages/editor-ui/src/views/TemplatesSearchView.vue +++ b/packages/editor-ui/src/views/TemplatesSearchView.vue @@ -81,14 +81,14 @@ import TemplateList from '@/components/TemplateList.vue'; import TemplatesView from './TemplatesView.vue'; import { genericHelpers } from '@/mixins/genericHelpers'; -import { +import type { ITemplatesCollection, ITemplatesWorkflow, ITemplatesQuery, ITemplatesCategory, } from '@/Interface'; import mixins from 'vue-typed-mixins'; -import { IDataObject } from 'n8n-workflow'; +import type { IDataObject } from 'n8n-workflow'; import { setPageTitle } from '@/utils'; import { VIEWS } from '@/constants'; import { debounceHelper } from '@/mixins/debounce'; diff --git a/packages/editor-ui/src/views/TemplatesView.vue b/packages/editor-ui/src/views/TemplatesView.vue index 9a7f5323124e7..2b5d8a48b14de 100644 --- a/packages/editor-ui/src/views/TemplatesView.vue +++ b/packages/editor-ui/src/views/TemplatesView.vue @@ -15,10 +15,10 @@ </template> <script lang="ts"> -import Vue from 'vue'; +import { defineComponent } from 'vue'; import GoBackButton from '@/components/GoBackButton.vue'; -export default Vue.extend({ +export default defineComponent({ name: 'TemplatesView', components: { GoBackButton, diff --git a/packages/editor-ui/src/views/TemplatesWorkflowView.vue b/packages/editor-ui/src/views/TemplatesWorkflowView.vue index 8d1812203f62c..47139550d23c9 100644 --- a/packages/editor-ui/src/views/TemplatesWorkflowView.vue +++ b/packages/editor-ui/src/views/TemplatesWorkflowView.vue @@ -59,7 +59,7 @@ import TemplateDetails from '@/components/TemplateDetails.vue'; import TemplatesView from './TemplatesView.vue'; import WorkflowPreview from '@/components/WorkflowPreview.vue'; -import { ITemplatesWorkflow, ITemplatesWorkflowFull } from '@/Interface'; +import type { ITemplatesWorkflow, ITemplatesWorkflowFull } from '@/Interface'; import { workflowHelpers } from '@/mixins/workflowHelpers'; import mixins from 'vue-typed-mixins'; import { setPageTitle } from '@/utils'; diff --git a/packages/editor-ui/src/views/VariablesView.vue b/packages/editor-ui/src/views/VariablesView.vue index 8f4b0a242a91c..da02446222465 100644 --- a/packages/editor-ui/src/views/VariablesView.vue +++ b/packages/editor-ui/src/views/VariablesView.vue @@ -7,7 +7,11 @@ import ResourcesListLayout from '@/components/layouts/ResourcesListLayout.vue'; import VariablesRow from '@/components/VariablesRow.vue'; import { EnterpriseEditionFeature } from '@/constants'; -import { DatatableColumn, EnvironmentVariable, TemporaryEnvironmentVariable } from '@/Interface'; +import type { + DatatableColumn, + EnvironmentVariable, + TemporaryEnvironmentVariable, +} from '@/Interface'; import { uid } from 'n8n-design-system/utils'; import { getVariablesPermissions } from '@/permissions'; @@ -16,7 +20,7 @@ const environmentsStore = useEnvironmentsStore(); const usersStore = useUsersStore(); const uiStore = useUIStore(); const telemetry = useTelemetry(); -const i18n = useI18n(); +const { i18n } = useI18n(); const message = useMessage(); const layoutRef = ref<InstanceType<typeof ResourcesListLayout> | null>(null); diff --git a/packages/editor-ui/src/views/WorkflowsView.vue b/packages/editor-ui/src/views/WorkflowsView.vue index dec78a5880d96..753b1a3ec2e5b 100644 --- a/packages/editor-ui/src/views/WorkflowsView.vue +++ b/packages/editor-ui/src/views/WorkflowsView.vue @@ -98,10 +98,10 @@ import PageViewLayout from '@/components/layouts/PageViewLayout.vue'; import PageViewLayoutList from '@/components/layouts/PageViewLayoutList.vue'; import WorkflowCard from '@/components/WorkflowCard.vue'; import TemplateCard from '@/components/TemplateCard.vue'; -import { EnterpriseEditionFeature, ASSUMPTION_EXPERIMENT, VIEWS } from '@/constants'; +import { EnterpriseEditionFeature, VIEWS } from '@/constants'; import { debounceHelper } from '@/mixins/debounce'; -import Vue from 'vue'; -import { ITag, IUser, IWorkflowDb } from '@/Interface'; +import type Vue from 'vue'; +import type { ITag, IUser, IWorkflowDb } from '@/Interface'; import TagsDropdown from '@/components/TagsDropdown.vue'; import { mapStores } from 'pinia'; import { useUIStore } from '@/stores/ui'; @@ -109,7 +109,6 @@ import { useSettingsStore } from '@/stores/settings'; import { useUsersStore } from '@/stores/users'; import { useWorkflowsStore } from '@/stores/workflows'; import { useCredentialsStore } from '@/stores/credentials'; -import { usePostHog } from '@/stores/posthog'; type IResourcesListLayoutInstance = Vue & { sendFiltersTelemetry: (source: string) => void }; diff --git a/packages/editor-ui/src/views/__tests__/SettingsPersonalView.test.ts b/packages/editor-ui/src/views/__tests__/SettingsPersonalView.test.ts new file mode 100644 index 0000000000000..c86816a8bd7ff --- /dev/null +++ b/packages/editor-ui/src/views/__tests__/SettingsPersonalView.test.ts @@ -0,0 +1,79 @@ +import { PiniaVuePlugin } from 'pinia'; +import { render } from '@testing-library/vue'; +import { createTestingPinia } from '@pinia/testing'; +import { merge } from 'lodash-es'; +import type { IN8nUISettings } from 'n8n-workflow'; +import { STORES } from '@/constants'; +import { SETTINGS_STORE_DEFAULT_STATE, waitAllPromises } from '@/__tests__/utils'; +import { i18n } from '@/plugins/i18n'; +import SettingsPersonalView from '@/views/SettingsPersonalView.vue'; +import { useSettingsStore } from '@/stores'; +import { useUsersStore } from '@/stores/users'; + +let pinia: ReturnType<typeof createTestingPinia>; +let settingsStore: ReturnType<typeof useSettingsStore>; +let usersStore: ReturnType<typeof useUsersStore>; + +const DEFAULT_SETTINGS: IN8nUISettings = SETTINGS_STORE_DEFAULT_STATE.settings; + +const renderComponent = (renderOptions: Parameters<typeof render>[1] = {}) => + render( + SettingsPersonalView, + merge( + { + pinia, + i18n, + }, + renderOptions, + ), + (vue) => { + vue.use(PiniaVuePlugin); + }, + ); + +describe('SettingsPersonalView', () => { + beforeEach(() => { + pinia = createTestingPinia({ + initialState: { + [STORES.SETTINGS]: { + settings: DEFAULT_SETTINGS, + }, + }, + }); + settingsStore = useSettingsStore(pinia); + usersStore = useUsersStore(pinia); + + vi.spyOn(usersStore, 'currentUser', 'get').mockReturnValue({ + id: '1', + firstName: 'John', + lastName: 'Doe', + email: 'joh.doe@example.com', + createdAt: Date().toString(), + isOwner: true, + isDefaultUser: false, + isPendingUser: false, + isPending: false, + }); + }); + + it('should enable email and pw change', async () => { + const { getByTestId, getAllByRole } = renderComponent(); + await waitAllPromises(); + + expect(getAllByRole('textbox').find((el) => el.getAttribute('type') === 'email')).toBeEnabled(); + expect(getByTestId('change-password-link')).toBeInTheDocument(); + }); + + it('should disable email and pw change when SAML login is enabled', async () => { + vi.spyOn(settingsStore, 'isSamlLoginEnabled', 'get').mockReturnValue(true); + vi.spyOn(settingsStore, 'isDefaultAuthenticationSaml', 'get').mockReturnValue(true); + + const { queryByTestId, getAllByRole } = renderComponent(); + await waitAllPromises(); + + expect( + getAllByRole('textbox').find((el) => el.getAttribute('type') === 'email'), + ).toBeDisabled(); + expect(queryByTestId('change-password-link')).not.toBeInTheDocument(); + }); +}); diff --git a/packages/editor-ui/src/views/__tests__/SettingsSso.test.ts b/packages/editor-ui/src/views/__tests__/SettingsSso.test.ts index a5675017186aa..20c64a8f7fefc 100644 --- a/packages/editor-ui/src/views/__tests__/SettingsSso.test.ts +++ b/packages/editor-ui/src/views/__tests__/SettingsSso.test.ts @@ -9,7 +9,7 @@ import { useSSOStore } from '@/stores/sso'; import { STORES } from '@/constants'; import { SETTINGS_STORE_DEFAULT_STATE, waitAllPromises } from '@/__tests__/utils'; import { i18nInstance } from '@/plugins/i18n'; -import { SamlPreferences, SamlPreferencesExtractedData } from '@/Interface'; +import type { SamlPreferences, SamlPreferencesExtractedData } from '@/Interface'; let pinia: ReturnType<typeof createTestingPinia>; let ssoStore: ReturnType<typeof useSSOStore>; diff --git a/packages/editor-ui/src/views/__tests__/SettingsVersionControl.test.ts b/packages/editor-ui/src/views/__tests__/SettingsVersionControl.test.ts new file mode 100644 index 0000000000000..c3bec841cc9ce --- /dev/null +++ b/packages/editor-ui/src/views/__tests__/SettingsVersionControl.test.ts @@ -0,0 +1,60 @@ +import { PiniaVuePlugin } from 'pinia'; +import { render } from '@testing-library/vue'; +import { createTestingPinia } from '@pinia/testing'; +import { merge } from 'lodash-es'; +import { STORES } from '@/constants'; +import { SETTINGS_STORE_DEFAULT_STATE } from '@/__tests__/utils'; +import { i18n } from '@/plugins/i18n'; +import SettingsVersionControl from '@/views/SettingsVersionControl.vue'; +import { useVersionControlStore } from '@/stores/versionControl'; + +let pinia: ReturnType<typeof createTestingPinia>; +let versionControlStore: ReturnType<typeof useVersionControlStore>; + +const renderComponent = (renderOptions: Parameters<typeof render>[1] = {}) => + render( + SettingsVersionControl, + merge( + { + pinia, + i18n, + }, + renderOptions, + ), + (vue) => { + vue.use(PiniaVuePlugin); + }, + ); + +describe('SettingsSso', () => { + beforeEach(() => { + pinia = createTestingPinia({ + initialState: { + [STORES.SETTINGS]: { + settings: merge({}, SETTINGS_STORE_DEFAULT_STATE.settings), + }, + }, + }); + versionControlStore = useVersionControlStore(pinia); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should render paywall state when there is no license', () => { + const { getByTestId, queryByTestId } = renderComponent(); + + expect(queryByTestId('version-control-content-licensed')).not.toBeInTheDocument(); + expect(getByTestId('version-control-content-unlicensed')).toBeInTheDocument(); + }); + + it('should render licensed content', () => { + vi.spyOn(versionControlStore, 'isEnterpriseVersionControlEnabled', 'get').mockReturnValue(true); + + const { getByTestId, queryByTestId } = renderComponent(); + + expect(getByTestId('version-control-content-licensed')).toBeInTheDocument(); + expect(queryByTestId('version-control-content-unlicensed')).not.toBeInTheDocument(); + }); +}); diff --git a/packages/editor-ui/src/views/__tests__/VariablesView.spec.ts b/packages/editor-ui/src/views/__tests__/VariablesView.spec.ts index d69c98d40eb72..99833feaae530 100644 --- a/packages/editor-ui/src/views/__tests__/VariablesView.spec.ts +++ b/packages/editor-ui/src/views/__tests__/VariablesView.spec.ts @@ -1,19 +1,21 @@ import { afterAll, beforeAll } from 'vitest'; import { setActivePinia, createPinia } from 'pinia'; import { setupServer } from '@/__tests__/server'; -import { render } from '@testing-library/vue'; import VariablesView from '@/views/VariablesView.vue'; import { useSettingsStore, useUsersStore } from '@/stores'; +import { renderComponent } from '@/__tests__/utils'; describe('store', () => { let server: ReturnType<typeof setupServer>; + let pinia: ReturnType<typeof createPinia>; beforeAll(() => { server = setupServer(); }); beforeEach(async () => { - setActivePinia(createPinia()); + pinia = createPinia(); + setActivePinia(pinia); await useSettingsStore().getSettings(); await useUsersStore().fetchUsers(); @@ -25,13 +27,13 @@ describe('store', () => { }); it('should render loading state', () => { - const wrapper = render(VariablesView); + const wrapper = renderComponent(VariablesView, { pinia }); expect(wrapper.container.querySelectorAll('.n8n-loading')).toHaveLength(3); }); it('should render empty state', async () => { - const wrapper = render(VariablesView); + const wrapper = renderComponent(VariablesView, { pinia }); await wrapper.findByTestId('empty-resources-list'); expect(wrapper.getByTestId('empty-resources-list')).toBeVisible(); @@ -40,7 +42,7 @@ describe('store', () => { it('should render variable entries', async () => { server.createList('variable', 3); - const wrapper = render(VariablesView); + const wrapper = renderComponent(VariablesView, { pinia }); await wrapper.findByTestId('resources-table'); expect(wrapper.getByTestId('resources-table')).toBeVisible(); diff --git a/packages/editor-ui/vite.config.ts b/packages/editor-ui/vite.config.ts index 4caa1176a937f..19c7f3bc4c708 100644 --- a/packages/editor-ui/vite.config.ts +++ b/packages/editor-ui/vite.config.ts @@ -1,6 +1,5 @@ import vue from '@vitejs/plugin-vue2'; import legacy from '@vitejs/plugin-legacy'; -import monacoEditorPlugin from 'vite-plugin-monaco-editor'; import path, { resolve } from 'path'; import { defineConfig, mergeConfig } from 'vite'; import { defineConfig as defineVitestConfig } from 'vitest/config'; @@ -73,24 +72,11 @@ const alias = [ }, ]; -// https://github.com/vitest-dev/vitest/discussions/1806 -if (NODE_ENV === 'test') { - alias.push({ - find: /^monaco-editor$/, - replacement: __dirname + '/node_modules/monaco-editor/esm/vs/editor/editor.api', - }); -} - const plugins = [ vue(), legacy({ targets: ['>1%', 'last 3 versions', 'not dead'], }), - monacoEditorPlugin({ - publicPath: 'assets/monaco-editor', - customDistPath: (root: string, buildOutDir: string, base: string) => - `${root}/${buildOutDir}/assets/monaco-editor`, - }), ]; const { SENTRY_AUTH_TOKEN: authToken, RELEASE: release } = process.env; diff --git a/packages/node-dev/.eslintrc.js b/packages/node-dev/.eslintrc.js index 8efac7bc90e71..228cb3e921520 100644 --- a/packages/node-dev/.eslintrc.js +++ b/packages/node-dev/.eslintrc.js @@ -10,7 +10,6 @@ module.exports = { 'templates/**', // TODO: remove this ], rules: { - '@typescript-eslint/consistent-type-imports': 'error', 'import/order': 'off', // TODO: remove this '@typescript-eslint/ban-ts-comment': ['warn', { 'ts-ignore': true }], }, diff --git a/packages/node-dev/package.json b/packages/node-dev/package.json index 2e392f9068aec..486567674f0a2 100644 --- a/packages/node-dev/package.json +++ b/packages/node-dev/package.json @@ -1,6 +1,6 @@ { "name": "n8n-node-dev", - "version": "0.103.0", + "version": "0.105.0", "description": "CLI to simplify n8n credentials/node development", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", diff --git a/packages/nodes-base/.eslintrc.js b/packages/nodes-base/.eslintrc.js index a5d9b31cce7a0..a3cf1ef0fc6f5 100644 --- a/packages/nodes-base/.eslintrc.js +++ b/packages/nodes-base/.eslintrc.js @@ -11,8 +11,6 @@ module.exports = { ignorePatterns: ['index.js'], rules: { - '@typescript-eslint/consistent-type-imports': 'error', - // TODO: remove all the following rules eqeqeq: 'off', 'id-denylist': 'off', @@ -24,7 +22,6 @@ module.exports = { '@typescript-eslint/naming-convention': ['error', { selector: 'memberLike', format: null }], '@typescript-eslint/no-explicit-any': 'off', //812 warnings, better to fix in separate PR '@typescript-eslint/no-non-null-assertion': 'off', //665 errors, better to fix in separate PR - // '@typescript-eslint/no-unsafe-argument': 'off', //1538 errors, better to fix in separate PR '@typescript-eslint/no-unsafe-assignment': 'off', //7084 problems, better to fix in separate PR '@typescript-eslint/no-unsafe-call': 'off', //541 errors, better to fix in separate PR '@typescript-eslint/no-unsafe-member-access': 'off', //4591 errors, better to fix in separate PR diff --git a/packages/nodes-base/credentials/AirtableApi.credentials.ts b/packages/nodes-base/credentials/AirtableApi.credentials.ts index 09eee15b4cb70..857ce45eaa8f5 100644 --- a/packages/nodes-base/credentials/AirtableApi.credentials.ts +++ b/packages/nodes-base/credentials/AirtableApi.credentials.ts @@ -8,6 +8,13 @@ export class AirtableApi implements ICredentialType { documentationUrl = 'airtable'; properties: INodeProperties[] = [ + { + displayName: + 'API Keys will be deprecated by the end of January 2024, see <a href="https://support.airtable.com/docs/airtable-api-key-deprecation-notice" target="_blank">this article</a> for more details. We recommend to use Personal Access Token instead.', + name: 'deprecated', + type: 'notice', + default: '', + }, { displayName: 'API Key', name: 'apiKey', diff --git a/packages/nodes-base/credentials/AirtableTokenApi.credentials.ts b/packages/nodes-base/credentials/AirtableTokenApi.credentials.ts new file mode 100644 index 0000000000000..b63a80d236033 --- /dev/null +++ b/packages/nodes-base/credentials/AirtableTokenApi.credentials.ts @@ -0,0 +1,39 @@ +import type { + IAuthenticateGeneric, + ICredentialTestRequest, + ICredentialType, + INodeProperties, +} from 'n8n-workflow'; + +export class AirtableTokenApi implements ICredentialType { + name = 'airtableTokenApi'; + + displayName = 'Airtable Personal Access Token API'; + + documentationUrl = 'airtable'; + + properties: INodeProperties[] = [ + { + displayName: 'Access Token', + name: 'accessToken', + type: 'string', + typeOptions: { password: true }, + default: '', + }, + ]; + + authenticate: IAuthenticateGeneric = { + type: 'generic', + properties: { + headers: { + Authorization: '=Bearer {{$credentials.accessToken}}', + }, + }, + }; + + test: ICredentialTestRequest = { + request: { + baseURL: 'https://api.airtable.com/v0/meta/whoami', + }, + }; +} diff --git a/packages/nodes-base/credentials/Aws.credentials.ts b/packages/nodes-base/credentials/Aws.credentials.ts index 336ee68df3859..4a7fd4efae5fc 100644 --- a/packages/nodes-base/credentials/Aws.credentials.ts +++ b/packages/nodes-base/credentials/Aws.credentials.ts @@ -9,6 +9,7 @@ import type { IHttpRequestOptions, INodeProperties, } from 'n8n-workflow'; +import { isObjectEmpty } from 'n8n-workflow'; import type { OptionsWithUri } from 'request'; export const regions = [ @@ -353,7 +354,7 @@ export class Aws implements ICredentialType { }); } - if (body && Object.keys(body).length === 0) { + if (body && typeof body === 'object' && isObjectEmpty(body)) { body = ''; } diff --git a/packages/nodes-base/credentials/JotFormApi.credentials.ts b/packages/nodes-base/credentials/JotFormApi.credentials.ts index 83e865443d995..4993229a00225 100644 --- a/packages/nodes-base/credentials/JotFormApi.credentials.ts +++ b/packages/nodes-base/credentials/JotFormApi.credentials.ts @@ -28,6 +28,10 @@ export class JotFormApi implements ICredentialType { name: 'eu-api.jotform.com', value: 'eu-api.jotform.com', }, + { + name: 'hipaa-api.jotform.com', + value: 'hipaa-api.jotform.com', + }, ], default: 'api.jotform.com', description: diff --git a/packages/nodes-base/credentials/MattermostApi.credentials.ts b/packages/nodes-base/credentials/MattermostApi.credentials.ts index afddc454fc7e1..51a5d657ede60 100644 --- a/packages/nodes-base/credentials/MattermostApi.credentials.ts +++ b/packages/nodes-base/credentials/MattermostApi.credentials.ts @@ -46,7 +46,7 @@ export class MattermostApi implements ICredentialType { test: ICredentialTestRequest = { request: { - baseURL: '={{$credentials.baseUrl}}/api/v4', + baseURL: '={{$credentials.baseUrl.replace(/\\/$/, "")}}/api/v4', url: '/users', skipSslCertificateValidation: '={{$credentials?.allowUnauthorizedCerts}}', }, diff --git a/packages/nodes-base/credentials/SshPrivateKey.credentials.ts b/packages/nodes-base/credentials/SshPrivateKey.credentials.ts index 11b166619c74b..e457bd491568e 100644 --- a/packages/nodes-base/credentials/SshPrivateKey.credentials.ts +++ b/packages/nodes-base/credentials/SshPrivateKey.credentials.ts @@ -35,7 +35,6 @@ export class SshPrivateKey implements ICredentialType { type: 'string', typeOptions: { rows: 4, - password: true, }, default: '', }, @@ -45,6 +44,7 @@ export class SshPrivateKey implements ICredentialType { type: 'string', default: '', description: 'Passphase used to create the key, if no passphase was used leave empty', + typeOptions: { password: true }, }, ]; } diff --git a/packages/nodes-base/jest.config.js b/packages/nodes-base/jest.config.js index 54696ffb30b28..5fcf33c3358dc 100644 --- a/packages/nodes-base/jest.config.js +++ b/packages/nodes-base/jest.config.js @@ -2,4 +2,7 @@ module.exports = { ...require('../../jest.config'), collectCoverageFrom: ['credentials/**/*.ts', 'nodes/**/*.ts', 'utils/**/*.ts'], + moduleNameMapper: { + '^@test/(.*)$': '<rootDir>/test/$1', + }, }; diff --git a/packages/nodes-base/nodes/Airtable/Airtable.node.ts b/packages/nodes-base/nodes/Airtable/Airtable.node.ts index eb8d499b32022..49c54919769a7 100644 --- a/packages/nodes-base/nodes/Airtable/Airtable.node.ts +++ b/packages/nodes-base/nodes/Airtable/Airtable.node.ts @@ -27,9 +27,39 @@ export class Airtable implements INodeType { { name: 'airtableApi', required: true, + displayOptions: { + show: { + authentication: ['airtableApi'], + }, + }, + }, + { + name: 'airtableTokenApi', + required: true, + displayOptions: { + show: { + authentication: ['airtableTokenApi'], + }, + }, }, ], properties: [ + { + displayName: 'Authentication', + name: 'authentication', + type: 'options', + options: [ + { + name: 'API Key', + value: 'airtableApi', + }, + { + name: 'Access Token', + value: 'airtableTokenApi', + }, + ], + default: 'airtableApi', + }, { displayName: 'Operation', name: 'operation', @@ -546,14 +576,15 @@ export class Airtable implements INodeType { delete (row.fields as any).id; } else { // Add only the specified fields - row.fields = {} as IDataObject; + const rowFields: IDataObject = {}; fields = this.getNodeParameter('fields', i, []) as string[]; for (const fieldName of fields) { - // @ts-ignore - row.fields[fieldName] = items[i].json[fieldName]; + rowFields[fieldName] = items[i].json[fieldName]; } + + row.fields = rowFields; } rows.push(row); @@ -761,10 +792,12 @@ export class Airtable implements INodeType { } else { fields = this.getNodeParameter('fields', i, []) as string[]; + const rowFields: IDataObject = {}; for (const fieldName of fields) { - // @ts-ignore - row.fields[fieldName] = items[i].json[fieldName]; + rowFields[fieldName] = items[i].json[fieldName]; } + + row.fields = rowFields; } row.id = this.getNodeParameter('id', i) as string; diff --git a/packages/nodes-base/nodes/Airtable/AirtableTrigger.node.ts b/packages/nodes-base/nodes/Airtable/AirtableTrigger.node.ts index b54ae8f8da0f4..ce3b868c4f559 100644 --- a/packages/nodes-base/nodes/Airtable/AirtableTrigger.node.ts +++ b/packages/nodes-base/nodes/Airtable/AirtableTrigger.node.ts @@ -28,12 +28,42 @@ export class AirtableTrigger implements INodeType { { name: 'airtableApi', required: true, + displayOptions: { + show: { + authentication: ['airtableApi'], + }, + }, + }, + { + name: 'airtableTokenApi', + required: true, + displayOptions: { + show: { + authentication: ['airtableTokenApi'], + }, + }, }, ], polling: true, inputs: [], outputs: ['main'], properties: [ + { + displayName: 'Authentication', + name: 'authentication', + type: 'options', + options: [ + { + name: 'API Key', + value: 'airtableApi', + }, + { + name: 'Access Token', + value: 'airtableTokenApi', + }, + ], + default: 'airtableApi', + }, { displayName: 'Base', name: 'baseId', @@ -192,19 +222,14 @@ export class AirtableTrigger implements INodeType { async poll(this: IPollFunctions): Promise<INodeExecutionData[][] | null> { const downloadAttachments = this.getNodeParameter('downloadAttachments', 0) as boolean; - const webhookData = this.getWorkflowStaticData('node'); - - const qs: IDataObject = {}; - const additionalFields = this.getNodeParameter('additionalFields') as IDataObject; - const base = this.getNodeParameter('baseId', '', { extractValue: true }) as string; - const table = this.getNodeParameter('tableId', '', { extractValue: true }) as string; - const triggerField = this.getNodeParameter('triggerField') as string; + const qs: IDataObject = {}; + const endpoint = `${base}/${table}`; const now = moment().utc().format(); diff --git a/packages/nodes-base/nodes/Airtable/GenericFunctions.ts b/packages/nodes-base/nodes/Airtable/GenericFunctions.ts index 80e769e18a22b..3b2b3c7b61f16 100644 --- a/packages/nodes-base/nodes/Airtable/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Airtable/GenericFunctions.ts @@ -58,8 +58,8 @@ export async function apiRequest( if (Object.keys(body).length === 0) { delete options.body; } - - return this.helpers.requestWithAuthentication.call(this, 'airtableApi', options); + const authenticationMethod = this.getNodeParameter('authentication', 0) as string; + return this.helpers.requestWithAuthentication.call(this, authenticationMethod, options); } /** diff --git a/packages/nodes-base/nodes/Aws/DynamoDB/types.d.ts b/packages/nodes-base/nodes/Aws/DynamoDB/types.d.ts index c03eb89ab59a1..201fecbc34ee3 100644 --- a/packages/nodes-base/nodes/Aws/DynamoDB/types.d.ts +++ b/packages/nodes-base/nodes/Aws/DynamoDB/types.d.ts @@ -51,7 +51,7 @@ export type PartitionKey = { }; }; -export enum EAttributeValueType { +export const enum EAttributeValueType { S = 'S', SS = 'SS', M = 'M', diff --git a/packages/nodes-base/nodes/Aws/Rekognition/AwsRekognition.node.ts b/packages/nodes-base/nodes/Aws/Rekognition/AwsRekognition.node.ts index a5a94fd2026fe..99345befbfc33 100644 --- a/packages/nodes-base/nodes/Aws/Rekognition/AwsRekognition.node.ts +++ b/packages/nodes-base/nodes/Aws/Rekognition/AwsRekognition.node.ts @@ -389,64 +389,62 @@ export class AwsRekognition implements INodeType { if (type === 'detectText') { action = 'RekognitionService.DetectText'; + } + body.Filters = {}; - body.Filters = {}; - - const box = - ((additionalFields.regionsOfInterestUi as IDataObject) - ?.regionsOfInterestValues as IDataObject[]) || []; + const box = + ((additionalFields.regionsOfInterestUi as IDataObject) + ?.regionsOfInterestValues as IDataObject[]) || []; - if (box.length !== 0) { - //@ts-ignore - body.Filters.RegionsOfInterest = box.map((entry: IDataObject) => { - return { BoundingBox: keysTPascalCase(entry) }; - }); - } + if (box.length !== 0) { + //@ts-ignore + body.Filters.RegionsOfInterest = box.map((entry: IDataObject) => { + return { BoundingBox: keysTPascalCase(entry) }; + }); + } - const wordFilter = (additionalFields.wordFilterUi as IDataObject) || {}; - if (Object.keys(wordFilter).length !== 0) { - //@ts-ignore - body.Filters.WordFilter = keysTPascalCase(wordFilter); - } + const wordFilter = (additionalFields.wordFilterUi as IDataObject) || {}; + if (Object.keys(wordFilter).length !== 0) { + //@ts-ignore + body.Filters.WordFilter = keysTPascalCase(wordFilter); + } - const isBinaryData = this.getNodeParameter('binaryData', i); - if (isBinaryData) { - const binaryPropertyName = this.getNodeParameter('binaryPropertyName', i); - const binaryPropertyData = this.helpers.assertBinaryData(i, binaryPropertyName); - Object.assign(body, { - Image: { - Bytes: binaryPropertyData.data, - }, - }); - } else { - const bucket = this.getNodeParameter('bucket', i) as string; - const name = this.getNodeParameter('name', i) as string; + const isBinaryData = this.getNodeParameter('binaryData', i); + if (isBinaryData) { + const binaryPropertyName = this.getNodeParameter('binaryPropertyName', i); + const binaryPropertyData = this.helpers.assertBinaryData(i, binaryPropertyName); + Object.assign(body, { + Image: { + Bytes: binaryPropertyData.data, + }, + }); + } else { + const bucket = this.getNodeParameter('bucket', i) as string; + const name = this.getNodeParameter('name', i) as string; - Object.assign(body, { - Image: { - S3Object: { - Bucket: bucket, - Name: name, - }, + Object.assign(body, { + Image: { + S3Object: { + Bucket: bucket, + Name: name, }, - }); + }, + }); - if (additionalFields.version) { - //@ts-ignore - body.Image.S3Object.Version = additionalFields.version as string; - } + if (additionalFields.version) { + //@ts-ignore + body.Image.S3Object.Version = additionalFields.version as string; } - - responseData = await awsApiRequestREST.call( - this, - 'rekognition', - 'POST', - '', - JSON.stringify(body), - {}, - { 'X-Amz-Target': action, 'Content-Type': 'application/x-amz-json-1.1' }, - ); } + responseData = await awsApiRequestREST.call( + this, + 'rekognition', + 'POST', + '', + JSON.stringify(body), + {}, + { 'X-Amz-Target': action, 'Content-Type': 'application/x-amz-json-1.1' }, + ); } } diff --git a/packages/nodes-base/nodes/Aws/Rekognition/test/AwsRekognition.node.test.ts b/packages/nodes-base/nodes/Aws/Rekognition/test/AwsRekognition.node.test.ts new file mode 100644 index 0000000000000..a0ce422e4400b --- /dev/null +++ b/packages/nodes-base/nodes/Aws/Rekognition/test/AwsRekognition.node.test.ts @@ -0,0 +1,307 @@ +import { getWorkflowFilenames, testWorkflows } from '../../../../test/nodes/Helpers'; + +import nock from 'nock'; + +const responseLabels = [ + { + LabelModelVersion: '3.0', + Labels: [ + { + Aliases: [], + Categories: [ + { + Name: 'Food and Beverage', + }, + ], + Confidence: 99.81004333496094, + Instances: [], + Name: 'Alcohol', + Parents: [ + { + Name: 'Beverage', + }, + ], + }, + { + Aliases: [ + { + Name: 'Drink', + }, + ], + Categories: [ + { + Name: 'Food and Beverage', + }, + ], + Confidence: 99.81004333496094, + Instances: [], + Name: 'Beverage', + Parents: [], + }, + { + Aliases: [], + Categories: [ + { + Name: 'Food and Beverage', + }, + ], + Confidence: 99.81004333496094, + Instances: [], + Name: 'Liquor', + Parents: [ + { + Name: 'Alcohol', + }, + { + Name: 'Beverage', + }, + ], + }, + { + Aliases: [], + Categories: [ + { + Name: 'Food and Beverage', + }, + ], + Confidence: 99.75448608398438, + Instances: [], + Name: 'Red Wine', + Parents: [ + { + Name: 'Alcohol', + }, + { + Name: 'Beverage', + }, + { + Name: 'Liquor', + }, + { + Name: 'Wine', + }, + ], + }, + { + Aliases: [], + Categories: [ + { + Name: 'Food and Beverage', + }, + ], + Confidence: 99.75448608398438, + Instances: [], + Name: 'Wine', + Parents: [ + { + Name: 'Alcohol', + }, + { + Name: 'Beverage', + }, + { + Name: 'Liquor', + }, + ], + }, + { + Aliases: [], + Categories: [ + { + Name: 'Everyday Objects', + }, + ], + Confidence: 99.52116394042969, + Instances: [], + Name: 'Bottle', + Parents: [], + }, + { + Aliases: [], + Categories: [ + { + Name: 'Food and Beverage', + }, + ], + Confidence: 94.69605255126953, + Instances: [], + Name: 'Wine Bottle', + Parents: [ + { + Name: 'Alcohol', + }, + { + Name: 'Beverage', + }, + { + Name: 'Bottle', + }, + { + Name: 'Liquor', + }, + { + Name: 'Wine', + }, + ], + }, + { + Aliases: [], + Categories: [ + { + Name: 'Food and Beverage', + }, + ], + Confidence: 90.0589370727539, + Instances: [], + Name: 'Food', + Parents: [], + }, + { + Aliases: [], + Categories: [ + { + Name: 'Food and Beverage', + }, + ], + Confidence: 90.0589370727539, + Instances: [ + { + BoundingBox: { + Height: 0.9467026591300964, + Left: 0.23295101523399353, + Top: 0.02573961764574051, + Width: 0.5303559899330139, + }, + Confidence: 90.0589370727539, + }, + ], + Name: 'Ketchup', + Parents: [ + { + Name: 'Food', + }, + ], + }, + { + Aliases: [], + Categories: [ + { + Name: 'Food and Beverage', + }, + ], + Confidence: 65.56095123291016, + Instances: [], + Name: 'Beer', + Parents: [ + { + Name: 'Alcohol', + }, + { + Name: 'Beverage', + }, + ], + }, + { + Aliases: [], + Categories: [ + { + Name: 'Text and Documents', + }, + ], + Confidence: 61.83842468261719, + Instances: [], + Name: 'Document', + Parents: [ + { + Name: 'Text', + }, + ], + }, + { + Aliases: [], + Categories: [ + { + Name: 'Text and Documents', + }, + ], + Confidence: 61.83842468261719, + Instances: [], + Name: 'Id Cards', + Parents: [ + { + Name: 'Document', + }, + { + Name: 'Text', + }, + ], + }, + { + Aliases: [], + Categories: [ + { + Name: 'Text and Documents', + }, + ], + Confidence: 61.83842468261719, + Instances: [ + { + BoundingBox: { + Height: 0.5003108382225037, + Left: 0.2603513300418854, + Top: 0.2912488579750061, + Width: 0.4734913110733032, + }, + Confidence: 61.83842468261719, + }, + ], + Name: 'Passport', + Parents: [ + { + Name: 'Document', + }, + { + Name: 'Id Cards', + }, + { + Name: 'Text', + }, + ], + }, + { + Aliases: [], + Categories: [ + { + Name: 'Text and Documents', + }, + ], + Confidence: 61.83842468261719, + Instances: [], + Name: 'Text', + Parents: [], + }, + ], + }, +]; + +describe('Test AWS Rekogntion Node', () => { + describe('Image Labels Recognition', () => { + const workflows = getWorkflowFilenames(__dirname); + const baseUrl = 'https://rekognition.eu-central-1.amazonaws.com'; + let mock: nock.Scope; + + beforeAll(async () => { + nock.disableNetConnect(); + mock = nock(baseUrl); + }); + + beforeEach(async () => { + mock.post('/').reply(200, responseLabels); + }); + + afterAll(() => { + nock.restore(); + }); + testWorkflows(workflows); + }); +}); diff --git a/packages/nodes-base/nodes/Aws/Rekognition/test/AwsRekognitionTestWorkflow.json b/packages/nodes-base/nodes/Aws/Rekognition/test/AwsRekognitionTestWorkflow.json new file mode 100644 index 0000000000000..4f11f27a45554 --- /dev/null +++ b/packages/nodes-base/nodes/Aws/Rekognition/test/AwsRekognitionTestWorkflow.json @@ -0,0 +1,344 @@ +{ + "name": "node-37-aws-rekognition-empty-response-data", + "nodes": [ + { + "parameters": {}, + "id": "6b79e1a7-8d17-470a-9423-d97042060170", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [ + 40, + 280 + ] + }, + { + "parameters": { + "type": "detectText", + "bucket": "test", + "name": "test", + "additionalFields": {} + }, + "id": "86b957c8-fdec-462e-b55d-40dccf7bc253", + "name": "AWS Rekognition", + "type": "n8n-nodes-base.awsRekognition", + "typeVersion": 1, + "position": [ + 500, + 280 + ], + "credentials": { + "aws": { + "id": "114", + "name": "NodeQA" + } + } + } + ], + "pinData": { + "AWS Rekognition": [ + { + "json": { + "LabelModelVersion": "3.0", + "Labels": [ + { + "Aliases": [], + "Categories": [ + { + "Name": "Food and Beverage" + } + ], + "Confidence": 99.81004333496094, + "Instances": [], + "Name": "Alcohol", + "Parents": [ + { + "Name": "Beverage" + } + ] + }, + { + "Aliases": [ + { + "Name": "Drink" + } + ], + "Categories": [ + { + "Name": "Food and Beverage" + } + ], + "Confidence": 99.81004333496094, + "Instances": [], + "Name": "Beverage", + "Parents": [] + }, + { + "Aliases": [], + "Categories": [ + { + "Name": "Food and Beverage" + } + ], + "Confidence": 99.81004333496094, + "Instances": [], + "Name": "Liquor", + "Parents": [ + { + "Name": "Alcohol" + }, + { + "Name": "Beverage" + } + ] + }, + { + "Aliases": [], + "Categories": [ + { + "Name": "Food and Beverage" + } + ], + "Confidence": 99.75448608398438, + "Instances": [], + "Name": "Red Wine", + "Parents": [ + { + "Name": "Alcohol" + }, + { + "Name": "Beverage" + }, + { + "Name": "Liquor" + }, + { + "Name": "Wine" + } + ] + }, + { + "Aliases": [], + "Categories": [ + { + "Name": "Food and Beverage" + } + ], + "Confidence": 99.75448608398438, + "Instances": [], + "Name": "Wine", + "Parents": [ + { + "Name": "Alcohol" + }, + { + "Name": "Beverage" + }, + { + "Name": "Liquor" + } + ] + }, + { + "Aliases": [], + "Categories": [ + { + "Name": "Everyday Objects" + } + ], + "Confidence": 99.52116394042969, + "Instances": [], + "Name": "Bottle", + "Parents": [] + }, + { + "Aliases": [], + "Categories": [ + { + "Name": "Food and Beverage" + } + ], + "Confidence": 94.69605255126953, + "Instances": [], + "Name": "Wine Bottle", + "Parents": [ + { + "Name": "Alcohol" + }, + { + "Name": "Beverage" + }, + { + "Name": "Bottle" + }, + { + "Name": "Liquor" + }, + { + "Name": "Wine" + } + ] + }, + { + "Aliases": [], + "Categories": [ + { + "Name": "Food and Beverage" + } + ], + "Confidence": 90.0589370727539, + "Instances": [], + "Name": "Food", + "Parents": [] + }, + { + "Aliases": [], + "Categories": [ + { + "Name": "Food and Beverage" + } + ], + "Confidence": 90.0589370727539, + "Instances": [ + { + "BoundingBox": { + "Height": 0.9467026591300964, + "Left": 0.23295101523399353, + "Top": 0.02573961764574051, + "Width": 0.5303559899330139 + }, + "Confidence": 90.0589370727539 + } + ], + "Name": "Ketchup", + "Parents": [ + { + "Name": "Food" + } + ] + }, + { + "Aliases": [], + "Categories": [ + { + "Name": "Food and Beverage" + } + ], + "Confidence": 65.56095123291016, + "Instances": [], + "Name": "Beer", + "Parents": [ + { + "Name": "Alcohol" + }, + { + "Name": "Beverage" + } + ] + }, + { + "Aliases": [], + "Categories": [ + { + "Name": "Text and Documents" + } + ], + "Confidence": 61.83842468261719, + "Instances": [], + "Name": "Document", + "Parents": [ + { + "Name": "Text" + } + ] + }, + { + "Aliases": [], + "Categories": [ + { + "Name": "Text and Documents" + } + ], + "Confidence": 61.83842468261719, + "Instances": [], + "Name": "Id Cards", + "Parents": [ + { + "Name": "Document" + }, + { + "Name": "Text" + } + ] + }, + { + "Aliases": [], + "Categories": [ + { + "Name": "Text and Documents" + } + ], + "Confidence": 61.83842468261719, + "Instances": [ + { + "BoundingBox": { + "Height": 0.5003108382225037, + "Left": 0.2603513300418854, + "Top": 0.2912488579750061, + "Width": 0.4734913110733032 + }, + "Confidence": 61.83842468261719 + } + ], + "Name": "Passport", + "Parents": [ + { + "Name": "Document" + }, + { + "Name": "Id Cards" + }, + { + "Name": "Text" + } + ] + }, + { + "Aliases": [], + "Categories": [ + { + "Name": "Text and Documents" + } + ], + "Confidence": 61.83842468261719, + "Instances": [], + "Name": "Text", + "Parents": [] + } + ] + } + } + ] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "AWS Rekognition", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": {}, + "versionId": "26ee97c0-3122-4281-96ac-ad4ded7b4ee5", + "id": "55", + "meta": { + "instanceId": "8e9416f42a954d0a370d988ac3c0f916f44074a6e45189164b1a8559394a7516" + }, + "tags": [] +} \ No newline at end of file diff --git a/packages/nodes-base/nodes/Aws/S3/test/AwsS3.file.upload.workflow.json b/packages/nodes-base/nodes/Aws/S3/test/AwsS3.file.upload.workflow.json new file mode 100644 index 0000000000000..b631ffe255739 --- /dev/null +++ b/packages/nodes-base/nodes/Aws/S3/test/AwsS3.file.upload.workflow.json @@ -0,0 +1,97 @@ +{ + "name": "Test S3 upload", + "nodes": [ + { + "parameters": {}, + "id": "8f35d24b-1493-43a4-846f-bacb577bfcb2", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [540, 340] + }, + { + "parameters": { + "mode": "jsonToBinary", + "options": {} + }, + "id": "eae2946a-1a1e-47e9-9fd6-e32119b13ec0", + "name": "Move Binary Data", + "type": "n8n-nodes-base.moveBinaryData", + "typeVersion": 1, + "position": [900, 340] + }, + { + "parameters": { + "operation": "upload", + "bucketName": "bucket", + "fileName": "binary.json", + "additionalFields": {} + }, + "id": "6f21fa3f-ede1-44b1-8182-a2c07152f666", + "name": "AWS S3", + "type": "n8n-nodes-base.awsS3", + "typeVersion": 1, + "position": [1080, 340], + "credentials": { + "aws": { + "id": "1", + "name": "AWS account" + } + } + }, + { + "parameters": { + "jsCode": "return [{ key: \"value\" }];" + }, + "id": "e12f1876-cfd1-47a4-a21b-d478452683bc", + "name": "Code", + "type": "n8n-nodes-base.code", + "typeVersion": 1, + "position": [720, 340] + } + ], + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Code", + "type": "main", + "index": 0 + } + ] + ] + }, + "Move Binary Data": { + "main": [ + [ + { + "node": "AWS S3", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code": { + "main": [ + [ + { + "node": "Move Binary Data", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "pinData": { + "AWS S3": [ + { + "json": { + "success": true + } + } + ] + } +} diff --git a/packages/nodes-base/nodes/Aws/S3/test/AwsS3.node.test.ts b/packages/nodes-base/nodes/Aws/S3/test/AwsS3.node.test.ts new file mode 100644 index 0000000000000..17faedca2895a --- /dev/null +++ b/packages/nodes-base/nodes/Aws/S3/test/AwsS3.node.test.ts @@ -0,0 +1,48 @@ +import nock from 'nock'; +import { getWorkflowFilenames, initBinaryDataManager, testWorkflows } from '@test/nodes/Helpers'; + +const workflows = getWorkflowFilenames(__dirname); + +describe('Test S3 Node', () => { + describe('File Upload', () => { + let mock: nock.Scope; + const now = 1683028800000; + + beforeAll(async () => { + jest.useFakeTimers({ doNotFake: ['nextTick'], now }); + + await initBinaryDataManager(); + + nock.disableNetConnect(); + mock = nock('https://bucket.s3.eu-central-1.amazonaws.com'); + }); + + beforeEach(async () => { + mock.get('/?location').reply( + 200, + `<?xml version="1.0" encoding="UTF-8"?> + <LocationConstraint> + <LocationConstraint>eu-central-1</LocationConstraint> + </LocationConstraint>`, + { + 'content-type': 'application/xml', + }, + ); + + mock + .put('/binary.json') + .matchHeader( + 'X-Amz-Content-Sha256', + 'e43abcf3375244839c012f9633f95862d232a95b00d5bc7348b3098b9fed7f32', + ) + .once() + .reply(200, { success: true }); + }); + + afterAll(() => { + nock.restore(); + }); + + testWorkflows(workflows); + }); +}); diff --git a/packages/nodes-base/nodes/Clockify/CommonDtos.ts b/packages/nodes-base/nodes/Clockify/CommonDtos.ts index 858b58bb97b5c..7e7f5bbd91419 100644 --- a/packages/nodes-base/nodes/Clockify/CommonDtos.ts +++ b/packages/nodes-base/nodes/Clockify/CommonDtos.ts @@ -3,14 +3,14 @@ export interface IHourlyRateDto { currency: string; } -enum MembershipStatusEnum { +const enum MembershipStatusEnum { PENDING = 'PENDING', ACTIVE = 'ACTIVE', DECLINED = 'DECLINED', INACTIVE = 'INACTIVE', } -enum TaskStatusEnum { +const enum TaskStatusEnum { ACTIVE = 'ACTIVE', DONE = 'DONE', } diff --git a/packages/nodes-base/nodes/Clockify/EntryTypeEnum.ts b/packages/nodes-base/nodes/Clockify/EntryTypeEnum.ts index 0df2a710191ae..d83881685f8ed 100644 --- a/packages/nodes-base/nodes/Clockify/EntryTypeEnum.ts +++ b/packages/nodes-base/nodes/Clockify/EntryTypeEnum.ts @@ -1,3 +1,3 @@ -export enum EntryTypeEnum { +export const enum EntryTypeEnum { NEW_TIME_ENTRY, } diff --git a/packages/nodes-base/nodes/Clockify/ProjectInterfaces.ts b/packages/nodes-base/nodes/Clockify/ProjectInterfaces.ts index 0e7419cb69927..623a9fe5176fe 100644 --- a/packages/nodes-base/nodes/Clockify/ProjectInterfaces.ts +++ b/packages/nodes-base/nodes/Clockify/ProjectInterfaces.ts @@ -1,6 +1,6 @@ import type { IHourlyRateDto, IMembershipDto } from './CommonDtos'; -enum EstimateEnum { +const enum EstimateEnum { AUTO = 'AUTO', MANUAL = 'MANUAL', } @@ -40,7 +40,7 @@ export interface IProjectRequest { tasks: ITaskDto; } -enum TaskStatusEnum { +const enum TaskStatusEnum { ACTIVE = 'ACTIVE', DONE = 'DONE', } diff --git a/packages/nodes-base/nodes/Clockify/UserDtos.ts b/packages/nodes-base/nodes/Clockify/UserDtos.ts index dc1f96c75bf2e..6365b057dfaeb 100644 --- a/packages/nodes-base/nodes/Clockify/UserDtos.ts +++ b/packages/nodes-base/nodes/Clockify/UserDtos.ts @@ -1,7 +1,7 @@ import type { IDataObject } from 'n8n-workflow'; import type { IMembershipDto } from './CommonDtos'; -enum UserStatusEnum { +const enum UserStatusEnum { ACTIVE, PENDING_EMAIL_VERIFICATION, DELETED, diff --git a/packages/nodes-base/nodes/Clockify/WorkpaceInterfaces.ts b/packages/nodes-base/nodes/Clockify/WorkpaceInterfaces.ts index f46be67faae2f..11eb0557e7dba 100644 --- a/packages/nodes-base/nodes/Clockify/WorkpaceInterfaces.ts +++ b/packages/nodes-base/nodes/Clockify/WorkpaceInterfaces.ts @@ -1,12 +1,12 @@ import type { IHourlyRateDto, IMembershipDto } from './CommonDtos'; -enum AdminOnlyPagesEnum { +const enum AdminOnlyPagesEnum { PROJECT = 'PROJECT', TEAM = 'TEAM', REPORTS = 'REPORTS', } -enum DaysOfWeekEnum { +const enum DaysOfWeekEnum { MONDAY = 'MONDAY', TUESDAY = 'TUESDAY', WEDNESDAY = 'WEDNESDAY', @@ -16,13 +16,13 @@ enum DaysOfWeekEnum { SUNDAY = 'SUNDAY', } -enum DatePeriodEnum { +const enum DatePeriodEnum { DAYS = 'DAYS', WEEKS = 'WEEKS', MONTHS = 'MONTHS', } -enum AutomaticLockTypeEnum { +const enum AutomaticLockTypeEnum { WEEKLY = 'WEEKLY', MONTHLY = 'MONTHLY', OLDER_THAN = 'OLDER_THAN', diff --git a/packages/nodes-base/nodes/Code/Code.node.json b/packages/nodes-base/nodes/Code/Code.node.json index 3ce876131cf47..8ec1cad6c98f2 100644 --- a/packages/nodes-base/nodes/Code/Code.node.json +++ b/packages/nodes-base/nodes/Code/Code.node.json @@ -11,7 +11,7 @@ } ] }, - "alias": ["cpde", "Javascript", "JS", "Script", "Custom Code", "Function"], + "alias": ["cpde", "Javascript", "JS", "Python", "Script", "Custom Code", "Function"], "subcategories": { "Core Nodes": ["Data Transformation"] } diff --git a/packages/nodes-base/nodes/Code/Code.node.ts b/packages/nodes-base/nodes/Code/Code.node.ts index ac64931d25fc7..32a905e87e931 100644 --- a/packages/nodes-base/nodes/Code/Code.node.ts +++ b/packages/nodes-base/nodes/Code/Code.node.ts @@ -1,12 +1,17 @@ import type { + CodeExecutionMode, + CodeNodeEditorLanguage, IExecuteFunctions, INodeExecutionData, INodeType, INodeTypeDescription, } from 'n8n-workflow'; -import { getSandboxContext, Sandbox } from './Sandbox'; +import { javascriptCodeDescription } from './descriptions/JavascriptCodeDescription'; +import { pythonCodeDescription } from './descriptions/PythonCodeDescription'; +import { JavaScriptSandbox } from './JavaScriptSandbox'; +import { PythonSandbox } from './PythonSandbox'; +import { getSandboxContext } from './Sandbox'; import { standardizeOutput } from './utils'; -import type { CodeNodeMode } from './utils'; export class Code implements INodeType { description: INodeTypeDescription = { @@ -14,7 +19,8 @@ export class Code implements INodeType { name: 'code', icon: 'fa:code', group: ['transform'], - version: 1, + version: [1, 2], + defaultVersion: 1, description: 'Run custom JavaScript code', defaults: { name: 'Code', @@ -44,59 +50,78 @@ export class Code implements INodeType { default: 'runOnceForAllItems', }, { - displayName: 'JavaScript', - name: 'jsCode', - typeOptions: { - editor: 'codeNodeEditor', - }, - type: 'string', - default: '', // set by component - description: - 'JavaScript code to execute.<br><br>Tip: You can use luxon vars like <code>$today</code> for dates and <code>$jmespath</code> for querying JSON structures. <a href="https://docs.n8n.io/nodes/n8n-nodes-base.function">Learn more</a>.', + displayName: 'Language', + name: 'language', + type: 'options', noDataExpression: true, + displayOptions: { + show: { + '@version': [2], + }, + }, + options: [ + { + name: 'JavaScript', + value: 'javaScript', + }, + { + name: 'Python (Beta)', + value: 'python', + }, + ], + default: 'javaScript', }, - { - displayName: - 'Type <code>$</code> for a list of <a target="_blank" href="https://docs.n8n.io/code-examples/methods-variables-reference/">special vars/methods</a>. Debug by using <code>console.log()</code> statements and viewing their output in the browser console.', - name: 'notice', - type: 'notice', - default: '', - }, + + ...javascriptCodeDescription, + ...pythonCodeDescription, ], }; async execute(this: IExecuteFunctions) { - const nodeMode = this.getNodeParameter('mode', 0) as CodeNodeMode; + const nodeMode = this.getNodeParameter<CodeExecutionMode>('mode', 0); const workflowMode = this.getMode(); + const language: CodeNodeEditorLanguage = + this.getNode()?.typeVersion === 2 ? this.getNodeParameter('language', 0) : 'javaScript'; + const codeParameterName = language === 'python' ? 'pythonCode' : 'jsCode'; + + const getSandbox = (index = 0) => { + const code = this.getNodeParameter<string>(codeParameterName, index); + const context = getSandboxContext.call(this, index); + if (language === 'python') { + const modules = this.getNodeParameter<string>('modules', index); + const moduleImports: string[] = modules ? modules.split(',').map((m) => m.trim()) : []; + context.printOverwrite = workflowMode === 'manual' ? this.sendMessageToUI : null; + return new PythonSandbox(context, code, moduleImports, index, this.helpers); + } else { + context.items = context.$input.all(); + const sandbox = new JavaScriptSandbox(context, code, index, workflowMode, this.helpers); + if (workflowMode === 'manual') { + sandbox.vm.on('console.log', this.sendMessageToUI); + } + return sandbox; + } + }; + // ---------------------------------- // runOnceForAllItems // ---------------------------------- if (nodeMode === 'runOnceForAllItems') { - const jsCodeAllItems = this.getNodeParameter('jsCode', 0) as string; - - const context = getSandboxContext.call(this); - context.items = context.$input.all(); - const sandbox = new Sandbox(context, jsCodeAllItems, workflowMode, this.helpers); - - if (workflowMode === 'manual') { - sandbox.on('console.log', this.sendMessageToUI); - } - - let result: INodeExecutionData[]; + const sandbox = getSandbox(); + let items: INodeExecutionData[]; try { - result = await sandbox.runCodeAllItems(); + items = await sandbox.runCodeAllItems(); } catch (error) { - if (!this.continueOnFail()) return Promise.reject(error); - result = [{ json: { error: error.message } }]; + if (!this.continueOnFail()) throw error; + items = [{ json: { error: error.message } }]; } - for (const item of result) { + for (const item of items) { standardizeOutput(item.json); } - return this.prepareOutputData(result); + return this.prepareOutputData(items); } // ---------------------------------- @@ -108,21 +133,12 @@ export class Code implements INodeType { const items = this.getInputData(); for (let index = 0; index < items.length; index++) { - const jsCodeEachItem = this.getNodeParameter('jsCode', index) as string; - - const context = getSandboxContext.call(this, index); - context.item = context.$input.item; - const sandbox = new Sandbox(context, jsCodeEachItem, workflowMode, this.helpers); - - if (workflowMode === 'manual') { - sandbox.on('console.log', this.sendMessageToUI); - } - + const sandbox = getSandbox(index); let result: INodeExecutionData | undefined; try { - result = await sandbox.runCodeEachItem(index); + result = await sandbox.runCodeEachItem(); } catch (error) { - if (!this.continueOnFail()) return Promise.reject(error); + if (!this.continueOnFail()) throw error; returnData.push({ json: { error: error.message } }); } diff --git a/packages/nodes-base/nodes/Code/JavaScriptSandbox.ts b/packages/nodes-base/nodes/Code/JavaScriptSandbox.ts new file mode 100644 index 0000000000000..9c726503f9758 --- /dev/null +++ b/packages/nodes-base/nodes/Code/JavaScriptSandbox.ts @@ -0,0 +1,115 @@ +import type { NodeVMOptions } from 'vm2'; +import { NodeVM } from 'vm2'; +import type { IExecuteFunctions, INodeExecutionData, WorkflowExecuteMode } from 'n8n-workflow'; + +import { ValidationError } from './ValidationError'; +import { ExecutionError } from './ExecutionError'; +import type { SandboxContext } from './Sandbox'; +import { Sandbox } from './Sandbox'; + +const { NODE_FUNCTION_ALLOW_BUILTIN: builtIn, NODE_FUNCTION_ALLOW_EXTERNAL: external } = + process.env; + +const getSandboxOptions = ( + context: SandboxContext, + workflowMode: WorkflowExecuteMode, +): NodeVMOptions => ({ + console: workflowMode === 'manual' ? 'redirect' : 'inherit', + sandbox: context, + require: { + builtin: builtIn ? builtIn.split(',') : [], + external: external ? { modules: external.split(','), transitive: false } : false, + }, +}); + +export class JavaScriptSandbox extends Sandbox { + readonly vm: NodeVM; + + constructor( + context: SandboxContext, + private jsCode: string, + itemIndex: number | undefined, + workflowMode: WorkflowExecuteMode, + helpers: IExecuteFunctions['helpers'], + ) { + super( + { + object: { + singular: 'object', + plural: 'objects', + }, + }, + itemIndex, + helpers, + ); + this.vm = new NodeVM(getSandboxOptions(context, workflowMode)); + } + + async runCodeAllItems(): Promise<INodeExecutionData[]> { + const script = `module.exports = async function() {${this.jsCode}\n}()`; + + let executionResult: INodeExecutionData | INodeExecutionData[]; + + try { + executionResult = await this.vm.run(script, __dirname); + } catch (error) { + // anticipate user expecting `items` to pre-exist as in Function Item node + if (error.message === 'items is not defined' && !/(let|const|var) items =/.test(script)) { + const quoted = error.message.replace('items', '`items`'); + error.message = (quoted as string) + '. Did you mean `$input.all()`?'; + } + + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + throw new ExecutionError(error); + } + + if (executionResult === null) return []; + + return this.validateRunCodeAllItems(executionResult); + } + + async runCodeEachItem(): Promise<INodeExecutionData | undefined> { + const script = `module.exports = async function() {${this.jsCode}\n}()`; + + const match = this.jsCode.match(/\$input\.(?<disallowedMethod>first|last|all|itemMatching)/); + + if (match?.groups?.disallowedMethod) { + const { disallowedMethod } = match.groups; + + const lineNumber = + this.jsCode.split('\n').findIndex((line) => { + return line.includes(disallowedMethod) && !line.startsWith('//') && !line.startsWith('*'); + }) + 1; + + const disallowedMethodFound = lineNumber !== 0; + + if (disallowedMethodFound) { + throw new ValidationError({ + message: `Can't use .${disallowedMethod}() here`, + description: "This is only available in 'Run Once for All Items' mode", + itemIndex: this.itemIndex, + lineNumber, + }); + } + } + + let executionResult: INodeExecutionData; + + try { + executionResult = await this.vm.run(script, __dirname); + } catch (error) { + // anticipate user expecting `item` to pre-exist as in Function Item node + if (error.message === 'item is not defined' && !/(let|const|var) item =/.test(script)) { + const quoted = error.message.replace('item', '`item`'); + error.message = (quoted as string) + '. Did you mean `$input.item.json`?'; + } + + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + throw new ExecutionError(error, this.itemIndex); + } + + if (executionResult === null) return; + + return this.validateRunCodeEachItem(executionResult); + } +} diff --git a/packages/nodes-base/nodes/Code/Pyodide.ts b/packages/nodes-base/nodes/Code/Pyodide.ts new file mode 100644 index 0000000000000..a2578efeaed8f --- /dev/null +++ b/packages/nodes-base/nodes/Code/Pyodide.ts @@ -0,0 +1,28 @@ +import type { PyodideInterface } from 'pyodide'; + +let pyodideInstance: PyodideInterface | undefined; + +export async function LoadPyodide(): Promise<PyodideInterface> { + if (pyodideInstance === undefined) { + // TODO: Find better way to suppress warnings + //@ts-ignore + globalThis.Blob = (await import('node:buffer')).Blob; + + // From: https://github.com/nodejs/node/issues/30810 + const { emitWarning } = process; + process.emitWarning = (warning, ...args) => { + if (args[0] === 'ExperimentalWarning') { + return; + } + if (args[0] && typeof args[0] === 'object' && args[0].type === 'ExperimentalWarning') { + return; + } + return emitWarning(warning, ...(args as string[])); + }; + + const { loadPyodide } = await import('pyodide'); + pyodideInstance = await loadPyodide(); + } + + return pyodideInstance; +} diff --git a/packages/nodes-base/nodes/Code/PythonSandbox.ts b/packages/nodes-base/nodes/Code/PythonSandbox.ts new file mode 100644 index 0000000000000..fccff17de9409 --- /dev/null +++ b/packages/nodes-base/nodes/Code/PythonSandbox.ts @@ -0,0 +1,119 @@ +import type { IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import type { PyProxyDict } from 'pyodide'; +import { LoadPyodide } from './Pyodide'; +import type { SandboxContext } from './Sandbox'; +import { Sandbox } from './Sandbox'; + +type PythonSandboxContext = { + [K in keyof SandboxContext as K extends `$${infer I}` ? `_${I}` : K]: SandboxContext[K]; +}; + +type PyodideError = Error & { type: string }; + +const envAccessBlocked = process.env.N8N_BLOCK_ENV_ACCESS_IN_NODE === 'true'; + +export class PythonSandbox extends Sandbox { + private readonly context: PythonSandboxContext; + + constructor( + context: SandboxContext, + private pythonCode: string, + private moduleImports: string[], + itemIndex: number | undefined, + helpers: IExecuteFunctions['helpers'], + ) { + super( + { + object: { + singular: 'dictionary', + plural: 'dictionaries', + }, + }, + itemIndex, + helpers, + ); + // Since python doesn't allow variable names starting with `$`, + // rename them to all to start with `_` instead + this.context = Object.keys(context).reduce((acc, key) => { + acc[key.startsWith('$') ? key.replace(/^\$/, '_') : key] = context[key]; + return acc; + }, {} as PythonSandboxContext); + } + + async runCodeAllItems() { + const executionResult = await this.runCodeInPython<INodeExecutionData[]>(); + return this.validateRunCodeAllItems(executionResult); + } + + async runCodeEachItem() { + const executionResult = await this.runCodeInPython<INodeExecutionData>(); + return this.validateRunCodeEachItem(executionResult); + } + + private async runCodeInPython<T>() { + // Below workaround from here: + // https://github.com/pyodide/pyodide/discussions/3537#discussioncomment-4864345 + const runCode = ` +from _pyodide_core import jsproxy_typedict +from js import Object +jsproxy_typedict[0] = type(Object.new().as_object_map()) + +if printOverwrite: + print = printOverwrite + +async def __main(): +${this.pythonCode + .split('\n') + .map((line) => ' ' + line) + .join('\n')} +await __main() +`; + const pyodide = await LoadPyodide(); + + const moduleImportsFiltered = this.moduleImports.filter( + (importModule) => !['asyncio', 'pyodide', 'math'].includes(importModule), + ); + + if (moduleImportsFiltered.length) { + await pyodide.loadPackage('micropip'); + const micropip = pyodide.pyimport('micropip'); + await Promise.all( + moduleImportsFiltered.map((importModule) => micropip.install(importModule)), + ); + } + + let executionResult; + try { + const dict = pyodide.globals.get('dict'); + const globalsDict: PyProxyDict = dict(); + for (const key of Object.keys(this.context)) { + if ((key === '_env' && envAccessBlocked) || key === '_node') continue; + const value = this.context[key]; + globalsDict.set(key, value); + } + + executionResult = await pyodide.runPythonAsync(runCode, { globals: globalsDict }); + globalsDict.destroy(); + } catch (error) { + throw this.getPrettyError(error as PyodideError); + } + + if (executionResult?.toJs) { + return executionResult.toJs({ + dict_converter: Object.fromEntries, + create_proxies: false, + }) as T; + } + + return executionResult as T; + } + + private getPrettyError(error: PyodideError): Error { + const errorTypeIndex = error.message.indexOf(error.type); + if (errorTypeIndex !== -1) { + return new Error(error.message.slice(errorTypeIndex)); + } + + return error; + } +} diff --git a/packages/nodes-base/nodes/Code/Sandbox.ts b/packages/nodes-base/nodes/Code/Sandbox.ts index 9fd9e9fd522df..541075262c0d6 100644 --- a/packages/nodes-base/nodes/Code/Sandbox.ts +++ b/packages/nodes-base/nodes/Code/Sandbox.ts @@ -1,70 +1,93 @@ -import { NodeVM } from 'vm2'; import { ValidationError } from './ValidationError'; -import { ExecutionError } from './ExecutionError'; -import { isObject, REQUIRED_N8N_ITEM_KEYS } from './utils'; - -import type { - IDataObject, - IExecuteFunctions, - INodeExecutionData, - IWorkflowDataProxyData, - WorkflowExecuteMode, -} from 'n8n-workflow'; - -interface SandboxContext extends IWorkflowDataProxyData { +import { isObject } from './utils'; + +import type { IExecuteFunctions, INodeExecutionData, IWorkflowDataProxyData } from 'n8n-workflow'; + +interface SandboxTextKeys { + object: { + singular: string; + plural: string; + }; +} + +export interface SandboxContext extends IWorkflowDataProxyData { $getNodeParameter: IExecuteFunctions['getNodeParameter']; $getWorkflowStaticData: IExecuteFunctions['getWorkflowStaticData']; helpers: IExecuteFunctions['helpers']; } -const { NODE_FUNCTION_ALLOW_BUILTIN: builtIn, NODE_FUNCTION_ALLOW_EXTERNAL: external } = - process.env; +export const REQUIRED_N8N_ITEM_KEYS = new Set(['json', 'binary', 'pairedItem']); + +export function getSandboxContext(this: IExecuteFunctions, index: number): SandboxContext { + return { + // from NodeExecuteFunctions + $getNodeParameter: this.getNodeParameter, + $getWorkflowStaticData: this.getWorkflowStaticData, + helpers: this.helpers, -export class Sandbox extends NodeVM { - private itemIndex: number | undefined = undefined; + // to bring in all $-prefixed vars and methods from WorkflowDataProxy + // $node, $items(), $parameter, $json, $env, etc. + ...this.getWorkflowDataProxy(index), + }; +} +export abstract class Sandbox { constructor( - context: SandboxContext, - private jsCode: string, - workflowMode: WorkflowExecuteMode, + private textKeys: SandboxTextKeys, + protected itemIndex: number | undefined, private helpers: IExecuteFunctions['helpers'], - ) { - super({ - console: workflowMode === 'manual' ? 'redirect' : 'inherit', - sandbox: context, - require: { - builtin: builtIn ? builtIn.split(',') : [], - external: external ? { modules: external.split(','), transitive: false } : false, - }, - }); - } + ) {} - async runCodeAllItems(): Promise<INodeExecutionData[]> { - const script = `module.exports = async function() {${this.jsCode}\n}()`; + abstract runCodeAllItems(): Promise<INodeExecutionData[]>; - let executionResult: INodeExecutionData | INodeExecutionData[]; + abstract runCodeEachItem(): Promise<INodeExecutionData | undefined>; - try { - executionResult = await this.run(script, __dirname); - } catch (error) { - // anticipate user expecting `items` to pre-exist as in Function Item node - if (error.message === 'items is not defined' && !/(let|const|var) items =/.test(script)) { - const quoted = error.message.replace('items', '`items`'); - error.message = (quoted as string) + '. Did you mean `$input.all()`?'; - } + validateRunCodeEachItem(executionResult: INodeExecutionData | undefined): INodeExecutionData { + if (typeof executionResult !== 'object') { + throw new ValidationError({ + message: `Code doesn't return ${this.getTextKey('object', { includeArticle: true })}`, + description: `Please return ${this.getTextKey('object', { + includeArticle: true, + })} representing the output item. ('${executionResult}' was returned instead.)`, + itemIndex: this.itemIndex, + }); + } - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - throw new ExecutionError(error); + if (Array.isArray(executionResult)) { + const firstSentence = + executionResult.length > 0 + ? `An array of ${typeof executionResult[0]}s was returned.` + : 'An empty array was returned.'; + throw new ValidationError({ + message: `Code doesn't return a single ${this.getTextKey('object')}`, + description: `${firstSentence} If you need to output multiple items, please use the 'Run Once for All Items' mode instead.`, + itemIndex: this.itemIndex, + }); } - if (executionResult === null) return []; + const [returnData] = this.helpers.normalizeItems([executionResult]); + + this.validateItem(returnData); + + // If at least one top-level key is a supported item key (`json`, `binary`, etc.), + // and another top-level key is unrecognized, then the user mis-added a property + // directly on the item, when they intended to add it on the `json` property + this.validateTopLevelKeys(returnData); - if (executionResult === undefined || typeof executionResult !== 'object') { + return returnData; + } + + validateRunCodeAllItems( + executionResult: INodeExecutionData | INodeExecutionData[] | undefined, + itemIndex?: number, + ): INodeExecutionData[] { + if (typeof executionResult !== 'object') { throw new ValidationError({ message: "Code doesn't return items properly", - description: - 'Please return an array of objects, one for each item you would like to output', - itemIndex: this.itemIndex, + description: `Please return an array of ${this.getTextKey('object', { + plural: true, + })}, one for each item you would like to output.`, + itemIndex, }); } @@ -77,109 +100,54 @@ export class Sandbox extends NodeVM { * item keys to be wrapped in `json` when normalizing items below. */ const mustHaveTopLevelN8nKey = executionResult.some((item) => - Object.keys(item as IDataObject).find((key) => REQUIRED_N8N_ITEM_KEYS.has(key)), + Object.keys(item).find((key) => REQUIRED_N8N_ITEM_KEYS.has(key)), ); - for (const item of executionResult) { - if (mustHaveTopLevelN8nKey) { + if (mustHaveTopLevelN8nKey) { + for (const item of executionResult) { this.validateTopLevelKeys(item); } } } const returnData = this.helpers.normalizeItems(executionResult); - returnData.forEach((item) => this.validateResult(item)); + returnData.forEach((item) => this.validateItem(item)); return returnData; } - async runCodeEachItem(itemIndex: number): Promise<INodeExecutionData | undefined> { - this.itemIndex = itemIndex; - const script = `module.exports = async function() {${this.jsCode}\n}()`; - - const match = this.jsCode.match(/\$input\.(?<disallowedMethod>first|last|all|itemMatching)/); - - if (match?.groups?.disallowedMethod) { - const { disallowedMethod } = match.groups; - - const lineNumber = - this.jsCode.split('\n').findIndex((line) => { - return line.includes(disallowedMethod) && !line.startsWith('//') && !line.startsWith('*'); - }) + 1; - - const disallowedMethodFound = lineNumber !== 0; - - if (disallowedMethodFound) { - throw new ValidationError({ - message: `Can't use .${disallowedMethod}() here`, - description: "This is only available in 'Run Once for All Items' mode", - itemIndex: this.itemIndex, - lineNumber, - }); - } - } - - let executionResult: INodeExecutionData; - - try { - executionResult = await this.run(script, __dirname); - } catch (error) { - // anticipate user expecting `item` to pre-exist as in Function Item node - if (error.message === 'item is not defined' && !/(let|const|var) item =/.test(script)) { - const quoted = error.message.replace('item', '`item`'); - error.message = (quoted as string) + '. Did you mean `$input.item.json`?'; - } - - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - throw new ExecutionError(error, this.itemIndex); - } - - if (executionResult === null) return; - - if (executionResult === undefined || typeof executionResult !== 'object') { - throw new ValidationError({ - message: "Code doesn't return an object", - description: `Please return an object representing the output item. ('${executionResult}' was returned instead.)`, - itemIndex: this.itemIndex, - }); + private getTextKey( + key: keyof SandboxTextKeys, + options?: { includeArticle?: boolean; plural?: boolean }, + ) { + const response = this.textKeys[key][options?.plural ? 'plural' : 'singular']; + if (!options?.includeArticle) { + return response; } - - if (Array.isArray(executionResult)) { - const firstSentence = - executionResult.length > 0 - ? `An array of ${typeof executionResult[0]}s was returned.` - : 'An empty array was returned.'; - - throw new ValidationError({ - message: "Code doesn't return a single object", - description: `${firstSentence} If you need to output multiple items, please use the 'Run Once for All Items' mode instead`, - itemIndex: this.itemIndex, - }); + if (['a', 'e', 'i', 'o', 'u'].some((value) => response.startsWith(value))) { + return `an ${response}`; } - - const [returnData] = this.helpers.normalizeItems([executionResult]); - this.validateResult(returnData); - - // If at least one top-level key is a supported item key (`json`, `binary`, etc.), - // and another top-level key is unrecognized, then the user mis-added a property - // directly on the item, when they intended to add it on the `json` property - this.validateTopLevelKeys(returnData); - - return returnData; + return `a ${response}`; } - private validateResult({ json, binary }: INodeExecutionData) { + private validateItem({ json, binary }: INodeExecutionData) { if (json === undefined || !isObject(json)) { throw new ValidationError({ - message: "A 'json' property isn't an object", - description: "In the returned data, every key named 'json' must point to an object", + message: `A 'json' property isn't ${this.getTextKey('object', { includeArticle: true })}`, + description: `In the returned data, every key named 'json' must point to ${this.getTextKey( + 'object', + { includeArticle: true }, + )}.`, itemIndex: this.itemIndex, }); } if (binary !== undefined && !isObject(binary)) { throw new ValidationError({ - message: "A 'binary' property isn't an object", - description: "In the returned data, every key named 'binaryā must point to an object.", + message: `A 'binary' property isn't ${this.getTextKey('object', { includeArticle: true })}`, + description: `In the returned data, every key named 'binaryā must point to ${this.getTextKey( + 'object', + { includeArticle: true }, + )}.`, itemIndex: this.itemIndex, }); } @@ -196,15 +164,3 @@ export class Sandbox extends NodeVM { }); } } - -export function getSandboxContext(this: IExecuteFunctions, index?: number): SandboxContext { - return { - // from NodeExecuteFunctions - $getNodeParameter: this.getNodeParameter, - $getWorkflowStaticData: this.getWorkflowStaticData, - helpers: this.helpers, - - // to bring in all $-prefixed vars and methods from WorkflowDataProxy - ...this.getWorkflowDataProxy(index ?? 0), - }; -} diff --git a/packages/nodes-base/nodes/Code/descriptions/JavascriptCodeDescription.ts b/packages/nodes-base/nodes/Code/descriptions/JavascriptCodeDescription.ts new file mode 100644 index 0000000000000..acfb833ce6b41 --- /dev/null +++ b/packages/nodes-base/nodes/Code/descriptions/JavascriptCodeDescription.ts @@ -0,0 +1,76 @@ +import type { INodeProperties } from 'n8n-workflow'; + +const commonDescription: INodeProperties = { + displayName: 'JavaScript', + name: 'jsCode', + type: 'string', + typeOptions: { + editor: 'codeNodeEditor', + editorLanguage: 'javaScript', + }, + default: '', + description: + 'JavaScript code to execute.<br><br>Tip: You can use luxon vars like <code>$today</code> for dates and <code>$jmespath</code> for querying JSON structures. <a href="https://docs.n8n.io/nodes/n8n-nodes-base.function">Learn more</a>.', + noDataExpression: true, +}; + +const v1Properties: INodeProperties[] = [ + { + ...commonDescription, + displayOptions: { + show: { + '@version': [1], + mode: ['runOnceForAllItems'], + }, + }, + }, + { + ...commonDescription, + displayOptions: { + show: { + '@version': [1], + mode: ['runOnceForEachItem'], + }, + }, + }, +]; + +const v2Properties: INodeProperties[] = [ + { + ...commonDescription, + displayOptions: { + show: { + '@version': [2], + language: ['javaScript'], + mode: ['runOnceForAllItems'], + }, + }, + }, + { + ...commonDescription, + displayOptions: { + show: { + '@version': [2], + language: ['javaScript'], + mode: ['runOnceForEachItem'], + }, + }, + }, +]; + +export const javascriptCodeDescription: INodeProperties[] = [ + ...v1Properties, + ...v2Properties, + { + displayName: + 'Type <code>$</code> for a list of <a target="_blank" href="https://docs.n8n.io/code-examples/methods-variables-reference/">special vars/methods</a>. Debug by using <code>console.log()</code> statements and viewing their output in the browser console.', + name: 'notice', + type: 'notice', + displayOptions: { + show: { + language: ['javaScript'], + }, + }, + default: '', + }, +]; diff --git a/packages/nodes-base/nodes/Code/descriptions/PythonCodeDescription.ts b/packages/nodes-base/nodes/Code/descriptions/PythonCodeDescription.ts new file mode 100644 index 0000000000000..f60066a5e37c5 --- /dev/null +++ b/packages/nodes-base/nodes/Code/descriptions/PythonCodeDescription.ts @@ -0,0 +1,63 @@ +import type { INodeProperties } from 'n8n-workflow'; + +const commonDescription: INodeProperties = { + displayName: 'Python', + name: 'pythonCode', + type: 'string', + typeOptions: { + editor: 'codeNodeEditor', + editorLanguage: 'python', + }, + default: '', + description: + 'Python code to execute.<br><br>Tip: You can use luxon vars like <code>_today</code> for dates and <code>$_mespath</code> for querying JSON structures. <a href="https://docs.n8n.io/nodes/n8n-nodes-base.function">Learn more</a>.', + noDataExpression: true, +}; + +export const pythonCodeDescription: INodeProperties[] = [ + { + ...commonDescription, + displayOptions: { + show: { + language: ['python'], + mode: ['runOnceForAllItems'], + }, + }, + }, + { + ...commonDescription, + displayOptions: { + show: { + language: ['python'], + mode: ['runOnceForEachItem'], + }, + }, + }, + { + displayName: + 'Debug by using <code>print()</code> statements and viewing their output in the browser console.', + name: 'notice', + type: 'notice', + displayOptions: { + show: { + language: ['python'], + }, + }, + default: '', + }, + { + displayName: 'Python Modules', + name: 'modules', + displayOptions: { + show: { + language: ['python'], + }, + }, + type: 'string', + default: '', + placeholder: 'opencv-python', + description: + 'Comma-separated list of Python modules to load. They have to be installed to be able to be loaded and imported.', + noDataExpression: true, + }, +]; diff --git a/packages/nodes-base/nodes/Code/test/Code.node.test.ts b/packages/nodes-base/nodes/Code/test/Code.node.test.ts index f21b3c42e6e51..15a42f5e8232f 100644 --- a/packages/nodes-base/nodes/Code/test/Code.node.test.ts +++ b/packages/nodes-base/nodes/Code/test/Code.node.test.ts @@ -1,15 +1,25 @@ import { anyNumber, mock } from 'jest-mock-extended'; +import { NodeVM } from 'vm2'; import type { IExecuteFunctions, IWorkflowDataProxyData } from 'n8n-workflow'; import { NodeHelpers } from 'n8n-workflow'; import { normalizeItems } from 'n8n-core'; -import { testWorkflows, getWorkflowFilenames } from '../../../test/nodes/Helpers'; +import { + testWorkflows, + getWorkflowFilenames, + initBinaryDataManager, +} from '../../../test/nodes/Helpers'; import { Code } from '../Code.node'; -import { Sandbox } from '../Sandbox'; import { ValidationError } from '../ValidationError'; -const workflows = getWorkflowFilenames(__dirname); +describe('Test Code Node', () => { + const workflows = getWorkflowFilenames(__dirname); -describe('Test Code Node', () => testWorkflows(workflows)); + beforeAll(async () => { + await initBinaryDataManager(); + }); + + testWorkflows(workflows); +}); describe('Code Node unit test', () => { const node = new Code(); @@ -48,7 +58,7 @@ describe('Code Node unit test', () => { Object.entries(tests).forEach(([title, [input, expected]]) => test(title, async () => { - jest.spyOn(Sandbox.prototype, 'run').mockResolvedValueOnce(input); + jest.spyOn(NodeVM.prototype, 'run').mockResolvedValueOnce(input); const output = await node.execute.call(thisArg); expect(output).toEqual([expected]); @@ -68,14 +78,14 @@ describe('Code Node unit test', () => { Object.entries(tests).forEach(([title, returnData]) => test(`return error if \`.json\` is ${title}`, async () => { - jest.spyOn(Sandbox.prototype, 'run').mockResolvedValueOnce([{ json: returnData }]); + jest.spyOn(NodeVM.prototype, 'run').mockResolvedValueOnce([{ json: returnData }]); try { await node.execute.call(thisArg); throw new Error("Validation error wasn't thrown"); } catch (error) { expect(error).toBeInstanceOf(ValidationError); - expect(error.message).toEqual("A 'json' property isn't an object"); + expect(error.message).toEqual("A 'json' property isn't an object [item 0]"); } }), ); @@ -100,7 +110,7 @@ describe('Code Node unit test', () => { Object.entries(tests).forEach(([title, [input, expected]]) => test(title, async () => { - jest.spyOn(Sandbox.prototype, 'run').mockResolvedValueOnce(input); + jest.spyOn(NodeVM.prototype, 'run').mockResolvedValueOnce(input); const output = await node.execute.call(thisArg); expect(output).toEqual([[{ json: expected?.json, pairedItem: { item: 0 } }]]); @@ -120,7 +130,7 @@ describe('Code Node unit test', () => { Object.entries(tests).forEach(([title, returnData]) => test(`return error if \`.json\` is ${title}`, async () => { - jest.spyOn(Sandbox.prototype, 'run').mockResolvedValueOnce({ json: returnData }); + jest.spyOn(NodeVM.prototype, 'run').mockResolvedValueOnce({ json: returnData }); try { await node.execute.call(thisArg); diff --git a/packages/nodes-base/nodes/Code/utils.ts b/packages/nodes-base/nodes/Code/utils.ts index 891768f6ffd89..28fa00f829210 100644 --- a/packages/nodes-base/nodes/Code/utils.ts +++ b/packages/nodes-base/nodes/Code/utils.ts @@ -36,7 +36,3 @@ export function standardizeOutput(output: IDataObject) { standardizeOutputRecursive(output); return output; } - -export type CodeNodeMode = 'runOnceForAllItems' | 'runOnceForEachItem'; - -export const REQUIRED_N8N_ITEM_KEYS = new Set(['json', 'binary', 'pairedItem']); diff --git a/packages/nodes-base/nodes/CompareDatasets/CompareDatasets.node.ts b/packages/nodes-base/nodes/CompareDatasets/CompareDatasets.node.ts index 96aff7586f1d1..69cb9c633bc4d 100644 --- a/packages/nodes-base/nodes/CompareDatasets/CompareDatasets.node.ts +++ b/packages/nodes-base/nodes/CompareDatasets/CompareDatasets.node.ts @@ -5,7 +5,12 @@ import type { INodeType, INodeTypeDescription, } from 'n8n-workflow'; -import { checkInput, checkMatchFieldsInput, findMatches } from './GenericFunctions'; +import { + checkInput, + checkInputAndThrowError, + checkMatchFieldsInput, + findMatches, +} from './GenericFunctions'; export class CompareDatasets implements INodeType { description: INodeTypeDescription = { @@ -13,7 +18,7 @@ export class CompareDatasets implements INodeType { name: 'compareDatasets', icon: 'file:compare.svg', group: ['transform'], - version: [1, 2, 2.1], + version: [1, 2, 2.1, 2.2], description: 'Compare two inputs for changes', defaults: { name: 'Compare Datasets' }, // eslint-disable-next-line n8n-nodes-base/node-class-description-inputs-wrong-regular-node @@ -254,19 +259,26 @@ export class CompareDatasets implements INodeType { options.fuzzyCompare = this.getNodeParameter('fuzzyCompare', 0, false) as boolean; } - const input1 = checkInput( - this.getInputData(0), - matchFields.map((pair) => pair.field1), - (options.disableDotNotation as boolean) || false, - 'Input A', - ); + let input1 = this.getInputData(0); + let input2 = this.getInputData(1); + if (options.nodeVersion < 2.2) { + input1 = checkInputAndThrowError( + input1, + matchFields.map((pair) => pair.field1), + (options.disableDotNotation as boolean) || false, + 'Input A', + ); - const input2 = checkInput( - this.getInputData(1), - matchFields.map((pair) => pair.field2), - (options.disableDotNotation as boolean) || false, - 'Input B', - ); + input2 = checkInputAndThrowError( + input2, + matchFields.map((pair) => pair.field2), + (options.disableDotNotation as boolean) || false, + 'Input B', + ); + } else { + input1 = checkInput(input1); + input2 = checkInput(input2); + } const resolve = this.getNodeParameter('resolve', 0, '') as string; options.resolve = resolve; diff --git a/packages/nodes-base/nodes/CompareDatasets/GenericFunctions.ts b/packages/nodes-base/nodes/CompareDatasets/GenericFunctions.ts index 144f52725f19d..cce9f2230737f 100644 --- a/packages/nodes-base/nodes/CompareDatasets/GenericFunctions.ts +++ b/packages/nodes-base/nodes/CompareDatasets/GenericFunctions.ts @@ -414,7 +414,15 @@ export function checkMatchFieldsInput(data: IDataObject[]) { return data as PairToMatch[]; } -export function checkInput( +export function checkInput(input: INodeExecutionData[]) { + if (!input) return []; + if (input.some((item) => isEmpty(item.json))) { + input = input.filter((item) => !isEmpty(item.json)); + } + return input; +} + +export function checkInputAndThrowError( input: INodeExecutionData[], fields: string[], disableDotNotation: boolean, diff --git a/packages/nodes-base/nodes/CompareDatasets/test/node/workflow.update_2_2.json b/packages/nodes-base/nodes/CompareDatasets/test/node/workflow.update_2_2.json new file mode 100644 index 0000000000000..708614c644e90 --- /dev/null +++ b/packages/nodes-base/nodes/CompareDatasets/test/node/workflow.update_2_2.json @@ -0,0 +1,212 @@ +{ + "name": "do not error on missing keys", + "nodes": [ + { + "parameters": {}, + "id": "c7c0cf66-790a-4da7-81c8-ba9e4bbcec9a", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [720, 300] + }, + { + "parameters": { + "jsCode": "return [\n {id: 1},\n {id: 2},\n {id: 3},\n {id: 4},\n];" + }, + "id": "cec18624-ced0-4de1-8987-d4b184b136b9", + "name": "Code", + "type": "n8n-nodes-base.code", + "typeVersion": 1, + "position": [1020, 200] + }, + { + "parameters": { + "jsCode": "return [\n];" + }, + "id": "754d549c-82ce-4625-ba2b-6f8edcbf715e", + "name": "Code1", + "type": "n8n-nodes-base.code", + "typeVersion": 1, + "position": [1020, 400] + }, + { + "parameters": { + "mergeByFields": { + "values": [ + { + "field1": "idd", + "field2": "idd" + } + ] + }, + "options": {} + }, + "id": "1b3660a2-a490-4524-a8ac-cd42fa2b340b", + "name": "Compare Datasets", + "type": "n8n-nodes-base.compareDatasets", + "typeVersion": 2.2, + "position": [1300, 420] + }, + { + "parameters": { + "mergeByFields": { + "values": [ + { + "field1": "idd", + "field2": "idd" + } + ] + }, + "options": {} + }, + "id": "17e9b83b-828c-41a0-a7ba-e33cc66d37ad", + "name": "Any skipped", + "type": "n8n-nodes-base.compareDatasets", + "typeVersion": 2.1, + "position": [1300, 180], + "continueOnFail": true + }, + { + "parameters": {}, + "id": "551ed574-607a-4d98-9b06-350df92c805e", + "name": "No Operation, do nothing", + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [1540, 200] + }, + { + "parameters": {}, + "id": "00e75760-d88e-413c-b276-c759db72411f", + "name": "No Operation, do nothing1", + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [1540, 360] + } + ], + "pinData": { + "No Operation, do nothing": [ + { + "json": { + "id": 1 + } + }, + { + "json": { + "id": 2 + } + }, + { + "json": { + "id": 3 + } + }, + { + "json": { + "id": 4 + } + } + ], + "No Operation, do nothing1": [ + { + "json": { + "id": 1 + } + }, + { + "json": { + "id": 2 + } + }, + { + "json": { + "id": 3 + } + }, + { + "json": { + "id": 4 + } + } + ] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Code", + "type": "main", + "index": 0 + }, + { + "node": "Code1", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code": { + "main": [ + [ + { + "node": "Compare Datasets", + "type": "main", + "index": 0 + }, + { + "node": "Any skipped", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code1": { + "main": [ + [ + { + "node": "Compare Datasets", + "type": "main", + "index": 1 + }, + { + "node": "Any skipped", + "type": "main", + "index": 1 + } + ] + ] + }, + "Any skipped": { + "main": [ + [ + { + "node": "No Operation, do nothing", + "type": "main", + "index": 0 + } + ] + ] + }, + "Compare Datasets": { + "main": [ + [ + { + "node": "No Operation, do nothing1", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": {}, + "versionId": "11347bae-96af-4b28-80d7-fe272a165790", + "id": "8", + "meta": { + "instanceId": "6ebec4953fe56f1c009e7c3b107578b375137523af057073c0b5da17350651bd" + }, + "tags": [] +} diff --git a/packages/nodes-base/nodes/CompareDatasets/test/utils/utils.test.ts b/packages/nodes-base/nodes/CompareDatasets/test/utils/utils.test.ts new file mode 100644 index 0000000000000..6af3b2f294b5b --- /dev/null +++ b/packages/nodes-base/nodes/CompareDatasets/test/utils/utils.test.ts @@ -0,0 +1,32 @@ +import type { INodeExecutionData } from 'n8n-workflow'; +import { checkInput } from '../../GenericFunctions'; + +describe('Test Compare Datasets Node utils', () => { + it('test checkInput', () => { + const input1 = [ + { json: {} }, + { + json: { + name: 'Test', + age: 30, + }, + }, + { + json: { + name: 'Test2', + age: 30, + }, + }, + ]; + + expect(checkInput(input1).length).toEqual(2); + + const input2: INodeExecutionData[] = [{ json: {} }]; + + expect(checkInput(input2).length).toEqual(0); + + const input3 = undefined; + + expect(checkInput(input3 as unknown as INodeExecutionData[]).length).toEqual(0); + }); +}); diff --git a/packages/nodes-base/nodes/Compression/Compression.node.ts b/packages/nodes-base/nodes/Compression/Compression.node.ts index e92d6c97a7142..bbf6666c509fa 100644 --- a/packages/nodes-base/nodes/Compression/Compression.node.ts +++ b/packages/nodes-base/nodes/Compression/Compression.node.ts @@ -52,7 +52,7 @@ export class Compression implements INodeType { group: ['transform'], subtitle: '={{$parameter["operation"]}}', version: 1, - description: 'Compress and uncompress files', + description: 'Compress and decompress files', defaults: { name: 'Compression', color: '#408000', @@ -197,7 +197,7 @@ export class Compression implements INodeType { const binaryData = this.helpers.assertBinaryData(i, binaryPropertyName); const binaryDataBuffer = await this.helpers.getBinaryDataBuffer(i, binaryPropertyName); - if (binaryData.fileExtension === 'zip') { + if (binaryData.fileExtension?.toLowerCase() === 'zip') { const files = await unzip(binaryDataBuffer); for (const key of Object.keys(files)) { @@ -213,7 +213,7 @@ export class Compression implements INodeType { binaryObject[`${outputPrefix}${zipIndex++}`] = data; } - } else if (binaryData.fileExtension === 'gz') { + } else if (binaryData.fileExtension?.toLowerCase() === 'gz') { const file = await gunzip(binaryDataBuffer); const fileName = binaryData.fileName?.split('.')[0]; diff --git a/packages/nodes-base/nodes/Cortex/AnalyzerInterface.ts b/packages/nodes-base/nodes/Cortex/AnalyzerInterface.ts index 09e3a6ff57fc2..5f9f8fe8274e1 100644 --- a/packages/nodes-base/nodes/Cortex/AnalyzerInterface.ts +++ b/packages/nodes-base/nodes/Cortex/AnalyzerInterface.ts @@ -1,6 +1,6 @@ import type { IDataObject } from 'n8n-workflow'; -export enum JobStatus { +export const enum JobStatus { WAITING = 'Waiting', INPROGRESS = 'InProgress', SUCCESS = 'Success', @@ -8,14 +8,14 @@ export enum JobStatus { DELETED = 'Deleted', } -export enum TLP { +export const enum TLP { white, green, amber, red, } -export enum ObservableDataType { +export const enum ObservableDataType { 'domain' = 'domain', 'file' = 'file', 'filename' = 'filename', diff --git a/packages/nodes-base/nodes/CrateDb/CrateDb.node.ts b/packages/nodes-base/nodes/CrateDb/CrateDb.node.ts index a11ff18670bcd..6bc61ee8ac119 100644 --- a/packages/nodes-base/nodes/CrateDb/CrateDb.node.ts +++ b/packages/nodes-base/nodes/CrateDb/CrateDb.node.ts @@ -73,6 +73,10 @@ export class CrateDb implements INodeType { displayName: 'Query', name: 'query', type: 'string', + typeOptions: { + editor: 'sqlEditor', + sqlDialect: 'postgres', + }, displayOptions: { show: { operation: ['executeQuery'], diff --git a/packages/nodes-base/nodes/DateTime/DateTime.node.ts b/packages/nodes-base/nodes/DateTime/DateTime.node.ts index dd5111b4851e2..7dcbb06655758 100644 --- a/packages/nodes-base/nodes/DateTime/DateTime.node.ts +++ b/packages/nodes-base/nodes/DateTime/DateTime.node.ts @@ -415,7 +415,10 @@ export class DateTime implements INodeType { item = items[i]; if (action === 'format') { - let currentDate = this.getNodeParameter('value', i) as string; + let currentDate: string | number | LuxonDateTime = this.getNodeParameter( + 'value', + i, + ) as string; const dataPropertyName = this.getNodeParameter('dataPropertyName', i); const toFormat = this.getNodeParameter('toFormat', i) as string; const options = this.getNodeParameter('options', i); @@ -425,13 +428,20 @@ export class DateTime implements INodeType { currentDate = (currentDate as unknown as LuxonDateTime).toISO(); } + // Check if the input is a number + if (!Number.isNaN(Number(currentDate))) { + //input is a number, convert to number in case it is a string + currentDate = Number(currentDate); + // check if the number is a timestamp in float format and convert to integer + if (!Number.isInteger(currentDate)) { + currentDate = currentDate * 1000; + } + } + if (currentDate === undefined) { continue; } - if ( - options.fromFormat === undefined && - !moment(currentDate as string | number).isValid() - ) { + if (options.fromFormat === undefined && !moment(currentDate).isValid()) { throw new NodeOperationError( this.getNode(), 'The date input format could not be recognized. Please set the "From Format" field', @@ -439,14 +449,20 @@ export class DateTime implements INodeType { ); } - if (Number.isInteger(currentDate as unknown as number)) { - newDate = moment.unix(currentDate as unknown as number); + if (Number.isInteger(currentDate)) { + const timestampLengthInMilliseconds1990 = 12; + // check if the number is a timestamp in seconds or milliseconds and create a moment object accordingly + if (currentDate.toString().length < timestampLengthInMilliseconds1990) { + newDate = moment.unix(currentDate as number); + } else { + newDate = moment(currentDate); + } } else { if (options.fromTimezone || options.toTimezone) { const fromTimezone = options.fromTimezone || workflowTimezone; if (options.fromFormat) { newDate = moment.tz( - currentDate, + currentDate as string, options.fromFormat as string, fromTimezone as string, ); diff --git a/packages/nodes-base/nodes/DateTime/test/node/DateTimeWorklfow.test.json b/packages/nodes-base/nodes/DateTime/test/node/DateTimeWorklfow.test.json index 124fc5b1d30b4..9d1d31723f316 100644 --- a/packages/nodes-base/nodes/DateTime/test/node/DateTimeWorklfow.test.json +++ b/packages/nodes-base/nodes/DateTime/test/node/DateTimeWorklfow.test.json @@ -1,331 +1,542 @@ { - "name": "My workflow", - "nodes": [ - { - "parameters": {}, - "id": "bd40f2b1-090e-4cf0-8342-0ebcca1089b4", - "name": "When clicking \"Execute Workflow\"", - "type": "n8n-nodes-base.manualTrigger", - "typeVersion": 1, - "position": [ - 40, - -320 - ] - }, - { - "parameters": { - "action": "calculate", - "value": "08/01/2022", - "duration": 10, - "options": {} - }, - "id": "cadf04c8-5900-414e-bf61-46adbf3687c2", - "name": "Calculate Date: Add Day", - "type": "n8n-nodes-base.dateTime", - "typeVersion": 1, - "position": [ - 520, - -400 - ] - }, - { - "parameters": { - "action": "calculate", - "value": "08/01/2022", - "duration": 2, - "timeUnit": "quarters", - "options": {} - }, - "id": "77939e5a-db02-4c85-bf31-b61a017f4f64", - "name": "Calculate Date: Substract Quarter", - "type": "n8n-nodes-base.dateTime", - "typeVersion": 1, - "position": [ - 520, - -40 - ] - }, - { - "parameters": { - "action": "calculate", - "value": "08/01/2022", - "operation": "subtract", - "duration": 2, - "options": {} - }, - "id": "6426aa5b-96ad-4516-a346-b9d0ee68b7bd", - "name": "Calculate Date: Substract Day", - "type": "n8n-nodes-base.dateTime", - "typeVersion": 1, - "position": [ - 520, - -220 - ] - }, - { - "parameters": { - "action": "calculate", - "value": "08/01/2022", - "duration": 2, - "timeUnit": "months", - "options": {} - }, - "id": "de76ce11-7865-4b15-9829-5ed7d6a287eb", - "name": "Calculate Date: Add months", - "type": "n8n-nodes-base.dateTime", - "typeVersion": 1, - "position": [ - 520, - 140 - ] - }, - { - "parameters": { - "action": "calculate", - "value": "08/01/2022", - "operation": "subtract", - "duration": 2, - "timeUnit": "months", - "options": {} - }, - "id": "ccbe4625-e470-45f8-b494-06ee1ecfb1a5", - "name": "Calculate Date: Substract Months", - "type": "n8n-nodes-base.dateTime", - "typeVersion": 1, - "position": [ - 520, - 300 - ] - }, - { - "parameters": { - "value": "08/01/2022", - "toFormat": "YYYY/MM/DD", - "options": {} - }, - "id": "aed1eed3-db4e-431b-be04-488b47880665", - "name": "Calculate Date: Format YYYY/MM/DD", - "type": "n8n-nodes-base.dateTime", - "typeVersion": 1, - "position": [ - 520, - 480 - ] - }, - { - "parameters": { - "value": "08/01/2022", - "toFormat": "YYYY/MM/DD", - "options": { - "fromTimezone": "US/Hawaii" - } - }, - "id": "3bc2aa26-bc98-40c0-be8a-75bc0d58601c", - "name": "Calculate Date: Format YYYY/MM/DD with Timezone", - "type": "n8n-nodes-base.dateTime", - "typeVersion": 1, - "position": [ - 520, - 660 - ] - }, - { - "parameters": { - "value": "08/01/2022", - "toFormat": "YYYY/MM/DD", - "options": { - "fromTimezone": "US/Hawaii", - "toTimezone": "US/Pacific" - } - }, - "id": "e2defb6b-e465-4d5f-9d65-d66ab91345f5", - "name": "Calculate Date: Format YYYY/MM/DD with Timezone1", - "type": "n8n-nodes-base.dateTime", - "typeVersion": 1, - "position": [ - 520, - 840 - ] - }, - { - "parameters": { - "value": "08/01/2022", - "toFormat": "x", - "options": { - "fromTimezone": "US/Hawaii", - "toTimezone": "US/Pacific" - } - }, - "id": "cdf19ab4-aa0c-4cad-8264-bd4356928baf", - "name": "Calculate Date: Format YYYY/MM/DD with Timezone Unix", - "type": "n8n-nodes-base.dateTime", - "typeVersion": 1, - "position": [ - 40, - 200 - ] - }, - { - "parameters": { - "value": "2022-01-12", - "toFormat": "MM-DD-YYYY", - "options": { - "fromFormat": "YYYY-DD-MM", - "fromTimezone": "US/Hawaii", - "toTimezone": "US/Pacific" - } - }, - "id": "3383f04e-f13d-438b-b1c4-1bc09332401c", - "name": "Calculate Date: Format YYYY/MM/DD with Timezone with special zone", - "type": "n8n-nodes-base.dateTime", - "typeVersion": 1, - "position": [ - 40, - 380 - ] - } - ], - "pinData": { - "Calculate Date: Format YYYY/MM/DD with Timezone Unix": [ - { - "json": { - "data": "1659312000000" - } - } - ], - "Calculate Date: Format YYYY/MM/DD with Timezone with special zone": [ - { - "json": { - "data": "12-01-2022" - } - } - ], - "Calculate Date: Add Day": [ - { - "json": { - "data": "2022-08-11T00:00:00.000Z" - } - } - ], - "Calculate Date: Substract Day": [ - { - "json": { - "data": "2022-07-30T00:00:00.000Z" - } - } - ], - "Calculate Date: Substract Quarter": [ - { - "json": { - "data": "2023-02-01T00:00:00.000Z" - } - } - ], - "Calculate Date: Add months": [ - { - "json": { - "data": "2022-10-01T00:00:00.000Z" - } - } - ], - "Calculate Date: Substract Months": [ - { - "json": { - "data": "2022-06-01T00:00:00.000Z" - } - } - ], - "Calculate Date: Format YYYY/MM/DD": [ - { - "json": { - "data": "2022/08/01" - } - } - ], - "Calculate Date: Format YYYY/MM/DD with Timezone": [ - { - "json": { - "data": "2022/07/31" - } - } - ], - "Calculate Date: Format YYYY/MM/DD with Timezone1": [ - { - "json": { - "data": "2022/07/31" - } - } - ] - }, - "connections": { - "When clicking \"Execute Workflow\"": { - "main": [ - [ - { - "node": "Calculate Date: Add Day", - "type": "main", - "index": 0 - }, - { - "node": "Calculate Date: Substract Day", - "type": "main", - "index": 0 - }, - { - "node": "Calculate Date: Substract Quarter", - "type": "main", - "index": 0 - }, - { - "node": "Calculate Date: Add months", - "type": "main", - "index": 0 - }, - { - "node": "Calculate Date: Substract Months", - "type": "main", - "index": 0 - }, - { - "node": "Calculate Date: Format YYYY/MM/DD", - "type": "main", - "index": 0 - }, - { - "node": "Calculate Date: Format YYYY/MM/DD with Timezone", - "type": "main", - "index": 0 - }, - { - "node": "Calculate Date: Format YYYY/MM/DD with Timezone1", - "type": "main", - "index": 0 - }, - { - "node": "Calculate Date: Format YYYY/MM/DD with Timezone Unix", - "type": "main", - "index": 0 - }, - { - "node": "Calculate Date: Format YYYY/MM/DD with Timezone with special zone", - "type": "main", - "index": 0 - } - ] - ] - } - }, - "active": false, - "settings": { - "saveManualExecutions": false, - "callerPolicy": "workflowsFromSameOwner", - "timezone": "America/New_York", - "saveExecutionProgress": "DEFAULT" - }, - "versionId": "2737913c-dd04-438a-ba00-b1ee5f734038", - "id": "230", - "meta": { - "instanceId": "27cc9b56542ad45b38725555722c50a1c3fee1670bbb67980558314ee08517c4" - }, - "tags": [] -} \ No newline at end of file + "name": "My workflow 10", + "nodes": [ + { + "parameters": {}, + "id": "0b90cdc3-807a-4849-b2e2-a017eb77df17", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [20, 1820] + }, + { + "parameters": { + "action": "calculate", + "value": "08/01/2022", + "duration": 10, + "options": {} + }, + "id": "4a7807ca-e44c-46b4-9d2c-3424b1883c61", + "name": "Calculate Date: Add Day", + "type": "n8n-nodes-base.dateTime", + "typeVersion": 1, + "position": [500, 1740] + }, + { + "parameters": { + "action": "calculate", + "value": "08/01/2022", + "duration": 2, + "timeUnit": "quarters", + "options": {} + }, + "id": "04d26b36-9a31-4fc8-bbf4-4b6d8b9304bb", + "name": "Calculate Date: Substract Quarter", + "type": "n8n-nodes-base.dateTime", + "typeVersion": 1, + "position": [500, 2100] + }, + { + "parameters": { + "action": "calculate", + "value": "08/01/2022", + "operation": "subtract", + "duration": 2, + "options": {} + }, + "id": "617145da-26c5-491b-bc58-a10fbb5a1825", + "name": "Calculate Date: Substract Day", + "type": "n8n-nodes-base.dateTime", + "typeVersion": 1, + "position": [500, 1920] + }, + { + "parameters": { + "action": "calculate", + "value": "08/01/2022", + "duration": 2, + "timeUnit": "months", + "options": {} + }, + "id": "c7b4967f-3ae4-452b-8d54-aa1623194f2b", + "name": "Calculate Date: Add months", + "type": "n8n-nodes-base.dateTime", + "typeVersion": 1, + "position": [500, 2280] + }, + { + "parameters": { + "action": "calculate", + "value": "08/01/2022", + "operation": "subtract", + "duration": 2, + "timeUnit": "months", + "options": {} + }, + "id": "a22a536c-bf23-4ff0-a3ad-b3761dfb3add", + "name": "Calculate Date: Substract Months", + "type": "n8n-nodes-base.dateTime", + "typeVersion": 1, + "position": [500, 2440] + }, + { + "parameters": { + "value": "08/01/2022", + "toFormat": "YYYY/MM/DD", + "options": {} + }, + "id": "3ecc8ea0-9605-4017-b5e0-a9ddde4c1a4a", + "name": "Calculate Date: Format YYYY/MM/DD", + "type": "n8n-nodes-base.dateTime", + "typeVersion": 1, + "position": [500, 2620] + }, + { + "parameters": { + "value": "08/01/2022", + "toFormat": "YYYY/MM/DD", + "options": { + "fromTimezone": "US/Hawaii" + } + }, + "id": "c99c7124-c388-4c97-8e6e-8158ba83bddd", + "name": "Calculate Date: Format YYYY/MM/DD with Timezone", + "type": "n8n-nodes-base.dateTime", + "typeVersion": 1, + "position": [500, 2800] + }, + { + "parameters": { + "value": "08/01/2022", + "toFormat": "YYYY/MM/DD", + "options": { + "fromTimezone": "US/Hawaii", + "toTimezone": "US/Pacific" + } + }, + "id": "40abfde6-66f2-44a9-a16f-3d981178da84", + "name": "Calculate Date: Format YYYY/MM/DD with Timezone1", + "type": "n8n-nodes-base.dateTime", + "typeVersion": 1, + "position": [500, 2980] + }, + { + "parameters": { + "value": "08/01/2022", + "toFormat": "x", + "options": { + "fromTimezone": "US/Hawaii", + "toTimezone": "US/Pacific" + } + }, + "id": "20ea9e76-d656-406f-8e3d-a51712c2657d", + "name": "Calculate Date: Format YYYY/MM/DD with Timezone Unix", + "type": "n8n-nodes-base.dateTime", + "typeVersion": 1, + "position": [20, 2340] + }, + { + "parameters": { + "value": "2022-01-12", + "toFormat": "MM-DD-YYYY", + "options": { + "fromFormat": "YYYY-DD-MM", + "fromTimezone": "US/Hawaii", + "toTimezone": "US/Pacific" + } + }, + "id": "53ddcd66-3cb5-47bb-bd59-64a70fc599b7", + "name": "Calculate Date: Format YYYY/MM/DD with Timezone with special zone", + "type": "n8n-nodes-base.dateTime", + "typeVersion": 1, + "position": [20, 2520] + }, + { + "parameters": { + "functionCode": "return [\n {\n \"Id\": \"18162\",\n \"Start_time\": 1663801200000,\n \"End_time\": 1663804800000,\n \"Room\": \" The Stadium\"\n },\n {\n \"Id\": \"18173\",\n \"Start_time\": 1663797600000,\n \"End_time\": 1663801200000,\n \"Room\": \" The Stadium\"\n },\n {\n \"Id\": \"18178\",\n \"Start_time\": 1663804800000,\n \"End_time\": 1663808400000,\n \"Room\": \" The Stadium\"\n },\n {\n \"Id\": \"18182\",\n \"Start_time\": 1663801200000,\n \"End_time\": 1663804800000,\n \"Room\": \" The Garden\"\n }\n]" + }, + "id": "0933e36d-43f4-46b5-8c3f-8d397d93a24c", + "name": "Mock Data", + "type": "n8n-nodes-base.function", + "typeVersion": 1, + "position": [-140, 1200] + }, + { + "parameters": { + "value": "1663801200000", + "options": { + "fromFormat": "x", + "toTimezone": "UTC" + } + }, + "id": "35eae914-b744-4a0b-a9ac-eab8bd685f02", + "name": "Fixed Value", + "type": "n8n-nodes-base.dateTime", + "typeVersion": 1, + "position": [160, 1080] + }, + { + "parameters": { + "value": "={{parseInt(1663801200000)}}", + "options": { + "fromFormat": "x", + "toTimezone": "UTC" + } + }, + "id": "e7d16772-843d-43da-a411-ee46253619e8", + "name": "parseInt on fixed", + "type": "n8n-nodes-base.dateTime", + "typeVersion": 1, + "position": [160, 1580] + }, + { + "parameters": { + "value": "={{ $json[\"Start_time\"] }}", + "options": { + "fromFormat": "x", + "toTimezone": "UTC" + } + }, + "id": "daf8a9da-7cab-4bb6-91fd-9a7315ecc61b", + "name": "Without toString()", + "type": "n8n-nodes-base.dateTime", + "typeVersion": 1, + "position": [160, 1400] + }, + { + "parameters": { + "value": "={{ $json[\"Start_time\"].toString() }}", + "options": { + "fromFormat": "x", + "toTimezone": "UTC" + } + }, + "id": "bc536546-4968-4b88-acaa-9a02bc013246", + "name": "With toString", + "type": "n8n-nodes-base.dateTime", + "typeVersion": 1, + "position": [160, 880] + } + ], + "pinData": { + "Calculate Date: Format YYYY/MM/DD with Timezone Unix": [ + { + "json": { + "data": "1659312000000" + } + } + ], + "Calculate Date: Format YYYY/MM/DD with Timezone with special zone": [ + { + "json": { + "data": "12-01-2022" + } + } + ], + "Calculate Date: Add Day": [ + { + "json": { + "data": "2022-08-11T00:00:00.000Z" + } + } + ], + "Calculate Date: Substract Day": [ + { + "json": { + "data": "2022-07-30T00:00:00.000Z" + } + } + ], + "Calculate Date: Substract Quarter": [ + { + "json": { + "data": "2023-02-01T00:00:00.000Z" + } + } + ], + "Calculate Date: Add months": [ + { + "json": { + "data": "2022-10-01T00:00:00.000Z" + } + } + ], + "Calculate Date: Substract Months": [ + { + "json": { + "data": "2022-06-01T00:00:00.000Z" + } + } + ], + "Calculate Date: Format YYYY/MM/DD": [ + { + "json": { + "data": "2022/08/01" + } + } + ], + "Calculate Date: Format YYYY/MM/DD with Timezone": [ + { + "json": { + "data": "2022/07/31" + } + } + ], + "Calculate Date: Format YYYY/MM/DD with Timezone1": [ + { + "json": { + "data": "2022/07/31" + } + } + ], + "With toString": [ + { + "json": { + "Id": "18162", + "Start_time": 1663801200000, + "End_time": 1663804800000, + "Room": " The Stadium", + "data": "09/21/2022" + } + }, + { + "json": { + "Id": "18173", + "Start_time": 1663797600000, + "End_time": 1663801200000, + "Room": " The Stadium", + "data": "09/21/2022" + } + }, + { + "json": { + "Id": "18178", + "Start_time": 1663804800000, + "End_time": 1663808400000, + "Room": " The Stadium", + "data": "09/22/2022" + } + }, + { + "json": { + "Id": "18182", + "Start_time": 1663801200000, + "End_time": 1663804800000, + "Room": " The Garden", + "data": "09/21/2022" + } + } + ], + "Fixed Value": [ + { + "json": { + "Id": "18162", + "Start_time": 1663801200000, + "End_time": 1663804800000, + "Room": " The Stadium", + "data": "09/21/2022" + } + }, + { + "json": { + "Id": "18173", + "Start_time": 1663797600000, + "End_time": 1663801200000, + "Room": " The Stadium", + "data": "09/21/2022" + } + }, + { + "json": { + "Id": "18178", + "Start_time": 1663804800000, + "End_time": 1663808400000, + "Room": " The Stadium", + "data": "09/21/2022" + } + }, + { + "json": { + "Id": "18182", + "Start_time": 1663801200000, + "End_time": 1663804800000, + "Room": " The Garden", + "data": "09/21/2022" + } + } + ], + "Without toString()": [ + { + "json": { + "Id": "18162", + "Start_time": 1663801200000, + "End_time": 1663804800000, + "Room": " The Stadium", + "data": "09/21/2022" + } + }, + { + "json": { + "Id": "18173", + "Start_time": 1663797600000, + "End_time": 1663801200000, + "Room": " The Stadium", + "data": "09/21/2022" + } + }, + { + "json": { + "Id": "18178", + "Start_time": 1663804800000, + "End_time": 1663808400000, + "Room": " The Stadium", + "data": "09/22/2022" + } + }, + { + "json": { + "Id": "18182", + "Start_time": 1663801200000, + "End_time": 1663804800000, + "Room": " The Garden", + "data": "09/21/2022" + } + } + ], + "parseInt on fixed": [ + { + "json": { + "Id": "18162", + "Start_time": 1663801200000, + "End_time": 1663804800000, + "Room": " The Stadium", + "data": "09/21/2022" + } + }, + { + "json": { + "Id": "18173", + "Start_time": 1663797600000, + "End_time": 1663801200000, + "Room": " The Stadium", + "data": "09/21/2022" + } + }, + { + "json": { + "Id": "18178", + "Start_time": 1663804800000, + "End_time": 1663808400000, + "Room": " The Stadium", + "data": "09/21/2022" + } + }, + { + "json": { + "Id": "18182", + "Start_time": 1663801200000, + "End_time": 1663804800000, + "Room": " The Garden", + "data": "09/21/2022" + } + } + ] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Calculate Date: Add Day", + "type": "main", + "index": 0 + }, + { + "node": "Calculate Date: Substract Day", + "type": "main", + "index": 0 + }, + { + "node": "Calculate Date: Substract Quarter", + "type": "main", + "index": 0 + }, + { + "node": "Calculate Date: Add months", + "type": "main", + "index": 0 + }, + { + "node": "Calculate Date: Substract Months", + "type": "main", + "index": 0 + }, + { + "node": "Calculate Date: Format YYYY/MM/DD", + "type": "main", + "index": 0 + }, + { + "node": "Calculate Date: Format YYYY/MM/DD with Timezone", + "type": "main", + "index": 0 + }, + { + "node": "Calculate Date: Format YYYY/MM/DD with Timezone1", + "type": "main", + "index": 0 + }, + { + "node": "Calculate Date: Format YYYY/MM/DD with Timezone Unix", + "type": "main", + "index": 0 + }, + { + "node": "Calculate Date: Format YYYY/MM/DD with Timezone with special zone", + "type": "main", + "index": 0 + }, + { + "node": "Mock Data", + "type": "main", + "index": 0 + } + ] + ] + }, + "Mock Data": { + "main": [ + [ + { + "node": "With toString", + "type": "main", + "index": 0 + }, + { + "node": "Without toString()", + "type": "main", + "index": 0 + }, + { + "node": "Fixed Value", + "type": "main", + "index": 0 + }, + { + "node": "parseInt on fixed", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": {}, + "versionId": "8ccd9e22-634a-4fc1-b41b-1a63062557e6", + "id": "151", + "meta": { + "instanceId": "36203ea1ce3cef713fa25999bd9874ae26b9e4c2c3a90a365f2882a154d031d0" + }, + "tags": [] +} diff --git a/packages/nodes-base/nodes/DateTime/test/node/workflow.timestamp.json b/packages/nodes-base/nodes/DateTime/test/node/workflow.timestamp.json new file mode 100644 index 0000000000000..6894f699891b8 --- /dev/null +++ b/packages/nodes-base/nodes/DateTime/test/node/workflow.timestamp.json @@ -0,0 +1,393 @@ +{ + "name": "date time", + "nodes": [ + { + "parameters": {}, + "id": "7192e181-4384-430d-a25c-25beb0ae6b1f", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [-100, 1040] + }, + { + "parameters": { + "values": { + "number": [ + { + "name": "dateMilis", + "value": 1682918315906 + }, + { + "name": "dateMilisFloat", + "value": 1682918315.906 + }, + { + "name": "dateUnix", + "value": 1682918315 + } + ], + "string": [ + { + "name": "dateMilisStr", + "value": "1682918315906" + }, + { + "name": "dateMilisFloatStr", + "value": "1682918315.906" + }, + { + "name": "dateUnixStr", + "value": "1682918315" + } + ] + }, + "options": {} + }, + "id": "dc933b29-a770-43b8-aa20-a8e82b0d034f", + "name": "Set", + "type": "n8n-nodes-base.set", + "typeVersion": 2, + "position": [60, 1040] + }, + { + "parameters": { + "value": "={{ $json.dateMilis }}", + "options": {} + }, + "id": "2bcbc9d8-af2f-4c44-b47e-80a9462350d0", + "name": "Date & Time", + "type": "n8n-nodes-base.dateTime", + "typeVersion": 1, + "position": [360, 560] + }, + { + "parameters": { + "keepOnlySet": true, + "values": { + "string": [ + { + "name": "data", + "value": "={{ $json.data }}" + } + ] + }, + "options": {} + }, + "id": "c1b947a8-45c6-4c6a-b4ce-467d57de6d50", + "name": "Set1", + "type": "n8n-nodes-base.set", + "typeVersion": 2, + "position": [580, 560] + }, + { + "parameters": { + "value": "={{ $json.dateMilisFloat }}", + "options": {} + }, + "id": "a91fc3d9-afb8-4c78-ad0b-2078639708da", + "name": "Date & Time1", + "type": "n8n-nodes-base.dateTime", + "typeVersion": 1, + "position": [360, 740] + }, + { + "parameters": { + "keepOnlySet": true, + "values": { + "string": [ + { + "name": "data", + "value": "={{ $json.data }}" + } + ] + }, + "options": {} + }, + "id": "370b58aa-555f-4dc7-9c48-564bddfcbe33", + "name": "Set2", + "type": "n8n-nodes-base.set", + "typeVersion": 2, + "position": [580, 740] + }, + { + "parameters": { + "value": "={{ $json.dateUnix }}", + "options": {} + }, + "id": "91d1e77d-3d25-4e87-9af8-f756ace4abcf", + "name": "Date & Time2", + "type": "n8n-nodes-base.dateTime", + "typeVersion": 1, + "position": [360, 940] + }, + { + "parameters": { + "keepOnlySet": true, + "values": { + "string": [ + { + "name": "data", + "value": "={{ $json.data }}" + } + ] + }, + "options": {} + }, + "id": "77cdf5fa-641e-46e1-8cb9-00160773b4b2", + "name": "Set3", + "type": "n8n-nodes-base.set", + "typeVersion": 2, + "position": [580, 940] + }, + { + "parameters": { + "value": "={{ $json.dateMilisStr }}", + "options": {} + }, + "id": "084e7b19-f03e-42a9-b4df-865f2f6610de", + "name": "Date & Time3", + "type": "n8n-nodes-base.dateTime", + "typeVersion": 1, + "position": [360, 1120] + }, + { + "parameters": { + "keepOnlySet": true, + "values": { + "string": [ + { + "name": "data", + "value": "={{ $json.data }}" + } + ] + }, + "options": {} + }, + "id": "eb46b668-2ebc-47ee-adca-a8f99205ac26", + "name": "Set4", + "type": "n8n-nodes-base.set", + "typeVersion": 2, + "position": [580, 1120] + }, + { + "parameters": { + "value": "={{ $json.dateMilisFloatStr }}", + "options": {} + }, + "id": "9cb46fb5-e6e4-4fb3-859a-8cd26bebf1c9", + "name": "Date & Time4", + "type": "n8n-nodes-base.dateTime", + "typeVersion": 1, + "position": [360, 1300] + }, + { + "parameters": { + "keepOnlySet": true, + "values": { + "string": [ + { + "name": "data", + "value": "={{ $json.data }}" + } + ] + }, + "options": {} + }, + "id": "0d09dc35-d572-41f5-984c-30a66184b80a", + "name": "Set5", + "type": "n8n-nodes-base.set", + "typeVersion": 2, + "position": [580, 1300] + }, + { + "parameters": { + "value": "={{ $json.dateUnixStr }}", + "options": {} + }, + "id": "208e26b1-db01-417b-b21f-fcb8198a1e27", + "name": "Date & Time5", + "type": "n8n-nodes-base.dateTime", + "typeVersion": 1, + "position": [360, 1480] + }, + { + "parameters": { + "keepOnlySet": true, + "values": { + "string": [ + { + "name": "data", + "value": "={{ $json.data }}" + } + ] + }, + "options": {} + }, + "id": "1076029a-91a7-4121-aadc-e9dde57b3d56", + "name": "Set6", + "type": "n8n-nodes-base.set", + "typeVersion": 2, + "position": [580, 1480] + } + ], + "pinData": { + "Set1": [ + { + "json": { + "data": "05/01/2023" + } + } + ], + "Set2": [ + { + "json": { + "data": "05/01/2023" + } + } + ], + "Set4": [ + { + "json": { + "data": "05/01/2023" + } + } + ], + "Set5": [ + { + "json": { + "data": "05/01/2023" + } + } + ], + "Set6": [ + { + "json": { + "data": "05/01/2023" + } + } + ] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Set", + "type": "main", + "index": 0 + } + ] + ] + }, + "Set": { + "main": [ + [ + { + "node": "Date & Time", + "type": "main", + "index": 0 + }, + { + "node": "Date & Time1", + "type": "main", + "index": 0 + }, + { + "node": "Date & Time2", + "type": "main", + "index": 0 + }, + { + "node": "Date & Time3", + "type": "main", + "index": 0 + }, + { + "node": "Date & Time4", + "type": "main", + "index": 0 + }, + { + "node": "Date & Time5", + "type": "main", + "index": 0 + } + ] + ] + }, + "Date & Time": { + "main": [ + [ + { + "node": "Set1", + "type": "main", + "index": 0 + } + ] + ] + }, + "Date & Time1": { + "main": [ + [ + { + "node": "Set2", + "type": "main", + "index": 0 + } + ] + ] + }, + "Date & Time2": { + "main": [ + [ + { + "node": "Set3", + "type": "main", + "index": 0 + } + ] + ] + }, + "Date & Time3": { + "main": [ + [ + { + "node": "Set4", + "type": "main", + "index": 0 + } + ] + ] + }, + "Date & Time4": { + "main": [ + [ + { + "node": "Set5", + "type": "main", + "index": 0 + } + ] + ] + }, + "Date & Time5": { + "main": [ + [ + { + "node": "Set6", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": {}, + "versionId": "d11f32bd-0d35-4aeb-9b0c-c4cac94f6445", + "id": "12", + "meta": { + "instanceId": "6ebec4953fe56f1c009e7c3b107578b375137523af057073c0b5da17350651bd" + }, + "tags": [] +} diff --git a/packages/nodes-base/nodes/EmailSend/v1/EmailSendV1.node.ts b/packages/nodes-base/nodes/EmailSend/v1/EmailSendV1.node.ts index a6b467634da9f..463052a487698 100644 --- a/packages/nodes-base/nodes/EmailSend/v1/EmailSendV1.node.ts +++ b/packages/nodes-base/nodes/EmailSend/v1/EmailSendV1.node.ts @@ -31,12 +31,6 @@ const versionDescription: INodeTypeDescription = { }, ], properties: [ - { - displayName: 'Version 1', - name: 'notice', - type: 'notice', - default: '', - }, // TODO: Add choice for text as text or html (maybe also from name) { displayName: 'From Email', diff --git a/packages/nodes-base/nodes/Formstack/GenericFunctions.ts b/packages/nodes-base/nodes/Formstack/GenericFunctions.ts index 7de8096dfc20e..de19384130ccc 100644 --- a/packages/nodes-base/nodes/Formstack/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Formstack/GenericFunctions.ts @@ -39,7 +39,7 @@ export interface IFormstackSubmissionFieldContainer { value: string; } -export enum FormstackFieldFormat { +export const enum FormstackFieldFormat { ID = 'id', Label = 'label', Name = 'name', diff --git a/packages/nodes-base/nodes/Freshdesk/Freshdesk.node.ts b/packages/nodes-base/nodes/Freshdesk/Freshdesk.node.ts index 428a8a7731cf6..dbeb8d14d64de 100644 --- a/packages/nodes-base/nodes/Freshdesk/Freshdesk.node.ts +++ b/packages/nodes-base/nodes/Freshdesk/Freshdesk.node.ts @@ -20,21 +20,21 @@ import type { ICreateContactBody } from './ContactInterface'; import { contactFields, contactOperations } from './ContactDescription'; -enum Status { +const enum Status { Open = 2, Pending = 3, Resolved = 4, Closed = 5, } -enum Priority { +const enum Priority { Low = 1, Medium = 2, High = 3, Urgent = 4, } -enum Source { +const enum Source { Email = 1, Portal = 2, Phone = 3, diff --git a/packages/nodes-base/nodes/Function/Function.node.ts b/packages/nodes-base/nodes/Function/Function.node.ts index cdf1a110edd42..f681676fe7900 100644 --- a/packages/nodes-base/nodes/Function/Function.node.ts +++ b/packages/nodes-base/nodes/Function/Function.node.ts @@ -233,7 +233,7 @@ return items;`, } } - return Promise.reject(error); + throw error; } } diff --git a/packages/nodes-base/nodes/FunctionItem/FunctionItem.node.ts b/packages/nodes-base/nodes/FunctionItem/FunctionItem.node.ts index a81948fcb9987..e0971f339ed20 100644 --- a/packages/nodes-base/nodes/FunctionItem/FunctionItem.node.ts +++ b/packages/nodes-base/nodes/FunctionItem/FunctionItem.node.ts @@ -208,14 +208,14 @@ return item;`, const lineNumber = lineParts.splice(-2, 1); if (!isNaN(lineNumber as number)) { error.message = `${error.message} [Line ${lineNumber} | Item Index: ${itemIndex}]`; - return Promise.reject(error); + throw error; } } } error.message = `${error.message} [Item Index: ${itemIndex}]`; - return Promise.reject(error); + throw error; } } diff --git a/packages/nodes-base/nodes/Google/Ads/CampaignDescription.ts b/packages/nodes-base/nodes/Google/Ads/CampaignDescription.ts index b5ce28439ac99..3e86ce50d2f2a 100644 --- a/packages/nodes-base/nodes/Google/Ads/CampaignDescription.ts +++ b/packages/nodes-base/nodes/Google/Ads/CampaignDescription.ts @@ -13,17 +13,13 @@ async function processCampaignSearchResponse( ): Promise<INodeExecutionData[]> { const results = (responseData.body as IDataObject).results as GoogleAdsCampaignElement; - return Promise.resolve( - results.map((result) => { - return { - json: { - ...result.campaign, - ...result.metrics, - ...result.campaignBudget, - }, - }; - }), - ); + return results.map((result) => ({ + json: { + ...result.campaign, + ...result.metrics, + ...result.campaignBudget, + }, + })); } export const campaignOperations: INodeProperties[] = [ diff --git a/packages/nodes-base/nodes/Google/Analytics/v1/GoogleAnalyticsV1.node.ts b/packages/nodes-base/nodes/Google/Analytics/v1/GoogleAnalyticsV1.node.ts index 6557252d8d10f..e2977a6d49bef 100644 --- a/packages/nodes-base/nodes/Google/Analytics/v1/GoogleAnalyticsV1.node.ts +++ b/packages/nodes-base/nodes/Google/Analytics/v1/GoogleAnalyticsV1.node.ts @@ -16,6 +16,8 @@ import { googleApiRequest, googleApiRequestAllItems, merge, simplify } from './G import moment from 'moment-timezone'; import type { IData } from './Interfaces'; +import { oldVersionNotice } from '../../../../utils/descriptions'; + const versionDescription: INodeTypeDescription = { displayName: 'Google Analytics', name: 'googleAnalytics', @@ -36,6 +38,7 @@ const versionDescription: INodeTypeDescription = { }, ], properties: [ + oldVersionNotice, { displayName: 'Resource', name: 'resource', diff --git a/packages/nodes-base/nodes/Google/BigQuery/test/v2/node/executeQuery.test.ts b/packages/nodes-base/nodes/Google/BigQuery/test/v2/node/executeQuery.test.ts index 7991f36da60e5..e424ba7a11cf8 100644 --- a/packages/nodes-base/nodes/Google/BigQuery/test/v2/node/executeQuery.test.ts +++ b/packages/nodes-base/nodes/Google/BigQuery/test/v2/node/executeQuery.test.ts @@ -14,21 +14,20 @@ jest.mock('../../../v2/transport', () => { ...originalModule, googleApiRequest: jest.fn(async (method: string, resource: string) => { if (resource === '/v2/projects/test-project/jobs' && method === 'POST') { - return Promise.resolve({ + return { jobReference: { jobId: 'job_123', }, status: { state: 'DONE', }, - }); + }; } if (resource === '/v2/projects/test-project/queries/job_123' && method === 'GET') { - return Promise.resolve({}); + return {}; } - return Promise.resolve(); }), - // googleApiRequestAllItems: jest.fn(async () => Promise.resolve()), + // googleApiRequestAllItems: jest.fn(async () => {}), }; }); diff --git a/packages/nodes-base/nodes/Google/BigQuery/test/v2/node/insert.autoMapMode.test.ts b/packages/nodes-base/nodes/Google/BigQuery/test/v2/node/insert.autoMapMode.test.ts index 14d5771f1b792..c79556283223d 100644 --- a/packages/nodes-base/nodes/Google/BigQuery/test/v2/node/insert.autoMapMode.test.ts +++ b/packages/nodes-base/nodes/Google/BigQuery/test/v2/node/insert.autoMapMode.test.ts @@ -17,25 +17,24 @@ jest.mock('../../../v2/transport', () => { '/v2/projects/test-project/datasets/bigquery_node_dev_test_dataset/tables/num_text' && method === 'GET' ) { - return Promise.resolve({ + return { schema: { fields: [ { name: 'id', type: 'INT' }, { name: 'test', type: 'STRING' }, ], }, - }); + }; } if ( resource === '/v2/projects/test-project/datasets/bigquery_node_dev_test_dataset/tables/num_text/insertAll' && method === 'POST' ) { - return Promise.resolve({ kind: 'bigquery#tableDataInsertAllResponse' }); + return { kind: 'bigquery#tableDataInsertAllResponse' }; } - return Promise.resolve(); }), - googleApiRequestAllItems: jest.fn(async () => Promise.resolve()), + googleApiRequestAllItems: jest.fn(async () => {}), }; }); diff --git a/packages/nodes-base/nodes/Google/BigQuery/test/v2/node/insert.manualMode.test.ts b/packages/nodes-base/nodes/Google/BigQuery/test/v2/node/insert.manualMode.test.ts index c4181e9495452..52005cb834e88 100644 --- a/packages/nodes-base/nodes/Google/BigQuery/test/v2/node/insert.manualMode.test.ts +++ b/packages/nodes-base/nodes/Google/BigQuery/test/v2/node/insert.manualMode.test.ts @@ -17,7 +17,7 @@ jest.mock('../../../v2/transport', () => { '/v2/projects/test-project/datasets/bigquery_node_dev_test_dataset/tables/test_json' && method === 'GET' ) { - return Promise.resolve({ + return { schema: { fields: [ { name: 'json', type: 'JSON' }, @@ -25,22 +25,21 @@ jest.mock('../../../v2/transport', () => { { name: 'active', type: 'BOOLEAN' }, ], }, - }); + }; } if ( resource === '/v2/projects/test-project/datasets/bigquery_node_dev_test_dataset/tables/test_json/insertAll' && method === 'POST' ) { - return Promise.resolve({ kind: 'bigquery#tableDataInsertAllResponse' }); + return { kind: 'bigquery#tableDataInsertAllResponse' }; } - return Promise.resolve(); }), - googleApiRequestAllItems: jest.fn(async () => Promise.resolve()), + googleApiRequestAllItems: jest.fn(async () => {}), }; }); -describe('Test Google BigQuery V2, insert define manualy', () => { +describe('Test Google BigQuery V2, insert define manually', () => { const workflows = ['nodes/Google/BigQuery/test/v2/node/insert.manualMode.workflow.json']; const tests = workflowToTests(workflows); diff --git a/packages/nodes-base/nodes/Google/BigQuery/v1/GoogleBigQueryV1.node.ts b/packages/nodes-base/nodes/Google/BigQuery/v1/GoogleBigQueryV1.node.ts index 49ed5b9599028..394e32c6ada48 100644 --- a/packages/nodes-base/nodes/Google/BigQuery/v1/GoogleBigQueryV1.node.ts +++ b/packages/nodes-base/nodes/Google/BigQuery/v1/GoogleBigQueryV1.node.ts @@ -20,6 +20,8 @@ import { recordFields, recordOperations } from './RecordDescription'; import { v4 as uuid } from 'uuid'; +import { oldVersionNotice } from '../../../../utils/descriptions'; + const versionDescription: INodeTypeDescription = { displayName: 'Google BigQuery', name: 'googleBigQuery', @@ -54,12 +56,7 @@ const versionDescription: INodeTypeDescription = { }, ], properties: [ - { - displayName: 'Version 1', - name: 'version1', - type: 'notice', - default: '', - }, + oldVersionNotice, { displayName: 'Authentication', name: 'authentication', diff --git a/packages/nodes-base/nodes/Google/BigQuery/v2/actions/database/executeQuery.operation.ts b/packages/nodes-base/nodes/Google/BigQuery/v2/actions/database/executeQuery.operation.ts index a806e5afacc33..0e78130098a31 100644 --- a/packages/nodes-base/nodes/Google/BigQuery/v2/actions/database/executeQuery.operation.ts +++ b/packages/nodes-base/nodes/Google/BigQuery/v2/actions/database/executeQuery.operation.ts @@ -14,6 +14,9 @@ const properties: INodeProperties[] = [ displayName: 'SQL Query', name: 'sqlQuery', type: 'string', + typeOptions: { + editor: 'sqlEditor', + }, displayOptions: { hide: { '/options.useLegacySql': [true], @@ -28,6 +31,9 @@ const properties: INodeProperties[] = [ displayName: 'SQL Query', name: 'sqlQuery', type: 'string', + typeOptions: { + editor: 'sqlEditor', + }, displayOptions: { show: { '/options.useLegacySql': [true], diff --git a/packages/nodes-base/nodes/Google/Chat/MessageInterface.ts b/packages/nodes-base/nodes/Google/Chat/MessageInterface.ts index 8dc2d0e570185..a03ab9f1306a3 100644 --- a/packages/nodes-base/nodes/Google/Chat/MessageInterface.ts +++ b/packages/nodes-base/nodes/Google/Chat/MessageInterface.ts @@ -31,7 +31,7 @@ export interface IUser { type?: Type; isAnonymous?: boolean; } -enum Type { +const enum Type { 'TYPE_UNSPECIFIED', 'HUMAN', 'BOT', diff --git a/packages/nodes-base/nodes/Google/Gmail/v1/GmailV1.node.ts b/packages/nodes-base/nodes/Google/Gmail/v1/GmailV1.node.ts index a473d64456255..6a8d0ece2fa96 100644 --- a/packages/nodes-base/nodes/Google/Gmail/v1/GmailV1.node.ts +++ b/packages/nodes-base/nodes/Google/Gmail/v1/GmailV1.node.ts @@ -30,6 +30,8 @@ import { draftFields, draftOperations } from './DraftDescription'; import isEmpty from 'lodash.isempty'; +import { oldVersionNotice } from '../../../../utils/descriptions'; + const versionDescription: INodeTypeDescription = { displayName: 'Gmail', name: 'gmail', @@ -64,6 +66,7 @@ const versionDescription: INodeTypeDescription = { }, ], properties: [ + oldVersionNotice, { displayName: 'Authentication', name: 'authentication', diff --git a/packages/nodes-base/nodes/Google/Sheet/v1/versionDescription.ts b/packages/nodes-base/nodes/Google/Sheet/v1/versionDescription.ts index a8fdd72362923..2d7798cdd74bf 100644 --- a/packages/nodes-base/nodes/Google/Sheet/v1/versionDescription.ts +++ b/packages/nodes-base/nodes/Google/Sheet/v1/versionDescription.ts @@ -1,6 +1,8 @@ /* eslint-disable n8n-nodes-base/node-filename-against-convention */ import type { INodeTypeDescription } from 'n8n-workflow'; +import { oldVersionNotice } from '../../../../utils/descriptions'; + export const versionDescription: INodeTypeDescription = { displayName: 'Google Sheets ', name: 'googleSheets', @@ -36,6 +38,7 @@ export const versionDescription: INodeTypeDescription = { }, ], properties: [ + oldVersionNotice, { displayName: 'Authentication', name: 'authentication', diff --git a/packages/nodes-base/nodes/Google/Sheet/v2/actions/sheet/Sheet.resource.ts b/packages/nodes-base/nodes/Google/Sheet/v2/actions/sheet/Sheet.resource.ts index b6ea2b41d5594..8dc3de896eee1 100644 --- a/packages/nodes-base/nodes/Google/Sheet/v2/actions/sheet/Sheet.resource.ts +++ b/packages/nodes-base/nodes/Google/Sheet/v2/actions/sheet/Sheet.resource.ts @@ -139,6 +139,9 @@ export const descriptions: INodeProperties[] = [ default: { mode: 'list', value: '' }, // default: '', //empty string set to progresivly reveal fields required: true, + typeOptions: { + loadOptionsDependsOn: ['documentId.value'], + }, modes: [ { displayName: 'From List', diff --git a/packages/nodes-base/nodes/Google/Sheet/v2/helpers/GoogleSheets.types.ts b/packages/nodes-base/nodes/Google/Sheet/v2/helpers/GoogleSheets.types.ts index aac00508c4595..a55fc71ae4dd7 100644 --- a/packages/nodes-base/nodes/Google/Sheet/v2/helpers/GoogleSheets.types.ts +++ b/packages/nodes-base/nodes/Google/Sheet/v2/helpers/GoogleSheets.types.ts @@ -64,11 +64,11 @@ export type SheetProperties = PropertiesOf<GoogleSheetsSheet>; export type ResourceLocator = 'id' | 'url' | 'list'; -export enum ResourceLocatorUiNames { - id = 'By ID', - url = 'By URL', - list = 'From List', -} +export const ResourceLocatorUiNames = { + id: 'By ID', + url: 'By URL', + list: 'From List', +}; export type SheetCellDecoded = { cell?: string; diff --git a/packages/nodes-base/nodes/HttpRequest/HttpRequest.node.ts b/packages/nodes-base/nodes/HttpRequest/HttpRequest.node.ts index 34394d555d42b..9dfa8054b09bb 100644 --- a/packages/nodes-base/nodes/HttpRequest/HttpRequest.node.ts +++ b/packages/nodes-base/nodes/HttpRequest/HttpRequest.node.ts @@ -14,7 +14,7 @@ export class HttpRequest extends VersionedNodeType { group: ['output'], subtitle: '={{$parameter["requestMethod"] + ": " + $parameter["url"]}}', description: 'Makes an HTTP request and returns the response data', - defaultVersion: 4, + defaultVersion: 4.1, }; const nodeVersions: IVersionedNodeType['nodeVersions'] = { @@ -22,6 +22,7 @@ export class HttpRequest extends VersionedNodeType { 2: new HttpRequestV2(baseDescription), 3: new HttpRequestV3(baseDescription), 4: new HttpRequestV3(baseDescription), + 4.1: new HttpRequestV3(baseDescription), }; super(nodeVersions, baseDescription); diff --git a/packages/nodes-base/nodes/HttpRequest/V3/HttpRequestV3.node.ts b/packages/nodes-base/nodes/HttpRequest/V3/HttpRequestV3.node.ts index f515b7ab2adb4..2e6eb673f3500 100644 --- a/packages/nodes-base/nodes/HttpRequest/V3/HttpRequestV3.node.ts +++ b/packages/nodes-base/nodes/HttpRequest/V3/HttpRequestV3.node.ts @@ -24,6 +24,7 @@ import { replaceNullValues, sanitizeUiMessage, } from '../GenericFunctions'; +import { keysToLowercase } from '../../../utils/utilities'; function toText<T>(data: T) { if (typeof data === 'object' && data !== null) { @@ -38,7 +39,7 @@ export class HttpRequestV3 implements INodeType { this.description = { ...baseDescription, subtitle: '={{$parameter["method"] + ": " + $parameter["url"]}}', - version: [3, 4], + version: [3, 4, 4.1], defaults: { name: 'HTTP Request', color: '#2200DD', @@ -394,7 +395,9 @@ export class HttpRequestV3 implements INodeType { }, ], default: 'keypair', - description: 'Asasas', + // eslint-disable-next-line n8n-nodes-base/node-param-description-miscased-json + description: + 'The body can be specified using explicit fields (<code>keypair</code>) or using a JavaScript object (<code>json</code>)', }, { displayName: 'Body Parameters', @@ -1033,17 +1036,21 @@ export class HttpRequestV3 implements INodeType { const body = this.getNodeParameter('body', itemIndex, '') as string; const sendHeaders = this.getNodeParameter('sendHeaders', itemIndex, false) as boolean; + const headerParameters = this.getNodeParameter( 'headerParameters.parameters', itemIndex, [], ) as [{ name: string; value: string }]; + const specifyHeaders = this.getNodeParameter( 'specifyHeaders', itemIndex, 'keypair', ) as string; + const jsonHeadersParameter = this.getNodeParameter('jsonHeaders', itemIndex, '') as string; + const { redirect, batching, @@ -1097,6 +1104,11 @@ export class HttpRequestV3 implements INodeType { if (autoDetectResponseFormat || fullResponse) { requestOptions.resolveWithFullResponse = true; } + + if (requestOptions.method !== 'GET' && nodeVersion >= 4.1) { + requestOptions = { ...requestOptions, followAllRedirects: false }; + } + const defaultRedirect = nodeVersion >= 4 && redirect === undefined; if (redirect?.redirect?.followRedirects || defaultRedirect) { @@ -1122,7 +1134,6 @@ export class HttpRequestV3 implements INodeType { // set default timeout to 1 hour requestOptions.timeout = 3600000; } - if (sendQuery && queryParameterArrays) { Object.assign(requestOptions, { qsStringifyOptions: { arrayFormat: queryParameterArrays }, @@ -1221,8 +1232,8 @@ export class HttpRequestV3 implements INodeType { requestOptions.body = uploadData; requestOptions.headers = { ...requestOptions.headers, - 'Content-Length': contentLength, - 'Content-Type': itemBinaryData.mimeType ?? 'application/octet-stream', + 'content-length': contentLength, + 'content-type': itemBinaryData.mimeType ?? 'application/octet-stream', }; } else if (bodyContentType === 'raw') { requestOptions.body = body; @@ -1253,8 +1264,9 @@ export class HttpRequestV3 implements INodeType { // Get parameters defined in the UI if (sendHeaders && headerParameters) { + let additionalHeaders: IDataObject = {}; if (specifyHeaders === 'keypair') { - requestOptions.headers = headerParameters.reduce(parametersToKeyValue, {}); + additionalHeaders = headerParameters.reduce(parametersToKeyValue, {}); } else if (specifyHeaders === 'json') { // body is specified using JSON try { @@ -1269,8 +1281,12 @@ export class HttpRequestV3 implements INodeType { ); } - requestOptions.headers = jsonParse(jsonHeadersParameter); + additionalHeaders = jsonParse(jsonHeadersParameter); } + requestOptions.headers = { + ...requestOptions.headers, + ...keysToLowercase(additionalHeaders), + }; } if (autoDetectResponseFormat || responseFormat === 'file') { @@ -1290,7 +1306,7 @@ export class HttpRequestV3 implements INodeType { requestOptions.headers = {}; } const rawContentType = this.getNodeParameter('rawContentType', itemIndex) as string; - requestOptions.headers['Content-Type'] = rawContentType; + requestOptions.headers['content-type'] = rawContentType; } const authDataKeys: IAuthDataSanitizeKeys = {}; @@ -1334,11 +1350,9 @@ export class HttpRequestV3 implements INodeType { 'application/json,text/html,application/xhtml+xml,application/xml,text/*;q=0.9, image/*;q=0.8, */*;q=0.7'; } } - try { this.sendMessageToUI(sanitizeUiMessage(requestOptions, authDataKeys)); } catch (e) {} - if (authentication === 'genericCredentialType' || authentication === 'none') { if (oAuth1Api) { const requestOAuth1 = this.helpers.requestOAuth1.call(this, 'oAuth1Api', requestOptions); diff --git a/packages/nodes-base/nodes/ItemLists/ItemLists.node.ts b/packages/nodes-base/nodes/ItemLists/ItemLists.node.ts index 25a640528043e..255c459fd7ac2 100644 --- a/packages/nodes-base/nodes/ItemLists/ItemLists.node.ts +++ b/packages/nodes-base/nodes/ItemLists/ItemLists.node.ts @@ -14,12 +14,13 @@ export class ItemLists extends VersionedNodeType { group: ['input'], subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', description: 'Helper for working with lists of items and transforming arrays', - defaultVersion: 2, + defaultVersion: 2.1, }; const nodeVersions: IVersionedNodeType['nodeVersions'] = { 1: new ItemListsV1(baseDescription), 2: new ItemListsV2(baseDescription), + 2.1: new ItemListsV2(baseDescription), }; super(nodeVersions, baseDescription); diff --git a/packages/nodes-base/nodes/ItemLists/V1/summarize.operation.ts b/packages/nodes-base/nodes/ItemLists/V1/summarize.operation.ts index 0e0db54bcb7c0..81ee759a3589a 100644 --- a/packages/nodes-base/nodes/ItemLists/V1/summarize.operation.ts +++ b/packages/nodes-base/nodes/ItemLists/V1/summarize.operation.ts @@ -29,16 +29,16 @@ type Aggregation = { type Aggregations = Aggregation[]; -enum AggregationDisplayNames { - append = 'appended_', - average = 'average_', - concatenate = 'concatenated_', - count = 'count_', - countUnique = 'unique_count_', - max = 'max_', - min = 'min_', - sum = 'sum_', -} +const AggregationDisplayNames = { + append: 'appended_', + average: 'average_', + concatenate: 'concatenated_', + count: 'count_', + countUnique: 'unique_count_', + max: 'max_', + min: 'min_', + sum: 'sum_', +}; const NUMERICAL_AGGREGATIONS = ['average', 'max', 'min', 'sum']; diff --git a/packages/nodes-base/nodes/ItemLists/V2/ItemListsV2.node.ts b/packages/nodes-base/nodes/ItemLists/V2/ItemListsV2.node.ts index 37b3d691d6630..53bfeb86592fc 100644 --- a/packages/nodes-base/nodes/ItemLists/V2/ItemListsV2.node.ts +++ b/packages/nodes-base/nodes/ItemLists/V2/ItemListsV2.node.ts @@ -68,7 +68,7 @@ export class ItemListsV2 implements INodeType { constructor(baseDescription: INodeTypeBaseDescription) { this.description = { ...baseDescription, - version: 2, + version: [2, 2.1], defaults: { name: 'Item Lists', }, @@ -121,7 +121,8 @@ export class ItemListsV2 implements INodeType { { name: 'Split Out Items', value: 'splitOutItems', - description: 'Turn a list inside item(s) into separate items', + description: + "Turn a list or values of object's properties inside item(s) into separate items", action: 'Split Out Items', }, { @@ -802,6 +803,8 @@ return 0;`, const returnData: INodeExecutionData[] = []; const resource = this.getNodeParameter('resource', 0); const operation = this.getNodeParameter('operation', 0); + const nodeVersion = this.getNode().typeVersion; + if (resource === 'itemList') { if (operation === 'splitOutItems') { for (let i = 0; i < length; i++) { @@ -826,99 +829,107 @@ return 0;`, } if (arrayToSplit === undefined) { - if (fieldToSplitOut.includes('.') && disableDotNotation) { - throw new NodeOperationError( - this.getNode(), - `Couldn't find the field '${fieldToSplitOut}' in the input data`, - { - description: - "If you're trying to use a nested field, make sure you turn off 'disable dot notation' in the node options", - }, - ); + if (nodeVersion < 2.1) { + if (fieldToSplitOut.includes('.') && disableDotNotation) { + throw new NodeOperationError( + this.getNode(), + `Couldn't find the field '${fieldToSplitOut}' in the input data`, + { + description: + "If you're trying to use a nested field, make sure you turn off 'disable dot notation' in the node options", + }, + ); + } else { + throw new NodeOperationError( + this.getNode(), + `Couldn't find the field '${fieldToSplitOut}' in the input data`, + { itemIndex: i }, + ); + } } else { - throw new NodeOperationError( - this.getNode(), - `Couldn't find the field '${fieldToSplitOut}' in the input data`, - { itemIndex: i }, - ); + arrayToSplit = []; } } - if (!Array.isArray(arrayToSplit)) { + if (typeof arrayToSplit !== 'object' || arrayToSplit === null) { throw new NodeOperationError( this.getNode(), - `The provided field '${fieldToSplitOut}' is not an array`, + `The provided field '${fieldToSplitOut}' is not an array or object`, { itemIndex: i }, ); - } else { - for (const element of arrayToSplit) { - let newItem = {}; + } - if (include === 'selectedOtherFields') { - const fieldsToInclude = ( - this.getNodeParameter('fieldsToInclude.fields', i, []) as [{ fieldName: string }] - ).map((field) => field.fieldName); + if (!Array.isArray(arrayToSplit)) { + arrayToSplit = Object.values(arrayToSplit); + } - if (!fieldsToInclude.length) { - throw new NodeOperationError(this.getNode(), 'No fields specified', { - description: 'Please add a field to include', - }); - } + for (const element of arrayToSplit) { + let newItem = {}; - newItem = { - ...fieldsToInclude.reduce((prev, field) => { - if (field === fieldToSplitOut) { - return prev; - } - let value; - if (!disableDotNotation) { - value = get(items[i].json, field); - } else { - value = items[i].json[field]; - } - prev = { ...prev, [field]: value }; - return prev; - }, {}), - }; - } else if (include === 'allOtherFields') { - const keys = Object.keys(items[i].json); - - newItem = { - ...keys.reduce((prev, field) => { - let value; - if (!disableDotNotation) { - value = get(items[i].json, field); - } else { - value = items[i].json[field]; - } - prev = { ...prev, [field]: value }; - return prev; - }, {}), - }; + if (include === 'selectedOtherFields') { + const fieldsToInclude = ( + this.getNodeParameter('fieldsToInclude.fields', i, []) as [{ fieldName: string }] + ).map((field) => field.fieldName); - unset(newItem, fieldToSplitOut); + if (!fieldsToInclude.length) { + throw new NodeOperationError(this.getNode(), 'No fields specified', { + description: 'Please add a field to include', + }); } - if ( - typeof element === 'object' && - include === 'noOtherFields' && - destinationFieldName === '' - ) { - newItem = { ...newItem, ...element }; - } else { - newItem = { - ...newItem, - [destinationFieldName || fieldToSplitOut]: element, - }; - } + newItem = { + ...fieldsToInclude.reduce((prev, field) => { + if (field === fieldToSplitOut) { + return prev; + } + let value; + if (!disableDotNotation) { + value = get(items[i].json, field); + } else { + value = items[i].json[field]; + } + prev = { ...prev, [field]: value }; + return prev; + }, {}), + }; + } else if (include === 'allOtherFields') { + const keys = Object.keys(items[i].json); + + newItem = { + ...keys.reduce((prev, field) => { + let value; + if (!disableDotNotation) { + value = get(items[i].json, field); + } else { + value = items[i].json[field]; + } + prev = { ...prev, [field]: value }; + return prev; + }, {}), + }; - returnData.push({ - json: newItem, - pairedItem: { - item: i, - }, - }); + unset(newItem, fieldToSplitOut); + } + + if ( + typeof element === 'object' && + include === 'noOtherFields' && + destinationFieldName === '' + ) { + newItem = { ...newItem, ...element }; + } else { + newItem = { + ...newItem, + [destinationFieldName || fieldToSplitOut]: element, + }; } + + returnData.push({ + json: newItem, + pairedItem: { + item: i, + }, + }); } } @@ -945,36 +956,39 @@ return 0;`, description: 'Please add a field to aggregate', }); } - for (const { fieldToAggregate } of fieldsToAggregate) { - let found = false; - for (const item of items) { - if (fieldToAggregate === '') { - throw new NodeOperationError(this.getNode(), 'Field to aggregate is blank', { - description: 'Please add a field to aggregate', - }); - } - if (!disableDotNotation) { - if (get(item.json, fieldToAggregate) !== undefined) { + + if (nodeVersion < 2.1) { + for (const { fieldToAggregate } of fieldsToAggregate) { + let found = false; + for (const item of items) { + if (fieldToAggregate === '') { + throw new NodeOperationError(this.getNode(), 'Field to aggregate is blank', { + description: 'Please add a field to aggregate', + }); + } + if (!disableDotNotation) { + if (get(item.json, fieldToAggregate) !== undefined) { + found = true; + } + } else if (item.json.hasOwnProperty(fieldToAggregate)) { found = true; } - } else if (item.json.hasOwnProperty(fieldToAggregate)) { - found = true; } - } - if (!found && disableDotNotation && fieldToAggregate.includes('.')) { - throw new NodeOperationError( - this.getNode(), - `Couldn't find the field '${fieldToAggregate}' in the input data`, - { - description: - "If you're trying to use a nested field, make sure you turn off 'disable dot notation' in the node options", - }, - ); - } else if (!found && !keepMissing) { - throw new NodeOperationError( - this.getNode(), - `Couldn't find the field '${fieldToAggregate}' in the input data`, - ); + if (!found && disableDotNotation && fieldToAggregate.includes('.')) { + throw new NodeOperationError( + this.getNode(), + `Couldn't find the field '${fieldToAggregate}' in the input data`, + { + description: + "If you're trying to use a nested field, make sure you turn off 'disable dot notation' in the node options", + }, + ); + } else if (!found && !keepMissing) { + throw new NodeOperationError( + this.getNode(), + `Couldn't find the field '${fieldToAggregate}' in the input data`, + ); + } } } diff --git a/packages/nodes-base/nodes/ItemLists/V2/summarize.operation.ts b/packages/nodes-base/nodes/ItemLists/V2/summarize.operation.ts index c3accbd330659..50c320b2dbf76 100644 --- a/packages/nodes-base/nodes/ItemLists/V2/summarize.operation.ts +++ b/packages/nodes-base/nodes/ItemLists/V2/summarize.operation.ts @@ -29,16 +29,17 @@ type Aggregation = { type Aggregations = Aggregation[]; -enum AggregationDisplayNames { - append = 'appended_', - average = 'average_', - concatenate = 'concatenated_', - count = 'count_', - countUnique = 'unique_count_', - max = 'max_', - min = 'min_', - sum = 'sum_', -} +// eslint-disable-next-line no-restricted-syntax +const AggregationDisplayNames = { + append: 'appended_', + average: 'average_', + concatenate: 'concatenated_', + count: 'count_', + countUnique: 'unique_count_', + max: 'max_', + min: 'min_', + sum: 'sum_', +}; const NUMERICAL_AGGREGATIONS = ['average', 'max', 'min', 'sum']; @@ -570,7 +571,11 @@ export async function execute( const getValue = fieldValueGetter(options.disableDotNotation); - checkIfFieldExists.call(this, newItems, fieldsToSummarize, getValue); + const nodeVersion = this.getNode().typeVersion; + + if (nodeVersion < 2.1) { + checkIfFieldExists.call(this, newItems, fieldsToSummarize, getValue); + } const aggregationResult = splitData( fieldsToSplitBy, diff --git a/packages/nodes-base/nodes/ItemLists/test/node/workflow.splitOutItems.split_object.json b/packages/nodes-base/nodes/ItemLists/test/node/workflow.splitOutItems.split_object.json new file mode 100644 index 0000000000000..39b9ecb6b1c74 --- /dev/null +++ b/packages/nodes-base/nodes/ItemLists/test/node/workflow.splitOutItems.split_object.json @@ -0,0 +1,391 @@ +{ + "name": "itemList split Object", + "nodes": [ + { + "parameters": {}, + "id": "ade46a75-ab57-48c6-886b-0c118f5ef1c6", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [520, 800] + }, + { + "parameters": { + "fieldToSplitOut": "data", + "include": "selectedOtherFields", + "fieldsToInclude": { + "fields": [ + { + "fieldName": "tag" + } + ] + }, + "options": {} + }, + "id": "45e1d7a3-d6e8-4b69-a68a-1038db13be4c", + "name": "Item Lists1", + "type": "n8n-nodes-base.itemLists", + "typeVersion": 2, + "position": [1120, 340] + }, + { + "parameters": { + "jsCode": "const data = {\n entry1: {\n id: 1,\n info: 'some info 1',\n },\n entry2: {\n id: 2,\n info: 'some info 2',\n },\n entry3: {\n id: 3,\n info: 'some info 3',\n },\n};\n\n\nconst data2 = [\n 'a', 'b', 'c'\n];\n\nconst data3 = {\n a: 1,\n b: 2,\n c: 3,\n};\n\nreturn {data, data2, data3, data4: null, tag: 'bar'};" + }, + "id": "faa78fac-468d-42b8-96e9-0fb62c312da3", + "name": "Code1", + "type": "n8n-nodes-base.code", + "typeVersion": 1, + "position": [760, 800] + }, + { + "parameters": {}, + "id": "5baaf321-7e89-473d-a313-7cb90b3f13b3", + "name": "No Operation, do nothing", + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [1380, 340] + }, + { + "parameters": { + "fieldToSplitOut": "data3", + "include": "allOtherFields", + "options": { + "destinationFieldName": "extracted" + } + }, + "id": "a786bea9-eb29-4c6d-aea6-a22aee622bc6", + "name": "Item Lists", + "type": "n8n-nodes-base.itemLists", + "typeVersion": 2, + "position": [1120, 720] + }, + { + "parameters": {}, + "id": "0521a24b-c74a-48fa-ae50-48a242b97806", + "name": "No Operation, do nothing1", + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [1380, 720] + }, + { + "parameters": { + "fieldToSplitOut": "data3", + "options": {} + }, + "id": "0c1c8827-72ab-4738-918c-d529e66505c6", + "name": "Item Lists2", + "type": "n8n-nodes-base.itemLists", + "typeVersion": 2, + "position": [1120, 540] + }, + { + "parameters": {}, + "id": "4c0dca36-c2ae-4d40-8952-0e728ac93fa3", + "name": "No Operation, do nothing2", + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [1380, 540] + }, + { + "parameters": { + "fieldToSplitOut": "data2", + "options": {} + }, + "id": "b2031380-b2a8-426d-8f7a-ab072d23b979", + "name": "Item Lists3", + "type": "n8n-nodes-base.itemLists", + "typeVersion": 2, + "position": [1120, 920] + }, + { + "parameters": {}, + "id": "617f7259-beee-42f1-bba2-4e75a83fe369", + "name": "No Operation, do nothing3", + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [1380, 920] + }, + { + "parameters": { + "fieldToSplitOut": "data4", + "options": {} + }, + "id": "8909b8eb-e5a9-4436-8e62-09d8c9670ac1", + "name": "Item Lists4", + "type": "n8n-nodes-base.itemLists", + "typeVersion": 2, + "position": [1120, 1140], + "continueOnFail": true + }, + { + "parameters": {}, + "id": "a9278f90-8ad9-42dc-85b6-28bf1b6764b7", + "name": "No Operation, do nothing4", + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [1380, 1140] + } + ], + "pinData": { + "No Operation, do nothing": [ + { + "json": { + "tag": "bar", + "data": { + "id": 1, + "info": "some info 1" + } + } + }, + { + "json": { + "tag": "bar", + "data": { + "id": 2, + "info": "some info 2" + } + } + }, + { + "json": { + "tag": "bar", + "data": { + "id": 3, + "info": "some info 3" + } + } + } + ], + "No Operation, do nothing2": [ + { + "json": { + "data3": 1 + } + }, + { + "json": { + "data3": 2 + } + }, + { + "json": { + "data3": 3 + } + } + ], + "No Operation, do nothing1": [ + { + "json": { + "data": { + "entry1": { + "id": 1, + "info": "some info 1" + }, + "entry2": { + "id": 2, + "info": "some info 2" + }, + "entry3": { + "id": 3, + "info": "some info 3" + } + }, + "data2": ["a", "b", "c"], + "data4": null, + "tag": "bar", + "extracted": 1 + } + }, + { + "json": { + "data": { + "entry1": { + "id": 1, + "info": "some info 1" + }, + "entry2": { + "id": 2, + "info": "some info 2" + }, + "entry3": { + "id": 3, + "info": "some info 3" + } + }, + "data2": ["a", "b", "c"], + "data4": null, + "tag": "bar", + "extracted": 2 + } + }, + { + "json": { + "data": { + "entry1": { + "id": 1, + "info": "some info 1" + }, + "entry2": { + "id": 2, + "info": "some info 2" + }, + "entry3": { + "id": 3, + "info": "some info 3" + } + }, + "data2": ["a", "b", "c"], + "data4": null, + "tag": "bar", + "extracted": 3 + } + } + ], + "No Operation, do nothing3": [ + { + "json": { + "data2": "a" + } + }, + { + "json": { + "data2": "b" + } + }, + { + "json": { + "data2": "c" + } + } + ], + "No Operation, do nothing4": [ + { + "json": { + "data": { + "entry1": { + "id": 1, + "info": "some info 1" + }, + "entry2": { + "id": 2, + "info": "some info 2" + }, + "entry3": { + "id": 3, + "info": "some info 3" + } + }, + "data2": ["a", "b", "c"], + "data3": { + "a": 1, + "b": 2, + "c": 3 + }, + "data4": null, + "tag": "bar" + } + } + ] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Code1", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code1": { + "main": [ + [ + { + "node": "Item Lists1", + "type": "main", + "index": 0 + }, + { + "node": "Item Lists2", + "type": "main", + "index": 0 + }, + { + "node": "Item Lists", + "type": "main", + "index": 0 + }, + { + "node": "Item Lists3", + "type": "main", + "index": 0 + }, + { + "node": "Item Lists4", + "type": "main", + "index": 0 + } + ] + ] + }, + "Item Lists1": { + "main": [ + [ + { + "node": "No Operation, do nothing", + "type": "main", + "index": 0 + } + ] + ] + }, + "Item Lists": { + "main": [ + [ + { + "node": "No Operation, do nothing1", + "type": "main", + "index": 0 + } + ] + ] + }, + "Item Lists2": { + "main": [ + [ + { + "node": "No Operation, do nothing2", + "type": "main", + "index": 0 + } + ] + ] + }, + "Item Lists3": { + "main": [ + [ + { + "node": "No Operation, do nothing3", + "type": "main", + "index": 0 + } + ] + ] + }, + "Item Lists4": { + "main": [ + [ + { + "node": "No Operation, do nothing4", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false +} diff --git a/packages/nodes-base/nodes/ItemLists/test/node/workflow.update_2_1.json b/packages/nodes-base/nodes/ItemLists/test/node/workflow.update_2_1.json new file mode 100644 index 0000000000000..283ce35d14269 --- /dev/null +++ b/packages/nodes-base/nodes/ItemLists/test/node/workflow.update_2_1.json @@ -0,0 +1,695 @@ +{ + "name": "My workflow 4", + "nodes": [ + { + "parameters": {}, + "id": "037f477b-6775-47e9-b735-71c1d984ceb6", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [440, 1300] + }, + { + "parameters": { + "fieldToSplitOut": "dataa", + "options": {} + }, + "id": "b9f156e1-ffb0-4121-abf3-8813b8cc738e", + "name": "Item Lists4", + "type": "n8n-nodes-base.itemLists", + "typeVersion": 2, + "position": [920, 260], + "continueOnFail": true + }, + { + "parameters": { + "fieldToSplitOut": "dataa", + "options": {} + }, + "id": "c8ed5ebc-18f2-4d94-8f19-c019278e5d0d", + "name": "Item Lists5", + "type": "n8n-nodes-base.itemLists", + "typeVersion": 2.1, + "position": [920, 420], + "alwaysOutputData": true + }, + { + "parameters": {}, + "id": "e2c1fc7c-3333-4849-a084-dac2c6edc1a7", + "name": "No Operation, do nothing", + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [1140, 240] + }, + { + "parameters": {}, + "id": "c8c9727d-232d-4ae2-a7cc-3dcf00b32474", + "name": "No Operation, do nothing1", + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [1140, 420] + }, + { + "parameters": { + "operation": "aggregateItems", + "fieldsToAggregate": { + "fieldToAggregate": [ + { + "fieldToAggregate": "idd" + } + ] + }, + "options": {} + }, + "id": "88c02d0a-0680-4564-8b2e-48ebfabe4864", + "name": "Item Lists", + "type": "n8n-nodes-base.itemLists", + "typeVersion": 2, + "position": [920, 1080], + "continueOnFail": true + }, + { + "parameters": { + "operation": "aggregateItems", + "fieldsToAggregate": { + "fieldToAggregate": [ + { + "fieldToAggregate": "idd" + } + ] + }, + "options": {} + }, + "id": "327ace17-571f-4aea-98bf-3cad71449f56", + "name": "Item Lists6", + "type": "n8n-nodes-base.itemLists", + "typeVersion": 2.1, + "position": [920, 1240] + }, + { + "parameters": {}, + "id": "38a5b83a-0f71-4788-983b-9f7719d59190", + "name": "No Operation, do nothing2", + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [1140, 1060] + }, + { + "parameters": {}, + "id": "aa76c9fb-5a15-4239-903f-570aeca80453", + "name": "No Operation, do nothing3", + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [1140, 1240] + }, + { + "parameters": { + "operation": "summarize", + "fieldsToSummarize": { + "values": [ + { + "field": "idd" + } + ] + }, + "options": {} + }, + "id": "246e42b2-62ab-4ef4-acf1-031ac6236052", + "name": "Item Lists7", + "type": "n8n-nodes-base.itemLists", + "typeVersion": 2, + "position": [920, 1880], + "continueOnFail": true + }, + { + "parameters": { + "operation": "summarize", + "fieldsToSummarize": { + "values": [ + { + "field": "idd" + } + ] + }, + "options": {} + }, + "id": "3145a0dd-035a-477b-8d5a-98a2106b46c8", + "name": "Item Lists8", + "type": "n8n-nodes-base.itemLists", + "typeVersion": 2.1, + "position": [920, 2040] + }, + { + "parameters": {}, + "id": "71bc9c78-cf27-4628-9f64-19a9bef353c3", + "name": "No Operation, do nothing4", + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [1140, 1860] + }, + { + "parameters": {}, + "id": "1ee8497c-e721-4741-b059-ff4b85cb0e73", + "name": "No Operation, do nothing5", + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [1140, 2040] + }, + { + "parameters": { + "jsCode": "return {data:[\n {id: 1},\n {id: 2},\n {id: 3},\n {id: 4},\n]};" + }, + "id": "b97e2dd3-8934-4f61-a217-e4251c3c018f", + "name": "Code2", + "type": "n8n-nodes-base.code", + "typeVersion": 1, + "position": [600, 500] + }, + { + "parameters": { + "jsCode": "return [\n {id: 1},\n {id: 2},\n {id: 3},\n {id: 4},\n];" + }, + "id": "cc63d7e0-0ecb-4aa8-ae15-69c6b32ce6d9", + "name": "Code", + "type": "n8n-nodes-base.code", + "typeVersion": 1, + "position": [660, 1260] + }, + { + "parameters": { + "operation": "summarize", + "fieldsToSummarize": { + "values": [ + { + "field": "id" + } + ] + }, + "options": {} + }, + "id": "50df7038-52d2-4b07-a729-a683ebbd769d", + "name": "Item Lists9", + "type": "n8n-nodes-base.itemLists", + "typeVersion": 2, + "position": [920, 2260], + "continueOnFail": true + }, + { + "parameters": { + "operation": "summarize", + "fieldsToSummarize": { + "values": [ + { + "field": "id" + } + ] + }, + "options": {} + }, + "id": "15347d3e-0e4a-4c48-ad24-80730f7015c8", + "name": "Item Lists10", + "type": "n8n-nodes-base.itemLists", + "typeVersion": 2.1, + "position": [920, 2420] + }, + { + "parameters": {}, + "id": "aa2bfbc8-bdfd-41fd-83f8-181bcd2fa9be", + "name": "No Operation, do nothing6", + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [1140, 2240] + }, + { + "parameters": {}, + "id": "9ca712dc-dd5a-474e-808b-224bf4149f85", + "name": "No Operation, do nothing7", + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [1140, 2420] + }, + { + "parameters": { + "operation": "aggregateItems", + "fieldsToAggregate": { + "fieldToAggregate": [ + { + "fieldToAggregate": "id" + } + ] + }, + "options": {} + }, + "id": "1cf43271-c87b-4523-9d8d-ed72b4ad49fa", + "name": "Item Lists1", + "type": "n8n-nodes-base.itemLists", + "typeVersion": 2, + "position": [920, 1460], + "continueOnFail": true + }, + { + "parameters": { + "operation": "aggregateItems", + "fieldsToAggregate": { + "fieldToAggregate": [ + { + "fieldToAggregate": "id" + } + ] + }, + "options": {} + }, + "id": "d1153bd8-a546-4ae2-a7ba-6f3ed4c41ba5", + "name": "Item Lists11", + "type": "n8n-nodes-base.itemLists", + "typeVersion": 2.1, + "position": [920, 1620] + }, + { + "parameters": {}, + "id": "32fa0e78-35ef-4a84-bc09-b94141db2bab", + "name": "No Operation, do nothing8", + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [1140, 1440] + }, + { + "parameters": {}, + "id": "d3c43cd0-7544-4cb9-8fad-6e8e8c8676bc", + "name": "No Operation, do nothing9", + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [1140, 1620] + }, + { + "parameters": { + "fieldToSplitOut": "data", + "options": {} + }, + "id": "94c419a1-a941-472b-8142-39ca1c428390", + "name": "Item Lists12", + "type": "n8n-nodes-base.itemLists", + "typeVersion": 2, + "position": [920, 660], + "continueOnFail": true + }, + { + "parameters": { + "fieldToSplitOut": "dataa", + "options": {} + }, + "id": "482d5523-a4d1-4bee-8717-c5949674a246", + "name": "Item Lists13", + "type": "n8n-nodes-base.itemLists", + "typeVersion": 2.1, + "position": [920, 820], + "alwaysOutputData": true + }, + { + "parameters": {}, + "id": "9dd44dc5-3525-4a22-b1dd-5d369db28759", + "name": "No Operation, do nothing10", + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [1140, 640] + }, + { + "parameters": {}, + "id": "f23f7fed-e7e9-4e9d-abdd-056973fc0bbc", + "name": "No Operation, do nothing11", + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [1140, 820] + } + ], + "pinData": { + "No Operation, do nothing1": [ + { + "json": {} + } + ], + "No Operation, do nothing": [ + { + "json": { + "data": [ + { + "id": 1 + }, + { + "id": 2 + }, + { + "id": 3 + }, + { + "id": 4 + } + ] + } + } + ], + "No Operation, do nothing10": [ + { + "json": { + "id": 1 + } + }, + { + "json": { + "id": 2 + } + }, + { + "json": { + "id": 3 + } + }, + { + "json": { + "id": 4 + } + } + ], + "No Operation, do nothing11": [ + { + "json": {} + } + ], + "No Operation, do nothing2": [ + { + "json": { + "id": 1 + } + }, + { + "json": { + "id": 2 + } + }, + { + "json": { + "id": 3 + } + }, + { + "json": { + "id": 4 + } + } + ], + "No Operation, do nothing3": [ + { + "json": { + "idd": [] + } + } + ], + "No Operation, do nothing8": [ + { + "json": { + "id": [1, 2, 3, 4] + } + } + ], + "No Operation, do nothing9": [ + { + "json": { + "id": [1, 2, 3, 4] + } + } + ], + "No Operation, do nothing4": [ + { + "json": { + "id": 1 + } + }, + { + "json": { + "id": 2 + } + }, + { + "json": { + "id": 3 + } + }, + { + "json": { + "id": 4 + } + } + ], + "No Operation, do nothing5": [ + { + "json": { + "count_idd": 0 + } + } + ], + "No Operation, do nothing6": [ + { + "json": { + "count_id": 4 + } + } + ], + "No Operation, do nothing7": [ + { + "json": { + "count_id": 4 + } + } + ] + }, + "connections": { + "Item Lists4": { + "main": [ + [ + { + "node": "No Operation, do nothing", + "type": "main", + "index": 0 + } + ] + ] + }, + "Item Lists5": { + "main": [ + [ + { + "node": "No Operation, do nothing1", + "type": "main", + "index": 0 + } + ] + ] + }, + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Code2", + "type": "main", + "index": 0 + }, + { + "node": "Code", + "type": "main", + "index": 0 + } + ] + ] + }, + "Item Lists": { + "main": [ + [ + { + "node": "No Operation, do nothing2", + "type": "main", + "index": 0 + } + ] + ] + }, + "Item Lists6": { + "main": [ + [ + { + "node": "No Operation, do nothing3", + "type": "main", + "index": 0 + } + ] + ] + }, + "Item Lists7": { + "main": [ + [ + { + "node": "No Operation, do nothing4", + "type": "main", + "index": 0 + } + ] + ] + }, + "Item Lists8": { + "main": [ + [ + { + "node": "No Operation, do nothing5", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code2": { + "main": [ + [ + { + "node": "Item Lists4", + "type": "main", + "index": 0 + }, + { + "node": "Item Lists5", + "type": "main", + "index": 0 + }, + { + "node": "Item Lists12", + "type": "main", + "index": 0 + }, + { + "node": "Item Lists13", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code": { + "main": [ + [ + { + "node": "Item Lists", + "type": "main", + "index": 0 + }, + { + "node": "Item Lists6", + "type": "main", + "index": 0 + }, + { + "node": "Item Lists7", + "type": "main", + "index": 0 + }, + { + "node": "Item Lists8", + "type": "main", + "index": 0 + }, + { + "node": "Item Lists9", + "type": "main", + "index": 0 + }, + { + "node": "Item Lists10", + "type": "main", + "index": 0 + }, + { + "node": "Item Lists1", + "type": "main", + "index": 0 + }, + { + "node": "Item Lists11", + "type": "main", + "index": 0 + } + ] + ] + }, + "Item Lists9": { + "main": [ + [ + { + "node": "No Operation, do nothing6", + "type": "main", + "index": 0 + } + ] + ] + }, + "Item Lists10": { + "main": [ + [ + { + "node": "No Operation, do nothing7", + "type": "main", + "index": 0 + } + ] + ] + }, + "Item Lists1": { + "main": [ + [ + { + "node": "No Operation, do nothing8", + "type": "main", + "index": 0 + } + ] + ] + }, + "Item Lists11": { + "main": [ + [ + { + "node": "No Operation, do nothing9", + "type": "main", + "index": 0 + } + ] + ] + }, + "Item Lists12": { + "main": [ + [ + { + "node": "No Operation, do nothing10", + "type": "main", + "index": 0 + } + ] + ] + }, + "Item Lists13": { + "main": [ + [ + { + "node": "No Operation, do nothing11", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": {}, + "versionId": "5d3c6a6b-df81-42e6-ae4c-2b297024a298", + "id": "9", + "meta": { + "instanceId": "6ebec4953fe56f1c009e7c3b107578b375137523af057073c0b5da17350651bd" + }, + "tags": [] +} diff --git a/packages/nodes-base/nodes/Kafka/KafkaTrigger.node.ts b/packages/nodes-base/nodes/Kafka/KafkaTrigger.node.ts index 1ff27bd302ec3..a3d871eaaa607 100644 --- a/packages/nodes-base/nodes/Kafka/KafkaTrigger.node.ts +++ b/packages/nodes-base/nodes/Kafka/KafkaTrigger.node.ts @@ -9,8 +9,9 @@ import type { INodeType, INodeTypeDescription, ITriggerResponse, + IRun, } from 'n8n-workflow'; -import { NodeOperationError } from 'n8n-workflow'; +import { createDeferredPromise, NodeOperationError } from 'n8n-workflow'; export class KafkaTrigger implements INodeType { description: INodeTypeDescription = { @@ -18,7 +19,7 @@ export class KafkaTrigger implements INodeType { name: 'kafkaTrigger', icon: 'file:kafka.svg', group: ['trigger'], - version: 1, + version: [1, 1.1], description: 'Consume messages from a Kafka topic', defaults: { name: 'Kafka Trigger', @@ -116,7 +117,7 @@ export class KafkaTrigger implements INodeType { type: 'number', default: 1, description: - 'Max number of requests that may be in progress at any time. If falsey then no limit.', + 'The maximum number of unacknowledged requests the client will send on a single connection', }, { displayName: 'Read Messages From Beginning', @@ -132,6 +133,19 @@ export class KafkaTrigger implements INodeType { default: false, description: 'Whether to try to parse the message to an object', }, + { + displayName: 'Parallel Processing', + name: 'parallelProcessing', + type: 'boolean', + default: true, + displayOptions: { + hide: { + '@version': [1], + }, + }, + description: + 'Whether to process messages in parallel or by keeping the message in order', + }, { displayName: 'Only Message', name: 'onlyMessage', @@ -177,6 +191,10 @@ export class KafkaTrigger implements INodeType { const ssl = credentials.ssl as boolean; + const options = this.getNodeParameter('options', {}) as IDataObject; + + options.nodeVersion = this.getNode().typeVersion; + const config: KafkaConfig = { clientId, brokers, @@ -213,9 +231,9 @@ export class KafkaTrigger implements INodeType { heartbeatInterval: this.getNodeParameter('options.heartbeatInterval', 3000) as number, }); - await consumer.connect(); + const parallelProcessing = options.parallelProcessing as boolean; - const options = this.getNodeParameter('options', {}) as IDataObject; + await consumer.connect(); await consumer.subscribe({ topic, fromBeginning: options.fromBeginning ? true : false }); @@ -261,8 +279,16 @@ export class KafkaTrigger implements INodeType { //@ts-ignore data = value; } - - this.emit([this.helpers.returnJsonArray([data])]); + let responsePromise = undefined; + if (!parallelProcessing && (options.nodeVersion as number) > 1) { + responsePromise = await createDeferredPromise<IRun>(); + this.emit([this.helpers.returnJsonArray([data])], undefined, responsePromise); + } else { + this.emit([this.helpers.returnJsonArray([data])]); + } + if (responsePromise) { + await responsePromise.promise(); + } }, }); }; diff --git a/packages/nodes-base/nodes/Mailchimp/Mailchimp.node.ts b/packages/nodes-base/nodes/Mailchimp/Mailchimp.node.ts index 8359700365176..0d3728ad2cdc5 100644 --- a/packages/nodes-base/nodes/Mailchimp/Mailchimp.node.ts +++ b/packages/nodes-base/nodes/Mailchimp/Mailchimp.node.ts @@ -17,7 +17,7 @@ import { import moment from 'moment'; -enum Status { +const enum Status { subscribe = 'subscribe', unsubscribed = 'unsubscribe', cleaned = 'cleaned', diff --git a/packages/nodes-base/nodes/Mattermost/v1/transport/index.ts b/packages/nodes-base/nodes/Mattermost/v1/transport/index.ts index 53433954a17a2..d18d5e596737b 100644 --- a/packages/nodes-base/nodes/Mattermost/v1/transport/index.ts +++ b/packages/nodes-base/nodes/Mattermost/v1/transport/index.ts @@ -19,12 +19,13 @@ export async function apiRequest( query: IDataObject = {}, ) { const credentials = await this.getCredentials('mattermostApi'); + const baseUrl = (credentials.baseUrl as string).replace(/\/$/, ''); const options: IHttpRequestOptions = { method, body, qs: query, - url: `${credentials.baseUrl}/api/v4/${endpoint}`, + url: `${baseUrl}/api/v4/${endpoint}`, headers: { 'content-type': 'application/json; charset=utf-8', }, diff --git a/packages/nodes-base/nodes/Merge/Merge.node.ts b/packages/nodes-base/nodes/Merge/Merge.node.ts index e00220f26079d..9801b5838b4a1 100644 --- a/packages/nodes-base/nodes/Merge/Merge.node.ts +++ b/packages/nodes-base/nodes/Merge/Merge.node.ts @@ -13,12 +13,13 @@ export class Merge extends VersionedNodeType { group: ['transform'], subtitle: '={{$parameter["mode"]}}', description: 'Merges data of multiple streams once data from both is available', - defaultVersion: 2, + defaultVersion: 2.1, }; const nodeVersions: IVersionedNodeType['nodeVersions'] = { 1: new MergeV1(baseDescription), 2: new MergeV2(baseDescription), + 2.1: new MergeV2(baseDescription), }; super(nodeVersions, baseDescription); diff --git a/packages/nodes-base/nodes/Merge/test/node/workflow.update_2_1.json b/packages/nodes-base/nodes/Merge/test/node/workflow.update_2_1.json new file mode 100644 index 0000000000000..a728473e27c82 --- /dev/null +++ b/packages/nodes-base/nodes/Merge/test/node/workflow.update_2_1.json @@ -0,0 +1,201 @@ +{ + "name": "do not error on missing keys", + "nodes": [ + { + "parameters": {}, + "id": "c7c0cf66-790a-4da7-81c8-ba9e4bbcec9a", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [720, 300] + }, + { + "parameters": { + "jsCode": "return [\n {id: 1},\n {id: 2},\n {id: 3},\n {id: 4},\n];" + }, + "id": "cec18624-ced0-4de1-8987-d4b184b136b9", + "name": "Code", + "type": "n8n-nodes-base.code", + "typeVersion": 1, + "position": [1020, 200] + }, + { + "parameters": { + "jsCode": "return [\n];" + }, + "id": "754d549c-82ce-4625-ba2b-6f8edcbf715e", + "name": "Code1", + "type": "n8n-nodes-base.code", + "typeVersion": 1, + "position": [1020, 400] + }, + { + "parameters": {}, + "id": "551ed574-607a-4d98-9b06-350df92c805e", + "name": "No Operation, do nothing", + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [1540, 200] + }, + { + "parameters": {}, + "id": "00e75760-d88e-413c-b276-c759db72411f", + "name": "No Operation, do nothing1", + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [1540, 380] + }, + { + "parameters": { + "mode": "combine", + "mergeByFields": { + "values": [ + { + "field1": "idd", + "field2": "idd" + } + ] + }, + "options": {} + }, + "id": "2d412290-4c43-40a6-be78-946602749aa1", + "name": "Merge1", + "type": "n8n-nodes-base.merge", + "typeVersion": 2.1, + "position": [1340, 520], + "alwaysOutputData": true + }, + { + "parameters": { + "mode": "combine", + "mergeByFields": { + "values": [ + { + "field1": "idd", + "field2": "idd" + } + ] + }, + "options": {} + }, + "id": "b6f05b31-3ca0-48c4-a5ec-c7f81dc7957f", + "name": "Merge2", + "type": "n8n-nodes-base.merge", + "typeVersion": 2, + "position": [1320, 160], + "continueOnFail": true + } + ], + "pinData": { + "No Operation, do nothing1": [ + { + "json": {} + } + ], + "No Operation, do nothing": [ + { + "json": { + "id": 1 + } + }, + { + "json": { + "id": 2 + } + }, + { + "json": { + "id": 3 + } + }, + { + "json": { + "id": 4 + } + } + ] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Code", + "type": "main", + "index": 0 + }, + { + "node": "Code1", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code": { + "main": [ + [ + { + "node": "Merge1", + "type": "main", + "index": 0 + }, + { + "node": "Merge2", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code1": { + "main": [ + [ + { + "node": "Merge1", + "type": "main", + "index": 1 + }, + { + "node": "Merge2", + "type": "main", + "index": 1 + } + ] + ] + }, + "No Operation, do nothing1": { + "main": [[]] + }, + "Merge1": { + "main": [ + [ + { + "node": "No Operation, do nothing1", + "type": "main", + "index": 0 + } + ] + ] + }, + "Merge2": { + "main": [ + [ + { + "node": "No Operation, do nothing", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": {}, + "versionId": "d0ebcbdd-b57d-495e-8f84-b9500d25c384", + "id": "8", + "meta": { + "instanceId": "6ebec4953fe56f1c009e7c3b107578b375137523af057073c0b5da17350651bd" + }, + "tags": [] +} diff --git a/packages/nodes-base/nodes/Merge/v1/MergeV1.node.ts b/packages/nodes-base/nodes/Merge/v1/MergeV1.node.ts index 1e928c6ce610e..44d726c22e6ad 100644 --- a/packages/nodes-base/nodes/Merge/v1/MergeV1.node.ts +++ b/packages/nodes-base/nodes/Merge/v1/MergeV1.node.ts @@ -12,6 +12,8 @@ import type { } from 'n8n-workflow'; import { deepCopy } from 'n8n-workflow'; +import { oldVersionNotice } from '../../../utils/descriptions'; + const versionDescription: INodeTypeDescription = { displayName: 'Merge', name: 'merge', @@ -29,6 +31,7 @@ const versionDescription: INodeTypeDescription = { outputs: ['main'], inputNames: ['Input 1', 'Input 2'], properties: [ + oldVersionNotice, { displayName: 'Mode', name: 'mode', diff --git a/packages/nodes-base/nodes/Merge/v2/MergeV2.node.ts b/packages/nodes-base/nodes/Merge/v2/MergeV2.node.ts index 5b8593bd9e590..5f7b9c2adc4cd 100644 --- a/packages/nodes-base/nodes/Merge/v2/MergeV2.node.ts +++ b/packages/nodes-base/nodes/Merge/v2/MergeV2.node.ts @@ -35,7 +35,7 @@ const versionDescription: INodeTypeDescription = { name: 'merge', icon: 'fa:code-branch', group: ['transform'], - version: 2, + version: [2, 2.1], subtitle: '={{$parameter["mode"]}}', description: 'Merges data of multiple streams once data from both is available', defaults: { @@ -450,20 +450,28 @@ export class MergeV2 implements INodeType { options.joinMode = joinMode; options.outputDataFrom = outputDataFrom; - const input1 = checkInput( - this.getInputData(0), - matchFields.map((pair) => pair.field1), - options.disableDotNotation || false, - 'Input 1', - ); - if (!input1) return [returnData]; + const nodeVersion = this.getNode().typeVersion; - const input2 = checkInput( - this.getInputData(1), - matchFields.map((pair) => pair.field2), - options.disableDotNotation || false, - 'Input 2', - ); + let input1 = this.getInputData(0); + let input2 = this.getInputData(1); + if (nodeVersion < 2.1) { + input1 = checkInput( + this.getInputData(0), + matchFields.map((pair) => pair.field1), + options.disableDotNotation || false, + 'Input 1', + ); + if (!input1) return [returnData]; + + input2 = checkInput( + this.getInputData(1), + matchFields.map((pair) => pair.field2), + options.disableDotNotation || false, + 'Input 2', + ); + } else { + if (!input1) return [returnData]; + } if (!input2 || !matchFields.length) { if ( diff --git a/packages/nodes-base/nodes/Microsoft/Excel/MicrosoftExcel.node.ts b/packages/nodes-base/nodes/Microsoft/Excel/MicrosoftExcel.node.ts index 086bbfdce760b..9c9d0318756de 100644 --- a/packages/nodes-base/nodes/Microsoft/Excel/MicrosoftExcel.node.ts +++ b/packages/nodes-base/nodes/Microsoft/Excel/MicrosoftExcel.node.ts @@ -1,678 +1,26 @@ -import type { - IExecuteFunctions, - IDataObject, - ILoadOptionsFunctions, - INodeExecutionData, - INodePropertyOptions, - INodeType, - INodeTypeDescription, - JsonObject, -} from 'n8n-workflow'; -import { NodeApiError } from 'n8n-workflow'; - -import { - microsoftApiRequest, - microsoftApiRequestAllItems, - microsoftApiRequestAllItemsSkip, -} from './GenericFunctions'; - -import { workbookFields, workbookOperations } from './WorkbookDescription'; - -import { worksheetFields, worksheetOperations } from './WorksheetDescription'; - -import { tableFields, tableOperations } from './TableDescription'; - -export class MicrosoftExcel implements INodeType { - description: INodeTypeDescription = { - displayName: 'Microsoft Excel', - name: 'microsoftExcel', - icon: 'file:excel.svg', - group: ['input'], - version: 1, - subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', - description: 'Consume Microsoft Excel API', - defaults: { - name: 'Microsoft Excel', - }, - inputs: ['main'], - outputs: ['main'], - credentials: [ - { - name: 'microsoftExcelOAuth2Api', - required: true, - }, - ], - properties: [ - { - displayName: 'Resource', - name: 'resource', - type: 'options', - noDataExpression: true, - options: [ - { - name: 'Table', - value: 'table', - description: 'Represents an Excel table', - }, - { - name: 'Workbook', - value: 'workbook', - description: - 'Workbook is the top level object which contains related workbook objects such as worksheets, tables, ranges, etc', - }, - { - name: 'Worksheet', - value: 'worksheet', - description: - 'An Excel worksheet is a grid of cells. It can contain data, tables, charts, etc.', - }, - ], - default: 'workbook', - }, - ...workbookOperations, - ...workbookFields, - ...worksheetOperations, - ...worksheetFields, - ...tableOperations, - ...tableFields, - ], - }; - - methods = { - loadOptions: { - // Get all the workbooks to display them to user so that they can - // select them easily - async getWorkbooks(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> { - const qs: IDataObject = { - select: 'id,name', - }; - const returnData: INodePropertyOptions[] = []; - const workbooks = await microsoftApiRequestAllItems.call( - this, - 'value', - 'GET', - "/drive/root/search(q='.xlsx')", - {}, - qs, - ); - for (const workbook of workbooks) { - const workbookName = workbook.name; - const workbookId = workbook.id; - returnData.push({ - name: workbookName, - value: workbookId, - }); - } - return returnData; - }, - // Get all the worksheets to display them to user so that they can - // select them easily - async getworksheets(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> { - const workbookId = this.getCurrentNodeParameter('workbook'); - const qs: IDataObject = { - select: 'id,name', - }; - const returnData: INodePropertyOptions[] = []; - const worksheets = await microsoftApiRequestAllItems.call( - this, - 'value', - 'GET', - `/drive/items/${workbookId}/workbook/worksheets`, - {}, - qs, - ); - for (const worksheet of worksheets) { - const worksheetName = worksheet.name; - const worksheetId = worksheet.id; - returnData.push({ - name: worksheetName, - value: worksheetId, - }); - } - return returnData; - }, - // Get all the tables to display them to user so that they can - // select them easily - async getTables(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> { - const workbookId = this.getCurrentNodeParameter('workbook'); - const worksheetId = this.getCurrentNodeParameter('worksheet'); - const qs: IDataObject = { - select: 'id,name', - }; - const returnData: INodePropertyOptions[] = []; - const tables = await microsoftApiRequestAllItems.call( - this, - 'value', - 'GET', - `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables`, - {}, - qs, - ); - for (const table of tables) { - const tableName = table.name; - const tableId = table.id; - returnData.push({ - name: tableName, - value: tableId, - }); - } - return returnData; - }, - }, - }; - - async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> { - const items = this.getInputData(); - const returnData: INodeExecutionData[] = []; - const length = items.length; - let qs: IDataObject = {}; - const result: IDataObject[] = []; - let responseData; - const resource = this.getNodeParameter('resource', 0); - const operation = this.getNodeParameter('operation', 0); - - if (resource === 'table') { - //https://docs.microsoft.com/en-us/graph/api/table-post-rows?view=graph-rest-1.0&tabs=http - if (operation === 'addRow') { - try { - // TODO: At some point it should be possible to use item dependent parameters. - // Is however important to then not make one separate request each. - const workbookId = this.getNodeParameter('workbook', 0) as string; - const worksheetId = this.getNodeParameter('worksheet', 0) as string; - const tableId = this.getNodeParameter('table', 0) as string; - const additionalFields = this.getNodeParameter('additionalFields', 0); - const body: IDataObject = {}; - - if (additionalFields.index) { - body.index = additionalFields.index as number; - } - - // Get table columns to eliminate any columns not needed on the input - responseData = await microsoftApiRequest.call( - this, - 'GET', - `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/columns`, - {}, - qs, - ); - const columns = responseData.value.map((column: IDataObject) => column.name); - - const rows: any[][] = []; - - // Bring the items into the correct format - for (const item of items) { - const row = []; - for (const column of columns) { - row.push(item.json[column]); - } - rows.push(row); - } - - body.values = rows; - const { id } = await microsoftApiRequest.call( - this, - 'POST', - `/drive/items/${workbookId}/workbook/createSession`, - { persistChanges: true }, - ); - responseData = await microsoftApiRequest.call( - this, - 'POST', - `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/rows/add`, - body, - {}, - '', - { 'workbook-session-id': id }, - ); - await microsoftApiRequest.call( - this, - 'POST', - `/drive/items/${workbookId}/workbook/closeSession`, - {}, - {}, - '', - { 'workbook-session-id': id }, - ); - - const executionData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray(responseData as IDataObject[]), - { itemData: { item: 0 } }, - ); - - returnData.push(...executionData); - } catch (error) { - if (this.continueOnFail()) { - const executionErrorData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray({ error: error.message }), - { itemData: { item: 0 } }, - ); - returnData.push(...executionErrorData); - } else { - throw error; - } - } - } - //https://docs.microsoft.com/en-us/graph/api/table-list-columns?view=graph-rest-1.0&tabs=http - if (operation === 'getColumns') { - for (let i = 0; i < length; i++) { - try { - qs = {}; - const workbookId = this.getNodeParameter('workbook', i) as string; - const worksheetId = this.getNodeParameter('worksheet', i) as string; - const tableId = this.getNodeParameter('table', i) as string; - const returnAll = this.getNodeParameter('returnAll', i); - const rawData = this.getNodeParameter('rawData', i); - if (rawData) { - const filters = this.getNodeParameter('filters', i); - if (filters.fields) { - qs.$select = filters.fields; - } - } - if (returnAll) { - responseData = await microsoftApiRequestAllItemsSkip.call( - this, - 'value', - 'GET', - `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/columns`, - {}, - qs, - ); - } else { - qs.$top = this.getNodeParameter('limit', i); - responseData = await microsoftApiRequest.call( - this, - 'GET', - `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/columns`, - {}, - qs, - ); - responseData = responseData.value; - } - if (!rawData) { - responseData = responseData.map((column: IDataObject) => ({ name: column.name })); - } else { - const dataProperty = this.getNodeParameter('dataProperty', i) as string; - responseData = { [dataProperty]: responseData }; - } - - const executionData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray(responseData as IDataObject[]), - { itemData: { item: i } }, - ); - - returnData.push(...executionData); - } catch (error) { - if (this.continueOnFail()) { - const executionErrorData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray({ error: error.message }), - { itemData: { item: i } }, - ); - returnData.push(...executionErrorData); - continue; - } - throw error; - } - } - } - //https://docs.microsoft.com/en-us/graph/api/table-list-rows?view=graph-rest-1.0&tabs=http - if (operation === 'getRows') { - for (let i = 0; i < length; i++) { - qs = {}; - try { - const workbookId = this.getNodeParameter('workbook', i) as string; - const worksheetId = this.getNodeParameter('worksheet', i) as string; - const tableId = this.getNodeParameter('table', i) as string; - const returnAll = this.getNodeParameter('returnAll', i); - const rawData = this.getNodeParameter('rawData', i); - if (rawData) { - const filters = this.getNodeParameter('filters', i); - if (filters.fields) { - qs.$select = filters.fields; - } - } - if (returnAll) { - responseData = await microsoftApiRequestAllItemsSkip.call( - this, - 'value', - 'GET', - `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/rows`, - {}, - qs, - ); - } else { - const rowsQs = { ...qs }; - rowsQs.$top = this.getNodeParameter('limit', i); - responseData = await microsoftApiRequest.call( - this, - 'GET', - `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/rows`, - {}, - rowsQs, - ); - responseData = responseData.value; - } - if (!rawData) { - const columnsQs = { ...qs }; - columnsQs.$select = 'name'; - // TODO: That should probably be cached in the future - let columns = await microsoftApiRequestAllItemsSkip.call( - this, - 'value', - 'GET', - `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/columns`, - {}, - columnsQs, - ); - //@ts-ignore - columns = columns.map((column) => column.name); - for (let index = 0; index < responseData.length; index++) { - const object: IDataObject = {}; - for (let y = 0; y < columns.length; y++) { - object[columns[y]] = responseData[index].values[0][y]; - } - const executionData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray({ ...object }), - { itemData: { item: index } }, - ); - - returnData.push(...executionData); - } - } else { - const dataProperty = this.getNodeParameter('dataProperty', i) as string; - const executionData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray({ [dataProperty]: responseData }), - { itemData: { item: i } }, - ); - - returnData.push(...executionData); - } - } catch (error) { - if (this.continueOnFail()) { - const executionErrorData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray({ error: error.message }), - { itemData: { item: i } }, - ); - returnData.push(...executionErrorData); - continue; - } - throw error; - } - } - } - if (operation === 'lookup') { - for (let i = 0; i < length; i++) { - qs = {}; - try { - const workbookId = this.getNodeParameter('workbook', i) as string; - const worksheetId = this.getNodeParameter('worksheet', i) as string; - const tableId = this.getNodeParameter('table', i) as string; - const lookupColumn = this.getNodeParameter('lookupColumn', i) as string; - const lookupValue = this.getNodeParameter('lookupValue', i) as string; - const options = this.getNodeParameter('options', i); - - responseData = await microsoftApiRequestAllItemsSkip.call( - this, - 'value', - 'GET', - `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/rows`, - {}, - {}, - ); - - qs.$select = 'name'; - // TODO: That should probably be cached in the future - let columns = await microsoftApiRequestAllItemsSkip.call( - this, - 'value', - 'GET', - `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/columns`, - {}, - qs, - ); - columns = columns.map((column: IDataObject) => column.name); - - if (!columns.includes(lookupColumn)) { - throw new NodeApiError(this.getNode(), responseData as JsonObject, { - message: `Column ${lookupColumn} does not exist on the table selected`, - }); - } - - result.length = 0; - for (let index = 0; index < responseData.length; index++) { - const object: IDataObject = {}; - for (let y = 0; y < columns.length; y++) { - object[columns[y]] = responseData[index].values[0][y]; - } - result.push({ ...object }); - } - - if (options.returnAllMatches) { - responseData = result.filter((data: IDataObject) => { - return data[lookupColumn]?.toString() === lookupValue; - }); - const executionData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray(responseData), - { itemData: { item: i } }, - ); - - returnData.push(...executionData); - } else { - responseData = result.find((data: IDataObject) => { - return data[lookupColumn]?.toString() === lookupValue; - }); - const executionData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray(responseData as IDataObject), - { itemData: { item: i } }, - ); - - returnData.push(...executionData); - } - } catch (error) { - if (this.continueOnFail()) { - const executionErrorData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray({ error: error.message }), - { itemData: { item: i } }, - ); - returnData.push(...executionErrorData); - continue; - } - throw error; - } - } - } - } - if (resource === 'workbook') { - for (let i = 0; i < length; i++) { - qs = {}; - try { - //https://docs.microsoft.com/en-us/graph/api/worksheetcollection-add?view=graph-rest-1.0&tabs=http - if (operation === 'addWorksheet') { - const workbookId = this.getNodeParameter('workbook', i) as string; - const additionalFields = this.getNodeParameter('additionalFields', i); - const body: IDataObject = {}; - if (additionalFields.name) { - body.name = additionalFields.name; - } - const { id } = await microsoftApiRequest.call( - this, - 'POST', - `/drive/items/${workbookId}/workbook/createSession`, - { persistChanges: true }, - ); - responseData = await microsoftApiRequest.call( - this, - 'POST', - `/drive/items/${workbookId}/workbook/worksheets/add`, - body, - {}, - '', - { 'workbook-session-id': id }, - ); - await microsoftApiRequest.call( - this, - 'POST', - `/drive/items/${workbookId}/workbook/closeSession`, - {}, - {}, - '', - { 'workbook-session-id': id }, - ); - } - if (operation === 'getAll') { - const returnAll = this.getNodeParameter('returnAll', i); - const filters = this.getNodeParameter('filters', i); - if (filters.fields) { - qs.$select = filters.fields; - } - if (returnAll) { - responseData = await microsoftApiRequestAllItems.call( - this, - 'value', - 'GET', - "/drive/root/search(q='.xlsx')", - {}, - qs, - ); - } else { - qs.$top = this.getNodeParameter('limit', i); - responseData = await microsoftApiRequest.call( - this, - 'GET', - "/drive/root/search(q='.xlsx')", - {}, - qs, - ); - responseData = responseData.value; - } - } - - if (Array.isArray(responseData)) { - const executionData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray(responseData), - { itemData: { item: i } }, - ); - - returnData.push(...executionData); - } else if (responseData !== undefined) { - const executionData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray(responseData as IDataObject), - { itemData: { item: i } }, - ); - - returnData.push(...executionData); - } - } catch (error) { - if (this.continueOnFail()) { - const executionErrorData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray({ error: error.message }), - { itemData: { item: i } }, - ); - returnData.push(...executionErrorData); - continue; - } - throw error; - } - } - } - if (resource === 'worksheet') { - for (let i = 0; i < length; i++) { - qs = {}; - try { - //https://docs.microsoft.com/en-us/graph/api/workbook-list-worksheets?view=graph-rest-1.0&tabs=http - if (operation === 'getAll') { - const returnAll = this.getNodeParameter('returnAll', i); - const workbookId = this.getNodeParameter('workbook', i) as string; - const filters = this.getNodeParameter('filters', i); - if (filters.fields) { - qs.$select = filters.fields; - } - if (returnAll) { - responseData = await microsoftApiRequestAllItems.call( - this, - 'value', - 'GET', - `/drive/items/${workbookId}/workbook/worksheets`, - {}, - qs, - ); - } else { - qs.$top = this.getNodeParameter('limit', i); - responseData = await microsoftApiRequest.call( - this, - 'GET', - `/drive/items/${workbookId}/workbook/worksheets`, - {}, - qs, - ); - responseData = responseData.value; - } - } - //https://docs.microsoft.com/en-us/graph/api/worksheet-range?view=graph-rest-1.0&tabs=http - if (operation === 'getContent') { - const workbookId = this.getNodeParameter('workbook', i) as string; - const worksheetId = this.getNodeParameter('worksheet', i) as string; - const range = this.getNodeParameter('range', i) as string; - const rawData = this.getNodeParameter('rawData', i); - if (rawData) { - const filters = this.getNodeParameter('filters', i); - if (filters.fields) { - qs.$select = filters.fields; - } - } - - responseData = await microsoftApiRequest.call( - this, - 'GET', - `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/range(address='${range}')`, - {}, - qs, - ); - - if (!rawData) { - const keyRow = this.getNodeParameter('keyRow', i) as number; - const dataStartRow = this.getNodeParameter('dataStartRow', i) as number; - if (responseData.values === null) { - throw new NodeApiError(this.getNode(), responseData as JsonObject, { - message: 'Range did not return data', - }); - } - const keyValues = responseData.values[keyRow]; - for (let index = dataStartRow; index < responseData.values.length; index++) { - const object: IDataObject = {}; - for (let y = 0; y < keyValues.length; y++) { - object[keyValues[y]] = responseData.values[index][y]; - } - const executionData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray({ ...object }), - { itemData: { item: index } }, - ); - - returnData.push(...executionData); - } - } else { - const dataProperty = this.getNodeParameter('dataProperty', i) as string; - const executionData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray({ [dataProperty]: responseData }), - { itemData: { item: i } }, - ); - - returnData.push(...executionData); - } - } - } catch (error) { - if (this.continueOnFail()) { - const executionErrorData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray({ error: error.message }), - { itemData: { item: i } }, - ); - returnData.push(...executionErrorData); - continue; - } - throw error; - } - } - } - - return this.prepareOutputData(returnData); +import type { INodeTypeBaseDescription, IVersionedNodeType } from 'n8n-workflow'; +import { VersionedNodeType } from 'n8n-workflow'; + +import { MicrosoftExcelV1 } from './v1/MicrosoftExcelV1.node'; +import { MicrosoftExcelV2 } from './v2/MicrosoftExcelV2.node'; + +export class MicrosoftExcel extends VersionedNodeType { + constructor() { + const baseDescription: INodeTypeBaseDescription = { + displayName: 'Microsoft Excel 365', + name: 'microsoftExcel', + icon: 'file:excel.svg', + group: ['input'], + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume Microsoft Excel API', + defaultVersion: 2, + }; + + const nodeVersions: IVersionedNodeType['nodeVersions'] = { + 1: new MicrosoftExcelV1(baseDescription), + 2: new MicrosoftExcelV2(baseDescription), + }; + + super(nodeVersions, baseDescription); } } diff --git a/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/addTable.test.ts b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/addTable.test.ts new file mode 100644 index 0000000000000..6cf70144f4a3e --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/addTable.test.ts @@ -0,0 +1,72 @@ +import type { INodeTypes } from 'n8n-workflow'; + +import { getResultNodeData, setup, workflowToTests } from '../../../../../../../test/nodes/Helpers'; +import type { WorkflowTestData } from '../../../../../../../test/nodes/types'; +import { executeWorkflow } from '../../../../../../../test/nodes/ExecuteWorkflow'; + +import * as transport from '../../../../v2/transport'; + +import nock from 'nock'; + +jest.mock('../../../../v2/transport', () => { + const originalModule = jest.requireActual('../../../../v2/transport'); + return { + ...originalModule, + microsoftApiRequest: jest.fn(async function (method: string) { + if (method === 'POST') { + return { + style: 'TableStyleMedium2', + name: 'Table3', + showFilterButton: true, + id: '{317CA469-7D1C-4A5D-9B0B-424444BF0336}', + highlightLastColumn: false, + highlightFirstColumn: false, + legacyId: '3', + showBandedColumns: false, + showBandedRows: true, + showHeaders: true, + showTotals: false, + }; + } + }), + }; +}); + +describe('Test MicrosoftExcelV2, table => addTable', () => { + const workflows = ['nodes/Microsoft/Excel/test/v2/node/table/addTable.workflow.json']; + const tests = workflowToTests(workflows); + + beforeAll(() => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + jest.unmock('../../../../v2/transport'); + }); + + const nodeTypes = setup(tests); + + const testNode = async (testData: WorkflowTestData, types: INodeTypes) => { + const { result } = await executeWorkflow(testData, types); + + const resultNodeData = getResultNodeData(result, testData); + + resultNodeData.forEach(({ nodeName, resultData }) => { + return expect(resultData).toEqual(testData.output.nodeData[nodeName]); + }); + + expect(transport.microsoftApiRequest).toHaveBeenCalledTimes(1); + expect(transport.microsoftApiRequest).toHaveBeenCalledWith( + 'POST', + '/drive/items/01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I/workbook/worksheets/{A0883CFE-D27E-4ECC-B94B-981830AAD55B}/tables/add', + { address: 'A1:D4', hasHeaders: true }, + ); + + expect(result.finished).toEqual(true); + }; + + for (const testData of tests) { + test(testData.description, async () => testNode(testData, nodeTypes)); + } +}); diff --git a/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/addTable.workflow.json b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/addTable.workflow.json new file mode 100644 index 0000000000000..eb8fbbd3fe5cf --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/addTable.workflow.json @@ -0,0 +1,78 @@ +{ + "name": "My workflow 5", + "nodes": [ + { + "parameters": {}, + "id": "875e8784-eb59-40d8-ba45-129a5e29881c", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [380, 140] + }, + { + "parameters": { + "resource": "table", + "operation": "addTable", + "workbook": { + "__rl": true, + "value": "01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I", + "mode": "list", + "cachedResultName": "ŠŠ Š Š ŠŗŠ¾ŠæŃŃ" + }, + "worksheet": { + "__rl": true, + "value": "{A0883CFE-D27E-4ECC-B94B-981830AAD55B}", + "mode": "list", + "cachedResultName": "Sheet4" + }, + "selectRange": "manual", + "range": "A1:D4" + }, + "id": "0e0ac1d2-242c-486a-9287-c70307645acc", + "name": "Microsoft Excel 365", + "type": "n8n-nodes-base.microsoftExcel", + "typeVersion": 2, + "position": [860, 140], + "credentials": { + "microsoftExcelOAuth2Api": { + "id": "70", + "name": "Microsoft Excel account" + } + } + } + ], + "pinData": { + "Microsoft Excel 365": [ + { + "json": { + "style": "TableStyleMedium2", + "name": "Table3", + "showFilterButton": true, + "id": "{317CA469-7D1C-4A5D-9B0B-424444BF0336}", + "highlightLastColumn": false, + "highlightFirstColumn": false, + "legacyId": "3", + "showBandedColumns": false, + "showBandedRows": true, + "showHeaders": true, + "showTotals": false + } + } + ] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Microsoft Excel 365", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": {} +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/append.test.ts b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/append.test.ts new file mode 100644 index 0000000000000..1eddfd1b38a48 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/append.test.ts @@ -0,0 +1,93 @@ +import type { INodeTypes } from 'n8n-workflow'; + +import { getResultNodeData, setup, workflowToTests } from '../../../../../../../test/nodes/Helpers'; +import type { WorkflowTestData } from '../../../../../../../test/nodes/types'; +import { executeWorkflow } from '../../../../../../../test/nodes/ExecuteWorkflow'; + +import * as transport from '../../../../v2/transport'; + +import nock from 'nock'; + +jest.mock('../../../../v2/transport', () => { + const originalModule = jest.requireActual('../../../../v2/transport'); + return { + ...originalModule, + microsoftApiRequest: jest.fn(async function (method: string, resource: string) { + if (method === 'GET') { + return { + value: [ + { + name: 'id', + }, + { + name: 'name', + }, + { + name: 'age', + }, + { + name: 'data', + }, + ], + }; + } + if (method === 'POST' && resource.includes('createSession')) { + return { + id: 12345, + }; + } + if (method === 'POST' && resource.includes('add')) { + return { + index: 3, + values: [[3, 'Donald', 99, 'data 5']], + }; + } + if (method === 'POST' && resource.includes('closeSession')) { + return; + } + }), + }; +}); + +describe('Test MicrosoftExcelV2, table => append', () => { + const workflows = ['nodes/Microsoft/Excel/test/v2/node/table/append.workflow.json']; + const tests = workflowToTests(workflows); + + beforeAll(() => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + jest.unmock('../../../../v2/transport'); + }); + + const nodeTypes = setup(tests); + + const testNode = async (testData: WorkflowTestData, types: INodeTypes) => { + const { result } = await executeWorkflow(testData, types); + + const resultNodeData = getResultNodeData(result, testData); + + resultNodeData.forEach(({ nodeName, resultData }) => { + return expect(resultData).toEqual(testData.output.nodeData[nodeName]); + }); + + expect(transport.microsoftApiRequest).toHaveBeenCalledTimes(4); + + expect(transport.microsoftApiRequest).toHaveBeenCalledWith( + 'POST', + '/drive/items/01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I/workbook/worksheets/{A0883CFE-D27E-4ECC-B94B-981830AAD55B}/tables/{317CA469-7D1C-4A5D-9B0B-424444BF0336}/rows/add', + { values: [['3', 'Donald', '99', 'data 5']] }, + {}, + '', + { 'workbook-session-id': 12345 }, + ); + + expect(result.finished).toEqual(true); + }; + + for (const testData of tests) { + test(testData.description, async () => testNode(testData, nodeTypes)); + } +}); diff --git a/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/append.workflow.json b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/append.workflow.json new file mode 100644 index 0000000000000..1a69bac4dd087 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/append.workflow.json @@ -0,0 +1,104 @@ +{ + "name": "My workflow 5", + "nodes": [ + { + "parameters": {}, + "id": "875e8784-eb59-40d8-ba45-129a5e29881c", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [380, 140] + }, + { + "parameters": { + "resource": "table", + "workbook": { + "__rl": true, + "value": "01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I", + "mode": "list", + "cachedResultName": "ŠŠ Š Š ŠŗŠ¾ŠæŃŃ", + "cachedResultUrl": "https://5w1hb7-my.sharepoint.com/personal/michaeldevsandbox_5w1hb7_onmicrosoft_com/_layouts/15/Doc.aspx?sourcedoc=%7BECC4041C-3AB6-4CF7-B079-0926470A1388%7D&file=%D0%9F%D0%A0%D0%A0%D0%9E%20%D0%BA%D0%BE%D0%BF%D1%96%D1%8F.xlsx&action=default&mobileredirect=true&DefaultItemOpen=1" + }, + "worksheet": { + "__rl": true, + "value": "{A0883CFE-D27E-4ECC-B94B-981830AAD55B}", + "mode": "list", + "cachedResultName": "Sheet4", + "cachedResultUrl": "https://5w1hb7-my.sharepoint.com/personal/michaeldevsandbox_5w1hb7_onmicrosoft_com/_layouts/15/Doc.aspx?sourcedoc=%7BECC4041C-3AB6-4CF7-B079-0926470A1388%7D&file=%D0%9F%D0%A0%D0%A0%D0%9E%20%D0%BA%D0%BE%D0%BF%D1%96%D1%8F.xlsx&action=default&mobileredirect=true&DefaultItemOpen=1&activeCell=Sheet4!A1" + }, + "table": { + "__rl": true, + "value": "{317CA469-7D1C-4A5D-9B0B-424444BF0336}", + "mode": "list", + "cachedResultName": "Table3", + "cachedResultUrl": "https://5w1hb7-my.sharepoint.com/personal/michaeldevsandbox_5w1hb7_onmicrosoft_com/_layouts/15/Doc.aspx?sourcedoc=%7BECC4041C-3AB6-4CF7-B079-0926470A1388%7D&file=%D0%9F%D0%A0%D0%A0%D0%9E%20%D0%BA%D0%BE%D0%BF%D1%96%D1%8F.xlsx&action=default&mobileredirect=true&DefaultItemOpen=1&activeCell=Sheet4!A1:D4" + }, + "fieldsUi": { + "values": [ + { + "column": "id", + "fieldValue": "3" + }, + { + "column": "name", + "fieldValue": "Donald" + }, + { + "column": "age", + "fieldValue": "99" + }, + { + "column": "data", + "fieldValue": "data 5" + } + ] + }, + "options": {} + }, + "id": "0e0ac1d2-242c-486a-9287-c70307645acc", + "name": "Microsoft Excel 365", + "type": "n8n-nodes-base.microsoftExcel", + "typeVersion": 2, + "position": [860, 140], + "credentials": { + "microsoftExcelOAuth2Api": { + "id": "70", + "name": "Microsoft Excel account" + } + } + } + ], + "pinData": { + "Microsoft Excel 365": [ + { + "json": { + "id": 3, + "name": "Donald", + "age": 99, + "data": "data 5" + } + } + ] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Microsoft Excel 365", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": {}, + "versionId": "b9eda2d8-e1a5-4a54-aaa9-5e81adaae909", + "id": "135", + "meta": { + "instanceId": "36203ea1ce3cef713fa25999bd9874ae26b9e4c2c3a90a365f2882a154d031d0" + }, + "tags": [] +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/convertToRange.test.ts b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/convertToRange.test.ts new file mode 100644 index 0000000000000..0c100f20526cd --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/convertToRange.test.ts @@ -0,0 +1,68 @@ +import type { INodeTypes } from 'n8n-workflow'; + +import { getResultNodeData, setup, workflowToTests } from '../../../../../../../test/nodes/Helpers'; +import type { WorkflowTestData } from '../../../../../../../test/nodes/types'; +import { executeWorkflow } from '../../../../../../../test/nodes/ExecuteWorkflow'; + +import * as transport from '../../../../v2/transport'; + +import nock from 'nock'; + +jest.mock('../../../../v2/transport', () => { + const originalModule = jest.requireActual('../../../../v2/transport'); + return { + ...originalModule, + microsoftApiRequest: jest.fn(async function (method: string) { + if (method === 'POST') { + return { + address: 'Sheet4!A1:D5', + values: [ + ['id', 'name', 'age', 'data'], + [1, 'Sam', 33, 'data 1'], + [2, 'Jon', 44, 'data 2'], + [3, 'Sam', 34, 'data 4'], + [3, 'Donald', 99, 'data 5'], + ], + }; + } + }), + }; +}); + +describe('Test MicrosoftExcelV2, table => convertToRange', () => { + const workflows = ['nodes/Microsoft/Excel/test/v2/node/table/convertToRange.workflow.json']; + const tests = workflowToTests(workflows); + + beforeAll(() => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + jest.unmock('../../../../v2/transport'); + }); + + const nodeTypes = setup(tests); + + const testNode = async (testData: WorkflowTestData, types: INodeTypes) => { + const { result } = await executeWorkflow(testData, types); + + const resultNodeData = getResultNodeData(result, testData); + + resultNodeData.forEach(({ nodeName, resultData }) => { + return expect(resultData).toEqual(testData.output.nodeData[nodeName]); + }); + + expect(transport.microsoftApiRequest).toHaveBeenCalledTimes(1); + expect(transport.microsoftApiRequest).toHaveBeenCalledWith( + 'POST', + '/drive/items/01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I/workbook/worksheets/{A0883CFE-D27E-4ECC-B94B-981830AAD55B}/tables/{6321EE4A-AC21-48AD-87D9-B527637D94B3}/convertToRange', + ); + + expect(result.finished).toEqual(true); + }; + + for (const testData of tests) { + test(testData.description, async () => testNode(testData, nodeTypes)); + } +}); diff --git a/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/convertToRange.workflow.json b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/convertToRange.workflow.json new file mode 100644 index 0000000000000..e5a02b985bbd4 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/convertToRange.workflow.json @@ -0,0 +1,79 @@ +{ + "name": "My workflow 5", + "nodes": [ + { + "parameters": {}, + "id": "875e8784-eb59-40d8-ba45-129a5e29881c", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [580, 140] + }, + { + "parameters": { + "resource": "table", + "operation": "convertToRange", + "workbook": { + "__rl": true, + "value": "01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I", + "mode": "list", + "cachedResultName": "ŠŠ Š Š ŠŗŠ¾ŠæŃŃ" + }, + "worksheet": { + "__rl": true, + "value": "{A0883CFE-D27E-4ECC-B94B-981830AAD55B}", + "mode": "list", + "cachedResultName": "Sheet4" + }, + "table": { + "__rl": true, + "value": "{6321EE4A-AC21-48AD-87D9-B527637D94B3}", + "mode": "list", + "cachedResultName": "Table3" + } + }, + "id": "0e0ac1d2-242c-486a-9287-c70307645acc", + "name": "Microsoft Excel 365", + "type": "n8n-nodes-base.microsoftExcel", + "typeVersion": 2, + "position": [860, 140], + "credentials": { + "microsoftExcelOAuth2Api": { + "id": "70", + "name": "Microsoft Excel account" + } + } + } + ], + "pinData": { + "Microsoft Excel 365": [ + { + "json": { + "address": "Sheet4!A1:D5", + "values": [ + ["id", "name", "age", "data"], + [1, "Sam", 33, "data 1"], + [2, "Jon", 44, "data 2"], + [3, "Sam", 34, "data 4"], + [3, "Donald", 99, "data 5"] + ] + } + } + ] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Microsoft Excel 365", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": {} +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/deleteTable.test.ts b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/deleteTable.test.ts new file mode 100644 index 0000000000000..77d13225eb16f --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/deleteTable.test.ts @@ -0,0 +1,59 @@ +import type { INodeTypes } from 'n8n-workflow'; + +import { getResultNodeData, setup, workflowToTests } from '../../../../../../../test/nodes/Helpers'; +import type { WorkflowTestData } from '../../../../../../../test/nodes/types'; +import { executeWorkflow } from '../../../../../../../test/nodes/ExecuteWorkflow'; + +import * as transport from '../../../../v2/transport'; + +import nock from 'nock'; + +jest.mock('../../../../v2/transport', () => { + const originalModule = jest.requireActual('../../../../v2/transport'); + return { + ...originalModule, + microsoftApiRequest: jest.fn(async function (method: string) { + if (method === 'DELETE') { + return; + } + }), + }; +}); + +describe('Test MicrosoftExcelV2, table => deleteTable', () => { + const workflows = ['nodes/Microsoft/Excel/test/v2/node/table/deleteTable.workflow.json']; + const tests = workflowToTests(workflows); + + beforeAll(() => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + jest.unmock('../../../../v2/transport'); + }); + + const nodeTypes = setup(tests); + + const testNode = async (testData: WorkflowTestData, types: INodeTypes) => { + const { result } = await executeWorkflow(testData, types); + + const resultNodeData = getResultNodeData(result, testData); + + resultNodeData.forEach(({ nodeName, resultData }) => { + return expect(resultData).toEqual(testData.output.nodeData[nodeName]); + }); + + expect(transport.microsoftApiRequest).toHaveBeenCalledTimes(1); + expect(transport.microsoftApiRequest).toHaveBeenCalledWith( + 'DELETE', + '/drive/items/01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I/workbook/worksheets/{A0883CFE-D27E-4ECC-B94B-981830AAD55B}/tables/{92FBE3F5-3180-47EE-8549-40892C38DA7F}', + ); + + expect(result.finished).toEqual(true); + }; + + for (const testData of tests) { + test(testData.description, async () => testNode(testData, nodeTypes)); + } +}); diff --git a/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/deleteTable.workflow.json b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/deleteTable.workflow.json new file mode 100644 index 0000000000000..2a9c585e613d4 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/deleteTable.workflow.json @@ -0,0 +1,72 @@ +{ + "name": "My workflow 5", + "nodes": [ + { + "parameters": {}, + "id": "875e8784-eb59-40d8-ba45-129a5e29881c", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [580, 140] + }, + { + "parameters": { + "resource": "table", + "operation": "deleteTable", + "workbook": { + "__rl": true, + "value": "01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I", + "mode": "list", + "cachedResultName": "ŠŠ Š Š ŠŗŠ¾ŠæŃŃ" + }, + "worksheet": { + "__rl": true, + "value": "{A0883CFE-D27E-4ECC-B94B-981830AAD55B}", + "mode": "list", + "cachedResultName": "Sheet4" + }, + "table": { + "__rl": true, + "value": "{92FBE3F5-3180-47EE-8549-40892C38DA7F}", + "mode": "list", + "cachedResultName": "Table3" + } + }, + "id": "0e0ac1d2-242c-486a-9287-c70307645acc", + "name": "Microsoft Excel 365", + "type": "n8n-nodes-base.microsoftExcel", + "typeVersion": 2, + "position": [860, 140], + "credentials": { + "microsoftExcelOAuth2Api": { + "id": "70", + "name": "Microsoft Excel account" + } + } + } + ], + "pinData": { + "Microsoft Excel 365": [ + { + "json": { + "success": true + } + } + ] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Microsoft Excel 365", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": {} +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/getColumns.test.ts b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/getColumns.test.ts new file mode 100644 index 0000000000000..148dc44f9d23a --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/getColumns.test.ts @@ -0,0 +1,73 @@ +import type { INodeTypes } from 'n8n-workflow'; + +import { getResultNodeData, setup, workflowToTests } from '../../../../../../../test/nodes/Helpers'; +import type { WorkflowTestData } from '../../../../../../../test/nodes/types'; +import { executeWorkflow } from '../../../../../../../test/nodes/ExecuteWorkflow'; + +import * as transport from '../../../../v2/transport'; + +import nock from 'nock'; + +jest.mock('../../../../v2/transport', () => { + const originalModule = jest.requireActual('../../../../v2/transport'); + return { + ...originalModule, + microsoftApiRequestAllItemsSkip: jest.fn(async function () { + return [ + { + name: 'country', + }, + { + name: 'browser', + }, + { + name: 'session_duration', + }, + { + name: 'visits', + }, + ]; + }), + }; +}); + +describe('Test MicrosoftExcelV2, table => getColumns', () => { + const workflows = ['nodes/Microsoft/Excel/test/v2/node/table/getColumns.workflow.json']; + const tests = workflowToTests(workflows); + + beforeAll(() => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + jest.unmock('../../../../v2/transport'); + }); + + const nodeTypes = setup(tests); + + const testNode = async (testData: WorkflowTestData, types: INodeTypes) => { + const { result } = await executeWorkflow(testData, types); + + const resultNodeData = getResultNodeData(result, testData); + + resultNodeData.forEach(({ nodeName, resultData }) => { + return expect(resultData).toEqual(testData.output.nodeData[nodeName]); + }); + + expect(transport.microsoftApiRequestAllItemsSkip).toHaveBeenCalledTimes(1); + expect(transport.microsoftApiRequestAllItemsSkip).toHaveBeenCalledWith( + 'value', + 'GET', + '/drive/items/01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I/workbook/worksheets/{00000000-0001-0000-0000-000000000000}/tables/{613E8967-D581-44ED-81D3-82A01AA6A05C}/columns', + {}, + {}, + ); + + expect(result.finished).toEqual(true); + }; + + for (const testData of tests) { + test(testData.description, async () => testNode(testData, nodeTypes)); + } +}); diff --git a/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/getColumns.workflow.json b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/getColumns.workflow.json new file mode 100644 index 0000000000000..ec2094099a147 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/getColumns.workflow.json @@ -0,0 +1,88 @@ +{ + "name": "My workflow 5", + "nodes": [ + { + "parameters": {}, + "id": "875e8784-eb59-40d8-ba45-129a5e29881c", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [580, 140] + }, + { + "parameters": { + "resource": "table", + "operation": "getColumns", + "workbook": { + "__rl": true, + "value": "01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I", + "mode": "list", + "cachedResultName": "ŠŠ Š Š ŠŗŠ¾ŠæŃŃ" + }, + "worksheet": { + "__rl": true, + "value": "{00000000-0001-0000-0000-000000000000}", + "mode": "list", + "cachedResultName": "Sheet1" + }, + "table": { + "__rl": true, + "value": "{613E8967-D581-44ED-81D3-82A01AA6A05C}", + "mode": "list", + "cachedResultName": "Table1" + }, + "returnAll": true + }, + "id": "0e0ac1d2-242c-486a-9287-c70307645acc", + "name": "Microsoft Excel 365", + "type": "n8n-nodes-base.microsoftExcel", + "typeVersion": 2, + "position": [860, 140], + "credentials": { + "microsoftExcelOAuth2Api": { + "id": "70", + "name": "Microsoft Excel account" + } + } + } + ], + "pinData": { + "Microsoft Excel 365": [ + { + "json": { + "name": "country" + } + }, + { + "json": { + "name": "browser" + } + }, + { + "json": { + "name": "session_duration" + } + }, + { + "json": { + "name": "visits" + } + } + ] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Microsoft Excel 365", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": {} +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/getRows.test.ts b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/getRows.test.ts new file mode 100644 index 0000000000000..1ab8a1f39c01a --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/getRows.test.ts @@ -0,0 +1,97 @@ +import type { INodeTypes } from 'n8n-workflow'; + +import { getResultNodeData, setup, workflowToTests } from '../../../../../../../test/nodes/Helpers'; +import type { WorkflowTestData } from '../../../../../../../test/nodes/types'; +import { executeWorkflow } from '../../../../../../../test/nodes/ExecuteWorkflow'; + +import * as transport from '../../../../v2/transport'; + +import nock from 'nock'; + +jest.mock('../../../../v2/transport', () => { + const originalModule = jest.requireActual('../../../../v2/transport'); + return { + ...originalModule, + microsoftApiRequest: jest.fn(async function (method: string) { + if (method === 'GET') { + return { + value: [ + { + index: 0, + values: [['uk', 'firefox', 1, 1]], + }, + { + index: 1, + values: [['us', 'chrome', 1, 12]], + }, + ], + }; + } + }), + microsoftApiRequestAllItemsSkip: jest.fn(async function () { + return [ + { + name: 'country', + }, + { + name: 'browser', + }, + { + name: 'session_duration', + }, + { + name: 'visits', + }, + ]; + }), + }; +}); + +describe('Test MicrosoftExcelV2, table => getRows', () => { + const workflows = ['nodes/Microsoft/Excel/test/v2/node/table/getRows.workflow.json']; + const tests = workflowToTests(workflows); + + beforeAll(() => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + jest.unmock('../../../../v2/transport'); + }); + + const nodeTypes = setup(tests); + + const testNode = async (testData: WorkflowTestData, types: INodeTypes) => { + const { result } = await executeWorkflow(testData, types); + + const resultNodeData = getResultNodeData(result, testData); + + resultNodeData.forEach(({ nodeName, resultData }) => { + return expect(resultData).toEqual(testData.output.nodeData[nodeName]); + }); + + expect(transport.microsoftApiRequest).toHaveBeenCalledTimes(1); + expect(transport.microsoftApiRequest).toHaveBeenCalledWith( + 'GET', + '/drive/items/01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I/workbook/worksheets/{00000000-0001-0000-0000-000000000000}/tables/{613E8967-D581-44ED-81D3-82A01AA6A05C}/rows', + {}, + { $top: 2 }, + ); + + expect(transport.microsoftApiRequestAllItemsSkip).toHaveBeenCalledTimes(1); + expect(transport.microsoftApiRequestAllItemsSkip).toHaveBeenCalledWith( + 'value', + 'GET', + '/drive/items/01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I/workbook/worksheets/{00000000-0001-0000-0000-000000000000}/tables/{613E8967-D581-44ED-81D3-82A01AA6A05C}/columns', + {}, + { $select: 'name' }, + ); + + expect(result.finished).toEqual(true); + }; + + for (const testData of tests) { + test(testData.description, async () => testNode(testData, nodeTypes)); + } +}); diff --git a/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/getRows.workflow.json b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/getRows.workflow.json new file mode 100644 index 0000000000000..77836894d1401 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/getRows.workflow.json @@ -0,0 +1,85 @@ +{ + "name": "My workflow 5", + "nodes": [ + { + "parameters": {}, + "id": "875e8784-eb59-40d8-ba45-129a5e29881c", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [580, 140] + }, + { + "parameters": { + "resource": "table", + "operation": "getRows", + "workbook": { + "__rl": true, + "value": "01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I", + "mode": "list", + "cachedResultName": "ŠŠ Š Š ŠŗŠ¾ŠæŃŃ" + }, + "worksheet": { + "__rl": true, + "value": "{00000000-0001-0000-0000-000000000000}", + "mode": "list", + "cachedResultName": "Sheet1" + }, + "table": { + "__rl": true, + "value": "{613E8967-D581-44ED-81D3-82A01AA6A05C}", + "mode": "list", + "cachedResultName": "Table1" + }, + "limit": 2, + "filters": {} + }, + "id": "0e0ac1d2-242c-486a-9287-c70307645acc", + "name": "Microsoft Excel 365", + "type": "n8n-nodes-base.microsoftExcel", + "typeVersion": 2, + "position": [860, 140], + "credentials": { + "microsoftExcelOAuth2Api": { + "id": "70", + "name": "Microsoft Excel account" + } + } + } + ], + "pinData": { + "Microsoft Excel 365": [ + { + "json": { + "country": "uk", + "browser": "firefox", + "session_duration": 1, + "visits": 1 + } + }, + { + "json": { + "country": "us", + "browser": "chrome", + "session_duration": 1, + "visits": 12 + } + } + ] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Microsoft Excel 365", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": {} +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/lookup.test.ts b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/lookup.test.ts new file mode 100644 index 0000000000000..17785d65ef5f2 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/lookup.test.ts @@ -0,0 +1,114 @@ +import type { INodeTypes } from 'n8n-workflow'; + +import { getResultNodeData, setup, workflowToTests } from '../../../../../../../test/nodes/Helpers'; +import type { WorkflowTestData } from '../../../../../../../test/nodes/types'; +import { executeWorkflow } from '../../../../../../../test/nodes/ExecuteWorkflow'; + +import * as transport from '../../../../v2/transport'; + +import nock from 'nock'; + +jest.mock('../../../../v2/transport', () => { + const originalModule = jest.requireActual('../../../../v2/transport'); + return { + ...originalModule, + microsoftApiRequestAllItemsSkip: jest.fn(async function ( + _property: string, + _method: string, + endpoint: string, + ) { + if (endpoint.includes('columns')) { + return [ + { + name: 'country', + }, + { + name: 'browser', + }, + { + name: 'session_duration', + }, + { + name: 'visits', + }, + ]; + } + if (endpoint.includes('rows')) { + return [ + { + index: 0, + values: [['uk', 'firefox', 1, 1]], + }, + { + index: 1, + values: [['us', 'chrome', 1, 12]], + }, + { + index: 2, + values: [['test', 'test', 55, 123]], + }, + { + index: 3, + values: [['ua', 'chrome', 1, 3]], + }, + { + index: 4, + values: [['ua', 'firefox', 1, 4]], + }, + { + index: 5, + values: [['uk', 'chrome', 1, 55]], + }, + ]; + } + }), + }; +}); + +describe('Test MicrosoftExcelV2, table => lookup', () => { + const workflows = ['nodes/Microsoft/Excel/test/v2/node/table/lookup.workflow.json']; + const tests = workflowToTests(workflows); + + beforeAll(() => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + jest.unmock('../../../../v2/transport'); + }); + + const nodeTypes = setup(tests); + + const testNode = async (testData: WorkflowTestData, types: INodeTypes) => { + const { result } = await executeWorkflow(testData, types); + + const resultNodeData = getResultNodeData(result, testData); + + resultNodeData.forEach(({ nodeName, resultData }) => { + return expect(resultData).toEqual(testData.output.nodeData[nodeName]); + }); + + expect(transport.microsoftApiRequestAllItemsSkip).toHaveBeenCalledTimes(2); + expect(transport.microsoftApiRequestAllItemsSkip).toHaveBeenCalledWith( + 'value', + 'GET', + '/drive/items/01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I/workbook/worksheets/{00000000-0001-0000-0000-000000000000}/tables/{613E8967-D581-44ED-81D3-82A01AA6A05C}/rows', + {}, + {}, + ); + expect(transport.microsoftApiRequestAllItemsSkip).toHaveBeenCalledWith( + 'value', + 'GET', + '/drive/items/01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I/workbook/worksheets/{00000000-0001-0000-0000-000000000000}/tables/{613E8967-D581-44ED-81D3-82A01AA6A05C}/columns', + {}, + { $select: 'name' }, + ); + + expect(result.finished).toEqual(true); + }; + + for (const testData of tests) { + test(testData.description, async () => testNode(testData, nodeTypes)); + } +}); diff --git a/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/lookup.workflow.json b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/lookup.workflow.json new file mode 100644 index 0000000000000..339c927fdc77a --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/table/lookup.workflow.json @@ -0,0 +1,88 @@ +{ + "name": "My workflow 5", + "nodes": [ + { + "parameters": {}, + "id": "875e8784-eb59-40d8-ba45-129a5e29881c", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [580, 140] + }, + { + "parameters": { + "resource": "table", + "operation": "lookup", + "workbook": { + "__rl": true, + "value": "01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I", + "mode": "list", + "cachedResultName": "ŠŠ Š Š ŠŗŠ¾ŠæŃŃ" + }, + "worksheet": { + "__rl": true, + "value": "{00000000-0001-0000-0000-000000000000}", + "mode": "list", + "cachedResultName": "Sheet1" + }, + "table": { + "__rl": true, + "value": "{613E8967-D581-44ED-81D3-82A01AA6A05C}", + "mode": "list", + "cachedResultName": "Table1" + }, + "lookupColumn": "country", + "lookupValue": "uk", + "options": { + "returnAllMatches": true + } + }, + "id": "0e0ac1d2-242c-486a-9287-c70307645acc", + "name": "Microsoft Excel 365", + "type": "n8n-nodes-base.microsoftExcel", + "typeVersion": 2, + "position": [860, 140], + "credentials": { + "microsoftExcelOAuth2Api": { + "id": "70", + "name": "Microsoft Excel account" + } + } + } + ], + "pinData": { + "Microsoft Excel 365": [ + { + "json": { + "country": "uk", + "browser": "firefox", + "session_duration": 1, + "visits": 1 + } + }, + { + "json": { + "country": "uk", + "browser": "chrome", + "session_duration": 1, + "visits": 55 + } + } + ] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Microsoft Excel 365", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": {} +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/workbook/addWorksheet.test.ts b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/workbook/addWorksheet.test.ts new file mode 100644 index 0000000000000..b6eea1cb54f01 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/workbook/addWorksheet.test.ts @@ -0,0 +1,89 @@ +import type { INodeTypes } from 'n8n-workflow'; + +import { getResultNodeData, setup, workflowToTests } from '../../../../../../../test/nodes/Helpers'; +import type { WorkflowTestData } from '../../../../../../../test/nodes/types'; +import { executeWorkflow } from '../../../../../../../test/nodes/ExecuteWorkflow'; + +import * as transport from '../../../../v2/transport'; + +import nock from 'nock'; + +jest.mock('../../../../v2/transport', () => { + const originalModule = jest.requireActual('../../../../v2/transport'); + return { + ...originalModule, + microsoftApiRequest: jest.fn(async function (method: string, resource: string) { + if (method === 'POST' && resource.includes('createSession')) { + return { + id: 12345, + }; + } + if (method === 'POST' && resource.includes('add')) { + return { + id: '{266ADAB7-25B6-4F28-A2D1-FD5BFBD7A4F0}', + name: 'Sheet42', + position: 8, + visibility: 'Visible', + }; + } + if (method === 'POST' && resource.includes('closeSession')) { + return; + } + }), + }; +}); + +describe('Test MicrosoftExcelV2, workbook => addWorksheet', () => { + const workflows = ['nodes/Microsoft/Excel/test/v2/node/workbook/addWorksheet.workflow.json']; + const tests = workflowToTests(workflows); + + beforeAll(() => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + jest.unmock('../../../../v2/transport'); + }); + + const nodeTypes = setup(tests); + + const testNode = async (testData: WorkflowTestData, types: INodeTypes) => { + const { result } = await executeWorkflow(testData, types); + + const resultNodeData = getResultNodeData(result, testData); + + resultNodeData.forEach(({ nodeName, resultData }) => { + return expect(resultData).toEqual(testData.output.nodeData[nodeName]); + }); + + expect(transport.microsoftApiRequest).toHaveBeenCalledTimes(3); + expect(transport.microsoftApiRequest).toHaveBeenCalledWith( + 'POST', + '/drive/items/01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I/workbook/createSession', + { persistChanges: true }, + ); + expect(transport.microsoftApiRequest).toHaveBeenCalledWith( + 'POST', + '/drive/items/01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I/workbook/worksheets/add', + { name: 'Sheet42' }, + {}, + '', + { 'workbook-session-id': 12345 }, + ); + expect(transport.microsoftApiRequest).toHaveBeenCalledWith( + 'POST', + '/drive/items/01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I/workbook/closeSession', + {}, + {}, + '', + { 'workbook-session-id': 12345 }, + ); + + expect(result.finished).toEqual(true); + }; + + for (const testData of tests) { + test(testData.description, async () => testNode(testData, nodeTypes)); + } +}); diff --git a/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/workbook/addWorksheet.workflow.json b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/workbook/addWorksheet.workflow.json new file mode 100644 index 0000000000000..0744375981399 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/workbook/addWorksheet.workflow.json @@ -0,0 +1,65 @@ +{ + "name": "My workflow 5", + "nodes": [ + { + "parameters": {}, + "id": "875e8784-eb59-40d8-ba45-129a5e29881c", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [380, 140] + }, + { + "parameters": { + "operation": "addWorksheet", + "workbook": { + "__rl": true, + "value": "01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I", + "mode": "list", + "cachedResultName": "ŠŠ Š Š ŠŗŠ¾ŠæŃŃ" + }, + "additionalFields": { + "name": "Sheet42" + } + }, + "id": "0e0ac1d2-242c-486a-9287-c70307645acc", + "name": "Microsoft Excel 365", + "type": "n8n-nodes-base.microsoftExcel", + "typeVersion": 2, + "position": [860, 140], + "credentials": { + "microsoftExcelOAuth2Api": { + "id": "70", + "name": "Microsoft Excel account" + } + } + } + ], + "pinData": { + "Microsoft Excel 365": [ + { + "json": { + "id": "{266ADAB7-25B6-4F28-A2D1-FD5BFBD7A4F0}", + "name": "Sheet42", + "position": 8, + "visibility": "Visible" + } + } + ] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Microsoft Excel 365", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": {} +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/workbook/deleteWorkbook.test.ts b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/workbook/deleteWorkbook.test.ts new file mode 100644 index 0000000000000..a4e63b1ebb4ab --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/workbook/deleteWorkbook.test.ts @@ -0,0 +1,59 @@ +import type { INodeTypes } from 'n8n-workflow'; + +import { getResultNodeData, setup, workflowToTests } from '../../../../../../../test/nodes/Helpers'; +import type { WorkflowTestData } from '../../../../../../../test/nodes/types'; +import { executeWorkflow } from '../../../../../../../test/nodes/ExecuteWorkflow'; + +import * as transport from '../../../../v2/transport'; + +import nock from 'nock'; + +jest.mock('../../../../v2/transport', () => { + const originalModule = jest.requireActual('../../../../v2/transport'); + return { + ...originalModule, + microsoftApiRequest: jest.fn(async function (method: string) { + if (method === 'DELETE') { + return; + } + }), + }; +}); + +describe('Test MicrosoftExcelV2, workbook => deleteWorkbook', () => { + const workflows = ['nodes/Microsoft/Excel/test/v2/node/workbook/deleteWorkbook.workflow.json']; + const tests = workflowToTests(workflows); + + beforeAll(() => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + jest.unmock('../../../../v2/transport'); + }); + + const nodeTypes = setup(tests); + + const testNode = async (testData: WorkflowTestData, types: INodeTypes) => { + const { result } = await executeWorkflow(testData, types); + + const resultNodeData = getResultNodeData(result, testData); + + resultNodeData.forEach(({ nodeName, resultData }) => { + return expect(resultData).toEqual(testData.output.nodeData[nodeName]); + }); + + expect(transport.microsoftApiRequest).toHaveBeenCalledTimes(1); + expect(transport.microsoftApiRequest).toHaveBeenCalledWith( + 'DELETE', + '/drive/items/01FUWX3BXJLISGF2CFWBGYPHXFCXPXOJUK', + ); + + expect(result.finished).toEqual(true); + }; + + for (const testData of tests) { + test(testData.description, async () => testNode(testData, nodeTypes)); + } +}); diff --git a/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/workbook/deleteWorkbook.workflow.json b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/workbook/deleteWorkbook.workflow.json new file mode 100644 index 0000000000000..1ac63b01748fa --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/workbook/deleteWorkbook.workflow.json @@ -0,0 +1,59 @@ +{ + "name": "My workflow 5", + "nodes": [ + { + "parameters": {}, + "id": "875e8784-eb59-40d8-ba45-129a5e29881c", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [380, 140] + }, + { + "parameters": { + "operation": "deleteWorkbook", + "workbook": { + "__rl": true, + "value": "01FUWX3BXJLISGF2CFWBGYPHXFCXPXOJUK", + "mode": "list", + "cachedResultName": "Book" + } + }, + "id": "0e0ac1d2-242c-486a-9287-c70307645acc", + "name": "Microsoft Excel 365", + "type": "n8n-nodes-base.microsoftExcel", + "typeVersion": 2, + "position": [860, 140], + "credentials": { + "microsoftExcelOAuth2Api": { + "id": "70", + "name": "Microsoft Excel account" + } + } + } + ], + "pinData": { + "Microsoft Excel 365": [ + { + "json": { + "success": true + } + } + ] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Microsoft Excel 365", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": {} +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/workbook/getAll.test.ts b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/workbook/getAll.test.ts new file mode 100644 index 0000000000000..a78e3b1d9e5c5 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/workbook/getAll.test.ts @@ -0,0 +1,74 @@ +import type { INodeTypes } from 'n8n-workflow'; + +import { getResultNodeData, setup, workflowToTests } from '../../../../../../../test/nodes/Helpers'; +import type { WorkflowTestData } from '../../../../../../../test/nodes/types'; +import { executeWorkflow } from '../../../../../../../test/nodes/ExecuteWorkflow'; + +import * as transport from '../../../../v2/transport'; + +import nock from 'nock'; + +jest.mock('../../../../v2/transport', () => { + const originalModule = jest.requireActual('../../../../v2/transport'); + return { + ...originalModule, + microsoftApiRequest: jest.fn(async function (method: string) { + if (method === 'GET') { + return { + value: [ + { + '@odata.type': '#microsoft.graph.driveItem', + name: 'ŠŠ Š Š ŠŗŠ¾ŠæŃŃ.xlsx', + }, + , + { + '@odata.type': '#microsoft.graph.driveItem', + name: 'Book 3.xlsx', + }, + , + ], + }; + } + }), + }; +}); + +describe('Test MicrosoftExcelV2, workbook => getAll', () => { + const workflows = ['nodes/Microsoft/Excel/test/v2/node/workbook/getAll.workflow.json']; + const tests = workflowToTests(workflows); + + beforeAll(() => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + jest.unmock('../../../../v2/transport'); + }); + + const nodeTypes = setup(tests); + + const testNode = async (testData: WorkflowTestData, types: INodeTypes) => { + const { result } = await executeWorkflow(testData, types); + + const resultNodeData = getResultNodeData(result, testData); + + resultNodeData.forEach(({ nodeName, resultData }) => { + return expect(resultData).toEqual(testData.output.nodeData[nodeName]); + }); + + expect(transport.microsoftApiRequest).toHaveBeenCalledTimes(1); + expect(transport.microsoftApiRequest).toHaveBeenCalledWith( + 'GET', + "/drive/root/search(q='.xlsx')", + {}, + { $select: 'name', $top: 2 }, + ); + + expect(result.finished).toEqual(true); + }; + + for (const testData of tests) { + test(testData.description, async () => testNode(testData, nodeTypes)); + } +}); diff --git a/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/workbook/getAll.workflow.json b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/workbook/getAll.workflow.json new file mode 100644 index 0000000000000..5499173661842 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/workbook/getAll.workflow.json @@ -0,0 +1,63 @@ +{ + "name": "My workflow 5", + "nodes": [ + { + "parameters": {}, + "id": "875e8784-eb59-40d8-ba45-129a5e29881c", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [380, 140] + }, + { + "parameters": { + "limit": 2, + "filters": { + "fields": "name" + } + }, + "id": "0e0ac1d2-242c-486a-9287-c70307645acc", + "name": "Microsoft Excel 365", + "type": "n8n-nodes-base.microsoftExcel", + "typeVersion": 2, + "position": [860, 140], + "credentials": { + "microsoftExcelOAuth2Api": { + "id": "70", + "name": "Microsoft Excel account" + } + } + } + ], + "pinData": { + "Microsoft Excel 365": [ + { + "json": { + "@odata.type": "#microsoft.graph.driveItem", + "name": "ŠŠ Š Š ŠŗŠ¾ŠæŃŃ.xlsx" + } + }, + { + "json": { + "@odata.type": "#microsoft.graph.driveItem", + "name": "Book 3.xlsx" + } + } + ] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Microsoft Excel 365", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": {} +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/append.test.ts b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/append.test.ts new file mode 100644 index 0000000000000..108f278800de3 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/append.test.ts @@ -0,0 +1,68 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import type { IDataObject } from 'n8n-workflow'; + +import { equalityTest, setup, workflowToTests } from '../../../../../../../test/nodes/Helpers'; + +// eslint-disable-next-line unused-imports/no-unused-imports +import * as transport from '../../../../v2/transport'; + +import nock from 'nock'; + +jest.mock('../../../../v2/transport', () => { + const originalModule = jest.requireActual('../../../../v2/transport'); + return { + ...originalModule, + microsoftApiRequest: jest.fn(async function ( + method: string, + resource: string, + body?: IDataObject, + qs?: IDataObject, + uri?: string, + headers?: IDataObject, + ) { + if (method === 'GET' && resource.includes('usedRange')) { + return { + address: 'Sheet4!A1:D6', + values: [ + ['id', 'name', 'age', 'data'], + [1, 'Sam', 33, 'data 1'], + [2, 'Jon', 44, 'data 2'], + [3, 'Ron', 55, 'data 3'], + ], + }; + } + + if (method === 'PATCH' && resource.includes('{A0883CFE-D27E-4ECC-B94B-981830AAD55B}')) { + return { + values: [[4, 'Sam', 34, 'data 4']], + }; + } + + if (method === 'PATCH' && resource.includes('{426949D7-797F-43A9-A8A4-8FE283495A82}')) { + return { + values: [[4, 'Don', 37, 'data 44']], + }; + } + }), + }; +}); + +describe('Test MicrosoftExcelV2, worksheet => append', () => { + const workflows = ['nodes/Microsoft/Excel/test/v2/node/worksheet/append.workflow.json']; + const tests = workflowToTests(workflows); + + beforeAll(() => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + jest.unmock('../../../../v2/transport'); + }); + + const nodeTypes = setup(tests); + + for (const testData of tests) { + test(testData.description, async () => equalityTest(testData, nodeTypes)); + } +}); diff --git a/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/append.workflow.json b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/append.workflow.json new file mode 100644 index 0000000000000..73d2a1f2d9649 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/append.workflow.json @@ -0,0 +1,166 @@ +{ + "name": "microsoft excel 365 - tests", + "nodes": [ + { + "parameters": {}, + "id": "2e1ec8f6-a2e2-4aa9-909c-d0a279584131", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [800, 260] + }, + { + "parameters": { + "resource": "worksheet", + "operation": "append", + "workbook": { + "__rl": true, + "value": "01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I", + "mode": "list", + "cachedResultName": "ŠŠ Š Š ŠŗŠ¾ŠæŃŃ" + }, + "worksheet": { + "__rl": true, + "value": "={A0883CFE-D27E-4ECC-B94B-981830AAD55B}", + "mode": "id" + }, + "fieldsUi": { + "values": [ + { + "column": "id", + "fieldValue": "4" + }, + { + "column": "name", + "fieldValue": "Sam" + }, + { + "column": "age", + "fieldValue": "34" + }, + { + "column": "data", + "fieldValue": "data 4" + } + ] + }, + "options": {} + }, + "id": "86f2a240-3acf-45c2-b97f-63dd655d296b", + "name": "Microsoft Excel 365", + "type": "n8n-nodes-base.microsoftExcel", + "typeVersion": 2, + "position": [1280, 260], + "credentials": { + "microsoftExcelOAuth2Api": { + "id": "70", + "name": "Microsoft Excel account" + } + } + }, + { + "parameters": { + "resource": "worksheet", + "operation": "append", + "workbook": { + "__rl": true, + "value": "01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I", + "mode": "list", + "cachedResultName": "ŠŠ Š Š ŠŗŠ¾ŠæŃŃ" + }, + "worksheet": { + "__rl": true, + "value": "{426949D7-797F-43A9-A8A4-8FE283495A82}", + "mode": "list", + "cachedResultName": "Sheet5" + }, + "dataMode": "autoMap", + "options": {} + }, + "id": "531949d8-1ffa-4e1c-ae3e-032360b74f06", + "name": "Microsoft Excel 3651", + "type": "n8n-nodes-base.microsoftExcel", + "typeVersion": 2, + "position": [1280, 500], + "credentials": { + "microsoftExcelOAuth2Api": { + "id": "70", + "name": "Microsoft Excel account" + } + } + }, + { + "parameters": { + "jsCode": "return {\n id: 4,\n name: 'Don',\n age: 37,\n data: 'data 44',\n};" + }, + "id": "2919f9b9-e3ac-42cd-a792-774738fd2195", + "name": "Code", + "type": "n8n-nodes-base.code", + "typeVersion": 1, + "position": [1080, 500] + } + ], + "pinData": { + "Microsoft Excel 3651": [ + { + "json": { + "id": 4, + "name": "Don", + "age": 37, + "data": "data 44" + } + } + ], + "Microsoft Excel 365": [ + { + "json": { + "id": 4, + "name": "Sam", + "age": 34, + "data": "data 4" + } + } + ], + "Code": [ + { + "json": { + "id": 4, + "name": "Don", + "age": 37, + "data": "data 44" + } + } + ] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Microsoft Excel 365", + "type": "main", + "index": 0 + }, + { + "node": "Code", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code": { + "main": [ + [ + { + "node": "Microsoft Excel 3651", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": {} +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/clear.test.ts b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/clear.test.ts new file mode 100644 index 0000000000000..0cd232a074bd6 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/clear.test.ts @@ -0,0 +1,68 @@ +import type { INodeTypes } from 'n8n-workflow'; + +import { getResultNodeData, setup, workflowToTests } from '../../../../../../../test/nodes/Helpers'; +import type { WorkflowTestData } from '../../../../../../../test/nodes/types'; +import { executeWorkflow } from '../../../../../../../test/nodes/ExecuteWorkflow'; + +import * as transport from '../../../../v2/transport'; + +import nock from 'nock'; + +jest.mock('../../../../v2/transport', () => { + const originalModule = jest.requireActual('../../../../v2/transport'); + return { + ...originalModule, + microsoftApiRequest: jest.fn(async function (method: string) { + if (method === 'POST') { + return { + values: [ + { + json: { + success: true, + }, + }, + ], + }; + } + }), + }; +}); + +describe('Test MicrosoftExcelV2, worksheet => clear', () => { + const workflows = ['nodes/Microsoft/Excel/test/v2/node/worksheet/clear.workflow.json']; + const tests = workflowToTests(workflows); + + beforeAll(() => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + jest.unmock('../../../../v2/transport'); + }); + + const nodeTypes = setup(tests); + + const testNode = async (testData: WorkflowTestData, types: INodeTypes) => { + const { result } = await executeWorkflow(testData, types); + + const resultNodeData = getResultNodeData(result, testData); + + resultNodeData.forEach(({ nodeName, resultData }) => { + return expect(resultData).toEqual(testData.output.nodeData[nodeName]); + }); + + expect(transport.microsoftApiRequest).toHaveBeenCalledTimes(1); + expect(transport.microsoftApiRequest).toHaveBeenCalledWith( + 'POST', + '/drive/items/01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I/workbook/worksheets/{F7AF92FE-D42D-452F-8E4A-901B1D1EBF3F}/range/clear', + { applyTo: 'All' }, + ); + + expect(result.finished).toEqual(true); + }; + + for (const testData of tests) { + test(testData.description, async () => testNode(testData, nodeTypes)); + } +}); diff --git a/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/clear.workflow.json b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/clear.workflow.json new file mode 100644 index 0000000000000..0578a49a68da4 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/clear.workflow.json @@ -0,0 +1,66 @@ +{ + "name": "My workflow 5", + "nodes": [ + { + "parameters": {}, + "id": "f0857ec9-0709-4657-a2f4-059837c94060", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [540, 220] + }, + { + "parameters": { + "resource": "worksheet", + "operation": "clear", + "workbook": { + "__rl": true, + "value": "01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I", + "mode": "list", + "cachedResultName": "ŠŠ Š Š ŠŗŠ¾ŠæŃŃ" + }, + "worksheet": { + "__rl": true, + "value": "{F7AF92FE-D42D-452F-8E4A-901B1D1EBF3F}", + "mode": "list", + "cachedResultName": "Sheet2" + } + }, + "id": "426ed055-0c9b-4ae2-a9fe-a6cce875d5ee", + "name": "Microsoft Excel 365", + "type": "n8n-nodes-base.microsoftExcel", + "typeVersion": 2, + "position": [1020, 220], + "credentials": { + "microsoftExcelOAuth2Api": { + "id": "70", + "name": "Microsoft Excel account" + } + } + } + ], + "pinData": { + "Microsoft Excel 365": [ + { + "json": { + "success": true + } + } + ] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Microsoft Excel 365", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": {} +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/deleteWorksheet.test.ts b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/deleteWorksheet.test.ts new file mode 100644 index 0000000000000..0b12ff7c59c6a --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/deleteWorksheet.test.ts @@ -0,0 +1,67 @@ +import type { INodeTypes } from 'n8n-workflow'; + +import { getResultNodeData, setup, workflowToTests } from '../../../../../../../test/nodes/Helpers'; +import type { WorkflowTestData } from '../../../../../../../test/nodes/types'; +import { executeWorkflow } from '../../../../../../../test/nodes/ExecuteWorkflow'; + +import * as transport from '../../../../v2/transport'; + +import nock from 'nock'; + +jest.mock('../../../../v2/transport', () => { + const originalModule = jest.requireActual('../../../../v2/transport'); + return { + ...originalModule, + microsoftApiRequest: jest.fn(async function (method: string) { + if (method === 'DELETE') { + return { + values: [ + { + json: { + success: true, + }, + }, + ], + }; + } + }), + }; +}); + +describe('Test MicrosoftExcelV2, worksheet => deleteWorksheet', () => { + const workflows = ['nodes/Microsoft/Excel/test/v2/node/worksheet/deleteWorksheet.workflow.json']; + const tests = workflowToTests(workflows); + + beforeAll(() => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + jest.unmock('../../../../v2/transport'); + }); + + const nodeTypes = setup(tests); + + const testNode = async (testData: WorkflowTestData, types: INodeTypes) => { + const { result } = await executeWorkflow(testData, types); + + const resultNodeData = getResultNodeData(result, testData); + + resultNodeData.forEach(({ nodeName, resultData }) => { + return expect(resultData).toEqual(testData.output.nodeData[nodeName]); + }); + + expect(transport.microsoftApiRequest).toHaveBeenCalledTimes(1); + expect(transport.microsoftApiRequest).toHaveBeenCalledWith( + 'DELETE', + '/drive/items/01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I/workbook/worksheets/{88D9C37A-4180-4B23-8996-BF11F32EB63C}', + ); + + expect(result.finished).toEqual(true); + }; + + for (const testData of tests) { + test(testData.description, async () => testNode(testData, nodeTypes)); + } +}); diff --git a/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/deleteWorksheet.workflow.json b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/deleteWorksheet.workflow.json new file mode 100644 index 0000000000000..1446f9a89a937 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/deleteWorksheet.workflow.json @@ -0,0 +1,66 @@ +{ + "name": "My workflow 5", + "nodes": [ + { + "parameters": {}, + "id": "f0857ec9-0709-4657-a2f4-059837c94060", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [540, 220] + }, + { + "parameters": { + "resource": "worksheet", + "operation": "deleteWorksheet", + "workbook": { + "__rl": true, + "value": "01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I", + "mode": "list", + "cachedResultName": "ŠŠ Š Š ŠŗŠ¾ŠæŃŃ" + }, + "worksheet": { + "__rl": true, + "value": "{88D9C37A-4180-4B23-8996-BF11F32EB63C}", + "mode": "list", + "cachedResultName": "188" + } + }, + "id": "426ed055-0c9b-4ae2-a9fe-a6cce875d5ee", + "name": "Microsoft Excel 365", + "type": "n8n-nodes-base.microsoftExcel", + "typeVersion": 2, + "position": [1020, 220], + "credentials": { + "microsoftExcelOAuth2Api": { + "id": "70", + "name": "Microsoft Excel account" + } + } + } + ], + "pinData": { + "Microsoft Excel 365": [ + { + "json": { + "success": true + } + } + ] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Microsoft Excel 365", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": {} +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/getAll.test.ts b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/getAll.test.ts new file mode 100644 index 0000000000000..a045e8675e716 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/getAll.test.ts @@ -0,0 +1,76 @@ +import type { INodeTypes } from 'n8n-workflow'; + +import { getResultNodeData, setup, workflowToTests } from '../../../../../../../test/nodes/Helpers'; +import type { WorkflowTestData } from '../../../../../../../test/nodes/types'; +import { executeWorkflow } from '../../../../../../../test/nodes/ExecuteWorkflow'; + +import * as transport from '../../../../v2/transport'; + +import nock from 'nock'; + +jest.mock('../../../../v2/transport', () => { + const originalModule = jest.requireActual('../../../../v2/transport'); + return { + ...originalModule, + microsoftApiRequest: jest.fn(async function (method: string) { + if (method === 'GET') { + return { + value: [ + { + id: '{00000000-0001-0000-0000-000000000000}', + name: 'Sheet1', + }, + { + id: '{F7AF92FE-D42D-452F-8E4A-901B1D1EBF3F}', + name: 'Sheet2', + }, + { + id: '{BF7BD843-4912-4B81-A0AC-4FBBC2783E20}', + name: 'foo2', + }, + ], + }; + } + }), + }; +}); + +describe('Test MicrosoftExcelV2, worksheet => getAll', () => { + const workflows = ['nodes/Microsoft/Excel/test/v2/node/worksheet/getAll.workflow.json']; + const tests = workflowToTests(workflows); + + beforeAll(() => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + jest.unmock('../../../../v2/transport'); + }); + + const nodeTypes = setup(tests); + + const testNode = async (testData: WorkflowTestData, types: INodeTypes) => { + const { result } = await executeWorkflow(testData, types); + + const resultNodeData = getResultNodeData(result, testData); + + resultNodeData.forEach(({ nodeName, resultData }) => { + return expect(resultData).toEqual(testData.output.nodeData[nodeName]); + }); + + expect(transport.microsoftApiRequest).toHaveBeenCalledTimes(1); + expect(transport.microsoftApiRequest).toHaveBeenCalledWith( + 'GET', + '/drive/items/01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I/workbook/worksheets', + {}, + { $select: 'name', $top: 3 }, + ); + + expect(result.finished).toEqual(true); + }; + + for (const testData of tests) { + test(testData.description, async () => testNode(testData, nodeTypes)); + } +}); diff --git a/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/getAll.workflow.json b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/getAll.workflow.json new file mode 100644 index 0000000000000..ef2f7bfe5a68c --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/getAll.workflow.json @@ -0,0 +1,76 @@ +{ + "name": "My workflow 5", + "nodes": [ + { + "parameters": {}, + "id": "f0857ec9-0709-4657-a2f4-059837c94060", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [540, 220] + }, + { + "parameters": { + "resource": "worksheet", + "workbook": { + "__rl": true, + "value": "01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I", + "mode": "list", + "cachedResultName": "ŠŠ Š Š ŠŗŠ¾ŠæŃŃ" + }, + "limit": 3, + "filters": { + "fields": "name" + } + }, + "id": "426ed055-0c9b-4ae2-a9fe-a6cce875d5ee", + "name": "Microsoft Excel 365", + "type": "n8n-nodes-base.microsoftExcel", + "typeVersion": 2, + "position": [1020, 220], + "credentials": { + "microsoftExcelOAuth2Api": { + "id": "70", + "name": "Microsoft Excel account" + } + } + } + ], + "pinData": { + "Microsoft Excel 365": [ + { + "json": { + "id": "{00000000-0001-0000-0000-000000000000}", + "name": "Sheet1" + } + }, + { + "json": { + "id": "{F7AF92FE-D42D-452F-8E4A-901B1D1EBF3F}", + "name": "Sheet2" + } + }, + { + "json": { + "id": "{BF7BD843-4912-4B81-A0AC-4FBBC2783E20}", + "name": "foo2" + } + } + ] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Microsoft Excel 365", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": {} +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/readRows.test.ts b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/readRows.test.ts new file mode 100644 index 0000000000000..014ff8d658686 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/readRows.test.ts @@ -0,0 +1,55 @@ +import { equalityTest, setup, workflowToTests } from '../../../../../../../test/nodes/Helpers'; + +// eslint-disable-next-line unused-imports/no-unused-imports +import * as _transport from '../../../../v2/transport'; + +import nock from 'nock'; + +jest.mock('../../../../v2/transport', () => { + const originalModule = jest.requireActual('../../../../v2/transport'); + return { + ...originalModule, + microsoftApiRequest: jest.fn(async function (method: string, resource: string) { + { + if (method === 'GET' && resource.includes('usedRange')) { + return { + values: [ + ['id', 'name', 'age', 'data'], + [1, 'Sam', 33, 'data 1'], + [2, 'Jon', 44, 'data 2'], + [3, 'Ron', 55, 'data 3'], + ], + }; + } + + return { + values: [ + ['id', 'name', 'age', 'data'], + [1, 'Sam', 33, 'data 1'], + [2, 'Jon', 44, 'data 2'], + ], + }; + } + }), + }; +}); + +describe('Test MicrosoftExcelV2, worksheet => readRows', () => { + const workflows = ['nodes/Microsoft/Excel/test/v2/node/worksheet/readRows.workflow.json']; + const tests = workflowToTests(workflows); + + beforeAll(() => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + jest.unmock('../../../../v2/transport'); + }); + + const nodeTypes = setup(tests); + + for (const testData of tests) { + test(testData.description, async () => equalityTest(testData, nodeTypes)); + } +}); diff --git a/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/readRows.workflow.json b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/readRows.workflow.json new file mode 100644 index 0000000000000..8ea8e6bdb8a86 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/readRows.workflow.json @@ -0,0 +1,137 @@ +{ + "name": "microsoft excel 365 - read rows", + "nodes": [ + { + "parameters": {}, + "id": "2e1ec8f6-a2e2-4aa9-909c-d0a279584131", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [820, 380] + }, + { + "parameters": { + "resource": "worksheet", + "operation": "readRows", + "workbook": { + "__rl": true, + "value": "01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I", + "mode": "list", + "cachedResultName": "ŠŠ Š Š ŠŗŠ¾ŠæŃŃ" + }, + "worksheet": { + "__rl": true, + "value": "{A0883CFE-D27E-4ECC-B94B-981830AAD55B}", + "mode": "list", + "cachedResultName": "Sheet4" + }, + "options": {} + }, + "id": "86f2a240-3acf-45c2-b97f-63dd655d296b", + "name": "Microsoft Excel 365", + "type": "n8n-nodes-base.microsoftExcel", + "typeVersion": 2, + "position": [1100, 260], + "credentials": { + "microsoftExcelOAuth2Api": { + "id": "70", + "name": "Microsoft Excel account" + } + } + }, + { + "parameters": { + "resource": "worksheet", + "operation": "readRows", + "workbook": { + "__rl": true, + "value": "01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I", + "mode": "list", + "cachedResultName": "ŠŠ Š Š ŠŗŠ¾ŠæŃŃ", + "cachedResultUrl": "https://5w1hb7-my.sharepoint.com/personal/michaeldevsandbox_5w1hb7_onmicrosoft_com/_layouts/15/Doc.aspx?sourcedoc=%7BECC4041C-3AB6-4CF7-B079-0926470A1388%7D&file=%D0%9F%D0%A0%D0%A0%D0%9E%20%D0%BA%D0%BE%D0%BF%D1%96%D1%8F.xlsx&action=default&mobileredirect=true&DefaultItemOpen=1" + }, + "worksheet": { + "__rl": true, + "value": "{A0883CFE-D27E-4ECC-B94B-981830AAD55B}", + "mode": "list", + "cachedResultName": "Sheet4", + "cachedResultUrl": "https://5w1hb7-my.sharepoint.com/personal/michaeldevsandbox_5w1hb7_onmicrosoft_com/_layouts/15/Doc.aspx?sourcedoc=%7BECC4041C-3AB6-4CF7-B079-0926470A1388%7D&file=%D0%9F%D0%A0%D0%A0%D0%9E%20%D0%BA%D0%BE%D0%BF%D1%96%D1%8F.xlsx&action=default&mobileredirect=true&DefaultItemOpen=1&activeCell=Sheet4!A1" + }, + "useRange": true, + "range": "A1:D3", + "dataStartRow": 2, + "options": {} + }, + "id": "8ce6ab42-8f38-452b-90da-598d8a958c2b", + "name": "Microsoft Excel 3651", + "type": "n8n-nodes-base.microsoftExcel", + "typeVersion": 2, + "position": [1100, 520], + "credentials": { + "microsoftExcelOAuth2Api": { + "id": "70", + "name": "Microsoft Excel account" + } + } + } + ], + "pinData": { + "Microsoft Excel 365": [ + { + "json": { + "id": 1, + "name": "Sam", + "age": 33, + "data": "data 1" + } + }, + { + "json": { + "id": 2, + "name": "Jon", + "age": 44, + "data": "data 2" + } + }, + { + "json": { + "id": 3, + "name": "Ron", + "age": 55, + "data": "data 3" + } + } + ], + "Microsoft Excel 3651": [ + { + "json": { + "id": 2, + "name": "Jon", + "age": 44, + "data": "data 2" + } + } + ] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Microsoft Excel 365", + "type": "main", + "index": 0 + }, + { + "node": "Microsoft Excel 3651", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": {}, + "tags": [] +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/update.test.ts b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/update.test.ts new file mode 100644 index 0000000000000..ca062b7784c6b --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/update.test.ts @@ -0,0 +1,68 @@ +import { equalityTest, setup, workflowToTests } from '../../../../../../../test/nodes/Helpers'; + +// eslint-disable-next-line unused-imports/no-unused-imports +import * as _transport from '../../../../v2/transport'; + +import nock from 'nock'; + +jest.mock('../../../../v2/transport', () => { + const originalModule = jest.requireActual('../../../../v2/transport'); + return { + ...originalModule, + microsoftApiRequest: jest.fn(async function (method: string, resource: string) { + if (method === 'GET' && resource.includes('usedRange')) { + return { + address: 'Sheet4!A1:D6', + values: [ + ['id', 'name', 'age', 'data'], + [1, 'Sam', 33, 'data 1'], + [2, 'Jon', 44, 'data 2'], + [3, 'Ron', 55, 'data 3'], + ], + }; + } + + if (method === 'PATCH' && resource.includes('{A0883CFE-D27E-4ECC-B94B-981830AAD55B}')) { + return { + values: [ + ['id', 'name', 'age', 'data'], + [1, 'Sam', 33, 'data 1'], + [2, 'Jon', 44, 'data 2'], + [3, 'Sam', 34, 'data 4'], + ], + }; + } + + if (method === 'PATCH' && resource.includes('{426949D7-797F-43A9-A8A4-8FE283495A82}')) { + return { + values: [ + ['id', 'name', 'age', 'data'], + [1, 'Sam', 33, 'data 1'], + [2, 'Jon', 44, 'data 2'], + [3, 'Don', 37, 'data 44'], + ], + }; + } + }), + }; +}); + +describe('Test MicrosoftExcelV2, worksheet => update', () => { + const workflows = ['nodes/Microsoft/Excel/test/v2/node/worksheet/update.workflow.json']; + const tests = workflowToTests(workflows); + + beforeAll(() => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + jest.unmock('../../../../v2/transport'); + }); + + const nodeTypes = setup(tests); + + for (const testData of tests) { + test(testData.description, async () => equalityTest(testData, nodeTypes)); + } +}); diff --git a/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/update.workflow.json b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/update.workflow.json new file mode 100644 index 0000000000000..5c94f2b42e28b --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/update.workflow.json @@ -0,0 +1,154 @@ +{ + "name": "My workflow 5", + "nodes": [ + { + "parameters": {}, + "id": "875e8784-eb59-40d8-ba45-129a5e29881c", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [380, 140] + }, + { + "parameters": { + "resource": "worksheet", + "operation": "update", + "workbook": { + "__rl": true, + "value": "01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I", + "mode": "list", + "cachedResultName": "ŠŠ Š Š ŠŗŠ¾ŠæŃŃ" + }, + "worksheet": { + "__rl": true, + "value": "={A0883CFE-D27E-4ECC-B94B-981830AAD55B}", + "mode": "id" + }, + "columnToMatchOn": "id", + "valueToMatchOn": "3", + "fieldsUi": { + "values": [ + { + "column": "name", + "fieldValue": "Sam" + }, + { + "column": "age", + "fieldValue": "34" + }, + { + "column": "data", + "fieldValue": "data 4" + } + ] + }, + "options": {} + }, + "id": "0e0ac1d2-242c-486a-9287-c70307645acc", + "name": "Microsoft Excel 365", + "type": "n8n-nodes-base.microsoftExcel", + "typeVersion": 2, + "position": [860, 140], + "credentials": { + "microsoftExcelOAuth2Api": { + "id": "70", + "name": "Microsoft Excel account" + } + } + }, + { + "parameters": { + "resource": "worksheet", + "operation": "update", + "workbook": { + "__rl": true, + "value": "01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I", + "mode": "list", + "cachedResultName": "ŠŠ Š Š ŠŗŠ¾ŠæŃŃ" + }, + "worksheet": { + "__rl": true, + "value": "={426949D7-797F-43A9-A8A4-8FE283495A82}", + "mode": "id" + }, + "dataMode": "autoMap", + "columnToMatchOn": "id", + "options": {} + }, + "id": "d3209da3-cfaf-40a6-a318-c66c2931a28a", + "name": "Microsoft Excel 3651", + "type": "n8n-nodes-base.microsoftExcel", + "typeVersion": 2, + "position": [860, 380], + "credentials": { + "microsoftExcelOAuth2Api": { + "id": "70", + "name": "Microsoft Excel account" + } + } + }, + { + "parameters": { + "jsCode": "return {\n id: 3,\n name: 'Don',\n age: 37,\n data: 'data 44',\n};" + }, + "id": "eb908630-7324-46a5-890d-b5cfccf17cb2", + "name": "Code", + "type": "n8n-nodes-base.code", + "typeVersion": 1, + "position": [660, 380] + } + ], + "pinData": { + "Microsoft Excel 365": [ + { + "json": { + "id": 3, + "name": "Sam", + "age": 34, + "data": "data 4" + } + } + ], + "Microsoft Excel 3651": [ + { + "json": { + "id": 3, + "name": "Don", + "age": 37, + "data": "data 44" + } + } + ] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Microsoft Excel 365", + "type": "main", + "index": 0 + }, + { + "node": "Code", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code": { + "main": [ + [ + { + "node": "Microsoft Excel 3651", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": {} +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/upsert.test.ts b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/upsert.test.ts new file mode 100644 index 0000000000000..61351fd1ff635 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/upsert.test.ts @@ -0,0 +1,70 @@ +import { equalityTest, setup, workflowToTests } from '../../../../../../../test/nodes/Helpers'; + +// eslint-disable-next-line unused-imports/no-unused-imports +import * as _transport from '../../../../v2/transport'; + +import nock from 'nock'; + +jest.mock('../../../../v2/transport', () => { + const originalModule = jest.requireActual('../../../../v2/transport'); + return { + ...originalModule, + microsoftApiRequest: jest.fn(async function (method: string, resource: string) { + if (method === 'GET' && resource.includes('usedRange')) { + return { + address: 'Sheet4!A1:D6', + values: [ + ['id', 'name', 'age', 'data'], + [1, 'Sam', 33, 'data 1'], + [2, 'Jon', 44, 'data 2'], + [3, 'Ron', 55, 'data 3'], + ], + }; + } + + if (method === 'PATCH' && resource.includes('{A0883CFE-D27E-4ECC-B94B-981830AAD55B}')) { + return { + values: [ + ['id', 'name', 'age', 'data'], + [1, 'Sam', 33, 'data 1'], + [2, 'Jon', 44, 'data 2'], + [3, 'Ron', 55, 'data 3'], + [4, 'Sam', 34, 'data 4'], + ], + }; + } + + if (method === 'PATCH' && resource.includes('{426949D7-797F-43A9-A8A4-8FE283495A82}')) { + return { + values: [ + ['id', 'name', 'age', 'data'], + [1, 'Sam', 33, 'data 1'], + [2, 'Jon', 44, 'data 2'], + [3, 'Ron', 55, 'data 3'], + [4, 'Don', 37, 'data 44'], + ], + }; + } + }), + }; +}); + +describe('Test MicrosoftExcelV2, worksheet => upsert', () => { + const workflows = ['nodes/Microsoft/Excel/test/v2/node/worksheet/upsert.workflow.json']; + const tests = workflowToTests(workflows); + + beforeAll(() => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + jest.unmock('../../../../v2/transport'); + }); + + const nodeTypes = setup(tests); + + for (const testData of tests) { + test(testData.description, async () => equalityTest(testData, nodeTypes)); + } +}); diff --git a/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/upsert.workflow.json b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/upsert.workflow.json new file mode 100644 index 0000000000000..2e8a762c02984 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/node/worksheet/upsert.workflow.json @@ -0,0 +1,162 @@ +{ + "name": "My workflow 5", + "nodes": [ + { + "parameters": {}, + "id": "f0857ec9-0709-4657-a2f4-059837c94060", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [540, 220] + }, + { + "parameters": { + "resource": "worksheet", + "operation": "upsert", + "workbook": { + "__rl": true, + "value": "01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I", + "mode": "list", + "cachedResultName": "ŠŠ Š Š ŠŗŠ¾ŠæŃŃ", + "cachedResultUrl": "https://5w1hb7-my.sharepoint.com/personal/michaeldevsandbox_5w1hb7_onmicrosoft_com/_layouts/15/Doc.aspx?sourcedoc=%7BECC4041C-3AB6-4CF7-B079-0926470A1388%7D&file=%D0%9F%D0%A0%D0%A0%D0%9E%20%D0%BA%D0%BE%D0%BF%D1%96%D1%8F.xlsx&action=default&mobileredirect=true&DefaultItemOpen=1" + }, + "worksheet": { + "__rl": true, + "value": "={A0883CFE-D27E-4ECC-B94B-981830AAD55B}", + "mode": "id" + }, + "columnToMatchOn": "id", + "valueToMatchOn": "4", + "fieldsUi": { + "values": [ + { + "column": "name", + "fieldValue": "Sam" + }, + { + "column": "age", + "fieldValue": "34" + }, + { + "column": "data", + "fieldValue": "data 4" + } + ] + }, + "options": {} + }, + "id": "426ed055-0c9b-4ae2-a9fe-a6cce875d5ee", + "name": "Microsoft Excel 365", + "type": "n8n-nodes-base.microsoftExcel", + "typeVersion": 2, + "position": [1020, 220], + "credentials": { + "microsoftExcelOAuth2Api": { + "id": "70", + "name": "Microsoft Excel account" + } + } + }, + { + "parameters": { + "resource": "worksheet", + "operation": "upsert", + "workbook": { + "__rl": true, + "value": "01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I", + "mode": "list", + "cachedResultName": "ŠŠ Š Š ŠŗŠ¾ŠæŃŃ", + "cachedResultUrl": "https://5w1hb7-my.sharepoint.com/personal/michaeldevsandbox_5w1hb7_onmicrosoft_com/_layouts/15/Doc.aspx?sourcedoc=%7BECC4041C-3AB6-4CF7-B079-0926470A1388%7D&file=%D0%9F%D0%A0%D0%A0%D0%9E%20%D0%BA%D0%BE%D0%BF%D1%96%D1%8F.xlsx&action=default&mobileredirect=true&DefaultItemOpen=1" + }, + "worksheet": { + "__rl": true, + "value": "={426949D7-797F-43A9-A8A4-8FE283495A82}", + "mode": "id" + }, + "dataMode": "autoMap", + "columnToMatchOn": "id", + "options": {} + }, + "id": "0b10bfae-4e15-48c5-a2e6-7bec1c2687ec", + "name": "Microsoft Excel 3651", + "type": "n8n-nodes-base.microsoftExcel", + "typeVersion": 2, + "position": [1020, 460], + "credentials": { + "microsoftExcelOAuth2Api": { + "id": "70", + "name": "Microsoft Excel account" + } + } + }, + { + "parameters": { + "jsCode": "return {\n id: 4,\n name: 'Don',\n age: 37,\n data: 'data 44',\n};" + }, + "id": "93453ccb-5ac3-425b-8ac4-d20f0dfe9bab", + "name": "Code", + "type": "n8n-nodes-base.code", + "typeVersion": 1, + "position": [820, 460] + } + ], + "pinData": { + "Microsoft Excel 365": [ + { + "json": { + "id": 4, + "name": "Sam", + "age": 34, + "data": "data 4" + } + } + ], + "Microsoft Excel 3651": [ + { + "json": { + "id": 4, + "name": "Don", + "age": 37, + "data": "data 44" + } + } + ] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Microsoft Excel 365", + "type": "main", + "index": 0 + }, + { + "node": "Code", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code": { + "main": [ + [ + { + "node": "Microsoft Excel 3651", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": {}, + "versionId": "f24273bf-ef07-49da-960b-a68b63961d4a", + "id": "135", + "meta": { + "instanceId": "36203ea1ce3cef713fa25999bd9874ae26b9e4c2c3a90a365f2882a154d031d0" + }, + "tags": [] +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/test/v2/utils/utils.test.ts b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/utils/utils.test.ts new file mode 100644 index 0000000000000..5786e9c2d6589 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/test/v2/utils/utils.test.ts @@ -0,0 +1,536 @@ +import { get } from 'lodash'; +import type { IDataObject, IExecuteFunctions, IGetNodeParameterOptions, INode } from 'n8n-workflow'; +import { + prepareOutput, + updateByAutoMaping, + updateByDefinedValues, +} from '../../../v2/helpers/utils'; + +const node: INode = { + id: '1', + name: 'Microsoft Excel 365', + typeVersion: 2, + type: 'n8n-nodes-base.microsoftExcel', + position: [60, 760], + parameters: {}, +}; + +const fakeExecute = (nodeParameters: IDataObject[]) => { + const fakeExecuteFunction = { + getNodeParameter( + parameterName: string, + itemIndex: number, + fallbackValue?: IDataObject | undefined, + options?: IGetNodeParameterOptions | undefined, + ) { + const parameter = options?.extractValue ? `${parameterName}.value` : parameterName; + return get(nodeParameters[itemIndex], parameter, fallbackValue); + }, + } as unknown as IExecuteFunctions; + return fakeExecuteFunction; +}; + +const responseData = { + address: 'Sheet4!A1:D4', + addressLocal: 'Sheet4!A1:D4', + columnCount: 4, + cellCount: 16, + columnHidden: false, + rowHidden: false, + numberFormat: [ + ['General', 'General', 'General', 'General'], + ['General', 'General', 'General', 'General'], + ['General', 'General', 'General', 'General'], + ['General', 'General', 'General', 'General'], + ], + columnIndex: 0, + text: [ + ['id', 'name', 'age', 'data'], + ['1', 'Sam', '33', 'data 1'], + ['2', 'Jon', '44', 'data 2'], + ['3', 'Ron', '55', 'data 3'], + ], + formulas: [ + ['id', 'name', 'age', 'data'], + [1, 'Sam', 33, 'data 1'], + [2, 'Jon', 44, 'data 2'], + [3, 'Ron', 55, 'data 3'], + ], + formulasLocal: [ + ['id', 'name', 'age', 'data'], + [1, 'Sam', 33, 'data 1'], + [2, 'Jon', 44, 'data 2'], + [3, 'Ron', 55, 'data 3'], + ], + formulasR1C1: [ + ['id', 'name', 'age', 'data'], + [1, 'Sam', 33, 'data 1'], + [2, 'Jon', 44, 'data 2'], + [3, 'Ron', 55, 'data 3'], + ], + hidden: false, + rowCount: 4, + rowIndex: 0, + valueTypes: [ + ['String', 'String', 'String', 'String'], + ['Double', 'String', 'Double', 'String'], + ['Double', 'String', 'Double', 'String'], + ['Double', 'String', 'Double', 'String'], + ], + values: [ + ['id', 'name', 'age', 'data'], + [1, 'Sam', 33, 'data 1'], + [2, 'Jon', 44, 'data 2'], + [3, 'Ron', 55, 'data 3'], + ], +}; + +describe('Test MicrosoftExcelV2, prepareOutput', () => { + it('should return empty array', () => { + const output = prepareOutput(node, { values: [] }, { rawData: false }); + expect(output).toBeDefined(); + expect(output).toEqual([]); + }); + + it('should return raw response', () => { + const output = prepareOutput(node, responseData, { rawData: true }); + expect(output).toBeDefined(); + expect(output[0].json.data).toEqual(responseData); + }); + + it('should return raw response in custom property', () => { + const customKey = 'customKey'; + const output = prepareOutput(node, responseData, { rawData: true, dataProperty: customKey }); + expect(output).toBeDefined(); + expect(output[0].json.customKey).toEqual(responseData); + }); + + it('should return formated response', () => { + const output = prepareOutput(node, responseData, { rawData: false }); + expect(output).toBeDefined(); + expect(output.length).toEqual(3); + expect(output[0].json).toEqual({ + id: 1, + name: 'Sam', + age: 33, + data: 'data 1', + }); + }); + + it('should return response with selected first data row', () => { + const output = prepareOutput(node, responseData, { rawData: false, firstDataRow: 3 }); + expect(output).toBeDefined(); + expect(output.length).toEqual(1); + expect(output[0].json).toEqual({ + id: 3, + name: 'Ron', + age: 55, + data: 'data 3', + }); + }); + + it('should return response with selected first data row', () => { + const [firstRow, ...rest] = responseData.values; + const response = { values: [...rest, firstRow] }; + const output = prepareOutput(node, response, { rawData: false, keyRow: 3, firstDataRow: 0 }); + expect(output).toBeDefined(); + expect(output.length).toEqual(3); + expect(output[0].json).toEqual({ + id: 1, + name: 'Sam', + age: 33, + data: 'data 1', + }); + }); +}); + +describe('Test MicrosoftExcelV2, updateByDefinedValues', () => { + it('should update single row', () => { + const nodeParameters = [ + { + columnToMatchOn: 'id', + valueToMatchOn: 2, + fieldsUi: { + values: [ + { + column: 'name', + fieldValue: 'Donald', + }, + ], + }, + }, + ]; + + const sheetData = responseData.values; + + const updateSummary = updateByDefinedValues.call( + fakeExecute(nodeParameters), + nodeParameters.length, + sheetData, + false, + ); + + expect(updateSummary).toBeDefined(); + expect(updateSummary.updatedRows).toContain(0); //header row + expect(updateSummary.updatedRows).toContain(2); //updated row + expect(updateSummary.updatedRows).toHaveLength(2); + expect(updateSummary.updatedData[2][1]).toEqual('Donald'); // updated value + }); + + it('should update multiple rows', () => { + const nodeParameters = [ + { + columnToMatchOn: 'id', + valueToMatchOn: 2, + fieldsUi: { + values: [ + { + column: 'name', + fieldValue: 'Donald', + }, + ], + }, + }, + { + columnToMatchOn: 'id', + valueToMatchOn: 3, + fieldsUi: { + values: [ + { + column: 'name', + fieldValue: 'Eduard', + }, + ], + }, + }, + { + columnToMatchOn: 'id', + valueToMatchOn: 4, + fieldsUi: { + values: [ + { + column: 'name', + fieldValue: 'Ismael', + }, + ], + }, + }, + ]; + + const sheetData = [ + ['id', 'name', 'age', 'data'], + [1, 'Sam', 33, 'data 1'], + [2, 'Jon', 44, 'data 2'], + [3, 'Ron', 55, 'data 3'], + [4, 'Ron', 55, 'data 3'], + ]; + + const updateSummary = updateByDefinedValues.call( + fakeExecute(nodeParameters), + nodeParameters.length, + sheetData, + false, + ); + + expect(updateSummary).toBeDefined(); + expect(updateSummary.updatedRows).toContain(0); //header row + expect(updateSummary.updatedRows).toContain(2); //updated row + expect(updateSummary.updatedRows).toContain(3); //updated row + expect(updateSummary.updatedRows).toContain(4); //updated row + expect(updateSummary.updatedRows).toHaveLength(4); + expect(updateSummary.updatedData[2][1]).toEqual('Donald'); // updated value + expect(updateSummary.updatedData[3][1]).toEqual('Eduard'); // updated value + expect(updateSummary.updatedData[4][1]).toEqual('Ismael'); // updated value + }); + + it('should update all occurances', () => { + const nodeParameters = [ + { + columnToMatchOn: 'data', + valueToMatchOn: 'data 3', + fieldsUi: { + values: [ + { + column: 'name', + fieldValue: 'Donald', + }, + ], + }, + }, + ]; + + const sheetData = [ + ['id', 'name', 'age', 'data'], + [1, 'Sam', 55, 'data 3'], + [2, 'Jon', 77, 'data 3'], + [3, 'Ron', 44, 'data 3'], + [4, 'Ron', 33, 'data 3'], + ]; + + const updateSummary = updateByDefinedValues.call( + fakeExecute(nodeParameters), + nodeParameters.length, + sheetData, + true, + ); + + expect(updateSummary).toBeDefined(); + expect(updateSummary.updatedRows).toContain(0); //header row + expect(updateSummary.updatedRows).toHaveLength(5); + + for (let i = 1; i < updateSummary.updatedRows.length; i++) { + expect(updateSummary.updatedData[i][1]).toEqual('Donald'); // updated value + } + }); + + it('should append rows', () => { + const nodeParameters = [ + { + columnToMatchOn: 'id', + valueToMatchOn: 4, + fieldsUi: { + values: [ + { + column: 'name', + fieldValue: 'Donald', + }, + { + column: 'age', + fieldValue: 45, + }, + { + column: 'data', + fieldValue: 'data 4', + }, + ], + }, + }, + { + columnToMatchOn: 'id', + valueToMatchOn: 5, + fieldsUi: { + values: [ + { + column: 'name', + fieldValue: 'Victor', + }, + { + column: 'age', + fieldValue: 67, + }, + { + column: 'data', + fieldValue: 'data 5', + }, + ], + }, + }, + ]; + + const sheetData = [ + ['id', 'name', 'age', 'data'], + [1, 'Sam', 55, 'data 3'], + [2, 'Jon', 77, 'data 3'], + [3, 'Ron', 44, 'data 3'], + ]; + + const updateSummary = updateByDefinedValues.call( + fakeExecute(nodeParameters), + nodeParameters.length, + sheetData, + true, + ); + + expect(updateSummary).toBeDefined(); + expect(updateSummary.updatedRows).toContain(0); + expect(updateSummary.updatedRows.length).toEqual(1); + expect(updateSummary.appendData[0]).toEqual({ id: 4, name: 'Donald', age: 45, data: 'data 4' }); + expect(updateSummary.appendData[1]).toEqual({ id: 5, name: 'Victor', age: 67, data: 'data 5' }); + }); +}); + +describe('Test MicrosoftExcelV2, updateByAutoMaping', () => { + it('should update single row', () => { + const items = [ + { + json: { + id: 2, + name: 'Donald', + }, + }, + ]; + + const sheetData = [ + ['id', 'name', 'age', 'data'], + [1, 'Sam', 33, 'data 1'], + [2, 'Jon', 44, 'data 2'], + [3, 'Ron', 55, 'data 3'], + ]; + + const updateSummary = updateByAutoMaping(items, sheetData, 'id'); + + expect(updateSummary).toBeDefined(); + expect(updateSummary.updatedRows).toHaveLength(2); + expect(updateSummary.updatedRows).toContain(0); //header row + expect(updateSummary.updatedRows).toContain(2); //updated row + expect(updateSummary.updatedData[2][1]).toEqual('Donald'); // updated value + }); + + it('should append single row', () => { + const items = [ + { + json: { + id: 5, + name: 'Donald', + }, + }, + ]; + + const sheetData = [ + ['id', 'name', 'age', 'data'], + [1, 'Sam', 33, 'data 1'], + [2, 'Jon', 44, 'data 2'], + [3, 'Ron', 55, 'data 3'], + ]; + + const updateSummary = updateByAutoMaping(items, sheetData, 'id'); + + expect(updateSummary).toBeDefined(); + expect(updateSummary.updatedRows).toHaveLength(1); + expect(updateSummary.updatedRows).toContain(0); //header row + expect(updateSummary.appendData[0]).toEqual({ id: 5, name: 'Donald' }); + }); + + it('should append skip row with match column undefined', () => { + const items = [ + { + json: { + id: 5, + name: 'Donald', + }, + }, + ]; + + const sheetData = [ + ['id', 'name', 'age', 'data'], + [1, 'Sam', 33, 'data 1'], + [2, 'Jon', 44, 'data 2'], + [3, 'Ron', 55, 'data 3'], + ]; + + const updateSummary = updateByAutoMaping(items, sheetData, 'idd'); + + expect(updateSummary).toBeDefined(); + expect(updateSummary.updatedRows).toHaveLength(1); + expect(updateSummary.updatedRows).toContain(0); //header row + expect(updateSummary.appendData.length).toEqual(0); + }); + + it('should update multiple rows', () => { + const items = [ + { + json: { + id: 2, + name: 'Donald', + }, + }, + { + json: { + id: 3, + name: 'Eduard', + }, + }, + { + json: { + id: 4, + name: 'Ismael', + }, + }, + ]; + + const sheetData = [ + ['id', 'name', 'age', 'data'], + [1, 'Sam', 33, 'data 1'], + [2, 'Jon', 44, 'data 2'], + [3, 'Ron', 55, 'data 3'], + [4, 'Ron', 55, 'data 3'], + ]; + + const updateSummary = updateByAutoMaping(items, sheetData, 'id'); + + expect(updateSummary).toBeDefined(); + expect(updateSummary.updatedRows).toContain(0); //header row + expect(updateSummary.updatedRows).toContain(2); //updated row + expect(updateSummary.updatedRows).toContain(3); //updated row + expect(updateSummary.updatedRows).toContain(4); //updated row + expect(updateSummary.updatedRows).toHaveLength(4); + expect(updateSummary.updatedData[2][1]).toEqual('Donald'); // updated value + expect(updateSummary.updatedData[3][1]).toEqual('Eduard'); // updated value + expect(updateSummary.updatedData[4][1]).toEqual('Ismael'); // updated value + }); + + it('should update all occurances', () => { + const items = [ + { + json: { + data: 'data 3', + name: 'Donald', + }, + }, + ]; + + const sheetData = [ + ['id', 'name', 'age', 'data'], + [1, 'Sam', 55, 'data 3'], + [2, 'Jon', 77, 'data 3'], + [3, 'Ron', 44, 'data 3'], + [4, 'Ron', 33, 'data 3'], + ]; + + const updateSummary = updateByAutoMaping(items, sheetData, 'data', true); + + expect(updateSummary).toBeDefined(); + expect(updateSummary.updatedRows).toContain(0); //header row + expect(updateSummary.updatedRows).toHaveLength(5); + + for (let i = 1; i < updateSummary.updatedRows.length; i++) { + expect(updateSummary.updatedData[i][1]).toEqual('Donald'); // updated value + } + }); + + it('should append rows', () => { + const items = [ + { + json: { + id: 4, + data: 'data 4', + name: 'Donald', + age: 45, + }, + }, + { + json: { + id: 5, + data: 'data 5', + name: 'Victor', + age: 67, + }, + }, + ]; + + const sheetData = [ + ['id', 'name', 'age', 'data'], + [1, 'Sam', 55, 'data 3'], + [2, 'Jon', 77, 'data 3'], + [3, 'Ron', 44, 'data 3'], + ]; + + const updateSummary = updateByAutoMaping(items, sheetData, 'data', true); + + expect(updateSummary).toBeDefined(); + expect(updateSummary.updatedRows).toContain(0); + expect(updateSummary.updatedRows.length).toEqual(1); + expect(updateSummary.appendData[0]).toEqual({ id: 4, name: 'Donald', age: 45, data: 'data 4' }); + expect(updateSummary.appendData[1]).toEqual({ id: 5, name: 'Victor', age: 67, data: 'data 5' }); + }); +}); diff --git a/packages/nodes-base/nodes/Microsoft/Excel/GenericFunctions.ts b/packages/nodes-base/nodes/Microsoft/Excel/v1/GenericFunctions.ts similarity index 99% rename from packages/nodes-base/nodes/Microsoft/Excel/GenericFunctions.ts rename to packages/nodes-base/nodes/Microsoft/Excel/v1/GenericFunctions.ts index f29ed80272cb8..5b5d79bb0aca6 100644 --- a/packages/nodes-base/nodes/Microsoft/Excel/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Microsoft/Excel/v1/GenericFunctions.ts @@ -32,7 +32,7 @@ export async function microsoftApiRequest( if (Object.keys(headers).length !== 0) { options.headers = Object.assign({}, options.headers, headers); } - //@ts-ignore + return await this.helpers.requestOAuth2.call(this, 'microsoftExcelOAuth2Api', options); } catch (error) { throw new NodeApiError(this.getNode(), error as JsonObject); diff --git a/packages/nodes-base/nodes/Microsoft/Excel/v1/MicrosoftExcelV1.node.ts b/packages/nodes-base/nodes/Microsoft/Excel/v1/MicrosoftExcelV1.node.ts new file mode 100644 index 0000000000000..0def98076b2bc --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/v1/MicrosoftExcelV1.node.ts @@ -0,0 +1,692 @@ +/* eslint-disable n8n-nodes-base/node-filename-against-convention */ +import type { IExecuteFunctions } from 'n8n-core'; + +import type { + IDataObject, + ILoadOptionsFunctions, + INodeExecutionData, + INodePropertyOptions, + INodeType, + INodeTypeBaseDescription, + INodeTypeDescription, + JsonObject, +} from 'n8n-workflow'; +import { NodeApiError } from 'n8n-workflow'; + +import { + microsoftApiRequest, + microsoftApiRequestAllItems, + microsoftApiRequestAllItemsSkip, +} from './GenericFunctions'; + +import { workbookFields, workbookOperations } from './WorkbookDescription'; + +import { worksheetFields, worksheetOperations } from './WorksheetDescription'; + +import { tableFields, tableOperations } from './TableDescription'; + +import { oldVersionNotice } from '../../../../utils/descriptions'; + +const versionDescription: INodeTypeDescription = { + displayName: 'Microsoft Excel', + name: 'microsoftExcel', + icon: 'file:excel.svg', + group: ['input'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume Microsoft Excel API', + defaults: { + name: 'Microsoft Excel', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'microsoftExcelOAuth2Api', + required: true, + }, + ], + properties: [ + oldVersionNotice, + { + displayName: 'Resource', + name: 'resource', + type: 'options', + noDataExpression: true, + options: [ + { + name: 'Table', + value: 'table', + description: 'Represents an Excel table', + }, + { + name: 'Workbook', + value: 'workbook', + description: + 'Workbook is the top level object which contains related workbook objects such as worksheets, tables, ranges, etc', + }, + { + name: 'Worksheet', + value: 'worksheet', + description: + 'An Excel worksheet is a grid of cells. It can contain data, tables, charts, etc.', + }, + ], + default: 'workbook', + }, + ...workbookOperations, + ...workbookFields, + ...worksheetOperations, + ...worksheetFields, + ...tableOperations, + ...tableFields, + ], +}; +export class MicrosoftExcelV1 implements INodeType { + description: INodeTypeDescription; + + constructor(baseDescription: INodeTypeBaseDescription) { + this.description = { + ...baseDescription, + ...versionDescription, + }; + } + + methods = { + loadOptions: { + // Get all the workbooks to display them to user so that he can + // select them easily + async getWorkbooks(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> { + const qs: IDataObject = { + select: 'id,name', + }; + const returnData: INodePropertyOptions[] = []; + const workbooks = await microsoftApiRequestAllItems.call( + this, + 'value', + 'GET', + "/drive/root/search(q='.xlsx')", + {}, + qs, + ); + for (const workbook of workbooks) { + const workbookName = workbook.name; + const workbookId = workbook.id; + returnData.push({ + name: workbookName, + value: workbookId, + }); + } + return returnData; + }, + // Get all the worksheets to display them to user so that he can + // select them easily + async getworksheets(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> { + const workbookId = this.getCurrentNodeParameter('workbook'); + const qs: IDataObject = { + select: 'id,name', + }; + const returnData: INodePropertyOptions[] = []; + const worksheets = await microsoftApiRequestAllItems.call( + this, + 'value', + 'GET', + `/drive/items/${workbookId}/workbook/worksheets`, + {}, + qs, + ); + for (const worksheet of worksheets) { + const worksheetName = worksheet.name; + const worksheetId = worksheet.id; + returnData.push({ + name: worksheetName, + value: worksheetId, + }); + } + return returnData; + }, + // Get all the tables to display them to user so that he can + // select them easily + async getTables(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> { + const workbookId = this.getCurrentNodeParameter('workbook'); + const worksheetId = this.getCurrentNodeParameter('worksheet'); + const qs: IDataObject = { + select: 'id,name', + }; + const returnData: INodePropertyOptions[] = []; + const tables = await microsoftApiRequestAllItems.call( + this, + 'value', + 'GET', + `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables`, + {}, + qs, + ); + for (const table of tables) { + const tableName = table.name; + const tableId = table.id; + returnData.push({ + name: tableName, + value: tableId, + }); + } + return returnData; + }, + }, + }; + + async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> { + const items = this.getInputData(); + const returnData: INodeExecutionData[] = []; + const length = items.length; + let qs: IDataObject = {}; + const result: IDataObject[] = []; + let responseData; + const resource = this.getNodeParameter('resource', 0); + const operation = this.getNodeParameter('operation', 0); + + if (resource === 'table') { + //https://docs.microsoft.com/en-us/graph/api/table-post-rows?view=graph-rest-1.0&tabs=http + if (operation === 'addRow') { + try { + // TODO: At some point it should be possible to use item dependent parameters. + // Is however important to then not make one separate request each. + const workbookId = this.getNodeParameter('workbook', 0) as string; + const worksheetId = this.getNodeParameter('worksheet', 0) as string; + const tableId = this.getNodeParameter('table', 0) as string; + const additionalFields = this.getNodeParameter('additionalFields', 0); + const body: IDataObject = {}; + + if (additionalFields.index) { + body.index = additionalFields.index as number; + } + + // Get table columns to eliminate any columns not needed on the input + responseData = await microsoftApiRequest.call( + this, + 'GET', + `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/columns`, + {}, + qs, + ); + const columns = responseData.value.map((column: IDataObject) => column.name); + + const rows: any[][] = []; + + // Bring the items into the correct format + for (const item of items) { + const row = []; + for (const column of columns) { + row.push(item.json[column]); + } + rows.push(row); + } + + body.values = rows; + const { id } = await microsoftApiRequest.call( + this, + 'POST', + `/drive/items/${workbookId}/workbook/createSession`, + { persistChanges: true }, + ); + responseData = await microsoftApiRequest.call( + this, + 'POST', + `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/rows/add`, + body, + {}, + '', + { 'workbook-session-id': id }, + ); + await microsoftApiRequest.call( + this, + 'POST', + `/drive/items/${workbookId}/workbook/closeSession`, + {}, + {}, + '', + { 'workbook-session-id': id }, + ); + + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(responseData as IDataObject[]), + { itemData: { item: 0 } }, + ); + + returnData.push(...executionData); + } catch (error) { + if (this.continueOnFail()) { + const executionErrorData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray({ error: error.message }), + { itemData: { item: 0 } }, + ); + returnData.push(...executionErrorData); + } else { + throw error; + } + } + } + //https://docs.microsoft.com/en-us/graph/api/table-list-columns?view=graph-rest-1.0&tabs=http + if (operation === 'getColumns') { + for (let i = 0; i < length; i++) { + try { + qs = {}; + const workbookId = this.getNodeParameter('workbook', i) as string; + const worksheetId = this.getNodeParameter('worksheet', i) as string; + const tableId = this.getNodeParameter('table', i) as string; + const returnAll = this.getNodeParameter('returnAll', i); + const rawData = this.getNodeParameter('rawData', i); + if (rawData) { + const filters = this.getNodeParameter('filters', i); + if (filters.fields) { + qs.$select = filters.fields; + } + } + if (returnAll) { + responseData = await microsoftApiRequestAllItemsSkip.call( + this, + 'value', + 'GET', + `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/columns`, + {}, + qs, + ); + } else { + qs.$top = this.getNodeParameter('limit', i); + responseData = await microsoftApiRequest.call( + this, + 'GET', + `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/columns`, + {}, + qs, + ); + responseData = responseData.value; + } + if (!rawData) { + responseData = responseData.map((column: IDataObject) => ({ name: column.name })); + } else { + const dataProperty = this.getNodeParameter('dataProperty', i) as string; + responseData = { [dataProperty]: responseData }; + } + + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(responseData as IDataObject[]), + { itemData: { item: i } }, + ); + + returnData.push(...executionData); + } catch (error) { + if (this.continueOnFail()) { + const executionErrorData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray({ error: error.message }), + { itemData: { item: i } }, + ); + returnData.push(...executionErrorData); + continue; + } + throw error; + } + } + } + //https://docs.microsoft.com/en-us/graph/api/table-list-rows?view=graph-rest-1.0&tabs=http + if (operation === 'getRows') { + for (let i = 0; i < length; i++) { + qs = {}; + try { + const workbookId = this.getNodeParameter('workbook', i) as string; + const worksheetId = this.getNodeParameter('worksheet', i) as string; + const tableId = this.getNodeParameter('table', i) as string; + const returnAll = this.getNodeParameter('returnAll', i); + const rawData = this.getNodeParameter('rawData', i); + if (rawData) { + const filters = this.getNodeParameter('filters', i); + if (filters.fields) { + qs.$select = filters.fields; + } + } + if (returnAll) { + responseData = await microsoftApiRequestAllItemsSkip.call( + this, + 'value', + 'GET', + `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/rows`, + {}, + qs, + ); + } else { + const rowsQs = { ...qs }; + rowsQs.$top = this.getNodeParameter('limit', i); + responseData = await microsoftApiRequest.call( + this, + 'GET', + `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/rows`, + {}, + rowsQs, + ); + responseData = responseData.value; + } + if (!rawData) { + const columnsQs = { ...qs }; + columnsQs.$select = 'name'; + // TODO: That should probably be cached in the future + let columns = await microsoftApiRequestAllItemsSkip.call( + this, + 'value', + 'GET', + `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/columns`, + {}, + columnsQs, + ); + + columns = (columns as IDataObject[]).map((column) => column.name); + for (let index = 0; index < responseData.length; index++) { + const object: IDataObject = {}; + for (let y = 0; y < columns.length; y++) { + object[columns[y]] = responseData[index].values[0][y]; + } + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray({ ...object }), + { itemData: { item: index } }, + ); + + returnData.push(...executionData); + } + } else { + const dataProperty = this.getNodeParameter('dataProperty', i) as string; + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray({ [dataProperty]: responseData }), + { itemData: { item: i } }, + ); + + returnData.push(...executionData); + } + } catch (error) { + if (this.continueOnFail()) { + const executionErrorData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray({ error: error.message }), + { itemData: { item: i } }, + ); + returnData.push(...executionErrorData); + continue; + } + throw error; + } + } + } + if (operation === 'lookup') { + for (let i = 0; i < length; i++) { + qs = {}; + try { + const workbookId = this.getNodeParameter('workbook', i) as string; + const worksheetId = this.getNodeParameter('worksheet', i) as string; + const tableId = this.getNodeParameter('table', i) as string; + const lookupColumn = this.getNodeParameter('lookupColumn', i) as string; + const lookupValue = this.getNodeParameter('lookupValue', i) as string; + const options = this.getNodeParameter('options', i); + + responseData = await microsoftApiRequestAllItemsSkip.call( + this, + 'value', + 'GET', + `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/rows`, + {}, + {}, + ); + + qs.$select = 'name'; + // TODO: That should probably be cached in the future + let columns = await microsoftApiRequestAllItemsSkip.call( + this, + 'value', + 'GET', + `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/columns`, + {}, + qs, + ); + columns = columns.map((column: IDataObject) => column.name); + + if (!columns.includes(lookupColumn)) { + throw new NodeApiError(this.getNode(), responseData as JsonObject, { + message: `Column ${lookupColumn} does not exist on the table selected`, + }); + } + + result.length = 0; + for (let index = 0; index < responseData.length; index++) { + const object: IDataObject = {}; + for (let y = 0; y < columns.length; y++) { + object[columns[y]] = responseData[index].values[0][y]; + } + result.push({ ...object }); + } + + if (options.returnAllMatches) { + responseData = result.filter((data: IDataObject) => { + return data[lookupColumn]?.toString() === lookupValue; + }); + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(responseData), + { itemData: { item: i } }, + ); + + returnData.push(...executionData); + } else { + responseData = result.find((data: IDataObject) => { + return data[lookupColumn]?.toString() === lookupValue; + }); + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(responseData as IDataObject), + { itemData: { item: i } }, + ); + + returnData.push(...executionData); + } + } catch (error) { + if (this.continueOnFail()) { + const executionErrorData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray({ error: error.message }), + { itemData: { item: i } }, + ); + returnData.push(...executionErrorData); + continue; + } + throw error; + } + } + } + } + if (resource === 'workbook') { + for (let i = 0; i < length; i++) { + qs = {}; + try { + //https://docs.microsoft.com/en-us/graph/api/worksheetcollection-add?view=graph-rest-1.0&tabs=http + if (operation === 'addWorksheet') { + const workbookId = this.getNodeParameter('workbook', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i); + const body: IDataObject = {}; + if (additionalFields.name) { + body.name = additionalFields.name; + } + const { id } = await microsoftApiRequest.call( + this, + 'POST', + `/drive/items/${workbookId}/workbook/createSession`, + { persistChanges: true }, + ); + responseData = await microsoftApiRequest.call( + this, + 'POST', + `/drive/items/${workbookId}/workbook/worksheets/add`, + body, + {}, + '', + { 'workbook-session-id': id }, + ); + await microsoftApiRequest.call( + this, + 'POST', + `/drive/items/${workbookId}/workbook/closeSession`, + {}, + {}, + '', + { 'workbook-session-id': id }, + ); + } + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i); + const filters = this.getNodeParameter('filters', i); + if (filters.fields) { + qs.$select = filters.fields; + } + if (returnAll) { + responseData = await microsoftApiRequestAllItems.call( + this, + 'value', + 'GET', + "/drive/root/search(q='.xlsx')", + {}, + qs, + ); + } else { + qs.$top = this.getNodeParameter('limit', i); + responseData = await microsoftApiRequest.call( + this, + 'GET', + "/drive/root/search(q='.xlsx')", + {}, + qs, + ); + responseData = responseData.value; + } + } + + if (Array.isArray(responseData)) { + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(responseData), + { itemData: { item: i } }, + ); + + returnData.push(...executionData); + } else if (responseData !== undefined) { + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(responseData as IDataObject[]), + { itemData: { item: i } }, + ); + + returnData.push(...executionData); + } + } catch (error) { + if (this.continueOnFail()) { + const executionErrorData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray({ error: error.message }), + { itemData: { item: i } }, + ); + returnData.push(...executionErrorData); + continue; + } + throw error; + } + } + } + if (resource === 'worksheet') { + for (let i = 0; i < length; i++) { + qs = {}; + try { + //https://docs.microsoft.com/en-us/graph/api/workbook-list-worksheets?view=graph-rest-1.0&tabs=http + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i); + const workbookId = this.getNodeParameter('workbook', i) as string; + const filters = this.getNodeParameter('filters', i); + if (filters.fields) { + qs.$select = filters.fields; + } + if (returnAll) { + responseData = await microsoftApiRequestAllItems.call( + this, + 'value', + 'GET', + `/drive/items/${workbookId}/workbook/worksheets`, + {}, + qs, + ); + } else { + qs.$top = this.getNodeParameter('limit', i); + responseData = await microsoftApiRequest.call( + this, + 'GET', + `/drive/items/${workbookId}/workbook/worksheets`, + {}, + qs, + ); + responseData = responseData.value; + } + } + //https://docs.microsoft.com/en-us/graph/api/worksheet-range?view=graph-rest-1.0&tabs=http + if (operation === 'getContent') { + const workbookId = this.getNodeParameter('workbook', i) as string; + const worksheetId = this.getNodeParameter('worksheet', i) as string; + const range = this.getNodeParameter('range', i) as string; + const rawData = this.getNodeParameter('rawData', i); + if (rawData) { + const filters = this.getNodeParameter('filters', i); + if (filters.fields) { + qs.$select = filters.fields; + } + } + + responseData = await microsoftApiRequest.call( + this, + 'GET', + `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/range(address='${range}')`, + {}, + qs, + ); + + if (!rawData) { + const keyRow = this.getNodeParameter('keyRow', i) as number; + const dataStartRow = this.getNodeParameter('dataStartRow', i) as number; + if (responseData.values === null) { + throw new NodeApiError(this.getNode(), responseData as JsonObject, { + message: 'Range did not return data', + }); + } + const keyValues = responseData.values[keyRow]; + for (let index = dataStartRow; index < responseData.values.length; index++) { + const object: IDataObject = {}; + for (let y = 0; y < keyValues.length; y++) { + object[keyValues[y]] = responseData.values[index][y]; + } + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray({ ...object }), + { itemData: { item: index } }, + ); + + returnData.push(...executionData); + } + } else { + const dataProperty = this.getNodeParameter('dataProperty', i) as string; + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray({ [dataProperty]: responseData }), + { itemData: { item: i } }, + ); + + returnData.push(...executionData); + } + } + } catch (error) { + if (this.continueOnFail()) { + const executionErrorData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray({ error: error.message }), + { itemData: { item: i } }, + ); + returnData.push(...executionErrorData); + continue; + } + throw error; + } + } + } + + return this.prepareOutputData(returnData); + } +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/TableDescription.ts b/packages/nodes-base/nodes/Microsoft/Excel/v1/TableDescription.ts similarity index 100% rename from packages/nodes-base/nodes/Microsoft/Excel/TableDescription.ts rename to packages/nodes-base/nodes/Microsoft/Excel/v1/TableDescription.ts diff --git a/packages/nodes-base/nodes/Microsoft/Excel/WorkbookDescription.ts b/packages/nodes-base/nodes/Microsoft/Excel/v1/WorkbookDescription.ts similarity index 100% rename from packages/nodes-base/nodes/Microsoft/Excel/WorkbookDescription.ts rename to packages/nodes-base/nodes/Microsoft/Excel/v1/WorkbookDescription.ts diff --git a/packages/nodes-base/nodes/Microsoft/Excel/WorksheetDescription.ts b/packages/nodes-base/nodes/Microsoft/Excel/v1/WorksheetDescription.ts similarity index 100% rename from packages/nodes-base/nodes/Microsoft/Excel/WorksheetDescription.ts rename to packages/nodes-base/nodes/Microsoft/Excel/v1/WorksheetDescription.ts diff --git a/packages/nodes-base/nodes/Microsoft/Excel/v2/MicrosoftExcelV2.node.ts b/packages/nodes-base/nodes/Microsoft/Excel/v2/MicrosoftExcelV2.node.ts new file mode 100644 index 0000000000000..660ca4fe871a9 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/v2/MicrosoftExcelV2.node.ts @@ -0,0 +1,25 @@ +/* eslint-disable n8n-nodes-base/node-filename-against-convention */ +import type { IExecuteFunctions } from 'n8n-core'; + +import type { INodeType, INodeTypeBaseDescription, INodeTypeDescription } from 'n8n-workflow'; + +import { listSearch, loadOptions } from './methods'; +import { versionDescription } from './actions/versionDescription'; +import { router } from './actions/router'; + +export class MicrosoftExcelV2 implements INodeType { + description: INodeTypeDescription; + + constructor(baseDescription: INodeTypeBaseDescription) { + this.description = { + ...baseDescription, + ...versionDescription, + }; + } + + methods = { listSearch, loadOptions }; + + async execute(this: IExecuteFunctions) { + return router.call(this); + } +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/common.descriptions.ts b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/common.descriptions.ts new file mode 100644 index 0000000000000..adad75623e1d3 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/common.descriptions.ts @@ -0,0 +1,140 @@ +import type { INodeProperties } from 'n8n-workflow'; + +export const workbookRLC: INodeProperties = { + displayName: 'Workbook', + name: 'workbook', + type: 'resourceLocator', + default: { mode: 'list', value: '' }, + required: true, + modes: [ + { + displayName: 'From List', + name: 'list', + type: 'list', + typeOptions: { + searchListMethod: 'searchWorkbooks', + searchable: true, + }, + }, + { + displayName: 'By ID', + name: 'id', + type: 'string', + validation: [ + { + type: 'regex', + properties: { + regex: '[a-zA-Z0-9]{2,}', + errorMessage: 'Not a valid Workbook ID', + }, + }, + ], + }, + ], +}; + +export const worksheetRLC: INodeProperties = { + displayName: 'Sheet', + name: 'worksheet', + type: 'resourceLocator', + default: { mode: 'list', value: '' }, + required: true, + modes: [ + { + displayName: 'From List', + name: 'list', + type: 'list', + typeOptions: { + searchListMethod: 'getWorksheetsList', + }, + }, + { + displayName: 'By ID', + name: 'id', + type: 'string', + validation: [ + { + type: 'regex', + properties: { + regex: '{[a-zA-Z0-9\\-_]{2,}}', + errorMessage: 'Not a valid Sheet ID', + }, + }, + ], + }, + ], +}; + +export const tableRLC: INodeProperties = { + displayName: 'Table', + name: 'table', + type: 'resourceLocator', + default: { mode: 'list', value: '' }, + required: true, + modes: [ + { + displayName: 'From List', + name: 'list', + type: 'list', + typeOptions: { + searchListMethod: 'getWorksheetTables', + }, + }, + { + displayName: 'By ID', + name: 'id', + type: 'string', + validation: [ + { + type: 'regex', + properties: { + regex: '{[a-zA-Z0-9\\-_]{2,}}', + errorMessage: 'Not a valid Table ID', + }, + }, + ], + }, + ], +}; + +export const rawDataOutput: INodeProperties = { + displayName: 'Raw Data Output', + name: 'rawDataOutput', + type: 'fixedCollection', + default: { values: { rawData: false } }, + options: [ + { + displayName: 'Values', + name: 'values', + values: [ + { + displayName: 'RAW Data', + name: 'rawData', + type: 'boolean', + // eslint-disable-next-line n8n-nodes-base/node-param-default-wrong-for-boolean + default: 0, + description: + 'Whether the data should be returned RAW instead of parsed into keys according to their header', + }, + { + displayName: 'Data Property', + name: 'dataProperty', + type: 'string', + default: 'data', + required: true, + displayOptions: { + show: { + rawData: [true], + }, + }, + description: 'The name of the property into which to write the RAW data', + }, + ], + }, + ], + displayOptions: { + hide: { + '/dataMode': ['nothing'], + }, + }, +}; diff --git a/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/node.type.ts b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/node.type.ts new file mode 100644 index 0000000000000..dcf09d02eec04 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/node.type.ts @@ -0,0 +1,20 @@ +import type { AllEntities, Entity } from 'n8n-workflow'; + +type MicrosoftExcelMap = { + table: + | 'append' + | 'addTable' + | 'convertToRange' + | 'deleteTable' + | 'getColumns' + | 'getRows' + | 'lookup'; + workbook: 'addWorksheet' | 'deleteWorkbook' | 'getAll'; + worksheet: 'append' | 'clear' | 'deleteWorksheet' | 'getAll' | 'readRows' | 'update' | 'upsert'; +}; + +export type MicrosoftExcel = AllEntities<MicrosoftExcelMap>; + +export type MicrosoftExcelChannel = Entity<MicrosoftExcelMap, 'table'>; +export type MicrosoftExcelMessage = Entity<MicrosoftExcelMap, 'workbook'>; +export type MicrosoftExcelMember = Entity<MicrosoftExcelMap, 'worksheet'>; diff --git a/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/router.ts b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/router.ts new file mode 100644 index 0000000000000..681b1e3bab271 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/router.ts @@ -0,0 +1,37 @@ +import type { IExecuteFunctions } from 'n8n-core'; +import type { INodeExecutionData } from 'n8n-workflow'; +import { NodeOperationError } from 'n8n-workflow'; +import type { MicrosoftExcel } from './node.type'; + +import * as table from './table/Table.resource'; +import * as workbook from './workbook/Workbook.resource'; +import * as worksheet from './worksheet/Worksheet.resource'; + +export async function router(this: IExecuteFunctions): Promise<INodeExecutionData[][]> { + const items = this.getInputData(); + let returnData: INodeExecutionData[] = []; + + const resource = this.getNodeParameter<MicrosoftExcel>('resource', 0); + const operation = this.getNodeParameter('operation', 0); + + const microsoftExcel = { + resource, + operation, + } as MicrosoftExcel; + + switch (microsoftExcel.resource) { + case 'table': + returnData = await table[microsoftExcel.operation].execute.call(this, items); + break; + case 'workbook': + returnData = await workbook[microsoftExcel.operation].execute.call(this, items); + break; + case 'worksheet': + returnData = await worksheet[microsoftExcel.operation].execute.call(this, items); + break; + default: + throw new NodeOperationError(this.getNode(), `The resource "${resource}" is not known`); + } + + return this.prepareOutputData(returnData); +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/table/Table.resource.ts b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/table/Table.resource.ts new file mode 100644 index 0000000000000..6fc4497805454 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/table/Table.resource.ts @@ -0,0 +1,77 @@ +import type { INodeProperties } from 'n8n-workflow'; + +import * as append from './append.operation'; +import * as addTable from './addTable.operation'; +import * as convertToRange from './convertToRange.operation'; +import * as deleteTable from './deleteTable.operation'; +import * as getColumns from './getColumns.operation'; +import * as getRows from './getRows.operation'; +import * as lookup from './lookup.operation'; + +export { append, addTable, convertToRange, deleteTable, getColumns, getRows, lookup }; + +export const description: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['table'], + }, + }, + options: [ + { + name: 'Append', + value: 'append', + description: 'Add rows to the end of the table', + action: 'Append rows to table', + }, + { + name: 'Convert to Range', + value: 'convertToRange', + description: 'Convert a table to a range', + action: 'Convert to range', + }, + { + name: 'Create', + value: 'addTable', + description: 'Add a table based on range', + action: 'Create a table', + }, + { + name: 'Delete', + value: 'deleteTable', + description: 'Delete a table', + action: 'Delete a table', + }, + { + name: 'Get Columns', + value: 'getColumns', + description: 'Retrieve a list of table columns', + action: 'Get columns', + }, + { + name: 'Get Rows', + value: 'getRows', + description: 'Retrieve a list of table rows', + action: 'Get rows', + }, + { + name: 'Lookup', + value: 'lookup', + description: 'Look for rows that match a given value in a column', + action: 'Lookup a column', + }, + ], + default: 'append', + }, + ...append.description, + ...addTable.description, + ...convertToRange.description, + ...deleteTable.description, + ...getColumns.description, + ...getRows.description, + ...lookup.description, +]; diff --git a/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/table/addTable.operation.ts b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/table/addTable.operation.ts new file mode 100644 index 0000000000000..da1d44554720a --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/table/addTable.operation.ts @@ -0,0 +1,127 @@ +import type { IExecuteFunctions } from 'n8n-core'; +import type { IDataObject, INodeExecutionData, INodeProperties } from 'n8n-workflow'; +import { updateDisplayOptions } from '../../../../../../utils/utilities'; +import { microsoftApiRequest } from '../../transport'; +import { workbookRLC, worksheetRLC } from '../common.descriptions'; + +const properties: INodeProperties[] = [ + workbookRLC, + worksheetRLC, + { + displayName: 'Select Range', + name: 'selectRange', + type: 'options', + options: [ + { + name: 'Automatically', + value: 'auto', + description: 'The whole used range on the selected sheet will be converted into a table', + }, + { + name: 'Manually', + value: 'manual', + description: 'Select a range that will be converted into a table', + }, + ], + default: 'auto', + }, + { + displayName: 'Range', + name: 'range', + type: 'string', + default: '', + placeholder: 'A1:B2', + description: 'The range of cells that will be converted to a table', + displayOptions: { + show: { + selectRange: ['manual'], + }, + }, + }, + { + displayName: 'Has Headers', + name: 'hasHeaders', + type: 'boolean', + default: true, + description: + 'Whether the range has column labels. When this property set to false Excel will automatically generate header shifting the data down by one row.', + }, +]; + +const displayOptions = { + show: { + resource: ['table'], + operation: ['addTable'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute( + this: IExecuteFunctions, + items: INodeExecutionData[], +): Promise<INodeExecutionData[]> { + //https://learn.microsoft.com/en-us/graph/api/worksheet-post-tables?view=graph-rest-1.0 + const returnData: INodeExecutionData[] = []; + + for (let i = 0; i < items.length; i++) { + try { + const workbookId = this.getNodeParameter('workbook', i, undefined, { + extractValue: true, + }) as string; + + const worksheetId = this.getNodeParameter('worksheet', i, undefined, { + extractValue: true, + }) as string; + + const selectRange = this.getNodeParameter('selectRange', i) as string; + + const hasHeaders = this.getNodeParameter('hasHeaders', i) as boolean; + + let range = ''; + if (selectRange === 'auto') { + const { address } = await microsoftApiRequest.call( + this, + 'GET', + `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/usedRange`, + undefined, + { + select: 'address', + }, + ); + range = address.split('!')[1]; + } else { + range = this.getNodeParameter('range', i) as string; + } + + const responseData = await microsoftApiRequest.call( + this, + 'POST', + `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/add`, + { + address: range, + hasHeaders, + }, + ); + + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(responseData as IDataObject), + { itemData: { item: i } }, + ); + + returnData.push(...executionData); + } catch (error) { + if (this.continueOnFail()) { + const executionErrorData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray({ error: error.message }), + { itemData: { item: i } }, + ); + returnData.push(...executionErrorData); + continue; + } + throw error; + } + } + + return returnData; +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/table/append.operation.ts b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/table/append.operation.ts new file mode 100644 index 0000000000000..e97b2d18e405f --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/table/append.operation.ts @@ -0,0 +1,282 @@ +import type { IExecuteFunctions } from 'n8n-core'; +import type { IDataObject, INodeExecutionData, INodeProperties } from 'n8n-workflow'; +import { processJsonInput, updateDisplayOptions } from '../../../../../../utils/utilities'; +import type { ExcelResponse } from '../../helpers/interfaces'; +import { prepareOutput } from '../../helpers/utils'; +import { microsoftApiRequest } from '../../transport'; +import { tableRLC, workbookRLC, worksheetRLC } from '../common.descriptions'; + +const properties: INodeProperties[] = [ + workbookRLC, + worksheetRLC, + tableRLC, + { + displayName: 'Data Mode', + name: 'dataMode', + type: 'options', + default: 'define', + options: [ + { + name: 'Auto-Map Input Data to Columns', + value: 'autoMap', + description: 'Use when node input properties match destination column names', + }, + { + name: 'Map Each Column Below', + value: 'define', + description: 'Set the value for each destination column', + }, + { + name: 'Raw', + value: 'raw', + description: 'Send raw data as JSON', + }, + ], + }, + { + displayName: 'Data', + name: 'data', + type: 'json', + default: '', + required: true, + placeholder: 'e.g. [["Sara","1/2/2006","Berlin"],["George","5/3/2010","Paris"]]', + description: 'Raw values for the specified range as array of string arrays in JSON format', + displayOptions: { + show: { + dataMode: ['raw'], + }, + }, + }, + { + displayName: 'Values to Send', + name: 'fieldsUi', + placeholder: 'Add Field', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + displayOptions: { + show: { + dataMode: ['define'], + }, + }, + default: {}, + options: [ + { + displayName: 'Field', + name: 'values', + values: [ + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options + displayName: 'Column', + name: 'column', + type: 'options', + description: + 'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>', + typeOptions: { + loadOptionsDependsOn: ['table.value', 'worksheet.value', 'workbook.value'], + loadOptionsMethod: 'getTableColumns', + }, + default: '', + }, + { + displayName: 'Value', + name: 'fieldValue', + type: 'string', + default: '', + requiresDataPath: 'single', + }, + ], + }, + ], + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + options: [ + { + displayName: 'Index', + name: 'index', + type: 'number', + default: 0, + typeOptions: { + minValue: 0, + }, + description: + 'Specifies the relative position of the new row. If not defined, the addition happens at the end. Any row below the inserted row will be shifted downwards. First row index is 0.', + }, + { + displayName: 'RAW Data', + name: 'rawData', + type: 'boolean', + // eslint-disable-next-line n8n-nodes-base/node-param-default-wrong-for-boolean + default: 0, + description: + 'Whether the data should be returned RAW instead of parsed into keys according to their header', + }, + { + displayName: 'Data Property', + name: 'dataProperty', + type: 'string', + default: 'data', + required: true, + displayOptions: { + show: { + rawData: [true], + }, + }, + description: 'The name of the property into which to write the RAW data', + }, + ], + }, +]; + +const displayOptions = { + show: { + resource: ['table'], + operation: ['append'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute( + this: IExecuteFunctions, + items: INodeExecutionData[], +): Promise<INodeExecutionData[]> { + //https://docs.microsoft.com/en-us/graph/api/table-post-rows?view=graph-rest-1.0&tabs=http + const returnData: INodeExecutionData[] = []; + + try { + // TODO: At some point it should be possible to use item dependent parameters. + // Is however important to then not make one separate request each. + const workbookId = this.getNodeParameter('workbook', 0, undefined, { + extractValue: true, + }) as string; + + const worksheetId = this.getNodeParameter('worksheet', 0, undefined, { + extractValue: true, + }) as string; + + const tableId = this.getNodeParameter('table', 0, undefined, { + extractValue: true, + }) as string; + + const dataMode = this.getNodeParameter('dataMode', 0) as string; + + // Get table columns to eliminate any columns not needed on the input + const columnsData = await microsoftApiRequest.call( + this, + 'GET', + `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/columns`, + {}, + ); + const columnsRow = columnsData.value.map((column: IDataObject) => column.name); + + const body: IDataObject = {}; + + let values: string[][] = []; + + if (dataMode === 'raw') { + const data = this.getNodeParameter('data', 0); + values = processJsonInput(data, 'Data') as string[][]; + } + + if (dataMode === 'autoMap') { + const itemsData = items.map((item) => item.json); + for (const item of itemsData) { + const updateRow: string[] = []; + + for (const column of columnsRow) { + updateRow.push(item[column] as string); + } + + values.push(updateRow); + } + } + + if (dataMode === 'define') { + const itemsData: IDataObject[] = []; + for (let itemIndex = 0; itemIndex < items.length; itemIndex++) { + const updateData: IDataObject = {}; + const definedFields = this.getNodeParameter('fieldsUi.values', itemIndex, []) as Array<{ + column: string; + fieldValue: string; + }>; + for (const entry of definedFields) { + updateData[entry.column] = entry.fieldValue; + } + itemsData.push(updateData); + } + + for (const item of itemsData) { + const updateRow: string[] = []; + + for (const column of columnsRow) { + updateRow.push(item[column] as string); + } + + values.push(updateRow); + } + } + + body.values = values; + + const options = this.getNodeParameter('options', 0); + + if (options.index) { + body.index = options.index as number; + } + + const { id } = await microsoftApiRequest.call( + this, + 'POST', + `/drive/items/${workbookId}/workbook/createSession`, + { persistChanges: true }, + ); + const responseData = await microsoftApiRequest.call( + this, + 'POST', + `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/rows/add`, + body, + {}, + '', + { 'workbook-session-id': id }, + ); + await microsoftApiRequest.call( + this, + 'POST', + `/drive/items/${workbookId}/workbook/closeSession`, + {}, + {}, + '', + { 'workbook-session-id': id }, + ); + + const rawData = options.rawData as boolean; + const dataProperty = (options.dataProperty as string) || 'data'; + + returnData.push( + ...prepareOutput(this.getNode(), responseData as ExcelResponse, { + columnsRow, + dataProperty, + rawData, + }), + ); + } catch (error) { + if (this.continueOnFail()) { + const executionErrorData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray({ error: error.message }), + { itemData: { item: 0 } }, + ); + returnData.push(...executionErrorData); + } else { + throw error; + } + } + + return returnData; +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/table/convertToRange.operation.ts b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/table/convertToRange.operation.ts new file mode 100644 index 0000000000000..281b15461d0df --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/table/convertToRange.operation.ts @@ -0,0 +1,64 @@ +import type { IExecuteFunctions } from 'n8n-core'; +import type { IDataObject, INodeExecutionData, INodeProperties } from 'n8n-workflow'; +import { updateDisplayOptions } from '../../../../../../utils/utilities'; +import { microsoftApiRequest } from '../../transport'; +import { tableRLC, workbookRLC, worksheetRLC } from '../common.descriptions'; + +const properties: INodeProperties[] = [workbookRLC, worksheetRLC, tableRLC]; + +const displayOptions = { + show: { + resource: ['table'], + operation: ['convertToRange'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute( + this: IExecuteFunctions, + items: INodeExecutionData[], +): Promise<INodeExecutionData[]> { + const returnData: INodeExecutionData[] = []; + + for (let i = 0; i < items.length; i++) { + try { + const workbookId = this.getNodeParameter('workbook', i, undefined, { + extractValue: true, + }) as string; + + const worksheetId = this.getNodeParameter('worksheet', i, undefined, { + extractValue: true, + }) as string; + + const tableId = this.getNodeParameter('table', i, undefined, { + extractValue: true, + }) as string; + + const responseData = await microsoftApiRequest.call( + this, + 'POST', + `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/convertToRange`, + ); + + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(responseData as IDataObject), + { itemData: { item: i } }, + ); + + returnData.push(...executionData); + } catch (error) { + if (this.continueOnFail()) { + const executionErrorData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray({ error: error.message }), + { itemData: { item: i } }, + ); + returnData.push(...executionErrorData); + continue; + } + throw error; + } + } + + return returnData; +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/table/deleteTable.operation.ts b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/table/deleteTable.operation.ts new file mode 100644 index 0000000000000..171c80044c9d5 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/table/deleteTable.operation.ts @@ -0,0 +1,64 @@ +import type { IExecuteFunctions } from 'n8n-core'; +import type { INodeExecutionData, INodeProperties } from 'n8n-workflow'; +import { updateDisplayOptions } from '../../../../../../utils/utilities'; +import { microsoftApiRequest } from '../../transport'; +import { tableRLC, workbookRLC, worksheetRLC } from '../common.descriptions'; + +const properties: INodeProperties[] = [workbookRLC, worksheetRLC, tableRLC]; + +const displayOptions = { + show: { + resource: ['table'], + operation: ['deleteTable'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute( + this: IExecuteFunctions, + items: INodeExecutionData[], +): Promise<INodeExecutionData[]> { + const returnData: INodeExecutionData[] = []; + + for (let i = 0; i < items.length; i++) { + try { + const workbookId = this.getNodeParameter('workbook', i, undefined, { + extractValue: true, + }) as string; + + const worksheetId = this.getNodeParameter('worksheet', i, undefined, { + extractValue: true, + }) as string; + + const tableId = this.getNodeParameter('table', i, undefined, { + extractValue: true, + }) as string; + + await microsoftApiRequest.call( + this, + 'DELETE', + `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}`, + ); + + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray({ success: true }), + { itemData: { item: i } }, + ); + + returnData.push(...executionData); + } catch (error) { + if (this.continueOnFail()) { + const executionErrorData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray({ error: error.message }), + { itemData: { item: i } }, + ); + returnData.push(...executionErrorData); + continue; + } + throw error; + } + } + + return returnData; +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/table/getColumns.operation.ts b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/table/getColumns.operation.ts new file mode 100644 index 0000000000000..18260d84d254d --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/table/getColumns.operation.ts @@ -0,0 +1,165 @@ +import type { IExecuteFunctions } from 'n8n-core'; +import type { IDataObject, INodeExecutionData, INodeProperties } from 'n8n-workflow'; +import { updateDisplayOptions } from '../../../../../../utils/utilities'; +import { microsoftApiRequest, microsoftApiRequestAllItemsSkip } from '../../transport'; +import { tableRLC, workbookRLC, worksheetRLC } from '../common.descriptions'; + +const properties: INodeProperties[] = [ + workbookRLC, + worksheetRLC, + tableRLC, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Whether to return all results or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + returnAll: [false], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 100, + description: 'Max number of results to return', + }, + { + displayName: 'RAW Data', + name: 'rawData', + type: 'boolean', + default: false, + description: + 'Whether the data should be returned RAW instead of parsed into keys according to their header', + }, + { + displayName: 'Data Property', + name: 'dataProperty', + type: 'string', + default: 'data', + displayOptions: { + show: { + rawData: [true], + }, + }, + description: 'The name of the property into which to write the RAW data', + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Filter', + default: {}, + displayOptions: { + show: { + rawData: [true], + }, + }, + options: [ + { + displayName: 'Fields', + name: 'fields', + type: 'string', + default: '', + description: 'A comma-separated list of the fields to include in the response', + }, + ], + }, +]; + +const displayOptions = { + show: { + resource: ['table'], + operation: ['getColumns'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute( + this: IExecuteFunctions, + items: INodeExecutionData[], +): Promise<INodeExecutionData[]> { + //https://docs.microsoft.com/en-us/graph/api/table-list-columns?view=graph-rest-1.0&tabs=http + const returnData: INodeExecutionData[] = []; + + for (let i = 0; i < items.length; i++) { + try { + const qs: IDataObject = {}; + const workbookId = this.getNodeParameter('workbook', i, undefined, { + extractValue: true, + }) as string; + + const worksheetId = this.getNodeParameter('worksheet', i, undefined, { + extractValue: true, + }) as string; + + const tableId = this.getNodeParameter('table', i, undefined, { + extractValue: true, + }) as string; + + const returnAll = this.getNodeParameter('returnAll', i); + const rawData = this.getNodeParameter('rawData', i); + if (rawData) { + const filters = this.getNodeParameter('filters', i); + if (filters.fields) { + qs.$select = filters.fields; + } + } + + let responseData; + if (returnAll) { + responseData = await microsoftApiRequestAllItemsSkip.call( + this, + 'value', + 'GET', + `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/columns`, + {}, + qs, + ); + } else { + qs.$top = this.getNodeParameter('limit', i); + responseData = await microsoftApiRequest.call( + this, + 'GET', + `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/columns`, + {}, + qs, + ); + responseData = responseData.value; + } + if (!rawData) { + responseData = responseData.map((column: IDataObject) => ({ name: column.name })); + } else { + const dataProperty = this.getNodeParameter('dataProperty', i) as string; + responseData = { [dataProperty]: responseData }; + } + + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(responseData as IDataObject[]), + { itemData: { item: i } }, + ); + + returnData.push(...executionData); + } catch (error) { + if (this.continueOnFail()) { + const executionErrorData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray({ error: error.message }), + { itemData: { item: i } }, + ); + returnData.push(...executionErrorData); + continue; + } + throw error; + } + } + + return returnData; +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/table/getRows.operation.ts b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/table/getRows.operation.ts new file mode 100644 index 0000000000000..bc0c56f940365 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/table/getRows.operation.ts @@ -0,0 +1,223 @@ +import type { IExecuteFunctions } from 'n8n-core'; +import type { IDataObject, INodeExecutionData, INodeProperties } from 'n8n-workflow'; +import { updateDisplayOptions } from '../../../../../../utils/utilities'; +import { microsoftApiRequest, microsoftApiRequestAllItemsSkip } from '../../transport'; +import { tableRLC, workbookRLC, worksheetRLC } from '../common.descriptions'; + +const properties: INodeProperties[] = [ + workbookRLC, + worksheetRLC, + tableRLC, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Whether to return all results or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + returnAll: [false], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 100, + description: 'Max number of results to return', + }, + { + displayName: 'RAW Data', + name: 'rawData', + type: 'boolean', + default: false, + description: + 'Whether the data should be returned RAW instead of parsed into keys according to their header', + }, + { + displayName: 'Data Property', + name: 'dataProperty', + type: 'string', + default: 'data', + displayOptions: { + show: { + rawData: [true], + }, + }, + description: 'The name of the property into which to write the RAW data', + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Filter', + default: {}, + options: [ + { + displayName: 'Fields', + name: 'fields', + type: 'string', + default: '', + description: 'A comma-separated list of the fields to include in the response', + displayOptions: { + show: { + '/rawData': [true], + }, + }, + }, + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options + displayName: 'Column Names or IDs', + name: 'column', + type: 'multiOptions', + description: + 'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>. Choose from the list, or specify IDs using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>.', + typeOptions: { + loadOptionsDependsOn: ['table.value', 'worksheet.value', 'workbook.value'], + loadOptionsMethod: 'getTableColumns', + }, + default: [], + displayOptions: { + show: { + '/rawData': [false], + }, + }, + }, + ], + }, +]; + +const displayOptions = { + show: { + resource: ['table'], + operation: ['getRows'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute( + this: IExecuteFunctions, + items: INodeExecutionData[], +): Promise<INodeExecutionData[]> { + //https://docs.microsoft.com/en-us/graph/api/table-list-rows?view=graph-rest-1.0&tabs=http + const returnData: INodeExecutionData[] = []; + + for (let i = 0; i < items.length; i++) { + const qs: IDataObject = {}; + try { + const workbookId = this.getNodeParameter('workbook', i, undefined, { + extractValue: true, + }) as string; + + const worksheetId = this.getNodeParameter('worksheet', i, undefined, { + extractValue: true, + }) as string; + + const tableId = this.getNodeParameter('table', i, undefined, { + extractValue: true, + }) as string; + + const filters = this.getNodeParameter('filters', i); + const returnAll = this.getNodeParameter('returnAll', i); + const rawData = this.getNodeParameter('rawData', i); + + if (rawData) { + if (filters.fields) { + qs.$select = filters.fields; + } + } + + let responseData; + if (returnAll) { + responseData = await microsoftApiRequestAllItemsSkip.call( + this, + 'value', + 'GET', + `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/rows`, + {}, + qs, + ); + } else { + const rowsQs = { ...qs }; + rowsQs.$top = this.getNodeParameter('limit', i); + responseData = await microsoftApiRequest.call( + this, + 'GET', + `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/rows`, + {}, + rowsQs, + ); + responseData = responseData.value; + } + if (!rawData) { + const columnsQs = { ...qs }; + columnsQs.$select = 'name'; + // TODO: That should probably be cached in the future + let columns = await microsoftApiRequestAllItemsSkip.call( + this, + 'value', + 'GET', + `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/columns`, + {}, + columnsQs, + ); + + columns = (columns as IDataObject[]).map((column) => column.name); + + let rows: INodeExecutionData[] = []; + for (let index = 0; index < responseData.length; index++) { + const object: IDataObject = {}; + for (let y = 0; y < columns.length; y++) { + object[columns[y]] = responseData[index].values[0][y]; + } + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray({ ...object }), + { itemData: { item: index } }, + ); + + rows.push(...executionData); + } + + if ((filters?.column as string[])?.length) { + rows = rows.map((row) => { + const rowData: IDataObject = {}; + Object.keys(row.json).forEach((key) => { + if ((filters.column as string[]).includes(key)) { + rowData[key] = row.json[key]; + } + }); + return { ...rowData, json: rowData }; + }); + } + + returnData.push(...rows); + } else { + const dataProperty = this.getNodeParameter('dataProperty', i) as string; + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray({ [dataProperty]: responseData }), + { itemData: { item: i } }, + ); + + returnData.push(...executionData); + } + } catch (error) { + if (this.continueOnFail()) { + const executionErrorData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray({ error: error.message }), + { itemData: { item: i } }, + ); + returnData.push(...executionErrorData); + continue; + } + throw error; + } + } + + return returnData; +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/table/lookup.operation.ts b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/table/lookup.operation.ts new file mode 100644 index 0000000000000..6229930ad88cc --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/table/lookup.operation.ts @@ -0,0 +1,156 @@ +import type { IExecuteFunctions } from 'n8n-core'; +import type { IDataObject, INodeExecutionData, INodeProperties, JsonObject } from 'n8n-workflow'; +import { NodeApiError } from 'n8n-workflow'; +import { updateDisplayOptions } from '../../../../../../utils/utilities'; +import { microsoftApiRequestAllItemsSkip } from '../../transport'; +import { tableRLC, workbookRLC, worksheetRLC } from '../common.descriptions'; + +const properties: INodeProperties[] = [ + workbookRLC, + worksheetRLC, + tableRLC, + { + displayName: 'Lookup Column', + name: 'lookupColumn', + type: 'string', + default: '', + placeholder: 'Email', + required: true, + description: 'The name of the column in which to look for value', + }, + { + displayName: 'Lookup Value', + name: 'lookupValue', + type: 'string', + default: '', + placeholder: 'frank@example.com', + required: true, + description: 'The value to look for in column', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + options: [ + { + displayName: 'Return All Matches', + name: 'returnAllMatches', + type: 'boolean', + default: false, + // eslint-disable-next-line n8n-nodes-base/node-param-description-boolean-without-whether + description: + 'By default only the first result gets returned. If options gets set all found matches get returned.', + }, + ], + }, +]; + +const displayOptions = { + show: { + resource: ['table'], + operation: ['lookup'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute( + this: IExecuteFunctions, + items: INodeExecutionData[], +): Promise<INodeExecutionData[]> { + const returnData: INodeExecutionData[] = []; + + for (let i = 0; i < items.length; i++) { + const qs: IDataObject = {}; + try { + const workbookId = this.getNodeParameter('workbook', i, undefined, { + extractValue: true, + }) as string; + + const worksheetId = this.getNodeParameter('worksheet', i, undefined, { + extractValue: true, + }) as string; + + const tableId = this.getNodeParameter('table', i, undefined, { + extractValue: true, + }) as string; + + const lookupColumn = this.getNodeParameter('lookupColumn', i) as string; + const lookupValue = this.getNodeParameter('lookupValue', i) as string; + const options = this.getNodeParameter('options', i); + + let responseData = await microsoftApiRequestAllItemsSkip.call( + this, + 'value', + 'GET', + `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/rows`, + {}, + {}, + ); + + qs.$select = 'name'; + // TODO: That should probably be cached in the future + let columns = await microsoftApiRequestAllItemsSkip.call( + this, + 'value', + 'GET', + `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/columns`, + {}, + qs, + ); + columns = columns.map((column: IDataObject) => column.name); + + if (!columns.includes(lookupColumn)) { + throw new NodeApiError(this.getNode(), responseData as JsonObject, { + message: `Column ${lookupColumn} does not exist on the table selected`, + }); + } + + const result: IDataObject[] = []; + + for (let index = 0; index < responseData.length; index++) { + const object: IDataObject = {}; + for (let y = 0; y < columns.length; y++) { + object[columns[y]] = responseData[index].values[0][y]; + } + result.push({ ...object }); + } + + if (options.returnAllMatches) { + responseData = result.filter((data: IDataObject) => { + return data[lookupColumn]?.toString() === lookupValue; + }); + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(responseData as IDataObject), + { itemData: { item: i } }, + ); + + returnData.push(...executionData); + } else { + responseData = result.find((data: IDataObject) => { + return data[lookupColumn]?.toString() === lookupValue; + }); + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(responseData as IDataObject), + { itemData: { item: i } }, + ); + + returnData.push(...executionData); + } + } catch (error) { + if (this.continueOnFail()) { + const executionErrorData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray({ error: error.message }), + { itemData: { item: i } }, + ); + returnData.push(...executionErrorData); + continue; + } + throw error; + } + } + + return returnData; +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/versionDescription.ts b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/versionDescription.ts new file mode 100644 index 0000000000000..128c25231ef5b --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/versionDescription.ts @@ -0,0 +1,63 @@ +/* eslint-disable n8n-nodes-base/node-filename-against-convention */ +import type { INodeTypeDescription } from 'n8n-workflow'; + +import * as table from './table/Table.resource'; +import * as workbook from './workbook/Workbook.resource'; +import * as worksheet from './worksheet/Worksheet.resource'; + +export const versionDescription: INodeTypeDescription = { + displayName: 'Microsoft Excel 365', + name: 'microsoftExcel', + icon: 'file:excel.svg', + group: ['input'], + version: 2, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume Microsoft Excel API', + defaults: { + name: 'Microsoft Excel 365', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'microsoftExcelOAuth2Api', + required: true, + }, + ], + properties: [ + { + displayName: + 'This node connects to the Microsoft 365 cloud platform. Use the \'Spreadsheet File\' node to directly manipulate spreadsheet files (.xls, .csv, etc). <a href="/templates/890" target="_blank">More info</a>.', + name: 'notice', + type: 'notice', + default: '', + }, + { + displayName: 'Resource', + name: 'resource', + type: 'options', + noDataExpression: true, + options: [ + { + name: 'Table', + value: 'table', + description: 'Represents an Excel table', + }, + { + name: 'Workbook', + value: 'workbook', + description: 'A workbook is the top level object which contains one or more worksheets', + }, + { + name: 'Sheet', + value: 'worksheet', + description: 'A sheet is a grid of cells which can contain data, tables, charts, etc', + }, + ], + default: 'workbook', + }, + ...table.description, + ...workbook.description, + ...worksheet.description, + ], +}; diff --git a/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/workbook/Workbook.resource.ts b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/workbook/Workbook.resource.ts new file mode 100644 index 0000000000000..be20b4c444e5c --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/workbook/Workbook.resource.ts @@ -0,0 +1,45 @@ +import type { INodeProperties } from 'n8n-workflow'; + +import * as addWorksheet from './addWorksheet.operation'; +import * as deleteWorkbook from './deleteWorkbook.operation'; +import * as getAll from './getAll.operation'; + +export { addWorksheet, deleteWorkbook, getAll }; + +export const description: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['workbook'], + }, + }, + options: [ + { + name: 'Add Sheet', + value: 'addWorksheet', + description: 'Add a new sheet to the workbook', + action: 'Add a sheet to a workbook', + }, + { + name: 'Delete', + value: 'deleteWorkbook', + description: 'Delete workbook', + action: 'Delete workbook', + }, + { + name: 'Get Many', + value: 'getAll', + description: 'Get workbooks', + action: 'Get workbooks', + }, + ], + default: 'getAll', + }, + ...addWorksheet.description, + ...deleteWorkbook.description, + ...getAll.description, +]; diff --git a/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/workbook/addWorksheet.operation.ts b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/workbook/addWorksheet.operation.ts new file mode 100644 index 0000000000000..856a909f28874 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/workbook/addWorksheet.operation.ts @@ -0,0 +1,109 @@ +import type { IExecuteFunctions } from 'n8n-core'; +import type { IDataObject, INodeExecutionData, INodeProperties } from 'n8n-workflow'; +import { updateDisplayOptions } from '../../../../../../utils/utilities'; +import { microsoftApiRequest } from '../../transport'; +import { workbookRLC } from '../common.descriptions'; + +const properties: INodeProperties[] = [ + workbookRLC, + { + displayName: 'Options', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Option', + default: {}, + options: [ + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + description: + 'The name of the sheet to be added. The name should be unique. If not specified, Excel will determine the name of the new worksheet.', + }, + ], + }, +]; + +const displayOptions = { + show: { + resource: ['workbook'], + operation: ['addWorksheet'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute( + this: IExecuteFunctions, + items: INodeExecutionData[], +): Promise<INodeExecutionData[]> { + //https://docs.microsoft.com/en-us/graph/api/worksheetcollection-add?view=graph-rest-1.0&tabs=http + const returnData: INodeExecutionData[] = []; + + for (let i = 0; i < items.length; i++) { + try { + const workbookId = this.getNodeParameter('workbook', i, undefined, { + extractValue: true, + }) as string; + + const additionalFields = this.getNodeParameter('additionalFields', i); + const body: IDataObject = {}; + if (additionalFields.name) { + body.name = additionalFields.name; + } + const { id } = await microsoftApiRequest.call( + this, + 'POST', + `/drive/items/${workbookId}/workbook/createSession`, + { persistChanges: true }, + ); + const responseData = await microsoftApiRequest.call( + this, + 'POST', + `/drive/items/${workbookId}/workbook/worksheets/add`, + body, + {}, + '', + { 'workbook-session-id': id }, + ); + await microsoftApiRequest.call( + this, + 'POST', + `/drive/items/${workbookId}/workbook/closeSession`, + {}, + {}, + '', + { 'workbook-session-id': id }, + ); + + if (Array.isArray(responseData)) { + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(responseData), + { itemData: { item: i } }, + ); + + returnData.push(...executionData); + } else if (responseData !== undefined) { + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(responseData as IDataObject), + { itemData: { item: i } }, + ); + + returnData.push(...executionData); + } + } catch (error) { + if (this.continueOnFail()) { + const executionErrorData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray({ error: error.message }), + { itemData: { item: i } }, + ); + returnData.push(...executionErrorData); + continue; + } + throw error; + } + } + + return returnData; +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/workbook/deleteWorkbook.operation.ts b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/workbook/deleteWorkbook.operation.ts new file mode 100644 index 0000000000000..b97acce63a518 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/workbook/deleteWorkbook.operation.ts @@ -0,0 +1,77 @@ +import type { IExecuteFunctions } from 'n8n-core'; +import type { INodeExecutionData, INodeProperties } from 'n8n-workflow'; +import { NodeOperationError } from 'n8n-workflow'; +import { updateDisplayOptions } from '../../../../../../utils/utilities'; +import { microsoftApiRequest } from '../../transport'; +import { workbookRLC } from '../common.descriptions'; + +const properties: INodeProperties[] = [workbookRLC]; + +const displayOptions = { + show: { + resource: ['workbook'], + operation: ['deleteWorkbook'], + }, +}; +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute( + this: IExecuteFunctions, + items: INodeExecutionData[], +): Promise<INodeExecutionData[]> { + const returnData: INodeExecutionData[] = []; + + for (let i = 0; i < items.length; i++) { + try { + const workbookId = this.getNodeParameter('workbook', i, undefined, { + extractValue: true, + }) as string; + + try { + await microsoftApiRequest.call(this, 'DELETE', `/drive/items/${workbookId}`); + } catch (error) { + if (error?.description.includes('Lock token does not match existing lock')) { + const errorDescription = + 'Lock token does not match existing lock, this error could happen if the file is opened in the browser or the Office client, please close file and try again.'; + + throw new NodeOperationError(this.getNode(), error as Error, { + itemIndex: i, + description: errorDescription, + }); + } else { + throw error; + } + } + + const responseData = { success: true }; + + if (Array.isArray(responseData)) { + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(responseData), + { itemData: { item: i } }, + ); + + returnData.push(...executionData); + } else if (responseData !== undefined) { + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(responseData), + { itemData: { item: i } }, + ); + + returnData.push(...executionData); + } + } catch (error) { + if (this.continueOnFail()) { + const executionErrorData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray({ error: error.message }), + { itemData: { item: i } }, + ); + returnData.push(...executionErrorData); + continue; + } + throw error; + } + } + + return returnData; +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/workbook/getAll.operation.ts b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/workbook/getAll.operation.ts new file mode 100644 index 0000000000000..bcff50920a95a --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/workbook/getAll.operation.ts @@ -0,0 +1,122 @@ +import type { IExecuteFunctions } from 'n8n-core'; +import type { IDataObject, INodeExecutionData, INodeProperties } from 'n8n-workflow'; +import { updateDisplayOptions } from '../../../../../../utils/utilities'; +import { microsoftApiRequest, microsoftApiRequestAllItems } from '../../transport'; + +const properties: INodeProperties[] = [ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Whether to return all results or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + returnAll: [false], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 100, + description: 'Max number of results to return', + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Filter', + default: {}, + options: [ + { + displayName: 'Fields', + name: 'fields', + type: 'string', + default: '', + description: 'A comma-separated list of the fields to include in the response', + }, + ], + }, +]; + +const displayOptions = { + show: { + resource: ['workbook'], + operation: ['getAll'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute( + this: IExecuteFunctions, + items: INodeExecutionData[], +): Promise<INodeExecutionData[]> { + const returnData: INodeExecutionData[] = []; + + for (let i = 0; i < items.length; i++) { + try { + const returnAll = this.getNodeParameter('returnAll', i); + const filters = this.getNodeParameter('filters', i); + const qs: IDataObject = {}; + if (filters.fields) { + qs.$select = filters.fields; + } + let responseData; + if (returnAll) { + responseData = await microsoftApiRequestAllItems.call( + this, + 'value', + 'GET', + "/drive/root/search(q='.xlsx')", + {}, + qs, + ); + } else { + qs.$top = this.getNodeParameter('limit', i); + responseData = await microsoftApiRequest.call( + this, + 'GET', + "/drive/root/search(q='.xlsx')", + {}, + qs, + ); + responseData = responseData.value; + } + + if (Array.isArray(responseData)) { + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(responseData), + { itemData: { item: i } }, + ); + + returnData.push(...executionData); + } else if (responseData !== undefined) { + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(responseData as IDataObject), + { itemData: { item: i } }, + ); + + returnData.push(...executionData); + } + } catch (error) { + if (this.continueOnFail()) { + const executionErrorData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray({ error: error.message }), + { itemData: { item: i } }, + ); + returnData.push(...executionErrorData); + continue; + } + throw error; + } + } + + return returnData; +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/worksheet/Worksheet.resource.ts b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/worksheet/Worksheet.resource.ts new file mode 100644 index 0000000000000..7f8a901b55f81 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/worksheet/Worksheet.resource.ts @@ -0,0 +1,79 @@ +import type { INodeProperties } from 'n8n-workflow'; + +import * as append from './append.operation'; +import * as clear from './clear.operation'; +import * as deleteWorksheet from './deleteWorksheet.operation'; +import * as getAll from './getAll.operation'; +import * as readRows from './readRows.operation'; +import * as update from './update.operation'; +import * as upsert from './upsert.operation'; + +export { append, clear, deleteWorksheet, getAll, readRows, update, upsert }; + +export const description: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['worksheet'], + }, + }, + options: [ + { + name: 'Append', + value: 'append', + description: 'Append data to sheet', + action: 'Append data to sheet', + }, + { + // eslint-disable-next-line n8n-nodes-base/node-param-option-name-wrong-for-upsert + name: 'Append or Update', + value: 'upsert', + // eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-upsert + description: 'Append a new row or update the current one if it already exists (upsert)', + action: 'Append or update a sheet', + }, + { + name: 'Clear', + value: 'clear', + description: 'Clear sheet', + action: 'Clear sheet', + }, + { + name: 'Delete', + value: 'deleteWorksheet', + description: 'Delete sheet', + action: 'Delete sheet', + }, + { + name: 'Get Many', + value: 'getAll', + description: 'Get a list of sheets', + action: 'Get sheets', + }, + { + name: 'Get Rows', + value: 'readRows', + description: 'Retrieve a list of sheet rows', + action: 'Get rows from sheet', + }, + { + name: 'Update', + value: 'update', + description: 'Update rows of a sheet or sheet range', + action: 'Update sheet', + }, + ], + default: 'getAll', + }, + ...append.description, + ...clear.description, + ...deleteWorksheet.description, + ...getAll.description, + ...readRows.description, + ...update.description, + ...upsert.description, +]; diff --git a/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/worksheet/append.operation.ts b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/worksheet/append.operation.ts new file mode 100644 index 0000000000000..7cb57d6782e97 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/worksheet/append.operation.ts @@ -0,0 +1,227 @@ +import type { IExecuteFunctions } from 'n8n-core'; +import type { IDataObject, INodeExecutionData, INodeProperties } from 'n8n-workflow'; +import { processJsonInput, updateDisplayOptions } from '../../../../../../utils/utilities'; +import type { ExcelResponse } from '../../helpers/interfaces'; +import { prepareOutput } from '../../helpers/utils'; +import { microsoftApiRequest } from '../../transport'; +import { workbookRLC, worksheetRLC } from '../common.descriptions'; + +const properties: INodeProperties[] = [ + workbookRLC, + worksheetRLC, + { + displayName: 'Data Mode', + name: 'dataMode', + type: 'options', + default: 'define', + options: [ + { + name: 'Auto-Map Input Data to Columns', + value: 'autoMap', + description: 'Use when node input properties match destination column names', + }, + { + name: 'Map Each Column Below', + value: 'define', + description: 'Set the value for each destination column', + }, + { + name: 'Raw', + value: 'raw', + description: 'Send raw data as JSON', + }, + ], + }, + { + displayName: 'Data', + name: 'data', + type: 'json', + default: '', + required: true, + placeholder: 'e.g. [["Sara","1/2/2006","Berlin"],["George","5/3/2010","Paris"]]', + description: 'Raw values for the specified range as array of string arrays in JSON format', + displayOptions: { + show: { + dataMode: ['raw'], + }, + }, + }, + { + displayName: 'Values to Send', + name: 'fieldsUi', + placeholder: 'Add Field', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + displayOptions: { + show: { + dataMode: ['define'], + }, + }, + default: {}, + options: [ + { + displayName: 'Field', + name: 'values', + values: [ + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options + displayName: 'Column', + name: 'column', + type: 'options', + description: + 'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>', + typeOptions: { + loadOptionsDependsOn: ['worksheet.value'], + loadOptionsMethod: 'getWorksheetColumnRow', + }, + default: '', + }, + { + displayName: 'Value', + name: 'fieldValue', + type: 'string', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + options: [ + { + displayName: 'RAW Data', + name: 'rawData', + type: 'boolean', + // eslint-disable-next-line n8n-nodes-base/node-param-default-wrong-for-boolean + default: 0, + description: + 'Whether the data should be returned RAW instead of parsed into keys according to their header', + }, + { + displayName: 'Data Property', + name: 'dataProperty', + type: 'string', + default: 'data', + required: true, + displayOptions: { + show: { + rawData: [true], + }, + }, + description: 'The name of the property into which to write the RAW data', + }, + ], + }, +]; + +const displayOptions = { + show: { + resource: ['worksheet'], + operation: ['append'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute( + this: IExecuteFunctions, + items: INodeExecutionData[], +): Promise<INodeExecutionData[]> { + const returnData: INodeExecutionData[] = []; + + const workbookId = this.getNodeParameter('workbook', 0, undefined, { + extractValue: true, + }) as string; + + const worksheetId = this.getNodeParameter('worksheet', 0, undefined, { + extractValue: true, + }) as string; + + const dataMode = this.getNodeParameter('dataMode', 0) as string; + + const worksheetData = await microsoftApiRequest.call( + this, + 'GET', + `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/usedRange`, + ); + + let values: string[][] = []; + + if (dataMode === 'raw') { + const data = this.getNodeParameter('data', 0); + values = processJsonInput(data, 'Data') as string[][]; + } + + const columnsRow = (worksheetData.values as string[][])[0]; + + if (dataMode === 'autoMap') { + const itemsData = items.map((item) => item.json); + for (const item of itemsData) { + const updateRow: string[] = []; + + for (const column of columnsRow) { + updateRow.push(item[column] as string); + } + + values.push(updateRow); + } + } + + if (dataMode === 'define') { + const itemsData: IDataObject[] = []; + for (let itemIndex = 0; itemIndex < items.length; itemIndex++) { + const updateData: IDataObject = {}; + const definedFields = this.getNodeParameter('fieldsUi.values', itemIndex, []) as Array<{ + column: string; + fieldValue: string; + }>; + for (const entry of definedFields) { + updateData[entry.column] = entry.fieldValue; + } + itemsData.push(updateData); + } + + for (const item of itemsData) { + const updateRow: string[] = []; + + for (const column of columnsRow) { + updateRow.push(item[column] as string); + } + + values.push(updateRow); + } + } + + const { address } = worksheetData; + const usedRange = address.split('!')[1]; + + const [rangeFrom, rangeTo] = usedRange.split(':'); + const cellDataFrom = rangeFrom.match(/([a-zA-Z]{1,10})([0-9]{0,10})/) || []; + const cellDataTo = rangeTo.match(/([a-zA-Z]{1,10})([0-9]{0,10})/) || []; + + const from = `${cellDataFrom[1]}${Number(cellDataTo[2]) + 1}`; + const to = `${cellDataTo[1]}${Number(cellDataTo[2]) + Number(values.length)}`; + + const responseData: ExcelResponse = await microsoftApiRequest.call( + this, + 'PATCH', + `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/range(address='${from}:${to}')`, + { values }, + ); + + const rawData = this.getNodeParameter('options.rawData', 0, false) as boolean; + const dataProperty = this.getNodeParameter('options.dataProperty', 0, 'data') as string; + + returnData.push( + ...prepareOutput(this.getNode(), responseData, { columnsRow, dataProperty, rawData }), + ); + + return returnData; +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/worksheet/clear.operation.ts b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/worksheet/clear.operation.ts new file mode 100644 index 0000000000000..4e9b5ab8cbce7 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/worksheet/clear.operation.ts @@ -0,0 +1,121 @@ +import type { IExecuteFunctions } from 'n8n-core'; +import type { INodeExecutionData, INodeProperties } from 'n8n-workflow'; +import { updateDisplayOptions } from '../../../../../../utils/utilities'; +import { microsoftApiRequest } from '../../transport'; +import { workbookRLC, worksheetRLC } from '../common.descriptions'; + +const properties: INodeProperties[] = [ + workbookRLC, + worksheetRLC, + { + displayName: 'Apply To', + name: 'applyTo', + type: 'options', + //values in capital case as required by api + options: [ + { + name: 'All', + value: 'All', + description: 'Clear data in cells and remove all formatting', + }, + { + name: 'Formats', + value: 'Formats', + description: 'Clear formatting(e.g. font size, color) of cells', + }, + { + name: 'Contents', + value: 'Contents', + description: 'Clear data contained in cells', + }, + ], + default: 'All', + }, + { + displayName: 'Select a Range', + name: 'useRange', + type: 'boolean', + default: false, + }, + { + displayName: 'Range', + name: 'range', + type: 'string', + displayOptions: { + show: { + useRange: [true], + }, + }, + placeholder: 'e.g. A1:B2', + default: '', + description: 'The sheet range that would be cleared, specified using a A1-style notation', + hint: 'Leave blank for entire worksheet', + }, +]; + +const displayOptions = { + show: { + resource: ['worksheet'], + operation: ['clear'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute( + this: IExecuteFunctions, + items: INodeExecutionData[], +): Promise<INodeExecutionData[]> { + const returnData: INodeExecutionData[] = []; + + for (let i = 0; i < items.length; i++) { + try { + const workbookId = this.getNodeParameter('workbook', i, undefined, { + extractValue: true, + }) as string; + + const worksheetId = this.getNodeParameter('worksheet', i, undefined, { + extractValue: true, + }) as string; + + const applyTo = this.getNodeParameter('applyTo', i) as string; + const useRange = this.getNodeParameter('useRange', i, false) as boolean; + + if (!useRange) { + await microsoftApiRequest.call( + this, + 'POST', + `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/range/clear`, + { applyTo }, + ); + } else { + const range = this.getNodeParameter('range', i, '') as string; + await microsoftApiRequest.call( + this, + 'POST', + `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/range(address='${range}')/clear`, + { applyTo }, + ); + } + + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray({ success: true }), + { itemData: { item: i } }, + ); + + returnData.push(...executionData); + } catch (error) { + if (this.continueOnFail()) { + const executionErrorData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray({ error: error.message }), + { itemData: { item: i } }, + ); + returnData.push(...executionErrorData); + continue; + } + throw error; + } + } + + return returnData; +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/worksheet/deleteWorksheet.operation.ts b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/worksheet/deleteWorksheet.operation.ts new file mode 100644 index 0000000000000..e9305e219ffd1 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/worksheet/deleteWorksheet.operation.ts @@ -0,0 +1,60 @@ +import type { IExecuteFunctions } from 'n8n-core'; +import type { INodeExecutionData, INodeProperties } from 'n8n-workflow'; +import { updateDisplayOptions } from '../../../../../../utils/utilities'; +import { microsoftApiRequest } from '../../transport'; +import { workbookRLC, worksheetRLC } from '../common.descriptions'; + +const properties: INodeProperties[] = [workbookRLC, worksheetRLC]; + +const displayOptions = { + show: { + resource: ['worksheet'], + operation: ['deleteWorksheet'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute( + this: IExecuteFunctions, + items: INodeExecutionData[], +): Promise<INodeExecutionData[]> { + const returnData: INodeExecutionData[] = []; + + for (let i = 0; i < items.length; i++) { + try { + const workbookId = this.getNodeParameter('workbook', i, undefined, { + extractValue: true, + }) as string; + + const worksheetId = this.getNodeParameter('worksheet', i, undefined, { + extractValue: true, + }) as string; + + await microsoftApiRequest.call( + this, + 'DELETE', + `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}`, + ); + + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray({ success: true }), + { itemData: { item: i } }, + ); + + returnData.push(...executionData); + } catch (error) { + if (this.continueOnFail()) { + const executionErrorData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray({ error: error.message }), + { itemData: { item: i } }, + ); + returnData.push(...executionErrorData); + continue; + } + throw error; + } + } + + return returnData; +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/worksheet/getAll.operation.ts b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/worksheet/getAll.operation.ts new file mode 100644 index 0000000000000..16cc37fab04cc --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/worksheet/getAll.operation.ts @@ -0,0 +1,119 @@ +import type { IExecuteFunctions } from 'n8n-core'; +import type { IDataObject, INodeExecutionData, INodeProperties } from 'n8n-workflow'; +import { updateDisplayOptions } from '../../../../../../utils/utilities'; +import { microsoftApiRequest, microsoftApiRequestAllItems } from '../../transport'; +import { workbookRLC } from '../common.descriptions'; + +const properties: INodeProperties[] = [ + workbookRLC, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Whether to return all results or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + returnAll: [false], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 100, + description: 'Max number of results to return', + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Filter', + default: {}, + options: [ + { + displayName: 'Fields', + name: 'fields', + type: 'string', + default: '', + description: 'A comma-separated list of the fields to include in the response', + }, + ], + }, +]; + +const displayOptions = { + show: { + resource: ['worksheet'], + operation: ['getAll'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute( + this: IExecuteFunctions, + items: INodeExecutionData[], +): Promise<INodeExecutionData[]> { + //https://docs.microsoft.com/en-us/graph/api/workbook-list-worksheets?view=graph-rest-1.0&tabs=http + const returnData: INodeExecutionData[] = []; + + for (let i = 0; i < items.length; i++) { + const qs: IDataObject = {}; + try { + const returnAll = this.getNodeParameter('returnAll', i); + const workbookId = this.getNodeParameter('workbook', i, undefined, { + extractValue: true, + }) as string; + const filters = this.getNodeParameter('filters', i); + if (filters.fields) { + qs.$select = filters.fields; + } + + let responseData; + if (returnAll) { + responseData = await microsoftApiRequestAllItems.call( + this, + 'value', + 'GET', + `/drive/items/${workbookId}/workbook/worksheets`, + {}, + qs, + ); + } else { + qs.$top = this.getNodeParameter('limit', i); + responseData = await microsoftApiRequest.call( + this, + 'GET', + `/drive/items/${workbookId}/workbook/worksheets`, + {}, + qs, + ); + responseData = responseData.value; + } + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(responseData as IDataObject[]), + { itemData: { item: i } }, + ); + + returnData.push(...executionData); + } catch (error) { + if (this.continueOnFail()) { + const executionErrorData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray({ error: error.message }), + { itemData: { item: i } }, + ); + returnData.push(...executionErrorData); + continue; + } + throw error; + } + } + + return returnData; +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/worksheet/readRows.operation.ts b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/worksheet/readRows.operation.ts new file mode 100644 index 0000000000000..eeb791c1ae2b9 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/worksheet/readRows.operation.ts @@ -0,0 +1,199 @@ +import type { IExecuteFunctions } from 'n8n-core'; +import type { IDataObject, INodeExecutionData, INodeProperties } from 'n8n-workflow'; +import { updateDisplayOptions } from '../../../../../../utils/utilities'; +import type { ExcelResponse } from '../../helpers/interfaces'; +import { prepareOutput } from '../../helpers/utils'; +import { microsoftApiRequest } from '../../transport'; +import { workbookRLC, worksheetRLC } from '../common.descriptions'; + +const properties: INodeProperties[] = [ + workbookRLC, + worksheetRLC, + { + displayName: 'Select a Range', + name: 'useRange', + type: 'boolean', + default: false, + }, + { + displayName: 'Range', + name: 'range', + type: 'string', + placeholder: 'e.g. A1:B2', + default: '', + description: 'The sheet range to read the data from specified using a A1-style notation', + hint: 'Leave blank to return entire sheet', + displayOptions: { + show: { + useRange: [true], + }, + }, + }, + { + displayName: 'Header Row', + name: 'keyRow', + type: 'number', + typeOptions: { + minValue: 0, + }, + default: 0, + hint: 'Index of the row which contains the column names', + description: "Relative to selected 'Range', first row index is 0", + displayOptions: { + show: { + useRange: [true], + }, + }, + }, + { + displayName: 'First Data Row', + name: 'dataStartRow', + type: 'number', + typeOptions: { + minValue: 0, + }, + default: 1, + hint: 'Index of first row which contains the actual data', + description: "Relative to selected 'Range', first row index is 0", + displayOptions: { + show: { + useRange: [true], + }, + }, + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + options: [ + { + displayName: 'RAW Data', + name: 'rawData', + type: 'boolean', + // eslint-disable-next-line n8n-nodes-base/node-param-default-wrong-for-boolean + default: 0, + description: + 'Whether the data should be returned RAW instead of parsed into keys according to their header', + }, + { + displayName: 'Data Property', + name: 'dataProperty', + type: 'string', + default: 'data', + required: true, + displayOptions: { + show: { + rawData: [true], + }, + }, + description: 'The name of the property into which to write the RAW data', + }, + { + displayName: 'Fields', + name: 'fields', + type: 'string', + default: '', + description: 'Fields the response will containt. Multiple can be added separated by ,.', + displayOptions: { + show: { + rawData: [true], + }, + }, + }, + ], + }, +]; + +const displayOptions = { + show: { + resource: ['worksheet'], + operation: ['readRows'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute( + this: IExecuteFunctions, + items: INodeExecutionData[], +): Promise<INodeExecutionData[]> { + //https://docs.microsoft.com/en-us/graph/api/worksheet-range?view=graph-rest-1.0&tabs=http + const returnData: INodeExecutionData[] = []; + + for (let i = 0; i < items.length; i++) { + const qs: IDataObject = {}; + try { + const workbookId = this.getNodeParameter('workbook', i, undefined, { + extractValue: true, + }) as string; + + const worksheetId = this.getNodeParameter('worksheet', i, undefined, { + extractValue: true, + }) as string; + + const options = this.getNodeParameter('options', i, {}); + + const range = this.getNodeParameter('range', i, '') as string; + + const rawData = (options.rawData as boolean) || false; + + if (rawData && options.fields) { + qs.$select = options.fields; + } + + let responseData; + if (range) { + responseData = await microsoftApiRequest.call( + this, + 'GET', + `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/range(address='${range}')`, + {}, + qs, + ); + } else { + responseData = await microsoftApiRequest.call( + this, + 'GET', + `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/usedRange`, + {}, + qs, + ); + } + + if (!rawData) { + const keyRow = this.getNodeParameter('keyRow', i, 0) as number; + const firstDataRow = this.getNodeParameter('dataStartRow', i, 1) as number; + + returnData.push( + ...prepareOutput(this.getNode(), responseData as ExcelResponse, { + rawData, + keyRow, + firstDataRow, + }), + ); + } else { + const dataProperty = (options.dataProperty as string) || 'data'; + returnData.push( + ...prepareOutput(this.getNode(), responseData as ExcelResponse, { + rawData, + dataProperty, + }), + ); + } + } catch (error) { + if (this.continueOnFail()) { + const executionErrorData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray({ error: error.message }), + { itemData: { item: i } }, + ); + returnData.push(...executionErrorData); + continue; + } + throw error; + } + } + + return returnData; +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/worksheet/update.operation.ts b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/worksheet/update.operation.ts new file mode 100644 index 0000000000000..58805994c6cb0 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/worksheet/update.operation.ts @@ -0,0 +1,376 @@ +import type { IExecuteFunctions } from 'n8n-core'; +import type { IDataObject, INodeExecutionData, INodeProperties } from 'n8n-workflow'; +import { NodeOperationError } from 'n8n-workflow'; +import { processJsonInput, updateDisplayOptions } from '../../../../../../utils/utilities'; +import type { ExcelResponse, UpdateSummary } from '../../helpers/interfaces'; +import { prepareOutput, updateByAutoMaping, updateByDefinedValues } from '../../helpers/utils'; +import { microsoftApiRequest } from '../../transport'; +import { workbookRLC, worksheetRLC } from '../common.descriptions'; + +const properties: INodeProperties[] = [ + workbookRLC, + worksheetRLC, + { + displayName: 'Select a Range', + name: 'useRange', + type: 'boolean', + default: false, + }, + { + displayName: 'Range', + name: 'range', + type: 'string', + displayOptions: { + show: { + dataMode: ['autoMap', 'define'], + useRange: [true], + }, + }, + placeholder: 'e.g. A1:B2', + default: '', + description: + 'The sheet range to read the data from specified using a A1-style notation. Leave blank to use whole used range in the sheet.', + hint: 'First row must contain column names', + }, + { + displayName: 'Range', + name: 'range', + type: 'string', + displayOptions: { + show: { + dataMode: ['raw'], + useRange: [true], + }, + }, + placeholder: 'e.g. A1:B2', + default: '', + description: 'The sheet range to read the data from specified using a A1-style notation', + hint: 'Leave blank for entire worksheet', + }, + { + displayName: 'Data Mode', + name: 'dataMode', + type: 'options', + default: 'define', + options: [ + { + name: 'Auto-Map Input Data to Columns', + value: 'autoMap', + description: 'Use when node input properties match destination column names', + }, + { + name: 'Map Each Column Below', + value: 'define', + description: 'Set the value for each destination column', + }, + { + name: 'Raw', + value: 'raw', + description: + 'Send raw data as JSON, the whole selected range would be updated with the new values', + }, + ], + }, + { + displayName: 'Data', + name: 'data', + type: 'json', + default: '', + required: true, + placeholder: 'e.g. [["Sara","1/2/2006","Berlin"],["George","5/3/2010","Paris"]]', + description: + 'Raw values for the specified range as array of string arrays in JSON format. Should match the specified range: one array item for each row.', + displayOptions: { + show: { + dataMode: ['raw'], + }, + }, + }, + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased, n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options + displayName: 'Column to match on', + name: 'columnToMatchOn', + type: 'options', + description: + 'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>', + typeOptions: { + loadOptionsDependsOn: ['worksheet.value', 'workbook.value', 'range'], + loadOptionsMethod: 'getWorksheetColumnRow', + }, + default: '', + hint: "Used to find the correct row to update. Doesn't get changed.", + displayOptions: { + show: { + dataMode: ['autoMap', 'define'], + }, + }, + }, + { + displayName: 'Value of Column to Match On', + name: 'valueToMatchOn', + type: 'string', + default: '', + displayOptions: { + show: { + dataMode: ['define'], + }, + }, + }, + { + displayName: 'Values to Send', + name: 'fieldsUi', + placeholder: 'Add Field', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + displayOptions: { + show: { + dataMode: ['define'], + }, + }, + default: {}, + options: [ + { + displayName: 'Field', + name: 'values', + values: [ + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options + displayName: 'Column', + name: 'column', + type: 'options', + description: + 'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>', + typeOptions: { + loadOptionsDependsOn: ['columnToMatchOn', 'range'], + loadOptionsMethod: 'getWorksheetColumnRowSkipColumnToMatchOn', + }, + default: '', + }, + { + displayName: 'Value', + name: 'fieldValue', + type: 'string', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + options: [ + { + displayName: 'RAW Data', + name: 'rawData', + type: 'boolean', + // eslint-disable-next-line n8n-nodes-base/node-param-default-wrong-for-boolean + default: 0, + description: + 'Whether the data should be returned RAW instead of parsed into keys according to their header', + }, + { + displayName: 'Data Property', + name: 'dataProperty', + type: 'string', + default: 'data', + required: true, + displayOptions: { + show: { + rawData: [true], + }, + }, + description: 'The name of the property into which to write the RAW data', + }, + { + displayName: 'Fields', + name: 'fields', + type: 'string', + default: '', + description: 'Fields the response will containt. Multiple can be added separated by ,.', + displayOptions: { + show: { + rawData: [true], + }, + }, + }, + { + displayName: 'Update All Matches', + name: 'updateAll', + type: 'boolean', + default: false, + description: 'Whether to update all matching rows or just the first match', + displayOptions: { + hide: { + '/dataMode': ['raw'], + }, + }, + }, + ], + }, +]; + +const displayOptions = { + show: { + resource: ['worksheet'], + operation: ['update'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute( + this: IExecuteFunctions, + items: INodeExecutionData[], +): Promise<INodeExecutionData[]> { + const returnData: INodeExecutionData[] = []; + + try { + const options = this.getNodeParameter('options', 0, {}); + + const rawData = options.rawData as boolean; + const dataProperty = options.dataProperty ? (options.dataProperty as string) : 'data'; + + const qs: IDataObject = {}; + if (rawData && options.fields) { + qs.$select = options.fields; + } + + const workbookId = this.getNodeParameter('workbook', 0, undefined, { + extractValue: true, + }) as string; + + const worksheetId = this.getNodeParameter('worksheet', 0, undefined, { + extractValue: true, + }) as string; + + let range = this.getNodeParameter('range', 0, '') as string; + const dataMode = this.getNodeParameter('dataMode', 0) as string; + + let worksheetData: IDataObject = {}; + + if (range && dataMode !== 'raw') { + worksheetData = await microsoftApiRequest.call( + this, + 'PATCH', + `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/range(address='${range}')`, + ); + } + + //get used range if range not provided; if 'raw' mode fetch only address information + if (range === '') { + const query: IDataObject = {}; + if (dataMode === 'raw') { + query.select = 'address'; + } + + worksheetData = await microsoftApiRequest.call( + this, + 'GET', + `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/usedRange`, + undefined, + query, + ); + + range = (worksheetData.address as string).split('!')[1]; + } + + let responseData; + if (dataMode === 'raw') { + const data = this.getNodeParameter('data', 0); + + const values = processJsonInput(data, 'Data') as string[][]; + + responseData = await microsoftApiRequest.call( + this, + 'PATCH', + `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/range(address='${range}')`, + { values }, + qs, + ); + + returnData.push( + ...prepareOutput(this.getNode(), responseData as ExcelResponse, { + rawData, + dataProperty, + }), + ); + } else { + if (worksheetData.values === undefined || (worksheetData.values as string[][]).length <= 1) { + throw new NodeOperationError( + this.getNode(), + 'No data found in the specified range, mapping not possible, you can use raw mode instead to update selected range', + ); + } + + const updateAll = this.getNodeParameter('options.updateAll', 0, false) as boolean; + + let updateSummary: UpdateSummary = { + updatedData: [], + updatedRows: [], + appendData: [], + }; + + if (dataMode === 'define') { + updateSummary = updateByDefinedValues.call( + this, + items.length, + worksheetData.values as string[][], + updateAll, + ); + } + + if (dataMode === 'autoMap') { + const columnToMatchOn = this.getNodeParameter('columnToMatchOn', 0) as string; + + if (!items.some(({ json }) => json[columnToMatchOn] !== undefined)) { + throw new NodeOperationError( + this.getNode(), + `Any item in input data contains column '${columnToMatchOn}', that is selected to match on`, + ); + } + + updateSummary = updateByAutoMaping( + items, + worksheetData.values as string[][], + columnToMatchOn, + updateAll, + ); + } + + responseData = await microsoftApiRequest.call( + this, + 'PATCH', + `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/range(address='${range}')`, + { values: updateSummary.updatedData }, + ); + + const { updatedRows } = updateSummary; + + returnData.push( + ...prepareOutput(this.getNode(), responseData as ExcelResponse, { + updatedRows, + rawData, + dataProperty, + }), + ); + } + } catch (error) { + if (this.continueOnFail()) { + const executionErrorData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray({ error: error.message }), + { itemData: { item: 0 } }, + ); + returnData.push(...executionErrorData); + } else { + throw error; + } + } + + return returnData; +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/worksheet/upsert.operation.ts b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/worksheet/upsert.operation.ts new file mode 100644 index 0000000000000..fa635aac8385e --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/worksheet/upsert.operation.ts @@ -0,0 +1,333 @@ +import type { IExecuteFunctions } from 'n8n-core'; +import type { IDataObject, INodeExecutionData, INodeProperties } from 'n8n-workflow'; +import { NodeOperationError } from 'n8n-workflow'; +import { processJsonInput, updateDisplayOptions } from '../../../../../../utils/utilities'; +import type { ExcelResponse, UpdateSummary } from '../../helpers/interfaces'; +import { prepareOutput, updateByAutoMaping, updateByDefinedValues } from '../../helpers/utils'; +import { microsoftApiRequest } from '../../transport'; +import { workbookRLC, worksheetRLC } from '../common.descriptions'; + +const properties: INodeProperties[] = [ + workbookRLC, + worksheetRLC, + { + displayName: 'Select a Range', + name: 'useRange', + type: 'boolean', + default: false, + }, + { + displayName: 'Range', + name: 'range', + type: 'string', + displayOptions: { + show: { + dataMode: ['autoMap', 'define'], + useRange: [true], + }, + }, + placeholder: 'e.g. A1:B2', + default: '', + description: + 'The sheet range to read the data from specified using a A1-style notation. Leave blank to use whole used range in the sheet.', + hint: 'First row must contain column names', + }, + { + displayName: 'Data Mode', + name: 'dataMode', + type: 'options', + default: 'define', + options: [ + { + name: 'Auto-Map Input Data to Columns', + value: 'autoMap', + description: 'Use when node input properties match destination column names', + }, + { + name: 'Map Each Column Below', + value: 'define', + description: 'Set the value for each destination column', + }, + ], + }, + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased, n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options + displayName: 'Column to match on', + name: 'columnToMatchOn', + type: 'options', + description: + 'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>', + typeOptions: { + loadOptionsDependsOn: ['worksheet.value', 'workbook.value', 'range'], + loadOptionsMethod: 'getWorksheetColumnRow', + }, + default: '', + hint: "Used to find the correct row to update. Doesn't get changed.", + displayOptions: { + show: { + dataMode: ['autoMap', 'define'], + }, + }, + }, + { + displayName: 'Value of Column to Match On', + name: 'valueToMatchOn', + type: 'string', + default: '', + displayOptions: { + show: { + dataMode: ['define'], + }, + }, + }, + { + displayName: 'Values to Send', + name: 'fieldsUi', + placeholder: 'Add Field', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + displayOptions: { + show: { + dataMode: ['define'], + }, + }, + default: {}, + options: [ + { + displayName: 'Field', + name: 'values', + values: [ + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options + displayName: 'Column', + name: 'column', + type: 'options', + description: + 'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>', + typeOptions: { + loadOptionsDependsOn: ['columnToMatchOn', 'range'], + loadOptionsMethod: 'getWorksheetColumnRowSkipColumnToMatchOn', + }, + default: '', + }, + { + displayName: 'Value', + name: 'fieldValue', + type: 'string', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + options: [ + { + displayName: 'RAW Data', + name: 'rawData', + type: 'boolean', + // eslint-disable-next-line n8n-nodes-base/node-param-default-wrong-for-boolean + default: 0, + description: + 'Whether the data should be returned RAW instead of parsed into keys according to their header', + }, + { + displayName: 'Data Property', + name: 'dataProperty', + type: 'string', + default: 'data', + required: true, + displayOptions: { + show: { + rawData: [true], + }, + }, + description: 'The name of the property into which to write the RAW data', + }, + { + displayName: 'Update All Matches', + name: 'updateAll', + type: 'boolean', + default: false, + description: 'Whether to update all matching rows or just the first match', + }, + ], + }, +]; + +const displayOptions = { + show: { + resource: ['worksheet'], + operation: ['upsert'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute( + this: IExecuteFunctions, + items: INodeExecutionData[], +): Promise<INodeExecutionData[]> { + const returnData: INodeExecutionData[] = []; + + try { + const workbookId = this.getNodeParameter('workbook', 0, undefined, { + extractValue: true, + }) as string; + + const worksheetId = this.getNodeParameter('worksheet', 0, undefined, { + extractValue: true, + }) as string; + + let range = this.getNodeParameter('range', 0, '') as string; + const dataMode = this.getNodeParameter('dataMode', 0) as string; + + let worksheetData: IDataObject = {}; + + if (range && dataMode !== 'raw') { + worksheetData = await microsoftApiRequest.call( + this, + 'PATCH', + `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/range(address='${range}')`, + ); + } + + //get used range if range not provided; if 'raw' mode fetch only address information + if (range === '') { + const query: IDataObject = {}; + if (dataMode === 'raw') { + query.select = 'address'; + } + + worksheetData = await microsoftApiRequest.call( + this, + 'GET', + `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/usedRange`, + undefined, + query, + ); + + range = (worksheetData.address as string).split('!')[1]; + } + + let responseData; + if (dataMode === 'raw') { + const data = this.getNodeParameter('data', 0); + + const values = processJsonInput(data, 'Data') as string[][]; + + responseData = await microsoftApiRequest.call( + this, + 'PATCH', + `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/range(address='${range}')`, + { values }, + ); + } + + if ( + dataMode !== 'raw' && + (worksheetData.values === undefined || (worksheetData.values as string[][]).length <= 1) + ) { + throw new NodeOperationError( + this.getNode(), + 'No data found in the specified range, mapping not possible, you can use raw mode instead to update selected range', + ); + } + + const updateAll = this.getNodeParameter('options.updateAll', 0, false) as boolean; + + let updateSummary: UpdateSummary = { + updatedData: [], + updatedRows: [], + appendData: [], + }; + + if (dataMode === 'define') { + updateSummary = updateByDefinedValues.call( + this, + items.length, + worksheetData.values as string[][], + updateAll, + ); + } + + if (dataMode === 'autoMap') { + const columnToMatchOn = this.getNodeParameter('columnToMatchOn', 0) as string; + + if (!items.some(({ json }) => json[columnToMatchOn] !== undefined)) { + throw new NodeOperationError( + this.getNode(), + `Any item in input data contains column '${columnToMatchOn}', that is selected to match on`, + ); + } + + updateSummary = updateByAutoMaping( + items, + worksheetData.values as string[][], + columnToMatchOn, + updateAll, + ); + } + + if (updateSummary.appendData.length) { + const appendValues: string[][] = []; + const columnsRow = (worksheetData.values as string[][])[0]; + + for (const [index, item] of updateSummary.appendData.entries()) { + const updateRow: string[] = []; + + for (const column of columnsRow) { + updateRow.push(item[column] as string); + } + + appendValues.push(updateRow); + updateSummary.updatedRows.push(index + updateSummary.updatedData.length); + } + + updateSummary.updatedData = updateSummary.updatedData.concat(appendValues); + const [rangeFrom, rangeTo] = range.split(':'); + const cellDataTo = rangeTo.match(/([a-zA-Z]{1,10})([0-9]{0,10})/) || []; + + range = `${rangeFrom}:${cellDataTo[1]}${Number(cellDataTo[2]) + appendValues.length}`; + } + + responseData = await microsoftApiRequest.call( + this, + 'PATCH', + `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/range(address='${range}')`, + { values: updateSummary.updatedData }, + ); + + const { updatedRows } = updateSummary; + + const rawData = this.getNodeParameter('options.rawData', 0, false) as boolean; + const dataProperty = this.getNodeParameter('options.dataProperty', 0, 'data') as string; + + returnData.push( + ...prepareOutput(this.getNode(), responseData as ExcelResponse, { + updatedRows, + rawData, + dataProperty, + }), + ); + } catch (error) { + if (this.continueOnFail()) { + const executionErrorData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray({ error: error.message }), + { itemData: { item: 0 } }, + ); + returnData.push(...executionErrorData); + } else { + throw error; + } + } + + return returnData; +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/v2/helpers/interfaces.ts b/packages/nodes-base/nodes/Microsoft/Excel/v2/helpers/interfaces.ts new file mode 100644 index 0000000000000..f3d84f3869b0f --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/v2/helpers/interfaces.ts @@ -0,0 +1,14 @@ +import type { IDataObject } from 'n8n-workflow'; + +export type SheetRow = Array<string | number | null>; +export type SheetData = SheetRow[]; + +export type ExcelResponse = { + values: SheetData; +}; + +export type UpdateSummary = { + updatedData: SheetData; + appendData: IDataObject[]; + updatedRows: number[]; +}; diff --git a/packages/nodes-base/nodes/Microsoft/Excel/v2/helpers/utils.ts b/packages/nodes-base/nodes/Microsoft/Excel/v2/helpers/utils.ts new file mode 100644 index 0000000000000..650ac8ad7fa68 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/v2/helpers/utils.ts @@ -0,0 +1,208 @@ +import type { IExecuteFunctions } from 'n8n-core'; +import type { IDataObject, INode, INodeExecutionData } from 'n8n-workflow'; +import { NodeOperationError } from 'n8n-workflow'; +import type { ExcelResponse, SheetData, UpdateSummary } from './interfaces'; +import { constructExecutionMetaData } from 'n8n-core'; +import { wrapData } from '../../../../../utils/utilities'; + +type PrepareOutputConfig = { + rawData: boolean; + dataProperty?: string; + keyRow?: number; + firstDataRow?: number; + columnsRow?: string[]; + updatedRows?: number[]; +}; + +export function prepareOutput( + node: INode, + responseData: ExcelResponse, + config: PrepareOutputConfig, +) { + const returnData: INodeExecutionData[] = []; + + const { rawData, keyRow, firstDataRow, columnsRow, updatedRows } = { + keyRow: 0, + firstDataRow: 1, + columnsRow: undefined, + updatedRows: undefined, + ...config, + }; + + if (!rawData) { + let values = responseData.values; + if (values === null) { + throw new NodeOperationError(node, 'Operation did not return data'); + } + + let columns = []; + + if (columnsRow?.length) { + columns = columnsRow; + values = [columns, ...values]; + } else { + columns = values[keyRow]; + } + + if (updatedRows) { + values = values.filter((_, index) => updatedRows.includes(index)); + } + + for (let rowIndex = firstDataRow; rowIndex < values.length; rowIndex++) { + if (rowIndex === keyRow) continue; + const data: IDataObject = {}; + for (let columnIndex = 0; columnIndex < columns.length; columnIndex++) { + data[columns[columnIndex] as string] = values[rowIndex][columnIndex]; + } + const executionData = constructExecutionMetaData(wrapData({ ...data }), { + itemData: { item: rowIndex }, + }); + + returnData.push(...executionData); + } + } else { + const executionData = constructExecutionMetaData( + wrapData({ [config.dataProperty || 'data']: responseData }), + { itemData: { item: 0 } }, + ); + + returnData.push(...executionData); + } + + return returnData; +} +// update values of spreadsheet when update mode is 'define' +export function updateByDefinedValues( + this: IExecuteFunctions, + itemslength: number, + sheetData: SheetData, + updateAllOccurences: boolean, +): UpdateSummary { + const [columns, ...originalValues] = sheetData; + const updateValues: SheetData = originalValues.map((row) => row.map(() => null)); + + const updatedRowsIndexes = new Set<number>(); + const appendData: IDataObject[] = []; + + for (let itemIndex = 0; itemIndex < itemslength; itemIndex++) { + const columnToMatchOn = this.getNodeParameter('columnToMatchOn', itemIndex) as string; + const valueToMatchOn = this.getNodeParameter('valueToMatchOn', itemIndex) as string; + + const definedFields = this.getNodeParameter('fieldsUi.values', itemIndex, []) as Array<{ + column: string; + fieldValue: string; + }>; + + const columnToMatchOnIndex = columns.indexOf(columnToMatchOn); + + const rowIndexes: number[] = []; + if (updateAllOccurences) { + for (const [index, row] of originalValues.entries()) { + if ( + row[columnToMatchOnIndex] === valueToMatchOn || + Number(row[columnToMatchOnIndex]) === Number(valueToMatchOn) + ) { + rowIndexes.push(index); + } + } + } else { + const rowIndex = originalValues.findIndex( + (row) => + row[columnToMatchOnIndex] === valueToMatchOn || + Number(row[columnToMatchOnIndex]) === Number(valueToMatchOn), + ); + + if (rowIndex !== -1) { + rowIndexes.push(rowIndex); + } + } + + if (!rowIndexes.length) { + const appendItem: IDataObject = {}; + appendItem[columnToMatchOn] = valueToMatchOn; + + for (const entry of definedFields) { + appendItem[entry.column] = entry.fieldValue; + } + appendData.push(appendItem); + continue; + } + + for (const rowIndex of rowIndexes) { + for (const entry of definedFields) { + const columnIndex = columns.indexOf(entry.column); + if (rowIndex === -1) continue; + updateValues[rowIndex][columnIndex] = entry.fieldValue; + //add rows index and shift by 1 to account for header row + updatedRowsIndexes.add(rowIndex + 1); + } + } + } + + const updatedData = [columns, ...updateValues]; + const updatedRows = [0, ...Array.from(updatedRowsIndexes)]; + + const summary: UpdateSummary = { updatedData, appendData, updatedRows }; + + return summary; +} + +// update values of spreadsheet when update mode is 'autoMap' +export function updateByAutoMaping( + items: INodeExecutionData[], + sheetData: SheetData, + columnToMatchOn: string, + updateAllOccurences = false, +): UpdateSummary { + const [columns, ...values] = sheetData; + const matchColumnIndex = columns.indexOf(columnToMatchOn); + const matchValuesMap = values.map((row) => row[matchColumnIndex]); + + const updatedRowsIndexes = new Set<number>(); + const appendData: IDataObject[] = []; + + for (const { json } of items) { + const columnValue = json[columnToMatchOn] as string; + if (columnValue === undefined) continue; + + const rowIndexes: number[] = []; + if (updateAllOccurences) { + matchValuesMap.forEach((value, index) => { + if (value === columnValue || Number(value) === Number(columnValue)) { + rowIndexes.push(index); + } + }); + } else { + const rowIndex = matchValuesMap.findIndex( + (value) => value === columnValue || Number(value) === Number(columnValue), + ); + + if (rowIndex !== -1) rowIndexes.push(rowIndex); + } + + if (!rowIndexes.length) { + appendData.push(json); + continue; + } + + const updatedRow: Array<string | null> = []; + + for (const columnName of columns as string[]) { + const updateValue = json[columnName] === undefined ? null : (json[columnName] as string); + updatedRow.push(updateValue); + } + + for (const rowIndex of rowIndexes) { + values[rowIndex] = updatedRow as string[]; + //add rows index and shift by 1 to account for header row + updatedRowsIndexes.add(rowIndex + 1); + } + } + + const updatedData = [columns, ...values]; + const updatedRows = [0, ...Array.from(updatedRowsIndexes)]; + + const summary: UpdateSummary = { updatedData, appendData, updatedRows }; + + return summary; +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/v2/methods/index.ts b/packages/nodes-base/nodes/Microsoft/Excel/v2/methods/index.ts new file mode 100644 index 0000000000000..a5508a3e0fa86 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/v2/methods/index.ts @@ -0,0 +1,2 @@ +export * as loadOptions from './loadOptions'; +export * as listSearch from './listSearch'; diff --git a/packages/nodes-base/nodes/Microsoft/Excel/v2/methods/listSearch.ts b/packages/nodes-base/nodes/Microsoft/Excel/v2/methods/listSearch.ts new file mode 100644 index 0000000000000..031c3e07d5596 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/v2/methods/listSearch.ts @@ -0,0 +1,134 @@ +import type { + IDataObject, + ILoadOptionsFunctions, + INodeListSearchItems, + INodeListSearchResult, +} from 'n8n-workflow'; +import { microsoftApiRequest } from '../transport'; + +export async function searchWorkbooks( + this: ILoadOptionsFunctions, + filter?: string, + paginationToken?: string, +): Promise<INodeListSearchResult> { + const q = filter ? encodeURI(`.xlsx AND ${filter}`) : '.xlsx'; + + let response: IDataObject = {}; + + if (paginationToken) { + response = await microsoftApiRequest.call( + this, + 'GET', + '', + undefined, + undefined, + paginationToken, // paginationToken contains the full URL + ); + } else { + response = await microsoftApiRequest.call( + this, + 'GET', + `/drive/root/search(q='${q}')`, + undefined, + { + select: 'id,name,webUrl', + $top: 100, + }, + ); + } + + return { + results: (response.value as IDataObject[]).map((workbook: IDataObject) => { + return { + name: (workbook.name as string).replace('.xlsx', ''), + value: workbook.id as string, + url: workbook.webUrl as string, + }; + }), + paginationToken: response['@odata.nextLink'], + }; +} + +export async function getWorksheetsList( + this: ILoadOptionsFunctions, +): Promise<INodeListSearchResult> { + const workbookRLC = this.getNodeParameter('workbook') as IDataObject; + const workbookId = workbookRLC.value as string; + let workbookURL = workbookRLC.cachedResultUrl as string; + + if (workbookURL.includes('1drv.ms')) { + workbookURL = `https://onedrive.live.com/edit.aspx?resid=${workbookId}`; + } + + let response: IDataObject = {}; + + response = await microsoftApiRequest.call( + this, + 'GET', + `/drive/items/${workbookId}/workbook/worksheets`, + undefined, + { + select: 'id,name', + }, + ); + + return { + results: (response.value as IDataObject[]).map((worksheet: IDataObject) => ({ + name: worksheet.name as string, + value: worksheet.id as string, + url: `${workbookURL}&activeCell=${encodeURIComponent(worksheet.name as string)}!A1`, + })), + }; +} + +export async function getWorksheetTables( + this: ILoadOptionsFunctions, +): Promise<INodeListSearchResult> { + const workbookRLC = this.getNodeParameter('workbook') as IDataObject; + const workbookId = workbookRLC.value as string; + let workbookURL = workbookRLC.cachedResultUrl as string; + + if (workbookURL.includes('1drv.ms')) { + workbookURL = `https://onedrive.live.com/edit.aspx?resid=${workbookId}`; + } + + const worksheetId = this.getNodeParameter('worksheet', undefined, { + extractValue: true, + }) as string; + + let response: IDataObject = {}; + + response = await microsoftApiRequest.call( + this, + 'GET', + `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables`, + undefined, + ); + + const results: INodeListSearchItems[] = []; + + for (const table of response.value as IDataObject[]) { + const name = table.name as string; + const value = table.id as string; + + const { address } = await microsoftApiRequest.call( + this, + 'GET', + `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${value}/range`, + undefined, + { + select: 'address', + }, + ); + + const [sheetName, sheetRange] = address.split('!' as string); + + const url = `${workbookURL}&activeCell=${encodeURIComponent(sheetName as string)}${ + sheetRange ? '!' + (sheetRange as string) : '' + }`; + + results.push({ name, value, url }); + } + + return { results }; +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/v2/methods/loadOptions.ts b/packages/nodes-base/nodes/Microsoft/Excel/v2/methods/loadOptions.ts new file mode 100644 index 0000000000000..576f02c8c293c --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/v2/methods/loadOptions.ts @@ -0,0 +1,89 @@ +import type { IDataObject, ILoadOptionsFunctions, INodePropertyOptions } from 'n8n-workflow'; +import { microsoftApiRequest } from '../transport'; + +export async function getWorksheetColumnRow( + this: ILoadOptionsFunctions, +): Promise<INodePropertyOptions[]> { + const workbookId = this.getNodeParameter('workbook', undefined, { + extractValue: true, + }) as string; + + const worksheetId = this.getNodeParameter('worksheet', undefined, { + extractValue: true, + }) as string; + + let range = this.getNodeParameter('range', '') as string; + let columns: string[] = []; + + if (range === '') { + const worksheetData = await microsoftApiRequest.call( + this, + 'GET', + `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/usedRange`, + undefined, + { select: 'values' }, + ); + + columns = worksheetData.values[0] as string[]; + } else { + const [rangeFrom, rangeTo] = range.split(':'); + const cellDataFrom = rangeFrom.match(/([a-zA-Z]{1,10})([0-9]{0,10})/) || []; + const cellDataTo = rangeTo.match(/([a-zA-Z]{1,10})([0-9]{0,10})/) || []; + + range = `${rangeFrom}:${cellDataTo[1]}${cellDataFrom[2]}`; + + const worksheetData = await microsoftApiRequest.call( + this, + 'PATCH', + `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/range(address='${range}')`, + { select: 'values' }, + ); + + columns = worksheetData.values[0] as string[]; + } + + const returnData: INodePropertyOptions[] = []; + for (const column of columns) { + returnData.push({ + name: column, + value: column, + }); + } + return returnData; +} + +export async function getWorksheetColumnRowSkipColumnToMatchOn( + this: ILoadOptionsFunctions, +): Promise<INodePropertyOptions[]> { + const returnData = await getWorksheetColumnRow.call(this); + const columnToMatchOn = this.getNodeParameter('columnToMatchOn', 0) as string; + return returnData.filter((column) => column.value !== columnToMatchOn); +} + +export async function getTableColumns( + this: ILoadOptionsFunctions, +): Promise<INodePropertyOptions[]> { + const workbookId = this.getNodeParameter('workbook', undefined, { + extractValue: true, + }) as string; + + const worksheetId = this.getNodeParameter('worksheet', undefined, { + extractValue: true, + }) as string; + + const tableId = this.getNodeParameter('table', undefined, { + extractValue: true, + }) as string; + + const response = await microsoftApiRequest.call( + this, + 'GET', + `/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/columns`, + {}, + ); + + return (response.value as IDataObject[]).map((column) => ({ + name: column.name as string, + value: column.name as string, + })); +} diff --git a/packages/nodes-base/nodes/Microsoft/Excel/v2/transport/index.ts b/packages/nodes-base/nodes/Microsoft/Excel/v2/transport/index.ts new file mode 100644 index 0000000000000..f28816ef8b0c1 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Excel/v2/transport/index.ts @@ -0,0 +1,82 @@ +import type { OptionsWithUri } from 'request'; +import type { IExecuteFunctions, IExecuteSingleFunctions, ILoadOptionsFunctions } from 'n8n-core'; +import type { IDataObject, JsonObject } from 'n8n-workflow'; +import { NodeApiError } from 'n8n-workflow'; + +export async function microsoftApiRequest( + this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, + method: string, + resource: string, + body: any = {}, + qs: IDataObject = {}, + uri?: string, + headers: IDataObject = {}, +): Promise<any> { + const options: OptionsWithUri = { + headers: { + 'Content-Type': 'application/json', + }, + method, + body, + qs, + uri: uri || `https://graph.microsoft.com/v1.0/me${resource}`, + json: true, + }; + try { + if (Object.keys(headers).length !== 0) { + options.headers = Object.assign({}, options.headers, headers); + } + return await this.helpers.requestOAuth2.call(this, 'microsoftExcelOAuth2Api', options); + } catch (error) { + throw new NodeApiError(this.getNode(), error as JsonObject); + } +} + +export async function microsoftApiRequestAllItems( + this: IExecuteFunctions | ILoadOptionsFunctions, + propertyName: string, + method: string, + endpoint: string, + body: any = {}, + query: IDataObject = {}, +): Promise<any> { + const returnData: IDataObject[] = []; + + let responseData; + let uri: string | undefined; + query.$top = 100; + + do { + responseData = await microsoftApiRequest.call(this, method, endpoint, body, query, uri); + uri = responseData['@odata.nextLink']; + if (uri?.includes('$top')) { + delete query.$top; + } + returnData.push.apply(returnData, responseData[propertyName] as IDataObject[]); + } while (responseData['@odata.nextLink'] !== undefined); + + return returnData; +} + +export async function microsoftApiRequestAllItemsSkip( + this: IExecuteFunctions | ILoadOptionsFunctions, + propertyName: string, + method: string, + endpoint: string, + body: any = {}, + query: IDataObject = {}, +): Promise<any> { + const returnData: IDataObject[] = []; + + let responseData; + query.$top = 100; + query.$skip = 0; + + do { + responseData = await microsoftApiRequest.call(this, method, endpoint, body, query); + query.$skip += query.$top; + returnData.push.apply(returnData, responseData[propertyName] as IDataObject[]); + } while (responseData.value.length !== 0); + + return returnData; +} diff --git a/packages/nodes-base/nodes/Microsoft/Sql/MicrosoftSql.node.ts b/packages/nodes-base/nodes/Microsoft/Sql/MicrosoftSql.node.ts index 8c7c00dee2787..14a39fdddb455 100644 --- a/packages/nodes-base/nodes/Microsoft/Sql/MicrosoftSql.node.ts +++ b/packages/nodes-base/nodes/Microsoft/Sql/MicrosoftSql.node.ts @@ -90,6 +90,10 @@ export class MicrosoftSql implements INodeType { displayName: 'Query', name: 'query', type: 'string', + typeOptions: { + editor: 'sqlEditor', + sqlDialect: 'mssql', + }, displayOptions: { show: { operation: ['executeQuery'], diff --git a/packages/nodes-base/nodes/MySql/MySql.node.ts b/packages/nodes-base/nodes/MySql/MySql.node.ts index 1d055b5ee5a80..db80bf1ae8188 100644 --- a/packages/nodes-base/nodes/MySql/MySql.node.ts +++ b/packages/nodes-base/nodes/MySql/MySql.node.ts @@ -11,13 +11,14 @@ export class MySql extends VersionedNodeType { name: 'mySql', icon: 'file:mysql.svg', group: ['input'], - defaultVersion: 2, + defaultVersion: 2.1, description: 'Get, add and update data in MySQL', }; const nodeVersions: IVersionedNodeType['nodeVersions'] = { 1: new MySqlV1(baseDescription), 2: new MySqlV2(baseDescription), + 2.1: new MySqlV2(baseDescription), }; super(nodeVersions, baseDescription); diff --git a/packages/nodes-base/nodes/MySql/test/v2/operations.test.ts b/packages/nodes-base/nodes/MySql/test/v2/operations.test.ts index 8f24baf49815e..a131e929be103 100644 --- a/packages/nodes-base/nodes/MySql/test/v2/operations.test.ts +++ b/packages/nodes-base/nodes/MySql/test/v2/operations.test.ts @@ -29,7 +29,7 @@ const fakeConnection = { format(query: string, values: any[]) { return mysql2.format(query, values); }, - query: jest.fn(async (_query = ''): Promise<any> => Promise.resolve([{}])), + query: jest.fn(async (_query = '') => [{}]), release: jest.fn(), beginTransaction: jest.fn(), commit: jest.fn(), @@ -41,7 +41,7 @@ const createFakePool = (connection: IDataObject) => { getConnection() { return connection; }, - query: jest.fn(async () => Promise.resolve([{}])), + query: jest.fn(async () => [{}]), } as unknown as Mysql2Pool; }; @@ -214,7 +214,7 @@ describe('Test MySql V2, operations', () => { } else { result.push({}); } - return Promise.resolve(result); + return result; }); const pool = createFakePool(fakeConnectionCopy); diff --git a/packages/nodes-base/nodes/MySql/test/v2/runQueries.test.ts b/packages/nodes-base/nodes/MySql/test/v2/runQueries.test.ts index 73a0eb3418ac5..3fd5ffce4eecb 100644 --- a/packages/nodes-base/nodes/MySql/test/v2/runQueries.test.ts +++ b/packages/nodes-base/nodes/MySql/test/v2/runQueries.test.ts @@ -23,7 +23,7 @@ const fakeConnection = { format(query: string, values: any[]) { return mysql2.format(query, values); }, - query: jest.fn(async () => Promise.resolve([{}])), + query: jest.fn(async () => [{}]), release: jest.fn(), beginTransaction: jest.fn(), commit: jest.fn(), @@ -35,7 +35,7 @@ const createFakePool = (connection: IDataObject) => { getConnection() { return connection; }, - query: jest.fn(async () => Promise.resolve([{}])), + query: jest.fn(async () => [{}]), } as unknown as Mysql2Pool; }; diff --git a/packages/nodes-base/nodes/MySql/v1/MySqlV1.node.ts b/packages/nodes-base/nodes/MySql/v1/MySqlV1.node.ts index 31abec978eae5..8938fb48f8798 100644 --- a/packages/nodes-base/nodes/MySql/v1/MySqlV1.node.ts +++ b/packages/nodes-base/nodes/MySql/v1/MySqlV1.node.ts @@ -17,6 +17,8 @@ import type mysql2 from 'mysql2/promise'; import { copyInputItems, createConnection, searchTables } from './GenericFunctions'; import type { IExecuteFunctions } from 'n8n-core'; +import { oldVersionNotice } from '../../../utils/descriptions'; + const versionDescription: INodeTypeDescription = { displayName: 'MySQL', name: 'mySql', @@ -37,12 +39,7 @@ const versionDescription: INodeTypeDescription = { }, ], properties: [ - { - displayName: 'Version 1', - name: 'versionNotice', - type: 'notice', - default: '', - }, + oldVersionNotice, { displayName: 'Operation', name: 'operation', @@ -78,6 +75,10 @@ const versionDescription: INodeTypeDescription = { displayName: 'Query', name: 'query', type: 'string', + typeOptions: { + editor: 'sqlEditor', + sqlDialect: 'mysql', + }, displayOptions: { show: { operation: ['executeQuery'], diff --git a/packages/nodes-base/nodes/MySql/v2/actions/database/executeQuery.operation.ts b/packages/nodes-base/nodes/MySql/v2/actions/database/executeQuery.operation.ts index bf247596d30a8..853be02ac3822 100644 --- a/packages/nodes-base/nodes/MySql/v2/actions/database/executeQuery.operation.ts +++ b/packages/nodes-base/nodes/MySql/v2/actions/database/executeQuery.operation.ts @@ -21,7 +21,8 @@ const properties: INodeProperties[] = [ description: "The SQL query to execute. You can use n8n expressions and $1, $2, $3, etc to refer to the 'Query Parameters' set in options below.", typeOptions: { - rows: 3, + editor: 'sqlEditor', + sqlDialect: 'mysql', }, hint: 'Prefer using query parameters over n8n expressions to avoid SQL injection attacks', }, diff --git a/packages/nodes-base/nodes/MySql/v2/actions/router.ts b/packages/nodes-base/nodes/MySql/v2/actions/router.ts index 9aa57586720fe..88cc1a3d08475 100644 --- a/packages/nodes-base/nodes/MySql/v2/actions/router.ts +++ b/packages/nodes-base/nodes/MySql/v2/actions/router.ts @@ -19,6 +19,8 @@ export async function router(this: IExecuteFunctions): Promise<INodeExecutionDat const operation = this.getNodeParameter('operation', 0); const nodeOptions = this.getNodeParameter('options', 0); + nodeOptions.nodeVersion = this.getNode().typeVersion; + const credentials = await this.getCredentials('mySql'); let sshClient: Client | undefined = undefined; diff --git a/packages/nodes-base/nodes/MySql/v2/actions/versionDescription.ts b/packages/nodes-base/nodes/MySql/v2/actions/versionDescription.ts index fd12c75352f66..32cd487c7e639 100644 --- a/packages/nodes-base/nodes/MySql/v2/actions/versionDescription.ts +++ b/packages/nodes-base/nodes/MySql/v2/actions/versionDescription.ts @@ -8,7 +8,7 @@ export const versionDescription: INodeTypeDescription = { name: 'mySql', icon: 'file:mysql.svg', group: ['input'], - version: 2, + version: [2, 2.1], subtitle: '={{ $parameter["operation"] }}', description: 'Get, add and update data in MySQL', defaults: { diff --git a/packages/nodes-base/nodes/MySql/v2/transport/index.ts b/packages/nodes-base/nodes/MySql/v2/transport/index.ts index fd662c4b76a2b..af40aa5c82375 100644 --- a/packages/nodes-base/nodes/MySql/v2/transport/index.ts +++ b/packages/nodes-base/nodes/MySql/v2/transport/index.ts @@ -78,6 +78,10 @@ export async function createPool( supportBigNumbers: true, }; + if (options?.nodeVersion && (options.nodeVersion as number) >= 2.1) { + connectionOptions.dateStrings = true; + } + if (options?.connectionLimit) { connectionOptions.connectionLimit = options.connectionLimit as number; } diff --git a/packages/nodes-base/nodes/PayPal/PaymentInteface.ts b/packages/nodes-base/nodes/PayPal/PaymentInteface.ts index 53b2ec1457918..d4e28c6ceef7d 100644 --- a/packages/nodes-base/nodes/PayPal/PaymentInteface.ts +++ b/packages/nodes-base/nodes/PayPal/PaymentInteface.ts @@ -1,10 +1,10 @@ -export enum RecipientType { +export const enum RecipientType { email = 'EMAIL', phone = 'PHONE', paypalId = 'PAYPAL_ID', } -export enum RecipientWallet { +export const enum RecipientWallet { paypal = 'PAYPAL', venmo = 'VENMO', } diff --git a/packages/nodes-base/nodes/Postgres/Postgres.node.ts b/packages/nodes-base/nodes/Postgres/Postgres.node.ts index 69aee12ada0f4..795c53f5f4b28 100644 --- a/packages/nodes-base/nodes/Postgres/Postgres.node.ts +++ b/packages/nodes-base/nodes/Postgres/Postgres.node.ts @@ -11,13 +11,14 @@ export class Postgres extends VersionedNodeType { name: 'postgres', icon: 'file:postgres.svg', group: ['input'], - defaultVersion: 2, + defaultVersion: 2.1, description: 'Get, add and update data in Postgres', }; const nodeVersions: IVersionedNodeType['nodeVersions'] = { 1: new PostgresV1(baseDescription), 2: new PostgresV2(baseDescription), + 2.1: new PostgresV2(baseDescription), }; super(nodeVersions, baseDescription); diff --git a/packages/nodes-base/nodes/Postgres/test/v2/operations.test.ts b/packages/nodes-base/nodes/Postgres/test/v2/operations.test.ts index 4de24f6b4cf96..4d0e30fa59fef 100644 --- a/packages/nodes-base/nodes/Postgres/test/v2/operations.test.ts +++ b/packages/nodes-base/nodes/Postgres/test/v2/operations.test.ts @@ -52,7 +52,7 @@ const createMockDb = (columnInfo: ColumnInfo[]) => { } as unknown as PgpDatabase; }; -// if node parameters copied from canvas all default parameters has to be added manualy as JSON would not have them +// if node parameters copied from canvas all default parameters has to be added manually as JSON would not have them describe('Test PostgresV2, deleteTable operation', () => { afterEach(() => { jest.clearAllMocks(); diff --git a/packages/nodes-base/nodes/Postgres/v1/PostgresV1.node.ts b/packages/nodes-base/nodes/Postgres/v1/PostgresV1.node.ts index ef5cef53e65be..ead4afb90db7f 100644 --- a/packages/nodes-base/nodes/Postgres/v1/PostgresV1.node.ts +++ b/packages/nodes-base/nodes/Postgres/v1/PostgresV1.node.ts @@ -16,6 +16,8 @@ import pgPromise from 'pg-promise'; import { pgInsertV2, pgQueryV2, pgUpdate, wrapData } from './genericFunctions'; +import { oldVersionNotice } from '../../../utils/descriptions'; + const versionDescription: INodeTypeDescription = { displayName: 'Postgres', name: 'postgres', @@ -36,12 +38,7 @@ const versionDescription: INodeTypeDescription = { }, ], properties: [ - { - displayName: 'Version 1', - name: 'versionNotice', - type: 'notice', - default: '', - }, + oldVersionNotice, { displayName: 'Operation', name: 'operation', @@ -77,6 +74,10 @@ const versionDescription: INodeTypeDescription = { displayName: 'Query', name: 'query', type: 'string', + typeOptions: { + editor: 'sqlEditor', + sqlDialect: 'postgres', + }, displayOptions: { show: { operation: ['executeQuery'], diff --git a/packages/nodes-base/nodes/Postgres/v2/actions/database/executeQuery.operation.ts b/packages/nodes-base/nodes/Postgres/v2/actions/database/executeQuery.operation.ts index 41aedcf156251..1d59228f0b98c 100644 --- a/packages/nodes-base/nodes/Postgres/v2/actions/database/executeQuery.operation.ts +++ b/packages/nodes-base/nodes/Postgres/v2/actions/database/executeQuery.operation.ts @@ -21,7 +21,8 @@ const properties: INodeProperties[] = [ description: "The SQL query to execute. You can use n8n expressions and $1, $2, $3, etc to refer to the 'Query Parameters' set in options below.", typeOptions: { - rows: 3, + editor: 'sqlEditor', + sqlDialect: 'postgres', }, hint: 'Prefer using query parameters over n8n expressions to avoid SQL injection attacks', }, diff --git a/packages/nodes-base/nodes/Postgres/v2/actions/router.ts b/packages/nodes-base/nodes/Postgres/v2/actions/router.ts index 68289d5a87664..bc4115e8b4572 100644 --- a/packages/nodes-base/nodes/Postgres/v2/actions/router.ts +++ b/packages/nodes-base/nodes/Postgres/v2/actions/router.ts @@ -17,6 +17,7 @@ export async function router(this: IExecuteFunctions): Promise<INodeExecutionDat const credentials = await this.getCredentials('postgres'); const options = this.getNodeParameter('options', 0, {}); + options.nodeVersion = this.getNode().typeVersion; const { db, pgp, sshClient } = (await Connections.getInstance( credentials, diff --git a/packages/nodes-base/nodes/Postgres/v2/actions/versionDescription.ts b/packages/nodes-base/nodes/Postgres/v2/actions/versionDescription.ts index 031f178d2e6e6..9e93d5b61ded1 100644 --- a/packages/nodes-base/nodes/Postgres/v2/actions/versionDescription.ts +++ b/packages/nodes-base/nodes/Postgres/v2/actions/versionDescription.ts @@ -8,7 +8,7 @@ export const versionDescription: INodeTypeDescription = { name: 'postgres', icon: 'file:postgres.svg', group: ['input'], - version: 2, + version: [2, 2.1], subtitle: '={{ $parameter["operation"] }}', description: 'Get, add and update data in Postgres', defaults: { diff --git a/packages/nodes-base/nodes/Postgres/v2/methods/listSearch.ts b/packages/nodes-base/nodes/Postgres/v2/methods/listSearch.ts index 8a8b35ab39cf9..055281ebc2406 100644 --- a/packages/nodes-base/nodes/Postgres/v2/methods/listSearch.ts +++ b/packages/nodes-base/nodes/Postgres/v2/methods/listSearch.ts @@ -4,8 +4,9 @@ import { Connections } from '../transport'; export async function schemaSearch(this: ILoadOptionsFunctions): Promise<INodeListSearchResult> { const credentials = await this.getCredentials('postgres'); + const options = { nodeVersion: this.getNode().typeVersion }; - const { db } = (await Connections.getInstance(credentials)) as ConnectionsData; + const { db } = (await Connections.getInstance(credentials, options)) as ConnectionsData; try { const response = await db.any('SELECT schema_name FROM information_schema.schemata'); @@ -22,8 +23,9 @@ export async function schemaSearch(this: ILoadOptionsFunctions): Promise<INodeLi } export async function tableSearch(this: ILoadOptionsFunctions): Promise<INodeListSearchResult> { const credentials = await this.getCredentials('postgres'); + const options = { nodeVersion: this.getNode().typeVersion }; - const { db } = (await Connections.getInstance(credentials)) as ConnectionsData; + const { db } = (await Connections.getInstance(credentials, options)) as ConnectionsData; const schema = this.getNodeParameter('schema', 0, { extractValue: true, diff --git a/packages/nodes-base/nodes/Postgres/v2/methods/loadOptions.ts b/packages/nodes-base/nodes/Postgres/v2/methods/loadOptions.ts index acc6fb92472f3..646d7153d7a55 100644 --- a/packages/nodes-base/nodes/Postgres/v2/methods/loadOptions.ts +++ b/packages/nodes-base/nodes/Postgres/v2/methods/loadOptions.ts @@ -5,8 +5,9 @@ import { Connections } from '../transport'; export async function getColumns(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> { const credentials = await this.getCredentials('postgres'); + const options = { nodeVersion: this.getNode().typeVersion }; - const { db } = (await Connections.getInstance(credentials)) as ConnectionsData; + const { db } = (await Connections.getInstance(credentials, options)) as ConnectionsData; const schema = this.getNodeParameter('schema', 0, { extractValue: true, diff --git a/packages/nodes-base/nodes/Postgres/v2/transport/index.ts b/packages/nodes-base/nodes/Postgres/v2/transport/index.ts index 76240a05ab704..8b78641678a5e 100644 --- a/packages/nodes-base/nodes/Postgres/v2/transport/index.ts +++ b/packages/nodes-base/nodes/Postgres/v2/transport/index.ts @@ -47,6 +47,15 @@ async function configurePostgres( ) { const pgp = pgPromise(); + if (typeof options.nodeVersion == 'number' && options.nodeVersion >= 2.1) { + // Always return dates as ISO strings + [pgp.pg.types.builtins.TIMESTAMP, pgp.pg.types.builtins.TIMESTAMPTZ].forEach((type) => { + pgp.pg.types.setTypeParser(type, (value: string) => { + return new Date(value).toISOString(); + }); + }); + } + if (options.largeNumbersOutput === 'numbers') { pgp.pg.types.setTypeParser(20, (value: string) => { return parseInt(value, 10); diff --git a/packages/nodes-base/nodes/QuestDb/QuestDb.node.ts b/packages/nodes-base/nodes/QuestDb/QuestDb.node.ts index 5987d4007eda1..90af31d753afa 100644 --- a/packages/nodes-base/nodes/QuestDb/QuestDb.node.ts +++ b/packages/nodes-base/nodes/QuestDb/QuestDb.node.ts @@ -60,6 +60,10 @@ export class QuestDb implements INodeType { displayName: 'Query', name: 'query', type: 'string', + typeOptions: { + editor: 'sqlEditor', + sqlDialect: 'postgres', + }, displayOptions: { show: { operation: ['executeQuery'], diff --git a/packages/nodes-base/nodes/QuickChart/QuickChart.node.ts b/packages/nodes-base/nodes/QuickChart/QuickChart.node.ts index 9b36934cded67..d9ad81b454989 100644 --- a/packages/nodes-base/nodes/QuickChart/QuickChart.node.ts +++ b/packages/nodes-base/nodes/QuickChart/QuickChart.node.ts @@ -18,7 +18,6 @@ import { } from './constants'; import type { IDataset } from './types'; -import _ from 'lodash'; export class QuickChart implements INodeType { description: INodeTypeDescription = { displayName: 'QuickChart', diff --git a/packages/nodes-base/nodes/SendInBlue/GenericFunctions.ts b/packages/nodes-base/nodes/SendInBlue/GenericFunctions.ts index 73ab3272cf970..7fa7a6e4bdb74 100644 --- a/packages/nodes-base/nodes/SendInBlue/GenericFunctions.ts +++ b/packages/nodes-base/nodes/SendInBlue/GenericFunctions.ts @@ -18,13 +18,13 @@ export namespace SendInBlueNode { type BBCEmail = { bbc: Email[] }; type ValidatedEmail = ToEmail | SenderEmail | CCEmail | BBCEmail; - enum OVERRIDE_MAP_VALUES { + const enum OVERRIDE_MAP_VALUES { 'CATEGORY' = 'category', 'NORMAL' = 'boolean', 'TRANSACTIONAL' = 'id', } - enum OVERRIDE_MAP_TYPE { + const enum OVERRIDE_MAP_TYPE { 'CATEGORY' = 'category', 'NORMAL' = 'normal', 'TRANSACTIONAL' = 'transactional', diff --git a/packages/nodes-base/nodes/Slack/V1/SlackV1.node.ts b/packages/nodes-base/nodes/Slack/V1/SlackV1.node.ts index 9055db4a16a7c..6bf2d6afcff49 100644 --- a/packages/nodes-base/nodes/Slack/V1/SlackV1.node.ts +++ b/packages/nodes-base/nodes/Slack/V1/SlackV1.node.ts @@ -22,6 +22,8 @@ import { userProfileFields, userProfileOperations } from './UserProfileDescripti import { slackApiRequest, slackApiRequestAllItems, validateJSON } from './GenericFunctions'; import type { IAttachment } from './MessageInterface'; +import { oldVersionNotice } from '../../../utils/descriptions'; + import moment from 'moment'; interface Attachment { @@ -97,6 +99,7 @@ export class SlackV1 implements INodeType { }, ], properties: [ + oldVersionNotice, { displayName: 'Authentication', name: 'authentication', diff --git a/packages/nodes-base/nodes/Slack/V2/MessageDescription.ts b/packages/nodes-base/nodes/Slack/V2/MessageDescription.ts index 7e1ecd27789e5..c3143e8a11784 100644 --- a/packages/nodes-base/nodes/Slack/V2/MessageDescription.ts +++ b/packages/nodes-base/nodes/Slack/V2/MessageDescription.ts @@ -331,6 +331,21 @@ export const messageFields: INodeProperties[] = [ hint: "To create blocks, use <a target='_blank' href='https://app.slack.com/block-kit-builder'>Slack's Block Kit Builder</a>", default: '', }, + { + displayName: 'Notification Text', + name: 'text', + type: 'string', + default: '', + displayOptions: { + show: { + operation: ['post'], + resource: ['message'], + messageType: ['block'], + }, + }, + description: + 'Fallback text to display in slack notifications. Supports <a href="https://api.slack.com/reference/surfaces/formatting">markdown</a> by default - this can be disabled in "Options".', + }, { displayName: 'This is a legacy Slack feature. Slack advises to instead use Blocks.', name: 'noticeAttachments', diff --git a/packages/nodes-base/nodes/Slack/V2/SlackV2.node.ts b/packages/nodes-base/nodes/Slack/V2/SlackV2.node.ts index 2a226c6c4fd5f..28b243b744d5a 100644 --- a/packages/nodes-base/nodes/Slack/V2/SlackV2.node.ts +++ b/packages/nodes-base/nodes/Slack/V2/SlackV2.node.ts @@ -756,18 +756,25 @@ export class SlackV2 implements INodeType { : (this.getNodeParameter('user', i, undefined, { extractValue: true, }) as string); - // @ts-ignore - if (select === 'user' && this.getNodeParameter('user', i).mode === 'username') { + + if ( + select === 'user' && + (this.getNodeParameter('user', i) as IDataObject).mode === 'username' + ) { target = target.slice(0, 1) === '@' ? target : `@${target}`; } const { sendAsUser } = this.getNodeParameter('otherOptions', i) as IDataObject; let content: IDataObject = {}; + const text = this.getNodeParameter('text', i, '') as string; switch (messageType) { case 'text': - content = { text: this.getNodeParameter('text', i) as string }; + content = { text }; break; case 'block': content = JSON.parse(this.getNodeParameter('blocksUi', i) as string); + if (text) { + content.text = text; + } break; case 'attachment': content = { attachments: this.getNodeParameter('attachments', i) } as IDataObject; @@ -803,8 +810,8 @@ export class SlackV2 implements INodeType { action = 'postEphemeral'; } } - //@ts-ignore - const replyValues = otherOptions.thread_ts?.replyValues as IDataObject; + + const replyValues = (otherOptions.thread_ts as IDataObject)?.replyValues as IDataObject; Object.assign(body, replyValues); delete otherOptions.thread_ts; delete otherOptions.ephemeral; @@ -879,8 +886,11 @@ export class SlackV2 implements INodeType { : (this.getNodeParameter('user', i, undefined, { extractValue: true, }) as string); - // @ts-ignore - if (select === 'user' && this.getNodeParameter('user', i).mode === 'username') { + + if ( + select === 'user' && + (this.getNodeParameter('user', i) as IDataObject).mode === 'username' + ) { target = target.slice(0, 1) === '@' ? target : `@${target}`; } const timestamp = this.getNodeParameter('timestamp', i)?.toString() as string; @@ -1182,8 +1192,7 @@ export class SlackV2 implements INodeType { const body: IDataObject = {}; let status; if (options.status) { - // @ts-ignore - status = options.status?.set_status[0] as IDataObject; + status = ((options.status as IDataObject)?.set_status as IDataObject[])[0]; if (status.status_expiration === undefined) { status.status_expiration = 0; } else { @@ -1199,15 +1208,16 @@ export class SlackV2 implements INodeType { const customFields = (options.customFieldUi as IDataObject) .customFieldValues as IDataObject[]; - options.fields = {}; + const fields: IDataObject = {}; for (const customField of customFields) { - //@ts-ignore - options.fields[customField.id] = { + fields[customField.id as string] = { value: customField.value, alt: customField.alt, }; } + + options.fields = fields; } Object.assign(body, options); responseData = await slackApiRequest.call( diff --git a/packages/nodes-base/nodes/Snowflake/Snowflake.node.ts b/packages/nodes-base/nodes/Snowflake/Snowflake.node.ts index 8264cdce7a83c..32ea9e477ff65 100644 --- a/packages/nodes-base/nodes/Snowflake/Snowflake.node.ts +++ b/packages/nodes-base/nodes/Snowflake/Snowflake.node.ts @@ -65,6 +65,9 @@ export class Snowflake implements INodeType { displayName: 'Query', name: 'query', type: 'string', + typeOptions: { + editor: 'sqlEditor', + }, displayOptions: { show: { operation: ['executeQuery'], diff --git a/packages/nodes-base/nodes/SpreadsheetFile/test/SpreadsheetFile.test.ts b/packages/nodes-base/nodes/SpreadsheetFile/test/SpreadsheetFile.test.ts index 9fae97cd527b0..d908c16895b34 100644 --- a/packages/nodes-base/nodes/SpreadsheetFile/test/SpreadsheetFile.test.ts +++ b/packages/nodes-base/nodes/SpreadsheetFile/test/SpreadsheetFile.test.ts @@ -87,9 +87,9 @@ describe('Execute Spreadsheet File Node', () => { mimeType: 'text/csv', fileType: 'text', fileExtension: 'csv', - data: '77u/QSxCLEMKMSwyLDMKNCw1LDYK', + data: '77u/QSxCLEMKMSwyLDMKNCw1LDY=', fileName: 'spreadsheet.csv', - fileSize: '21 B', + fileSize: '20 B', }, }, }, diff --git a/packages/nodes-base/nodes/TheHive/interfaces/AlertInterface.ts b/packages/nodes-base/nodes/TheHive/interfaces/AlertInterface.ts index f8ee11271a102..0286d8247a88d 100644 --- a/packages/nodes-base/nodes/TheHive/interfaces/AlertInterface.ts +++ b/packages/nodes-base/nodes/TheHive/interfaces/AlertInterface.ts @@ -1,11 +1,11 @@ import type { IDataObject } from 'n8n-workflow'; -export enum AlertStatus { +export const enum AlertStatus { NEW = 'New', UPDATED = 'Updated', IGNORED = 'Ignored', IMPORTED = 'Imported', } -export enum TLP { +export const enum TLP { white, green, amber, diff --git a/packages/nodes-base/nodes/TheHive/interfaces/CaseInterface.ts b/packages/nodes-base/nodes/TheHive/interfaces/CaseInterface.ts index 58e910c6ffd0a..297ae58f5185f 100644 --- a/packages/nodes-base/nodes/TheHive/interfaces/CaseInterface.ts +++ b/packages/nodes-base/nodes/TheHive/interfaces/CaseInterface.ts @@ -31,13 +31,13 @@ export interface ICase { upadtedAt?: Date; } -export enum CaseStatus { +export const enum CaseStatus { OPEN = 'Open', RESOLVED = 'Resolved', DELETED = 'Deleted', } -export enum CaseResolutionStatus { +export const enum CaseResolutionStatus { INDETERMINATE = 'Indeterminate', FALSEPOSITIVE = 'FalsePositive', TRUEPOSITIVE = 'TruePositive', @@ -45,7 +45,7 @@ export enum CaseResolutionStatus { DUPLICATED = 'Duplicated', } -export enum CaseImpactStatus { +export const enum CaseImpactStatus { NOIMPACT = 'NoImpact', WITHIMPACT = 'WithImpact', NOTAPPLICABLE = 'NotApplicable', diff --git a/packages/nodes-base/nodes/TheHive/interfaces/LogInterface.ts b/packages/nodes-base/nodes/TheHive/interfaces/LogInterface.ts index 5e02a41c627f1..2e786a962f89a 100644 --- a/packages/nodes-base/nodes/TheHive/interfaces/LogInterface.ts +++ b/packages/nodes-base/nodes/TheHive/interfaces/LogInterface.ts @@ -1,5 +1,5 @@ import type { IAttachment } from './ObservableInterface'; -export enum LogStatus { +export const enum LogStatus { OK = 'Ok', DELETED = 'Deleted', } diff --git a/packages/nodes-base/nodes/TheHive/interfaces/ObservableInterface.ts b/packages/nodes-base/nodes/TheHive/interfaces/ObservableInterface.ts index a18b85c2db2fd..cb35df2189870 100644 --- a/packages/nodes-base/nodes/TheHive/interfaces/ObservableInterface.ts +++ b/packages/nodes-base/nodes/TheHive/interfaces/ObservableInterface.ts @@ -1,10 +1,10 @@ import type { TLP } from './AlertInterface'; -export enum ObservableStatus { +export const enum ObservableStatus { OK = 'Ok', DELETED = 'Deleted', } -export enum ObservableDataType { +export const enum ObservableDataType { 'domain' = 'domain', 'file' = 'file', 'filename' = 'filename', diff --git a/packages/nodes-base/nodes/TheHive/interfaces/TaskInterface.ts b/packages/nodes-base/nodes/TheHive/interfaces/TaskInterface.ts index c453e10b1ede4..daff422198e8b 100644 --- a/packages/nodes-base/nodes/TheHive/interfaces/TaskInterface.ts +++ b/packages/nodes-base/nodes/TheHive/interfaces/TaskInterface.ts @@ -17,7 +17,7 @@ export interface ITask { upadtedAt?: Date; } -export enum TaskStatus { +export const enum TaskStatus { WAITING = 'Waiting', INPROGRESS = 'InProgress', COMPLETED = 'Completed', diff --git a/packages/nodes-base/nodes/TimescaleDb/TimescaleDb.node.ts b/packages/nodes-base/nodes/TimescaleDb/TimescaleDb.node.ts index 5a43f7c500f53..1d0c2c21ac650 100644 --- a/packages/nodes-base/nodes/TimescaleDb/TimescaleDb.node.ts +++ b/packages/nodes-base/nodes/TimescaleDb/TimescaleDb.node.ts @@ -65,6 +65,10 @@ export class TimescaleDb implements INodeType { displayName: 'Query', name: 'query', type: 'string', + typeOptions: { + editor: 'sqlEditor', + sqlDialect: 'postgres', + }, displayOptions: { show: { operation: ['executeQuery'], diff --git a/packages/nodes-base/nodes/Todoist/v1/OperationHandler.ts b/packages/nodes-base/nodes/Todoist/v1/OperationHandler.ts index 8225bad42e402..a45773a8809c7 100644 --- a/packages/nodes-base/nodes/Todoist/v1/OperationHandler.ts +++ b/packages/nodes-base/nodes/Todoist/v1/OperationHandler.ts @@ -42,7 +42,7 @@ export interface Command { }; } -export enum CommandType { +export const enum CommandType { ITEM_MOVE = 'item_move', ITEM_ADD = 'item_add', ITEM_UPDATE = 'item_update', diff --git a/packages/nodes-base/nodes/Todoist/v1/Service.ts b/packages/nodes-base/nodes/Todoist/v1/Service.ts index 3e951608a89c3..e40fe25be02e4 100644 --- a/packages/nodes-base/nodes/Todoist/v1/Service.ts +++ b/packages/nodes-base/nodes/Todoist/v1/Service.ts @@ -35,17 +35,16 @@ export class TodoistService implements Service { }; } -export enum OperationType { - create = 'create', - close = 'close', - delete = 'delete', - get = 'get', - getAll = 'getAll', - reopen = 'reopen', - update = 'update', - move = 'move', - sync = 'sync', -} +export type OperationType = + | 'create' + | 'close' + | 'delete' + | 'get' + | 'getAll' + | 'reopen' + | 'update' + | 'move' + | 'sync'; export interface Section { name: string; diff --git a/packages/nodes-base/nodes/Todoist/v1/TodoistV1.node.ts b/packages/nodes-base/nodes/Todoist/v1/TodoistV1.node.ts index 6dcecc9787644..adf48942cfe3b 100644 --- a/packages/nodes-base/nodes/Todoist/v1/TodoistV1.node.ts +++ b/packages/nodes-base/nodes/Todoist/v1/TodoistV1.node.ts @@ -13,7 +13,8 @@ import type { import { todoistApiRequest } from '../GenericFunctions'; -import { OperationType, TodoistService } from './Service'; +import type { OperationType } from './Service'; +import { TodoistService } from './Service'; // interface IBodyCreateTask { // content?: string; @@ -702,15 +703,11 @@ export class TodoistV1 implements INodeType { const service = new TodoistService(); let responseData; const resource = this.getNodeParameter('resource', 0); - const operation = this.getNodeParameter('operation', 0); + const operation = this.getNodeParameter('operation', 0) as OperationType; for (let i = 0; i < length; i++) { try { if (resource === 'task') { - responseData = await service.execute( - this, - OperationType[operation as keyof typeof OperationType], - i, - ); + responseData = await service.execute(this, operation, i); } if (Array.isArray(responseData?.data)) { returnData.push.apply(returnData, responseData?.data as IDataObject[]); diff --git a/packages/nodes-base/nodes/Todoist/v2/OperationHandler.ts b/packages/nodes-base/nodes/Todoist/v2/OperationHandler.ts index 179d958be7ec0..fbe65589c8343 100644 --- a/packages/nodes-base/nodes/Todoist/v2/OperationHandler.ts +++ b/packages/nodes-base/nodes/Todoist/v2/OperationHandler.ts @@ -42,7 +42,7 @@ export interface Command { }; } -export enum CommandType { +export const enum CommandType { ITEM_MOVE = 'item_move', ITEM_ADD = 'item_add', ITEM_UPDATE = 'item_update', diff --git a/packages/nodes-base/nodes/Todoist/v2/Service.ts b/packages/nodes-base/nodes/Todoist/v2/Service.ts index 3e951608a89c3..e40fe25be02e4 100644 --- a/packages/nodes-base/nodes/Todoist/v2/Service.ts +++ b/packages/nodes-base/nodes/Todoist/v2/Service.ts @@ -35,17 +35,16 @@ export class TodoistService implements Service { }; } -export enum OperationType { - create = 'create', - close = 'close', - delete = 'delete', - get = 'get', - getAll = 'getAll', - reopen = 'reopen', - update = 'update', - move = 'move', - sync = 'sync', -} +export type OperationType = + | 'create' + | 'close' + | 'delete' + | 'get' + | 'getAll' + | 'reopen' + | 'update' + | 'move' + | 'sync'; export interface Section { name: string; diff --git a/packages/nodes-base/nodes/Todoist/v2/TodoistV2.node.ts b/packages/nodes-base/nodes/Todoist/v2/TodoistV2.node.ts index 6cb751d787161..f3813ca59be54 100644 --- a/packages/nodes-base/nodes/Todoist/v2/TodoistV2.node.ts +++ b/packages/nodes-base/nodes/Todoist/v2/TodoistV2.node.ts @@ -13,7 +13,8 @@ import type { import { todoistApiRequest } from '../GenericFunctions'; -import { OperationType, TodoistService } from './Service'; +import type { OperationType } from './Service'; +import { TodoistService } from './Service'; // interface IBodyCreateTask { // content?: string; @@ -701,15 +702,11 @@ export class TodoistV2 implements INodeType { const service = new TodoistService(); let responseData; const resource = this.getNodeParameter('resource', 0); - const operation = this.getNodeParameter('operation', 0); + const operation = this.getNodeParameter('operation', 0) as OperationType; for (let i = 0; i < length; i++) { try { if (resource === 'task') { - responseData = await service.execute( - this, - OperationType[operation as keyof typeof OperationType], - i, - ); + responseData = await service.execute(this, operation, i); } if (responseData !== undefined && Array.isArray(responseData?.data)) { diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index a26fc3d6f2c00..ab802acfc8153 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -1,6 +1,6 @@ { "name": "n8n-nodes-base", - "version": "0.223.0", + "version": "0.225.0", "description": "Base nodes of n8n", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", @@ -21,8 +21,8 @@ "build:translations": "gulp build:translations", "build:metadata": "pnpm n8n-generate-known && pnpm n8n-generate-ui-types", "format": "prettier --write . --ignore-path ../../.prettierignore", - "lint": "eslint --quiet nodes credentials", - "lintfix": "eslint nodes credentials --fix", + "lint": "eslint --quiet .; node ./scripts/validate-load-options-methods.js", + "lintfix": "eslint . --fix", "watch": "tsc-watch -p tsconfig.build.json --onSuccess \"pnpm n8n-generate-ui-types\"", "test": "jest" }, @@ -39,6 +39,7 @@ "dist/credentials/AffinityApi.credentials.js", "dist/credentials/AgileCrmApi.credentials.js", "dist/credentials/AirtableApi.credentials.js", + "dist/credentials/AirtableTokenApi.credentials.js", "dist/credentials/Amqp.credentials.js", "dist/credentials/ApiTemplateIoApi.credentials.js", "dist/credentials/AsanaApi.credentials.js", @@ -890,6 +891,7 @@ "pg-promise": "^10.5.8", "pretty-bytes": "^5.6.0", "promise-ftp": "^1.3.5", + "pyodide": "^0.22.1", "redis": "^3.1.1", "request": "^2.88.2", "rhea": "^1.0.11", @@ -901,7 +903,7 @@ "tmp-promise": "^3.0.2", "uuid": "^8.3.2", "vm2": "~3.9.17", - "xlsx": "^0.17.0", + "xlsx": "^0.19.3", "xml2js": "^0.5.0" } } diff --git a/packages/nodes-base/test/nodes/Airtable/Airtable.node.test.ts b/packages/nodes-base/test/nodes/Airtable/Airtable.node.test.ts index 02f91ebdd72b6..b2a2b71d0225c 100644 --- a/packages/nodes-base/test/nodes/Airtable/Airtable.node.test.ts +++ b/packages/nodes-base/test/nodes/Airtable/Airtable.node.test.ts @@ -1,6 +1,6 @@ import { executeWorkflow } from '../ExecuteWorkflow'; import * as Helpers from '../Helpers'; -import { WorkflowTestData } from '../types'; +import type { WorkflowTestData } from '../types'; import nock from 'nock'; const records = [ @@ -26,7 +26,7 @@ describe('Execute Airtable Node', () => { nock.restore(); }); - const tests: Array<WorkflowTestData> = [ + const tests: WorkflowTestData[] = [ { description: 'List Airtable Records', input: { diff --git a/packages/nodes-base/test/nodes/ExecuteWorkflow.ts b/packages/nodes-base/test/nodes/ExecuteWorkflow.ts index cf0b5dd01e1f8..d88304a355ea6 100644 --- a/packages/nodes-base/test/nodes/ExecuteWorkflow.ts +++ b/packages/nodes-base/test/nodes/ExecuteWorkflow.ts @@ -1,5 +1,6 @@ import { WorkflowExecute } from 'n8n-core'; -import { createDeferredPromise, INodeTypes, IRun, Workflow } from 'n8n-workflow'; +import type { INodeTypes, IRun } from 'n8n-workflow'; +import { createDeferredPromise, Workflow } from 'n8n-workflow'; import * as Helpers from './Helpers'; import type { WorkflowTestData } from './types'; diff --git a/packages/nodes-base/test/nodes/FakeCredentialsMap.ts b/packages/nodes-base/test/nodes/FakeCredentialsMap.ts index ab7b4680fad73..875083385d719 100644 --- a/packages/nodes-base/test/nodes/FakeCredentialsMap.ts +++ b/packages/nodes-base/test/nodes/FakeCredentialsMap.ts @@ -1,4 +1,4 @@ -import { IDataObject } from 'n8n-workflow'; +import type { IDataObject } from 'n8n-workflow'; // If your test needs data from credentials, you can add it here. // as JSON.stringify({ id: 'credentials_ID', name: 'credentials_name' }) for specific credentials @@ -19,4 +19,9 @@ export const FAKE_CREDENTIALS_DATA: IDataObject = { label: 'GitHub:john-doe', secret: 'BVDRSBXQB2ZEL5HE', }, + aws: { + region: 'eu-central-1', + accessKeyId: 'key', + secretAccessKey: 'secret', + }, }; diff --git a/packages/nodes-base/test/nodes/Helpers.ts b/packages/nodes-base/test/nodes/Helpers.ts index baa5871a31984..d26141226a718 100644 --- a/packages/nodes-base/test/nodes/Helpers.ts +++ b/packages/nodes-base/test/nodes/Helpers.ts @@ -1,8 +1,15 @@ import { readFileSync, readdirSync, mkdtempSync } from 'fs'; +import path from 'path'; +import { tmpdir } from 'os'; +import { isEmpty } from 'lodash'; +import { get } from 'lodash'; import { BinaryDataManager, Credentials, constructExecutionMetaData } from 'n8n-core'; -import { +import type { + CredentialLoadingDetails, ICredentialDataDecryptedObject, - ICredentialsHelper, + ICredentialType, + ICredentialTypeData, + ICredentialTypes, IDataObject, IDeferredPromise, IExecuteFunctions, @@ -21,20 +28,16 @@ import { IVersionedNodeType, IWorkflowBase, IWorkflowExecuteAdditionalData, - LoadingDetails, - LoggerProxy, - NodeHelpers, - WorkflowHooks, + NodeLoadingDetails, } from 'n8n-workflow'; +import { ICredentialsHelper, LoggerProxy, NodeHelpers, WorkflowHooks } from 'n8n-workflow'; import { executeWorkflow } from './ExecuteWorkflow'; -import { WorkflowTestData } from './types'; -import path from 'path'; -import { tmpdir } from 'os'; -import { isEmpty } from 'lodash'; -import { get } from 'lodash'; +import type { WorkflowTestData } from './types'; import { FAKE_CREDENTIALS_DATA } from './FakeCredentialsMap'; +const baseDir = path.resolve(__dirname, '../..'); + const getFakeDecryptedCredentials = ( nodeCredentials: INodeCredentialsDetails, type: string, @@ -51,12 +54,56 @@ const getFakeDecryptedCredentials = ( return {}; }; +export const readJsonFileSync = <T = any>(filePath: string) => + JSON.parse(readFileSync(path.join(baseDir, filePath), 'utf-8')) as T; + +const knownCredentials = readJsonFileSync<Record<string, CredentialLoadingDetails>>( + 'dist/known/credentials.json', +); + +const knownNodes = readJsonFileSync<Record<string, NodeLoadingDetails>>('dist/known/nodes.json'); + +class CredentialType implements ICredentialTypes { + credentialTypes: ICredentialTypeData = {}; + + addCredential(credentialTypeName: string, credentialType: ICredentialType) { + this.credentialTypes[credentialTypeName] = { + sourcePath: '', + type: credentialType, + }; + } + + recognizes(credentialType: string): boolean { + return credentialType in this.credentialTypes; + } + + getByName(credentialType: string): ICredentialType { + return this.credentialTypes[credentialType].type; + } + + getNodeTypesToTestWith(type: string): string[] { + return knownCredentials[type]?.nodesToTestWith ?? []; + } + + getParentTypes(typeName: string): string[] { + return []; + } +} + export class CredentialsHelper extends ICredentialsHelper { + constructor(private credentialTypes: ICredentialTypes) { + super(''); + } + async authenticate( credentials: ICredentialDataDecryptedObject, typeName: string, requestParams: IHttpRequestOptions, ): Promise<IHttpRequestOptions> { + const credentialType = this.credentialTypes.getByName(typeName); + if (typeof credentialType.authenticate === 'function') { + return credentialType.authenticate(credentials, requestParams); + } return requestParams; } @@ -122,7 +169,7 @@ export function WorkflowExecuteAdditionalData( }; return { - credentialsHelper: new CredentialsHelper(''), + credentialsHelper: new CredentialsHelper(credentialTypes), hooks: new WorkflowHooks(hookFunctions, 'trigger', '1', workflowData), executeWorkflow: async (workflowInfo: IExecuteWorkflowInfo): Promise<any> => {}, sendMessageToUI: (message: string) => {}, @@ -137,8 +184,9 @@ export function WorkflowExecuteAdditionalData( }; } -class NodeTypesClass implements INodeTypes { +class NodeTypes implements INodeTypes { nodeTypes: INodeTypeData = {}; + getByName(nodeType: string): INodeType | IVersionedNodeType { return this.nodeTypes[nodeType].type; } @@ -154,7 +202,6 @@ class NodeTypesClass implements INodeTypes { ...this.nodeTypes, ...loadedNode, }; - //Object.assign(this.nodeTypes, loadedNode); } getByNameAndVersion(nodeType: string, version?: number): INodeType { @@ -162,25 +209,7 @@ class NodeTypesClass implements INodeTypes { } } -let nodeTypesInstance: NodeTypesClass | undefined; - -export function NodeTypes(): NodeTypesClass { - if (nodeTypesInstance === undefined) { - nodeTypesInstance = new NodeTypesClass(); - } - return nodeTypesInstance; -} - -let knownNodes: Record<string, LoadingDetails> | null = null; - -const loadKnownNodes = (): Record<string, LoadingDetails> => { - if (knownNodes === null) { - knownNodes = JSON.parse(readFileSync('dist/known/nodes.json').toString()); - } - return knownNodes!; -}; - -export function createTemporaryDir(prefix: string = 'n8n') { +export function createTemporaryDir(prefix = 'n8n') { return mkdtempSync(path.join(tmpdir(), prefix)); } @@ -196,18 +225,31 @@ export async function initBinaryDataManager(mode: 'default' | 'filesystem' = 'de return temporaryDir; } -export function setup(testData: Array<WorkflowTestData> | WorkflowTestData) { +const credentialTypes = new CredentialType(); + +export function setup(testData: WorkflowTestData[] | WorkflowTestData) { if (!Array.isArray(testData)) { testData = [testData]; } - const knownNodes = loadKnownNodes(); + const nodeTypes = new NodeTypes(); - const nodeTypes = NodeTypes(); - const nodeNames = Array.from( - new Set(testData.flatMap((data) => data.input.workflowData.nodes.map((n) => n.type))), - ); + const nodes = [...new Set(testData.flatMap((data) => data.input.workflowData.nodes))]; + const credentialNames = nodes + .filter((n) => n.credentials) + .flatMap(({ credentials }) => Object.keys(credentials!)); + for (const credentialName of credentialNames) { + const loadInfo = knownCredentials[credentialName]; + if (!loadInfo) { + throw new Error(`Unknown credential type: ${credentialName}`); + } + const sourcePath = loadInfo.sourcePath.replace(/^dist\//, './').replace(/\.js$/, '.ts'); + const nodeSourcePath = path.join(baseDir, sourcePath); + const credential = new (require(nodeSourcePath)[loadInfo.className])() as ICredentialType; + credentialTypes.addCredential(credentialName, credential); + } + const nodeNames = nodes.map((n) => n.type); for (const nodeName of nodeNames) { if (!nodeName.startsWith('n8n-nodes-base.')) { throw new Error(`Unknown node type: ${nodeName}`); @@ -217,7 +259,7 @@ export function setup(testData: Array<WorkflowTestData> | WorkflowTestData) { throw new Error(`Unknown node type: ${nodeName}`); } const sourcePath = loadInfo.sourcePath.replace(/^dist\//, './').replace(/\.js$/, '.ts'); - const nodeSourcePath = path.join(process.cwd(), sourcePath); + const nodeSourcePath = path.join(baseDir, sourcePath); const node = new (require(nodeSourcePath)[loadInfo.className])() as INodeType; nodeTypes.addNode(nodeName, node); } @@ -236,7 +278,21 @@ export function setup(testData: Array<WorkflowTestData> | WorkflowTestData) { export function getResultNodeData(result: IRun, testData: WorkflowTestData) { return Object.keys(testData.output.nodeData).map((nodeName) => { + const error = result.data.resultData.error; + // If there was an error running the workflow throw it for easier debugging + // and to surface all issues + if (error?.cause) throw error.cause; + if (error) throw error; + if (result.data.resultData.runData[nodeName] === undefined) { + // log errors from other nodes + Object.keys(result.data.resultData.runData).forEach((key) => { + const error = result.data.resultData.runData[key][0]?.error; + if (error) { + console.log(`Node ${key}\n`, error); + } + }); + throw new Error(`Data for node "${nodeName}" is missing!`); } const resultData = result.data.resultData.runData[nodeName].map((nodeData) => { @@ -256,10 +312,6 @@ export function getResultNodeData(result: IRun, testData: WorkflowTestData) { }); } -export function readJsonFileSync(path: string) { - return JSON.parse(readFileSync(path, 'utf-8')); -} - export const equalityTest = async (testData: WorkflowTestData, types: INodeTypes) => { // execute workflow const { result } = await executeWorkflow(testData, types); @@ -278,7 +330,7 @@ const preparePinData = (pinData: IDataObject) => { const returnData = Object.keys(pinData).reduce( (acc, key) => { const data = pinData[key] as IDataObject[]; - acc[key] = [data as IDataObject[]]; + acc[key] = [data]; return acc; }, {} as { @@ -292,7 +344,7 @@ export const workflowToTests = (workflowFiles: string[]) => { const testCases: WorkflowTestData[] = []; for (const filePath of workflowFiles) { const description = filePath.replace('.json', ''); - const workflowData = readJsonFileSync(filePath); + const workflowData = readJsonFileSync<IWorkflowBase>(filePath); if (workflowData.pinData === undefined) { throw new Error('Workflow data does not contain pinData'); } @@ -325,7 +377,7 @@ export const getWorkflowFilenames = (dirname: string) => { const filenames = readdirSync(dirname); const testFolder = dirname.split(`${path.sep}nodes-base${path.sep}`)[1]; filenames.forEach((file) => { - if (file.includes('.json')) { + if (file.endsWith('.json')) { workflows.push(path.join(testFolder, file)); } }); diff --git a/packages/nodes-base/test/nodes/Postgres/Postgres.node.functions.test.js b/packages/nodes-base/test/nodes/Postgres/Postgres.node.functions.test.ts similarity index 68% rename from packages/nodes-base/test/nodes/Postgres/Postgres.node.functions.test.js rename to packages/nodes-base/test/nodes/Postgres/Postgres.node.functions.test.ts index 29b72932c8645..e8c40be353aa3 100644 --- a/packages/nodes-base/test/nodes/Postgres/Postgres.node.functions.test.js +++ b/packages/nodes-base/test/nodes/Postgres/Postgres.node.functions.test.ts @@ -1,10 +1,12 @@ const PostgresFun = require('../../../nodes/Postgres/v1/genericFunctions'); const pgPromise = require('pg-promise'); +type NodeParams = Record<string, string | {}>; + describe('pgUpdate', () => { it('runs query to update db', async () => { const updateItem = { id: 1234, name: 'test' }; - const nodeParams = { + const nodeParams: NodeParams = { table: 'mytable', schema: 'myschema', updateKey: 'id', @@ -12,7 +14,7 @@ describe('pgUpdate', () => { additionalFields: {}, returnFields: '*', }; - const getNodeParam = (key) => nodeParams[key]; + const getNodeParam = (key: string) => nodeParams[key]; const pgp = pgPromise(); const any = jest.fn(); const db = { any }; @@ -26,13 +28,13 @@ describe('pgUpdate', () => { await PostgresFun.pgUpdate(getNodeParam, pgp, db, items); expect(db.any).toHaveBeenCalledWith( - `update \"myschema\".\"mytable\" as t set \"id\"=v.\"id\",\"name\"=v.\"name\" from (values(1234,'test')) as v(\"id\",\"name\") WHERE v.\"id\" = t.\"id\" RETURNING *`, + 'update "myschema"."mytable" as t set "id"=v."id","name"=v."name" from (values(1234,\'test\')) as v("id","name") WHERE v."id" = t."id" RETURNING *', ); }); it('runs query to update db if updateKey is not in columns', async () => { const updateItem = { id: 1234, name: 'test' }; - const nodeParams = { + const nodeParams: NodeParams = { table: 'mytable', schema: 'myschema', updateKey: 'id', @@ -40,7 +42,7 @@ describe('pgUpdate', () => { additionalFields: {}, returnFields: '*', }; - const getNodeParam = (key) => nodeParams[key]; + const getNodeParam = (key: string) => nodeParams[key]; const pgp = pgPromise(); const any = jest.fn(); const db = { any }; @@ -54,13 +56,13 @@ describe('pgUpdate', () => { const results = await PostgresFun.pgUpdate(getNodeParam, pgp, db, items); expect(db.any).toHaveBeenCalledWith( - `update \"myschema\".\"mytable\" as t set \"id\"=v.\"id\",\"name\"=v.\"name\" from (values(1234,'test')) as v(\"id\",\"name\") WHERE v.\"id\" = t.\"id\" RETURNING *`, + 'update "myschema"."mytable" as t set "id"=v."id","name"=v."name" from (values(1234,\'test\')) as v("id","name") WHERE v."id" = t."id" RETURNING *', ); }); it('runs query to update db with cast as updateKey', async () => { const updateItem = { id: '1234', name: 'test' }; - const nodeParams = { + const nodeParams: NodeParams = { table: 'mytable', schema: 'myschema', updateKey: 'id:uuid', @@ -68,7 +70,7 @@ describe('pgUpdate', () => { additionalFields: {}, returnFields: '*', }; - const getNodeParam = (key) => nodeParams[key]; + const getNodeParam = (key: string) => nodeParams[key]; const pgp = pgPromise(); const any = jest.fn(); const db = { any }; @@ -82,13 +84,13 @@ describe('pgUpdate', () => { await PostgresFun.pgUpdate(getNodeParam, pgp, db, items); expect(db.any).toHaveBeenCalledWith( - `update \"myschema\".\"mytable\" as t set \"id\"=v.\"id\",\"name\"=v.\"name\" from (values('1234'::uuid,'test')) as v(\"id\",\"name\") WHERE v.\"id\" = t.\"id\" RETURNING *`, + 'update "myschema"."mytable" as t set "id"=v."id","name"=v."name" from (values(\'1234\'::uuid,\'test\')) as v("id","name") WHERE v."id" = t."id" RETURNING *', ); }); it('runs query to update db with cast in target columns', async () => { const updateItem = { id: '1234', name: 'test' }; - const nodeParams = { + const nodeParams: NodeParams = { table: 'mytable', schema: 'myschema', updateKey: 'id', @@ -96,7 +98,7 @@ describe('pgUpdate', () => { additionalFields: {}, returnFields: '*', }; - const getNodeParam = (key) => nodeParams[key]; + const getNodeParam = (key: string) => nodeParams[key]; const pgp = pgPromise(); const any = jest.fn(); const db = { any }; @@ -110,7 +112,7 @@ describe('pgUpdate', () => { await PostgresFun.pgUpdate(getNodeParam, pgp, db, items); expect(db.any).toHaveBeenCalledWith( - `update \"myschema\".\"mytable\" as t set \"id\"=v.\"id\",\"name\"=v.\"name\" from (values('1234'::uuid,'test')) as v(\"id\",\"name\") WHERE v.\"id\" = t.\"id\" RETURNING *`, + 'update "myschema"."mytable" as t set "id"=v."id","name"=v."name" from (values(\'1234\'::uuid,\'test\')) as v("id","name") WHERE v."id" = t."id" RETURNING *', ); }); }); @@ -118,14 +120,14 @@ describe('pgUpdate', () => { describe('pgInsert', () => { it('runs query to insert', async () => { const insertItem = { id: 1234, name: 'test', age: 34 }; - const nodeParams = { + const nodeParams: NodeParams = { table: 'mytable', schema: 'myschema', columns: 'id,name,age', returnFields: '*', additionalFields: {}, }; - const getNodeParam = (key) => nodeParams[key]; + const getNodeParam = (key: string) => nodeParams[key]; const pgp = pgPromise(); const any = jest.fn(); const db = { any }; @@ -139,20 +141,20 @@ describe('pgInsert', () => { await PostgresFun.pgInsert(getNodeParam, pgp, db, items); expect(db.any).toHaveBeenCalledWith( - `insert into \"myschema\".\"mytable\"(\"id\",\"name\",\"age\") values(1234,'test',34) RETURNING *`, + 'insert into "myschema"."mytable"("id","name","age") values(1234,\'test\',34) RETURNING *', ); }); it('runs query to insert with type casting', async () => { const insertItem = { id: 1234, name: 'test', age: 34 }; - const nodeParams = { + const nodeParams: NodeParams = { table: 'mytable', schema: 'myschema', columns: 'id:int,name:text,age', returnFields: '*', additionalFields: {}, }; - const getNodeParam = (key) => nodeParams[key]; + const getNodeParam = (key: string) => nodeParams[key]; const pgp = pgPromise(); const any = jest.fn(); const db = { any }; @@ -166,7 +168,7 @@ describe('pgInsert', () => { await PostgresFun.pgInsert(getNodeParam, pgp, db, items); expect(db.any).toHaveBeenCalledWith( - `insert into \"myschema\".\"mytable\"(\"id\",\"name\",\"age\") values(1234::int,'test'::text,34) RETURNING *`, + 'insert into "myschema"."mytable"("id","name","age") values(1234::int,\'test\'::text,34) RETURNING *', ); }); }); diff --git a/packages/nodes-base/test/nodes/Start/StartNode.test.ts b/packages/nodes-base/test/nodes/Start/StartNode.test.ts index 3c29c006ca61b..c399bd4554bf3 100644 --- a/packages/nodes-base/test/nodes/Start/StartNode.test.ts +++ b/packages/nodes-base/test/nodes/Start/StartNode.test.ts @@ -1,9 +1,9 @@ import * as Helpers from '../Helpers'; -import { WorkflowTestData } from '../types'; +import type { WorkflowTestData } from '../types'; import { executeWorkflow } from '../ExecuteWorkflow'; describe('Execute Start Node', () => { - const tests: Array<WorkflowTestData> = [ + const tests: WorkflowTestData[] = [ { description: 'should run start node', input: { diff --git a/packages/nodes-base/test/nodes/Stripe/helpers.test.js b/packages/nodes-base/test/nodes/Stripe/helpers.test.ts similarity index 100% rename from packages/nodes-base/test/nodes/Stripe/helpers.test.js rename to packages/nodes-base/test/nodes/Stripe/helpers.test.ts diff --git a/packages/nodes-base/test/nodes/types.ts b/packages/nodes-base/test/nodes/types.ts index 37ffd12aea03b..59f069b9fc0a3 100644 --- a/packages/nodes-base/test/nodes/types.ts +++ b/packages/nodes-base/test/nodes/types.ts @@ -1,4 +1,4 @@ -import { INode, IConnections } from 'n8n-workflow'; +import type { INode, IConnections } from 'n8n-workflow'; export interface WorkflowTestData { description: string; diff --git a/packages/nodes-base/test/utils/utilities.test.ts b/packages/nodes-base/test/utils/utilities.test.ts index 78fa5631bc0c5..e98ca10b20011 100644 --- a/packages/nodes-base/test/utils/utilities.test.ts +++ b/packages/nodes-base/test/utils/utilities.test.ts @@ -1,4 +1,4 @@ -import { fuzzyCompare } from '../../utils/utilities'; +import { fuzzyCompare, keysToLowercase, wrapData } from '../../utils/utilities'; //most test cases for fuzzyCompare are done in Compare Datasets node tests describe('Test fuzzyCompare', () => { @@ -29,3 +29,75 @@ describe('Test fuzzyCompare', () => { expect(compareFunction(null, '0')).toEqual(false); }); }); + +describe('Test wrapData', () => { + it('should wrap object in json', () => { + const data = { + id: 1, + name: 'Name', + }; + const wrappedData = wrapData(data); + expect(wrappedData).toBeDefined(); + expect(wrappedData).toEqual([{ json: data }]); + }); + it('should wrap each object in array in json', () => { + const data = [ + { + id: 1, + name: 'Name', + }, + { + id: 2, + name: 'Name 2', + }, + ]; + const wrappedData = wrapData(data); + expect(wrappedData).toBeDefined(); + expect(wrappedData).toEqual([{ json: data[0] }, { json: data[1] }]); + }); + it('json key from source should be inside json', () => { + const data = { + json: { + id: 1, + name: 'Name', + }, + }; + const wrappedData = wrapData(data); + expect(wrappedData).toBeDefined(); + expect(wrappedData).toEqual([{ json: data }]); + expect(Object.keys(wrappedData[0].json)).toContain('json'); + }); +}); + +describe('Test keysToLowercase', () => { + it('should convert keys to lowercase', () => { + const headers = { + 'Content-Type': 'application/json', + 'X-Test-Header': 'Test', + Accept: 'application/json', + }; + + const newHeaders = keysToLowercase(headers); + + expect(newHeaders).toEqual({ + 'content-type': 'application/json', + 'x-test-header': 'Test', + accept: 'application/json', + }); + }); + it('should return original value if it is not an object', () => { + const test1 = keysToLowercase(['hello']); + const test2 = keysToLowercase('test'); + const test3 = keysToLowercase(1); + const test4 = keysToLowercase(true); + const test5 = keysToLowercase(null); + const test6 = keysToLowercase(undefined); + + expect(test1).toEqual(['hello']); + expect(test2).toEqual('test'); + expect(test3).toEqual(1); + expect(test4).toEqual(true); + expect(test5).toEqual(null); + expect(test6).toEqual(undefined); + }); +}); diff --git a/packages/nodes-base/tsconfig.json b/packages/nodes-base/tsconfig.json index 44bdf1d0a0ae4..6caa6cf9bb494 100644 --- a/packages/nodes-base/tsconfig.json +++ b/packages/nodes-base/tsconfig.json @@ -4,12 +4,15 @@ "lib": ["dom", "es2020", "es2022.error"], "types": ["node", "jest"], "noEmit": true, + "paths": { + "@test/*": ["./test/*"] + }, // TODO: remove all options below this line "noImplicitReturns": false, "noUnusedLocals": false, "useUnknownInCatchVariables": false }, - "include": ["credentials/**/*.ts", "nodes/**/*.ts"], + "include": ["credentials/**/*.ts", "nodes/**/*.ts", "test/**/*.ts", "utils/**/*.ts"], "references": [ { "path": "../workflow/tsconfig.build.json" }, { "path": "../core/tsconfig.build.json" } diff --git a/packages/nodes-base/utils/descriptions.ts b/packages/nodes-base/utils/descriptions.ts new file mode 100644 index 0000000000000..78abc2225145c --- /dev/null +++ b/packages/nodes-base/utils/descriptions.ts @@ -0,0 +1,9 @@ +import type { INodeProperties } from 'n8n-workflow'; + +export const oldVersionNotice: INodeProperties = { + displayName: + '<strong>New node version available:</strong> get the latest version with added features from the nodes panel.', + name: 'oldVersionNotice', + type: 'notice', + default: '', +}; diff --git a/packages/nodes-base/utils/utilities.ts b/packages/nodes-base/utils/utilities.ts index 28fa916896bf3..cca0d35084680 100644 --- a/packages/nodes-base/utils/utilities.ts +++ b/packages/nodes-base/utils/utilities.ts @@ -1,4 +1,10 @@ -import type { IDataObject, IDisplayOptions, INodeProperties } from 'n8n-workflow'; +import type { + IDataObject, + IDisplayOptions, + INodeExecutionData, + INodeProperties, +} from 'n8n-workflow'; + import { jsonParse } from 'n8n-workflow'; import { isEqual, isNull, merge } from 'lodash'; @@ -73,6 +79,25 @@ export function updateDisplayOptions( }); } +export function processJsonInput<T>(jsonData: T, inputName?: string) { + let values; + const input = `'${inputName}' ` || ''; + + if (typeof jsonData === 'string') { + try { + values = jsonParse(jsonData); + } catch (error) { + throw new Error(`Input ${input}must contain a valid JSON`); + } + } else if (typeof jsonData === 'object') { + values = jsonData; + } else { + throw new Error(`Input ${input}must contain a valid JSON`); + } + + return values; +} + function isFalsy<T>(value: T) { if (isNull(value)) return true; if (typeof value === 'string' && value === '') return true; @@ -172,3 +197,20 @@ export const fuzzyCompare = (useFuzzyCompare: boolean, compareVersion = 1) => { return isEqual(item1, item2); }; }; + +export function wrapData(data: IDataObject | IDataObject[]): INodeExecutionData[] { + if (!Array.isArray(data)) { + return [{ json: data }]; + } + return data.map((item) => ({ + json: item, + })); +} + +export const keysToLowercase = <T>(headers: T) => { + if (typeof headers !== 'object' || Array.isArray(headers) || headers === null) return headers; + return Object.entries(headers).reduce((acc, [key, value]) => { + acc[key.toLowerCase()] = value; + return acc; + }, {} as IDataObject); +}; diff --git a/packages/workflow/.eslintrc.js b/packages/workflow/.eslintrc.js index 52e41e2d2d03a..51b395f058b56 100644 --- a/packages/workflow/.eslintrc.js +++ b/packages/workflow/.eslintrc.js @@ -9,7 +9,6 @@ module.exports = { ...sharedOptions(__dirname), rules: { - '@typescript-eslint/consistent-type-imports': 'error', 'import/order': 'off', // TODO: remove this }, }; diff --git a/packages/workflow/package.json b/packages/workflow/package.json index 7b2085ef723fe..d5a146dd4e110 100644 --- a/packages/workflow/package.json +++ b/packages/workflow/package.json @@ -1,6 +1,6 @@ { "name": "n8n-workflow", - "version": "0.146.0", + "version": "0.148.0", "description": "Workflow base code of n8n", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", diff --git a/packages/workflow/src/AugmentObject.ts b/packages/workflow/src/AugmentObject.ts index 80a80b905a936..27602b3a236f3 100644 --- a/packages/workflow/src/AugmentObject.ts +++ b/packages/workflow/src/AugmentObject.ts @@ -90,6 +90,11 @@ export function augmentObject<T extends object>(data: T): T { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const value = Reflect.get(target, key, receiver); + + if (typeof value !== 'object' || value === null) return value; + if (value instanceof RegExp) return value.toString(); + if ('toJSON' in value && typeof value.toJSON === 'function') return value.toJSON() as T; + const newValue = augment(value); if (newValue !== value) { Object.assign(newData, { [key]: newValue }); @@ -138,7 +143,8 @@ export function augmentObject<T extends object>(data: T): T { }, getOwnPropertyDescriptor(target, key) { - return Object.getOwnPropertyDescriptor(data, key) ?? defaultPropertyDescriptor; + if (deletedProperties.indexOf(key) !== -1) return undefined; + return Object.getOwnPropertyDescriptor(key in newData ? newData : data, key); }, }); diff --git a/packages/workflow/src/Constants.ts b/packages/workflow/src/Constants.ts index e2a6fdf1e8540..48a44de600eb1 100644 --- a/packages/workflow/src/Constants.ts +++ b/packages/workflow/src/Constants.ts @@ -1,3 +1,16 @@ /* eslint-disable @typescript-eslint/naming-convention */ export const BINARY_ENCODING = 'base64'; export const WAIT_TIME_UNLIMITED = '3000-01-01T00:00:00.000Z'; + +export const CODE_LANGUAGES = ['javaScript', 'json', 'python'] as const; +export const CODE_EXECUTION_MODES = ['runOnceForAllItems', 'runOnceForEachItem'] as const; + +/** + * Nodes whose parameter values may refer to other nodes without expressions. + * Their content may need to be updated when the referenced node is renamed. + */ +export const NODES_WITH_RENAMABLE_CONTENT = new Set([ + 'n8n-nodes-base.code', + 'n8n-nodes-base.function', + 'n8n-nodes-base.functionItem', +]); diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index 6c1b687b3f792..d087b7a49e6c1 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -7,7 +7,9 @@ import type { Readable } from 'stream'; import type { URLSearchParams } from 'url'; import type { OptionsWithUri, OptionsWithUrl } from 'request'; import type { RequestPromiseOptions, RequestPromiseAPI } from 'request-promise-native'; +import type { PathLike } from 'fs'; +import type { CODE_EXECUTION_MODES, CODE_LANGUAGES } from './Constants'; import type { IDeferredPromise } from './DeferredPromise'; import type { Workflow } from './Workflow'; import type { WorkflowHooks } from './WorkflowHooks'; @@ -15,8 +17,8 @@ import type { WorkflowActivationError } from './WorkflowActivationError'; import type { WorkflowOperationError } from './WorkflowErrors'; import type { NodeApiError, NodeOperationError } from './NodeErrors'; import type { ExpressionError } from './ExpressionError'; -import type { PathLike } from 'fs'; import type { ExecutionStatus } from './ExecutionStatus'; +import type { AuthenticationMethod } from './Authentication'; export interface IAdditionalCredentialOptions { oauth2?: IOAuth2Options; @@ -385,7 +387,6 @@ export interface IDataObject { [key: string]: GenericValue | IDataObject | GenericValue[] | IDataObject[]; } -// export type IExecuteResponsePromiseData = IDataObject; export type IExecuteResponsePromiseData = IDataObject | IN8nHttpFullResponse; export interface INodeTypeNameVersion { @@ -635,12 +636,12 @@ namespace ExecuteFunctions { fallbackValue?: number, options?: IGetNodeParameterOptions, ): number; - getNodeParameter( + getNodeParameter<T = NodeParameterValueType | object>( parameterName: string, itemIndex: number, fallbackValue?: any, options?: IGetNodeParameterOptions, - ): NodeParameterValueType | object; + ): T; }; } @@ -1019,7 +1020,10 @@ export type NodePropertyTypes = export type CodeAutocompleteTypes = 'function' | 'functionItem'; -export type EditorTypes = 'code' | 'codeNodeEditor' | 'htmlEditor' | 'json'; +export type EditorType = 'code' | 'codeNodeEditor' | 'htmlEditor' | 'sqlEditor' | 'json'; +export type CodeNodeEditorLanguage = (typeof CODE_LANGUAGES)[number]; +export type CodeExecutionMode = (typeof CODE_EXECUTION_MODES)[number]; +export type SQLDialect = 'mssql' | 'mysql' | 'postgres'; export interface ILoadOptions { routing?: { @@ -1032,7 +1036,9 @@ export interface ILoadOptions { export interface INodePropertyTypeOptions { alwaysOpenEditWindow?: boolean; // Supported by: json codeAutocomplete?: CodeAutocompleteTypes; // Supported by: string - editor?: EditorTypes; // Supported by: string + editor?: EditorType; // Supported by: string + editorLanguage?: CodeNodeEditorLanguage; // Supported by: string in combination with editor: codeNodeEditor + sqlDialect?: SQLDialect; // Supported by: sqlEditor loadOptionsDependsOn?: string[]; // Supported by: options loadOptionsMethod?: string; // Supported by: options loadOptions?: ILoadOptions; // Supported by: options @@ -1389,12 +1395,6 @@ export interface IPostReceiveSort extends IPostReceiveBase { }; } -export interface INodeActionTypeDescription extends INodeTypeDescription { - displayOptions?: IDisplayOptions; - values?: IDataObject; - actionKey: string; -} - export interface INodeTypeDescription extends INodeTypeBaseDescription { version: number | number[]; defaults: INodeParameters; @@ -1433,7 +1433,6 @@ export interface INodeTypeDescription extends INodeTypeBaseDescription { inactive: string; }; }; - actions?: INodeActionTypeDescription[]; __loadOptionsMethods?: string[]; // only for validation during build } @@ -1868,7 +1867,7 @@ export interface IConnectedNode { depth: number; } -export enum OAuth2GrantType { +export const enum OAuth2GrantType { authorizationCode = 'authorizationCode', clientCredentials = 'clientCredentials', } @@ -1948,3 +1947,111 @@ export interface ExecutionFilters { waitTill?: boolean; workflowId?: number | string; } + +export interface IVersionNotificationSettings { + enabled: boolean; + endpoint: string; + infoUrl: string; +} + +export interface IUserManagementSettings { + enabled: boolean; + showSetupOnFirstLoad?: boolean; + smtpSetup: boolean; + authenticationMethod: AuthenticationMethod; +} + +export interface IPublicApiSettings { + enabled: boolean; + latestVersion: number; + path: string; + swaggerUi: { + enabled: boolean; + }; +} + +export type ILogLevel = 'info' | 'debug' | 'warn' | 'error' | 'verbose' | 'silent'; + +export interface IN8nUISettings { + endpointWebhook: string; + endpointWebhookTest: string; + saveDataErrorExecution: WorkflowSettings.SaveDataExecution; + saveDataSuccessExecution: WorkflowSettings.SaveDataExecution; + saveManualExecutions: boolean; + executionTimeout: number; + maxExecutionTimeout: number; + workflowCallerPolicyDefaultOption: WorkflowSettings.CallerPolicy; + oauthCallbackUrls: { + oauth1: string; + oauth2: string; + }; + timezone: string; + urlBaseWebhook: string; + urlBaseEditor: string; + versionCli: string; + n8nMetadata?: { + [key: string]: string | number | undefined; + }; + versionNotifications: IVersionNotificationSettings; + instanceId: string; + telemetry: ITelemetrySettings; + posthog: { + enabled: boolean; + apiHost: string; + apiKey: string; + autocapture: boolean; + disableSessionRecording: boolean; + debug: boolean; + }; + personalizationSurveyEnabled: boolean; + userActivationSurveyEnabled: boolean; + defaultLocale: string; + userManagement: IUserManagementSettings; + sso: { + saml: { + loginLabel: string; + loginEnabled: boolean; + }; + ldap: { + loginLabel: string; + loginEnabled: boolean; + }; + }; + publicApi: IPublicApiSettings; + workflowTagsDisabled: boolean; + logLevel: ILogLevel; + hiringBannerEnabled: boolean; + templates: { + enabled: boolean; + host: string; + }; + onboardingCallPromptEnabled: boolean; + missingPackages?: boolean; + executionMode: 'regular' | 'queue'; + pushBackend: 'sse' | 'websocket'; + communityNodesEnabled: boolean; + deployment: { + type: string | 'default' | 'n8n-internal' | 'cloud' | 'desktop_mac' | 'desktop_win'; + }; + isNpmAvailable: boolean; + allowedModules: { + builtIn?: string[]; + external?: string[]; + }; + enterprise: { + sharing: boolean; + ldap: boolean; + saml: boolean; + logStreaming: boolean; + advancedExecutionFilters: boolean; + variables: boolean; + versionControl: boolean; + }; + hideUsagePage: boolean; + license: { + environment: 'development' | 'production' | 'staging'; + }; + variables: { + limit: number; + }; +} diff --git a/packages/workflow/src/MessageEventBus.ts b/packages/workflow/src/MessageEventBus.ts index cf604ea6555c2..2da8c7a20d826 100644 --- a/packages/workflow/src/MessageEventBus.ts +++ b/packages/workflow/src/MessageEventBus.ts @@ -5,7 +5,7 @@ import type { INodeCredentials } from './Interfaces'; // General Enums And Interfaces // =============================== -export enum EventMessageTypeNames { +export const enum EventMessageTypeNames { generic = '$$EventMessage', audit = '$$EventMessageAudit', confirm = '$$EventMessageConfirm', @@ -13,7 +13,7 @@ export enum EventMessageTypeNames { node = '$$EventMessageNode', } -export enum MessageEventBusDestinationTypeNames { +export const enum MessageEventBusDestinationTypeNames { abstract = '$$AbstractMessageEventBusDestination', webhook = '$$MessageEventBusDestinationWebhook', sentry = '$$MessageEventBusDestinationSentry', diff --git a/packages/workflow/src/NodeErrors.ts b/packages/workflow/src/NodeErrors.ts index c2225d46fbf4f..4c8e014e42fa7 100644 --- a/packages/workflow/src/NodeErrors.ts +++ b/packages/workflow/src/NodeErrors.ts @@ -270,7 +270,8 @@ const STATUS_CODE_MESSAGES: IStatusCodeMessages = { '5XX': 'The service failed to process your request', '500': 'The service was not able to process your request', '502': 'Bad gateway - the service failed to handle your request', - '503': 'Service unavailable - perhaps try again later?', + '503': + 'Service unavailable - try again later or consider setting this node to retry automatically (in the node settings)', '504': 'Gateway timed out - perhaps try again later?', ECONNREFUSED: 'The service refused the connection - perhaps it is offline', @@ -298,15 +299,56 @@ export class NodeApiError extends NodeError { { message, description, httpCode, parseXml, runIndex, itemIndex }: NodeApiErrorOptions = {}, ) { super(node, error); + if (error.error) { // only for request library error this.removeCircularRefs(error.error as JsonObject); } + if ((!message && (error.message || (error?.reason as IDataObject)?.message)) || description) { + this.message = (error.message ?? + (error?.reason as IDataObject)?.message ?? + description) as string; + } + + if (!description && (error.description || (error?.reason as IDataObject)?.description)) { + this.description = (error.description ?? + (error?.reason as IDataObject)?.description) as string; + } + + if ( + !httpCode && + !message && + this.message && + this.message.toUpperCase().includes('ECONNREFUSED') + ) { + httpCode = 'ECONNREFUSED'; + + const originalMessage = this.message; + if (!description) { + this.description = `${originalMessage}; ${this.description ?? ''}`; + } + } + + if ( + !httpCode && + !message && + this.message && + this.message.toLowerCase().includes('bad gateway') + ) { + httpCode = '502'; + + const originalMessage = this.message; + if (!description) { + this.description = `${originalMessage}; ${this.description ?? ''}`; + } + } + // if it's an error generated by axios // look for descriptions in the response object if (error.reason) { const reason: IDataObject = error.reason as unknown as IDataObject; + if (reason.isAxiosError && reason.response) { error = reason.response as JsonObject; } @@ -319,7 +361,12 @@ export class NodeApiError extends NodeError { return; } - this.httpCode = this.findProperty(error, ERROR_STATUS_PROPERTIES, ERROR_NESTING_PROPERTIES); + if (httpCode) { + this.httpCode = httpCode; + } else { + this.httpCode = this.findProperty(error, ERROR_STATUS_PROPERTIES, ERROR_NESTING_PROPERTIES); + } + this.setMessage(); if (parseXml) { @@ -356,7 +403,8 @@ export class NodeApiError extends NodeError { private setMessage() { if (!this.httpCode) { this.httpCode = null; - this.message = UNKNOWN_ERROR_MESSAGE; + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + this.message = this.message || this.description || UNKNOWN_ERROR_MESSAGE; return; } @@ -373,7 +421,8 @@ export class NodeApiError extends NodeError { this.message = STATUS_CODE_MESSAGES['5XX']; break; default: - this.message = UNKNOWN_ERROR_MESSAGE; + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + this.message = this.message || this.description || UNKNOWN_ERROR_MESSAGE; } if (this.node.type === 'n8n-nodes-base.noOp' && this.message === UNKNOWN_ERROR_MESSAGE) { this.message = `${UNKNOWN_ERROR_MESSAGE_CRED} - ${this.httpCode}`; diff --git a/packages/workflow/src/Workflow.ts b/packages/workflow/src/Workflow.ts index feb9c699d88fd..ae5de6ac9824b 100644 --- a/packages/workflow/src/Workflow.ts +++ b/packages/workflow/src/Workflow.ts @@ -55,6 +55,7 @@ import * as NodeHelpers from './NodeHelpers'; import * as ObservableObject from './ObservableObject'; import { RoutingNode } from './RoutingNode'; import { Expression } from './Expression'; +import { NODES_WITH_RENAMABLE_CONTENT } from './Constants'; function dedupe<T>(arr: T[]): T[] { return [...new Set(arr)]; @@ -405,21 +406,18 @@ export class Workflow { return this.pinData ? this.pinData[nodeName] : undefined; } - /** - * Renames nodes in expressions - * - * @param {(NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[])} parameterValue The parameters to check for expressions - * @param {string} currentName The current name of the node - * @param {string} newName The new name - */ - renameNodeInExpressions( + renameNodeInParameterValue( parameterValue: NodeParameterValueType, currentName: string, newName: string, + { hasRenamableContent } = { hasRenamableContent: false }, ): NodeParameterValueType { if (typeof parameterValue !== 'object') { // Reached the actual value - if (typeof parameterValue === 'string' && parameterValue.charAt(0) === '=') { + if ( + typeof parameterValue === 'string' && + (parameterValue.charAt(0) === '=' || hasRenamableContent) + ) { // Is expression so has to be rewritten // To not run the "expensive" regex stuff when it is not needed // make a simple check first if it really contains the the node-name @@ -467,7 +465,7 @@ export class Workflow { const returnArray: any[] = []; for (const currentValue of parameterValue) { - returnArray.push(this.renameNodeInExpressions(currentValue, currentName, newName)); + returnArray.push(this.renameNodeInParameterValue(currentValue, currentName, newName)); } return returnArray; @@ -478,10 +476,11 @@ export class Workflow { for (const parameterName of Object.keys(parameterValue || {})) { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - returnData[parameterName] = this.renameNodeInExpressions( + returnData[parameterName] = this.renameNodeInParameterValue( parameterValue![parameterName as keyof typeof parameterValue], currentName, newName, + { hasRenamableContent }, ); } @@ -505,11 +504,20 @@ export class Workflow { // Update the expressions which reference the node // with its old name for (const node of Object.values(this.nodes)) { - node.parameters = this.renameNodeInExpressions( + node.parameters = this.renameNodeInParameterValue( node.parameters, currentName, newName, ) as INodeParameters; + + if (NODES_WITH_RENAMABLE_CONTENT.has(node.type)) { + node.parameters.jsCode = this.renameNodeInParameterValue( + node.parameters.jsCode, + currentName, + newName, + { hasRenamableContent: true }, + ); + } } // Change all source connections @@ -1236,13 +1244,7 @@ export class Workflow { return { data: null }; } - let promiseResults; - try { - promiseResults = await Promise.all(returnPromises); - } catch (error) { - return Promise.reject(error); - } - + const promiseResults = await Promise.all(returnPromises); if (promiseResults) { return { data: [promiseResults] }; } diff --git a/packages/workflow/src/index.ts b/packages/workflow/src/index.ts index 63e71425fa225..eee0e5f3431a0 100644 --- a/packages/workflow/src/index.ts +++ b/packages/workflow/src/index.ts @@ -23,7 +23,15 @@ export * from './WorkflowErrors'; export * from './WorkflowHooks'; export * from './VersionedNodeType'; export { LoggerProxy, NodeHelpers, ObservableObject, TelemetryHelpers }; -export { deepCopy, jsonParse, jsonStringify, sleep, fileTypeFromMimeType, assert } from './utils'; +export { + isObjectEmpty, + deepCopy, + jsonParse, + jsonStringify, + sleep, + fileTypeFromMimeType, + assert, +} from './utils'; export { isINodeProperties, isINodePropertyOptions, diff --git a/packages/workflow/src/utils.ts b/packages/workflow/src/utils.ts index 71d4a50028a35..4db97aac100dc 100644 --- a/packages/workflow/src/utils.ts +++ b/packages/workflow/src/utils.ts @@ -1,5 +1,19 @@ import type { BinaryFileType } from './Interfaces'; +const readStreamClasses = new Set(['ReadStream', 'Readable', 'ReadableStream']); + +export const isObjectEmpty = (obj: object | null | undefined): boolean => { + if (obj === undefined || obj === null) return true; + if (typeof obj === 'object') { + if (Array.isArray(obj)) return obj.length === 0; + if (obj instanceof Set || obj instanceof Map) return obj.size === 0; + if (ArrayBuffer.isView(obj) || obj instanceof ArrayBuffer) return obj.byteLength === 0; + if (Symbol.iterator in obj || readStreamClasses.has(obj.constructor.name)) return false; + return Object.keys(obj).length === 0; + } + return true; +}; + export type Primitives = string | number | boolean | bigint | symbol | null | undefined; /* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-argument */ diff --git a/packages/workflow/test/AugmentObject.test.ts b/packages/workflow/test/AugmentObject.test.ts index e9fe6ee948697..b9871d347d4b3 100644 --- a/packages/workflow/test/AugmentObject.test.ts +++ b/packages/workflow/test/AugmentObject.test.ts @@ -6,7 +6,7 @@ describe('AugmentObject', () => { describe('augmentArray', () => { test('should work with arrays', () => { const originalObject = [1, 2, 3, 4, null]; - const copyOriginal = JSON.parse(JSON.stringify(originalObject)); + const copyOriginal = deepCopy(originalObject); const augmentedObject = augmentArray(originalObject); @@ -70,7 +70,7 @@ describe('AugmentObject', () => { }, ], }; - const copyOriginal = JSON.parse(JSON.stringify(originalObject)); + const copyOriginal = deepCopy(originalObject); const augmentedObject = augmentObject(originalObject); @@ -81,13 +81,13 @@ describe('AugmentObject', () => { // Make sure that also array operations as push and length work as expected // On lower levels - augmentedObject.a.b.c[0].a3!.b3.c3 = '033'; - expect(augmentedObject.a.b.c[0].a3!.b3.c3).toEqual('033'); - expect(originalObject.a.b.c[0].a3!.b3.c3).toEqual('03'); + augmentedObject.a.b.c[0].a3.b3.c3 = '033'; + expect(augmentedObject.a.b.c[0].a3.b3.c3).toEqual('033'); + expect(originalObject.a.b.c[0].a3.b3.c3).toEqual('03'); - augmentedObject.a.b.c[1].a3!.b3.c3 = '133'; - expect(augmentedObject.a.b.c[1].a3!.b3.c3).toEqual('133'); - expect(originalObject.a.b.c[1].a3!.b3.c3).toEqual('13'); + augmentedObject.a.b.c[1].a3.b3.c3 = '133'; + expect(augmentedObject.a.b.c[1].a3.b3.c3).toEqual('133'); + expect(originalObject.a.b.c[1].a3.b3.c3).toEqual('13'); augmentedObject.a.b.c.push({ a3: { @@ -203,7 +203,7 @@ describe('AugmentObject', () => { d: date, r: regexp, }; - const copyOriginal = JSON.parse(JSON.stringify(originalObject)); + const copyOriginal = deepCopy(originalObject); const augmentedObject = augmentObject(originalObject); @@ -233,8 +233,8 @@ describe('AugmentObject', () => { a: 9111, b: '9222', c: 3, - d: date, - r: regexp, + d: date.toJSON(), + r: regexp.toString(), }); }); @@ -248,30 +248,30 @@ describe('AugmentObject', () => { }, aa: '1', }; - const copyOriginal = JSON.parse(JSON.stringify(originalObject)); + const copyOriginal = deepCopy(originalObject); const augmentedObject = augmentObject(originalObject); augmentedObject.a.bb = '92'; expect(originalObject.a.bb).toEqual('2'); - expect(augmentedObject.a!.bb!).toEqual('92'); + expect(augmentedObject.a.bb).toEqual('92'); - augmentedObject.a!.b!.cc = '93'; + augmentedObject.a.b.cc = '93'; expect(originalObject.a.b.cc).toEqual('3'); - expect(augmentedObject.a!.b!.cc).toEqual('93'); + expect(augmentedObject.a.b.cc).toEqual('93'); // @ts-ignore - augmentedObject.a!.b!.ccc = { + augmentedObject.a.b.ccc = { d: '4', }; // @ts-ignore - expect(augmentedObject.a!.b!.ccc).toEqual({ d: '4' }); + expect(augmentedObject.a.b.ccc).toEqual({ d: '4' }); // @ts-ignore - augmentedObject.a!.b!.ccc.d = '94'; + augmentedObject.a.b.ccc.d = '94'; // @ts-ignore - expect(augmentedObject.a!.b!.ccc.d).toEqual('94'); + expect(augmentedObject.a.b.ccc.d).toEqual('94'); expect(originalObject).toEqual(copyOriginal); @@ -300,7 +300,7 @@ describe('AugmentObject', () => { }, aa: '1', }; - const copyOriginal = JSON.parse(JSON.stringify(originalObject)); + const copyOriginal = deepCopy(originalObject); const augmentedObject = augmentObject(originalObject); @@ -351,7 +351,7 @@ describe('AugmentObject', () => { }, aa: '1' as string | undefined, }; - const copyOriginal = JSON.parse(JSON.stringify(originalObject)); + const copyOriginal = deepCopy(originalObject); const augmentedObject = augmentObject(originalObject); @@ -423,7 +423,7 @@ describe('AugmentObject', () => { }, aa: '1' as string | undefined, }; - const copyOriginal = JSON.parse(JSON.stringify(originalObject)); + const copyOriginal = deepCopy(originalObject); const augmentedObject = augmentObject(originalObject); @@ -528,5 +528,34 @@ describe('AugmentObject', () => { const augmentedObject = augmentObject(originalObject); expect(Object.keys(augmentedObject)).toEqual(['a', 'b']); }); + + test('should return property descriptors', () => { + const originalObject = { + x: { + y: {}, + z: {}, + }, + }; + const augmentedObject = augmentObject(originalObject); + + expect(Object.getOwnPropertyDescriptor(augmentedObject.x, 'y')).toEqual({ + configurable: true, + enumerable: true, + value: {}, + writable: true, + }); + + delete augmentedObject.x.y; + expect(augmentedObject.x.hasOwnProperty('y')).toEqual(false); + + augmentedObject.x.y = 42; + expect(augmentedObject.x.hasOwnProperty('y')).toEqual(true); + expect(Object.getOwnPropertyDescriptor(augmentedObject.x, 'y')).toEqual({ + configurable: true, + enumerable: true, + value: 42, + writable: true, + }); + }); }); }); diff --git a/packages/workflow/test/Expression.test.ts b/packages/workflow/test/Expression.test.ts index 73bc289311be1..d2ef93e07a805 100644 --- a/packages/workflow/test/Expression.test.ts +++ b/packages/workflow/test/Expression.test.ts @@ -6,12 +6,9 @@ import { DateTime, Duration, Interval } from 'luxon'; import { Expression } from '@/Expression'; import { Workflow } from '@/Workflow'; import * as Helpers from './Helpers'; -import { - baseFixtures, - ExpressionTestEvaluation, - ExpressionTestTransform, -} from './ExpressionFixtures/base'; -import { INodeExecutionData } from '@/Interfaces'; +import type { ExpressionTestEvaluation, ExpressionTestTransform } from './ExpressionFixtures/base'; +import { baseFixtures } from './ExpressionFixtures/base'; +import type { INodeExecutionData } from '@/Interfaces'; import { extendSyntax } from '@/Extensions/ExpressionExtension'; describe('Expression', () => { @@ -88,7 +85,7 @@ describe('Expression', () => { expect(evaluate('={{new Object()}}')).toEqual(new Object()); - expect(evaluate('={{new Array()}}')).toEqual(new Array()); + expect(evaluate('={{new Array()}}')).toEqual([]); expect(evaluate('={{new Int8Array()}}')).toEqual(new Int8Array()); expect(evaluate('={{new Uint8Array()}}')).toEqual(new Uint8Array()); expect(evaluate('={{new Uint8ClampedArray()}}')).toEqual(new Uint8ClampedArray()); diff --git a/packages/workflow/test/ExpressionExtensions/ArrayExtensions.test.ts b/packages/workflow/test/ExpressionExtensions/ArrayExtensions.test.ts index fbd62432e4bc5..4af03ff073023 100644 --- a/packages/workflow/test/ExpressionExtensions/ArrayExtensions.test.ts +++ b/packages/workflow/test/ExpressionExtensions/ArrayExtensions.test.ts @@ -25,9 +25,7 @@ describe('Data Transformation Functions', () => { { value: 6, string: '6' }, { value: { something: 'else' } } ].pluck("value") }}`), - ).toEqual( - expect.arrayContaining([1, 2, 3, 4, 5, 6, { something: 'else' }]), - ); + ).toEqual(expect.arrayContaining([1, 2, 3, 4, 5, 6, { something: 'else' }])); }); test('.pluck() should work correctly for multiple values', () => { @@ -50,7 +48,10 @@ describe('Data Transformation Functions', () => { } ].pluck("firstName", "lastName") }}`), ).toEqual( - expect.arrayContaining([["John", "Doe"],["Jane", "Doe"]]), + expect.arrayContaining([ + ['John', 'Doe'], + ['Jane', 'Doe'], + ]), ); }); @@ -73,7 +74,7 @@ describe('Data Transformation Functions', () => { { value: 4, string: '4' }, { value: 5, string: '5' }, { value: 6, string: '6' }, - { value: { something: 'else' } } + { value: { something: 'else' } }, ]), ); }); @@ -86,10 +87,8 @@ describe('Data Transformation Functions', () => { test('.unique() should work on an arrays containing nulls, objects and arrays', () => { expect( - evaluate( - '={{ [1, 2, 3, "as", {}, {}, 1, 2, [1,2], "[sad]", "[sad]", null].unique() }}', - ), - ).toEqual([1, 2, 3, "as", {}, [1,2], "[sad]", null]); + evaluate('={{ [1, 2, 3, "as", {}, {}, 1, 2, [1,2], "[sad]", "[sad]", null].unique() }}'), + ).toEqual([1, 2, 3, 'as', {}, [1, 2], '[sad]', null]); }); test('.isEmpty() should work correctly on an array', () => { @@ -113,7 +112,7 @@ describe('Data Transformation Functions', () => { evaluate( '={{ [{ test1: 1, test2: 2 }, { test1: 1, test3: 3 }].merge([{ test1: 2, test3: 3 }, { test4: 4 }]) }}', ), - ).toEqual({"test1": 1, "test2": 2, "test3": 3, "test4": 4}); + ).toEqual({ test1: 1, test2: 2, test3: 3, test4: 4 }); }); test('.merge() should work correctly without arguments', () => { @@ -121,7 +120,7 @@ describe('Data Transformation Functions', () => { evaluate( '={{ [{ a: 1, some: null }, { a: 2, c: "something" }, 2, "asds", { b: 23 }, null, [1, 2]].merge() }}', ), - ).toEqual({"a": 1, "some": null, "c": "something", "b": 23}); + ).toEqual({ a: 1, some: null, c: 'something', b: 23 }); }); test('.smartJoin() should work correctly on an array of objects', () => { @@ -175,11 +174,14 @@ describe('Data Transformation Functions', () => { }); test('.union() should work on an arrays containing nulls, objects and arrays', () => { - expect( - evaluate( - '={{ [1, 2, "dd", {}, null].union([1, {}, null, 3]) }}', - ), - ).toEqual([1, 2, "dd", {}, null, 3]); + expect(evaluate('={{ [1, 2, "dd", {}, null].union([1, {}, null, 3]) }}')).toEqual([ + 1, + 2, + 'dd', + {}, + null, + 3, + ]); }); test('.intersection() should work on an array of objects', () => { @@ -191,11 +193,11 @@ describe('Data Transformation Functions', () => { }); test('.intersection() should work on an arrays containing nulls, objects and arrays', () => { - expect( - evaluate( - '={{ [1, 2, "dd", {}, null].intersection([1, {}, null]) }}', - ), - ).toEqual([1, {}, null]); + expect(evaluate('={{ [1, 2, "dd", {}, null].intersection([1, {}, null]) }}')).toEqual([ + 1, + {}, + null, + ]); }); test('.difference() should work on an array of objects', () => { @@ -212,10 +214,8 @@ describe('Data Transformation Functions', () => { test('.difference() should work on an arrays containing nulls, objects and arrays', () => { expect( - evaluate( - '={{ [1, 2, "dd", {}, null, ["a", 1]].difference([1, {}, null, ["a", 1]]) }}', - ), - ).toEqual([2, "dd"]); + evaluate('={{ [1, 2, "dd", {}, null, ["a", 1]].difference([1, {}, null, ["a", 1]]) }}'), + ).toEqual([2, 'dd']); }); test('.compact() should work on an array', () => { diff --git a/packages/workflow/test/ExpressionExtensions/DateExtensions.test.ts b/packages/workflow/test/ExpressionExtensions/DateExtensions.test.ts index 917c20cb48870..8ddfe0ad536d7 100644 --- a/packages/workflow/test/ExpressionExtensions/DateExtensions.test.ts +++ b/packages/workflow/test/ExpressionExtensions/DateExtensions.test.ts @@ -61,7 +61,6 @@ describe('Data Transformation Functions', () => { expect(evaluate('={{ DateTime.local(2023, 1, 20).extract() }}')).toEqual(3); }); - test('.format("yyyy LLL dd") should work correctly on a date', () => { expect(evaluate('={{ DateTime.local(2023, 1, 16).format("yyyy LLL dd") }}')).toEqual( '2023 Jan 16', @@ -74,27 +73,29 @@ describe('Data Transformation Functions', () => { }); test('.inBetween() should work on string and Date', () => { - expect(evaluate(`={{ $now.isBetween('2023-06-23'.toDate(), '2023-06-23') }}`)).toBeDefined(); + expect(evaluate("={{ $now.isBetween('2023-06-23'.toDate(), '2023-06-23') }}")).toBeDefined(); }); test('.inBetween() should work on string and DateTime', () => { - expect(evaluate(`={{ $now.isBetween($now, '2023-06-23') }}`)).toBeDefined(); + expect(evaluate("={{ $now.isBetween($now, '2023-06-23') }}")).toBeDefined(); }); test('.inBetween() should not work for invalid strings', () => { - expect(evaluate(`={{ $now.isBetween($now, 'invalid') }}`)).toBeUndefined(); + expect(evaluate("={{ $now.isBetween($now, 'invalid') }}")).toBeUndefined(); }); test('.inBetween() should not work for numbers', () => { - expect(evaluate(`={{ $now.isBetween($now, 1) }}`)).toBeUndefined(); + expect(evaluate('={{ $now.isBetween($now, 1) }}')).toBeUndefined(); }); test('.inBetween() should not work for a single argument', () => { - expect(() => evaluate(`={{ $now.isBetween($now) }}`)).toThrow(); + expect(() => evaluate('={{ $now.isBetween($now) }}')).toThrow(); }); test('.inBetween() should not work for a more than two arguments', () => { - expect(() => evaluate(`={{ $now.isBetween($now, '2023-06-23', '2023-09-21'.toDate()) }}`)).toThrow(); + expect(() => + evaluate("={{ $now.isBetween($now, '2023-06-23', '2023-09-21'.toDate()) }}"), + ).toThrow(); }); }); }); diff --git a/packages/workflow/test/ExpressionExtensions/ExpressionExtension.test.ts b/packages/workflow/test/ExpressionExtensions/ExpressionExtension.test.ts index 71dfe82fe9955..cfc825fdaeb13 100644 --- a/packages/workflow/test/ExpressionExtensions/ExpressionExtension.test.ts +++ b/packages/workflow/test/ExpressionExtensions/ExpressionExtension.test.ts @@ -2,6 +2,8 @@ * @jest-environment jsdom */ +/* eslint-disable n8n-local-rules/no-interpolation-in-regular-string */ + import { extendTransform } from '@/Extensions'; import { joinExpression, splitExpression } from '@/Extensions/ExpressionParser'; import { evaluate } from './Helpers'; diff --git a/packages/workflow/test/ExpressionExtensions/Helpers.ts b/packages/workflow/test/ExpressionExtensions/Helpers.ts index 9d6bd1ad7b066..65361b1f05358 100644 --- a/packages/workflow/test/ExpressionExtensions/Helpers.ts +++ b/packages/workflow/test/ExpressionExtensions/Helpers.ts @@ -1,4 +1,5 @@ -import { Expression, IDataObject, Workflow } from '../../src'; +import type { IDataObject } from '../../src'; +import { Expression, Workflow } from '../../src'; import * as Helpers from '../Helpers'; export const TEST_TIMEZONE = 'America/New_York'; @@ -20,7 +21,7 @@ export const workflow = new Workflow({ nodeTypes, settings: { timezone: TEST_TIMEZONE, - } + }, }); export const expression = new Expression(workflow); diff --git a/packages/workflow/test/ExpressionExtensions/NumberExtensions.test.ts b/packages/workflow/test/ExpressionExtensions/NumberExtensions.test.ts index 1a514c79284b0..21259d904ba53 100644 --- a/packages/workflow/test/ExpressionExtensions/NumberExtensions.test.ts +++ b/packages/workflow/test/ExpressionExtensions/NumberExtensions.test.ts @@ -59,6 +59,7 @@ describe('Data Transformation Functions', () => { describe('Multiple expressions', () => { test('Basic multiple expressions', () => { + // eslint-disable-next-line n8n-local-rules/no-interpolation-in-regular-string expect(evaluate('={{ "test abc".toSnakeCase() }} you have ${{ (100).format() }}.')).toEqual( 'test_abc you have $100.', ); diff --git a/packages/workflow/test/ExpressionExtensions/ObjectExtensions.test.ts b/packages/workflow/test/ExpressionExtensions/ObjectExtensions.test.ts index 71e5445f11f02..78c2689783af7 100644 --- a/packages/workflow/test/ExpressionExtensions/ObjectExtensions.test.ts +++ b/packages/workflow/test/ExpressionExtensions/ObjectExtensions.test.ts @@ -37,8 +37,8 @@ describe('Data Transformation Functions', () => { }); test('.removeFieldsContaining should not work for empty string', () => { - expect( - () => evaluate( + expect(() => + evaluate( '={{ ({ test1: "i exist", test2: "i should be removed", test3: "i should also be removed" }).removeFieldsContaining("") }}', ), ).toThrow(); @@ -65,8 +65,8 @@ describe('Data Transformation Functions', () => { }); test('.keepFieldsContaining should not work for empty string', () => { - expect( - () => evaluate( + expect(() => + evaluate( '={{ ({ test1: "i exist", test2: "i should be removed", test3: "i should also be removed" }).keepFieldsContaining("") }}', ), ).toThrow(); diff --git a/packages/workflow/test/ExpressionExtensions/StringExtensions.test.ts b/packages/workflow/test/ExpressionExtensions/StringExtensions.test.ts index 2b31634480d24..ce67491ab20cd 100644 --- a/packages/workflow/test/ExpressionExtensions/StringExtensions.test.ts +++ b/packages/workflow/test/ExpressionExtensions/StringExtensions.test.ts @@ -3,7 +3,6 @@ */ import { stringExtensions } from '@/Extensions/StringExtensions'; -import { dateExtensions } from '@/Extensions/DateExtensions'; import { evaluate } from './Helpers'; describe('Data Transformation Functions', () => { @@ -157,13 +156,29 @@ describe('Data Transformation Functions', () => { '={{ "I am a test with a url: https://example.net/ and I am a test with an email: test@example.org".extractUrl() }}', ), ).toEqual('https://example.net/'); - expect(evaluate('={{ "Check this out: https://subdomain.example.com:3000/path?q=1#hash".extractUrl() }}')).toEqual('https://subdomain.example.com:3000/path?q=1#hash'); + expect( + evaluate( + '={{ "Check this out: https://subdomain.example.com:3000/path?q=1#hash".extractUrl() }}', + ), + ).toEqual('https://subdomain.example.com:3000/path?q=1#hash'); expect(evaluate('={{ "Invalid URL: http:///example.com".extractUrl() }}')).toEqual(undefined); - expect(evaluate('={{ "Mixed content: https://www.example.com and http://www.example.org".extractUrl() }}')).toEqual('https://www.example.com'); - expect(evaluate('={{ "Text without URL: This is just a simple text".extractUrl() }}')).toEqual(undefined); - expect(evaluate('={{ "URL with Unicode: http://www.xn--80aswg.xn--j1amh".extractUrl() }}')).toEqual('http://www.xn--80aswg.xn--j1amh'); - expect(evaluate('={{ "Localhost URL: http://localhost:8080/test?x=1".extractUrl() }}')).toEqual('http://localhost:8080/test?x=1'); - expect(evaluate('={{ "IP URL: http://192.168.1.1:8000/path?q=value#frag".extractUrl() }}')).toEqual('http://192.168.1.1:8000/path?q=value#frag'); + expect( + evaluate( + '={{ "Mixed content: https://www.example.com and http://www.example.org".extractUrl() }}', + ), + ).toEqual('https://www.example.com'); + expect( + evaluate('={{ "Text without URL: This is just a simple text".extractUrl() }}'), + ).toEqual(undefined); + expect( + evaluate('={{ "URL with Unicode: http://www.xn--80aswg.xn--j1amh".extractUrl() }}'), + ).toEqual('http://www.xn--80aswg.xn--j1amh'); + expect( + evaluate('={{ "Localhost URL: http://localhost:8080/test?x=1".extractUrl() }}'), + ).toEqual('http://localhost:8080/test?x=1'); + expect( + evaluate('={{ "IP URL: http://192.168.1.1:8000/path?q=value#frag".extractUrl() }}'), + ).toEqual('http://192.168.1.1:8000/path?q=value#frag'); }); test('.extractDomain should work on a string', () => { @@ -175,24 +190,46 @@ describe('Data Transformation Functions', () => { expect(evaluate('={{ "google.com".extractDomain() }}')).toEqual('google.com'); expect(evaluate('={{ "www.example.net".extractDomain() }}')).toEqual('www.example.net'); expect(evaluate('={{ "//example.com".extractDomain() }}')).toEqual('example.com'); - expect(evaluate('={{ "mailto:john.doe@example.com".extractDomain() }}')).toEqual('example.com'); + expect(evaluate('={{ "mailto:john.doe@example.com".extractDomain() }}')).toEqual( + 'example.com', + ); expect(evaluate('={{ "tel:+1-555-123-4567".extractDomain() }}')).toEqual(undefined); expect(evaluate('={{ "jane.doe@example.org".extractDomain() }}')).toEqual('example.org'); expect(evaluate('={{ "name+tag@example.com".extractDomain() }}')).toEqual('example.com'); - expect(evaluate('={{ "first.last@example.co.uk".extractDomain() }}')).toEqual('example.co.uk'); - expect(evaluate('={{ "user@subdomain.example.com".extractDomain() }}')).toEqual('subdomain.example.com'); - expect(evaluate('={{ "www.example.net?test=1213".extractDomain() }}')).toEqual('www.example.net'); + expect(evaluate('={{ "first.last@example.co.uk".extractDomain() }}')).toEqual( + 'example.co.uk', + ); + expect(evaluate('={{ "user@subdomain.example.com".extractDomain() }}')).toEqual( + 'subdomain.example.com', + ); + expect(evaluate('={{ "www.example.net?test=1213".extractDomain() }}')).toEqual( + 'www.example.net', + ); expect(evaluate('={{ "www.example.net?test".extractDomain() }}')).toEqual('www.example.net'); - expect(evaluate('={{ "www.example.net#tesdt123".extractDomain() }}')).toEqual('www.example.net'); - expect(evaluate('={{ "https://www.example.net?test=1213".extractDomain() }}')).toEqual('www.example.net'); - expect(evaluate('={{ "https://www.example.net?test".extractDomain() }}')).toEqual('www.example.net'); - expect(evaluate('={{ "https://www.example.net#tesdt123".extractDomain() }}')).toEqual('www.example.net'); + expect(evaluate('={{ "www.example.net#tesdt123".extractDomain() }}')).toEqual( + 'www.example.net', + ); + expect(evaluate('={{ "https://www.example.net?test=1213".extractDomain() }}')).toEqual( + 'www.example.net', + ); + expect(evaluate('={{ "https://www.example.net?test".extractDomain() }}')).toEqual( + 'www.example.net', + ); + expect(evaluate('={{ "https://www.example.net#tesdt123".extractDomain() }}')).toEqual( + 'www.example.net', + ); expect(evaluate('={{ "https://192.168.1.1".extractDomain() }}')).toEqual('192.168.1.1'); - expect(evaluate('={{ "http://www.xn--80aswg.xn--j1amh".extractDomain() }}')).toEqual('www.xn--80aswg.xn--j1amh'); + expect(evaluate('={{ "http://www.xn--80aswg.xn--j1amh".extractDomain() }}')).toEqual( + 'www.xn--80aswg.xn--j1amh', + ); expect(evaluate('={{ "https://localhost".extractDomain() }}')).toEqual('localhost'); expect(evaluate('={{ "https://localhost?test=123".extractDomain() }}')).toEqual('localhost'); - expect(evaluate('={{ "https://www.example_with_underscore.com".extractDomain() }}')).toEqual('www.example_with_underscore.com'); - expect(evaluate('={{ "https://www.example.com:8080".extractDomain() }}')).toEqual('www.example.com'); + expect(evaluate('={{ "https://www.example_with_underscore.com".extractDomain() }}')).toEqual( + 'www.example_with_underscore.com', + ); + expect(evaluate('={{ "https://www.example.com:8080".extractDomain() }}')).toEqual( + 'www.example.com', + ); expect(evaluate('={{ "https://example.space".extractDomain() }}')).toEqual('example.space'); }); diff --git a/packages/workflow/test/Helpers.ts b/packages/workflow/test/Helpers.ts index d6a93d575d07c..b7b79c048903d 100644 --- a/packages/workflow/test/Helpers.ts +++ b/packages/workflow/test/Helpers.ts @@ -34,7 +34,7 @@ import type { WorkflowExecuteMode, } from '@/Interfaces'; import { ICredentials, ICredentialsHelper } from '@/Interfaces'; -import { Workflow } from '@/Workflow'; +import type { Workflow } from '@/Workflow'; import { WorkflowDataProxy } from '@/WorkflowDataProxy'; import { WorkflowHooks } from '@/WorkflowHooks'; import * as NodeHelpers from '@/NodeHelpers'; @@ -76,7 +76,7 @@ export class Credentials extends ICredentials { const fullData = this.getData(encryptionKey, nodeType); if (fullData === null) { - throw new Error(`No data was set.`); + throw new Error('No data was set.'); } // eslint-disable-next-line no-prototype-builtins @@ -89,7 +89,7 @@ export class Credentials extends ICredentials { getDataToSave(): ICredentialsEncrypted { if (this.data === undefined) { - throw new Error(`No credentials were set to save.`); + throw new Error('No credentials were set to save.'); } return { @@ -277,7 +277,7 @@ export function getExecuteFunctions( return mode; }, getNode: () => { - return JSON.parse(JSON.stringify(node)); + return deepCopy(node); }, getRestApiUrl: (): string => { return additionalData.restApiUrl; @@ -444,7 +444,7 @@ export function getExecuteSingleFunctions( return mode; }, getNode: () => { - return JSON.parse(JSON.stringify(node)); + return deepCopy(node); }, getRestApiUrl: (): string => { return additionalData.restApiUrl; diff --git a/packages/workflow/test/NodeErrors.test.ts b/packages/workflow/test/NodeErrors.test.ts new file mode 100644 index 0000000000000..9dd1024621d61 --- /dev/null +++ b/packages/workflow/test/NodeErrors.test.ts @@ -0,0 +1,79 @@ +import type { INode } from '../src/Interfaces'; +import { NodeApiError } from '../src/NodeErrors'; + +const node: INode = { + id: '1', + name: 'Postgres node', + typeVersion: 2, + type: 'n8n-nodes-base.postgres', + position: [60, 760], + parameters: { + operation: 'executeQuery', + }, +}; + +describe('NodeErrors tests', () => { + it('should return unknown error message', () => { + const nodeApiError = new NodeApiError(node, {}); + + expect(nodeApiError.message).toEqual( + 'UNKNOWN ERROR - check the detailed error for more information', + ); + }); + + it('should return the error message', () => { + const nodeApiError = new NodeApiError(node, { message: 'test error message' }); + + expect(nodeApiError.message).toEqual('test error message'); + }); + + it('should return the error message defined in reason', () => { + const nodeApiError = new NodeApiError(node, { reason: { message: 'test error message' } }); + + expect(nodeApiError.message).toEqual('test error message'); + }); + + it('should return the error message defined in options', () => { + const nodeApiError = new NodeApiError(node, {}, { message: 'test error message' }); + + expect(nodeApiError.message).toEqual('test error message'); + }); + + it('should return description error message', () => { + const nodeApiError = new NodeApiError(node, { description: 'test error description' }); + + expect(nodeApiError.message).toEqual('test error description'); + }); + + it('should return description as error message defined in reason', () => { + const nodeApiError = new NodeApiError(node, { + reason: { description: 'test error description' }, + }); + + expect(nodeApiError.message).toEqual('test error description'); + }); + + it('should return description as error message defined in options', () => { + const nodeApiError = new NodeApiError(node, {}, { description: 'test error description' }); + + expect(nodeApiError.message).toEqual('test error description'); + }); + + it('should return default message for ECONNREFUSED', () => { + const nodeApiError = new NodeApiError(node, { + message: 'ECONNREFUSED', + }); + + expect(nodeApiError.message).toEqual( + 'The service refused the connection - perhaps it is offline', + ); + }); + + it('should return default message for 502', () => { + const nodeApiError = new NodeApiError(node, { + message: '502 Bad Gateway', + }); + + expect(nodeApiError.message).toEqual('Bad gateway - the service failed to handle your request'); + }); +}); diff --git a/packages/workflow/test/RoutingNode.test.ts b/packages/workflow/test/RoutingNode.test.ts index ab9c55df10152..a2ca525ba71a8 100644 --- a/packages/workflow/test/RoutingNode.test.ts +++ b/packages/workflow/test/RoutingNode.test.ts @@ -5,7 +5,6 @@ import type { DeclarativeRestApiSettings, IRunExecutionData, INodeProperties, - IDataObject, IExecuteSingleFunctions, IHttpRequestOptions, IN8nHttpFullResponse, @@ -34,7 +33,7 @@ const preSendFunction1 = async function ( this: IExecuteSingleFunctions, requestOptions: IHttpRequestOptions, ): Promise<IHttpRequestOptions> { - requestOptions.headers = (requestOptions.headers || {}) as IDataObject; + requestOptions.headers = requestOptions.headers || {}; requestOptions.headers.addedIn = 'preSendFunction1'; return requestOptions; }; @@ -344,6 +343,7 @@ describe('RoutingNode', () => { type: 'string', routing: { send: { + // eslint-disable-next-line n8n-local-rules/no-interpolation-in-regular-string property: '={{ `value${5+1}A` }}', type: 'query', value: '={{$value.toUpperCase()}}', @@ -357,6 +357,7 @@ describe('RoutingNode', () => { type: 'string', routing: { send: { + // eslint-disable-next-line n8n-local-rules/no-interpolation-in-regular-string property: '={{ `value${6+1}B` }}', type: 'body', value: "={{$value.split(',')}}", diff --git a/packages/workflow/test/Workflow.test.ts b/packages/workflow/test/Workflow.test.ts index 98c151988be38..ad8ba4f29038a 100644 --- a/packages/workflow/test/Workflow.test.ts +++ b/packages/workflow/test/Workflow.test.ts @@ -20,7 +20,7 @@ interface StubNode { } describe('Workflow', () => { - describe('renameNodeInExpressions', () => { + describe('renameNodeInParameterValue for expressions', () => { const tests = [ { description: 'do nothing if there is no expression', @@ -257,7 +257,7 @@ describe('Workflow', () => { for (const testData of tests) { test(testData.description, () => { - const result = workflow.renameNodeInExpressions( + const result = workflow.renameNodeInParameterValue( testData.input.parameters, testData.input.currentName, testData.input.newName, @@ -267,6 +267,58 @@ describe('Workflow', () => { } }); + describe('renameNodeInParameterValue for node with renamable content', () => { + const tests = [ + { + description: "should work with $('name')", + input: { + currentName: 'Old', + newName: 'New', + parameters: { jsCode: "$('Old').first();" }, + }, + output: { jsCode: "$('New').first();" }, + }, + { + description: "should work with $node['name'] and $node.name", + input: { + currentName: 'Old', + newName: 'New', + parameters: { jsCode: "$node['Old'].first(); $node.Old.first();" }, + }, + output: { jsCode: "$node['New'].first(); $node.New.first();" }, + }, + { + description: 'should work with $items()', + input: { + currentName: 'Old', + newName: 'New', + parameters: { jsCode: "$items('Old').first();" }, + }, + output: { jsCode: "$items('New').first();" }, + }, + ]; + + const workflow = new Workflow({ + nodes: [], + connections: {}, + active: false, + nodeTypes: Helpers.NodeTypes(), + }); + + for (const t of tests) { + test(t.description, () => { + expect( + workflow.renameNodeInParameterValue( + t.input.parameters, + t.input.currentName, + t.input.newName, + { hasRenamableContent: true }, + ), + ).toEqual(t.output); + }); + } + }); + describe('renameNode', () => { const tests = [ { @@ -605,9 +657,9 @@ describe('Workflow', () => { }, }, }, - // This does just a basic test if "renameNodeInExpressions" gets used. More complex + // This does just a basic test if "renameNodeInParameterValue" gets used. More complex // tests with different formats and levels are in the separate tests for the function - // "renameNodeInExpressions" + // "renameNodeInParameterValue" { description: 'change name also in expressions which use node-name (dot notation)', input: { @@ -699,7 +751,7 @@ describe('Workflow', () => { }); describe('getParameterValue', () => { - const tests: { + const tests: Array<{ description: string; input: { [nodeName: string]: { @@ -709,7 +761,7 @@ describe('Workflow', () => { }; }; output: Record<string, unknown>; - }[] = [ + }> = [ { description: 'read simple not expression value', input: { @@ -1244,7 +1296,7 @@ describe('Workflow', () => { const itemIndex = 0; const runIndex = 0; const connectionInputData: INodeExecutionData[] = - runExecutionData.resultData.runData!['Node1']![0]!.data!.main[0]!; + runExecutionData.resultData.runData.Node1[0]!.data!.main[0]!; for (const parameterName of Object.keys(testData.output)) { const parameterValue = nodes.find((node) => node.name === activeNodeName)!.parameters[ @@ -1399,7 +1451,7 @@ describe('Workflow', () => { const itemIndex = 0; const runIndex = 0; const connectionInputData: INodeExecutionData[] = - runExecutionData.resultData.runData!['Node1']![0]!.data!.main[0]!; + runExecutionData.resultData.runData.Node1[0]!.data!.main[0]!; const parameterName = 'values'; const parameterValue = nodes.find((node) => node.name === activeNodeName)!.parameters[ diff --git a/packages/workflow/test/WorkflowDataProxy.test.ts b/packages/workflow/test/WorkflowDataProxy.test.ts index f3f360ac774cc..301d2bf0a425a 100644 --- a/packages/workflow/test/WorkflowDataProxy.test.ts +++ b/packages/workflow/test/WorkflowDataProxy.test.ts @@ -1,4 +1,4 @@ -import { IConnections, IExecuteData, INode, IRunExecutionData } from '@/Interfaces'; +import type { IConnections, IExecuteData, INode, IRunExecutionData } from '@/Interfaces'; import { Workflow } from '@/Workflow'; import { WorkflowDataProxy } from '@/WorkflowDataProxy'; import * as Helpers from './Helpers'; @@ -234,7 +234,7 @@ describe('WorkflowDataProxy', () => { data: runExecutionData.resultData.runData[nameLastNode][0].data!, node: nodes.find((node) => node.name === nameLastNode) as INode, source: { - main: runExecutionData.resultData.runData[nameLastNode][0].source!, + main: runExecutionData.resultData.runData[nameLastNode][0].source, }, }; diff --git a/packages/workflow/test/utils.test.ts b/packages/workflow/test/utils.test.ts index 268f270e02624..e26d964976620 100644 --- a/packages/workflow/test/utils.test.ts +++ b/packages/workflow/test/utils.test.ts @@ -1,4 +1,77 @@ -import { jsonParse, jsonStringify, deepCopy } from '@/utils'; +import { jsonParse, jsonStringify, deepCopy, isObjectEmpty } from '@/utils'; + +describe('isObjectEmpty', () => { + it('should handle null and undefined', () => { + expect(isObjectEmpty(null)).toEqual(true); + expect(isObjectEmpty(undefined)).toEqual(true); + }); + + it('should handle arrays', () => { + expect(isObjectEmpty([])).toEqual(true); + expect(isObjectEmpty([1, 2, 3])).toEqual(false); + }); + + it('should handle Set and Map', () => { + expect(isObjectEmpty(new Set())).toEqual(true); + expect(isObjectEmpty(new Set([1, 2, 3]))).toEqual(false); + + expect(isObjectEmpty(new Map())).toEqual(true); + expect( + isObjectEmpty( + new Map([ + ['a', 1], + ['b', 2], + ]), + ), + ).toEqual(false); + }); + + it('should handle Buffer, ArrayBuffer, and Uint8Array', () => { + expect(isObjectEmpty(Buffer.from(''))).toEqual(true); + expect(isObjectEmpty(Buffer.from('abcd'))).toEqual(false); + + expect(isObjectEmpty(Uint8Array.from([]))).toEqual(true); + expect(isObjectEmpty(Uint8Array.from([1, 2, 3]))).toEqual(false); + + expect(isObjectEmpty(new ArrayBuffer(0))).toEqual(true); + expect(isObjectEmpty(new ArrayBuffer(1))).toEqual(false); + }); + + it('should handle plain objects', () => { + expect(isObjectEmpty({})).toEqual(true); + expect(isObjectEmpty({ a: 1, b: 2 })).toEqual(false); + }); + + it('should handle instantiated classes', () => { + expect(isObjectEmpty(new (class Test {})())).toEqual(true); + expect( + isObjectEmpty( + new (class Test { + prop = 123; + })(), + ), + ).toEqual(false); + }); + + it('should not call Object.keys unless a plain object', () => { + const keySpy = jest.spyOn(Object, 'keys'); + const { calls } = keySpy.mock; + + const assertCalls = (count: number) => { + if (calls.length !== count) throw new Error(`Object.keys was called ${calls.length} times`); + }; + + assertCalls(0); + isObjectEmpty(null); + assertCalls(0); + isObjectEmpty([1, 2, 3]); + assertCalls(0); + isObjectEmpty(Buffer.from('123')); + assertCalls(0); + isObjectEmpty({}); + assertCalls(1); + }); +}); describe('jsonParse', () => { it('parses JSON', () => { diff --git a/patches/@typescript-eslint__eslint-plugin@5.59.0.patch b/patches/@typescript-eslint__eslint-plugin@5.59.0.patch new file mode 100644 index 0000000000000..e07dd173c4951 --- /dev/null +++ b/patches/@typescript-eslint__eslint-plugin@5.59.0.patch @@ -0,0 +1,13 @@ +diff --git a/dist/rules/consistent-type-imports.js b/dist/rules/consistent-type-imports.js +index 1844dc32b19d10abbe13556b9ee2f69c0aabac05..01320c5212fd61e08ca8a438db3ccd59949f9421 100644 +--- a/dist/rules/consistent-type-imports.js ++++ b/dist/rules/consistent-type-imports.js +@@ -81,6 +81,8 @@ exports.default = util.createRule({ + ImportDeclaration(node) { + var _a; + const source = node.source.value; ++ if (source.endsWith('.vue')) return; ++ + // sourceImports is the object containing all the specifics for a particular import source, type or value + const sourceImports = (_a = sourceImportsMap[source]) !== null && _a !== void 0 ? _a : (sourceImportsMap[source] = { + source, \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ab2f019ed9351..8e8ba5e779f12 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,7 @@ overrides: tslib: ^2.5.0 ts-node: ^10.9.1 typescript: ^5.0.3 + xlsx: https://cdn.sheetjs.com/xlsx-0.19.3/xlsx-0.19.3.tgz xml2js: ^0.5.0 cpy@8>globby: ^11.1.0 qqjs>globby: ^11.1.0 @@ -25,6 +26,9 @@ patchedDependencies: '@sentry/cli@2.17.0': hash: nchnoezkq6p37qaiku3vrpwraq path: patches/@sentry__cli@2.17.0.patch + '@typescript-eslint/eslint-plugin@5.59.0': + hash: tk3n6hvmqwfzrfqe3awfxnqtuy + path: patches/@typescript-eslint__eslint-plugin@5.59.0.patch element-ui@2.15.12: hash: prckukfdop5sl2her6de25cod4 path: patches/element-ui@2.15.12.patch @@ -99,7 +103,7 @@ importers: version: 6.3.3 ts-jest: specifier: ^29.1.0 - version: 29.1.0(@babel/core@7.20.12)(jest@29.5.0)(typescript@5.0.3) + version: 29.1.0(@babel/core@7.21.8)(jest@29.5.0)(typescript@5.0.3) tsc-watch: specifier: ^6.0.0 version: 6.0.0(typescript@5.0.3) @@ -113,50 +117,56 @@ importers: packages/@n8n_io/eslint-config: devDependencies: '@types/eslint': - specifier: ~8.4 - version: 8.4.6 + specifier: ~8.37 + version: 8.37.0 '@typescript-eslint/eslint-plugin': - specifier: ~5.45 - version: 5.45.0(@typescript-eslint/parser@5.45.0)(eslint@8.28.0)(typescript@5.0.3) + specifier: ~5.59 + version: 5.59.0(patch_hash=tk3n6hvmqwfzrfqe3awfxnqtuy)(@typescript-eslint/parser@5.59.0)(eslint@8.39.0)(typescript@5.0.3) '@typescript-eslint/parser': - specifier: ~5.45 - version: 5.45.0(eslint@8.28.0)(typescript@5.0.3) + specifier: ~5.59 + version: 5.59.0(eslint@8.39.0)(typescript@5.0.3) '@vue/eslint-config-typescript': specifier: ~8.0 - version: 8.0.0(@typescript-eslint/eslint-plugin@5.45.0)(@typescript-eslint/parser@5.45.0)(eslint-plugin-vue@7.17.0)(eslint@8.28.0)(typescript@5.0.3) + version: 8.0.0(@typescript-eslint/eslint-plugin@5.59.0)(@typescript-eslint/parser@5.59.0)(eslint-plugin-vue@7.17.0)(eslint@8.39.0)(typescript@5.0.3) eslint: - specifier: ~8.28 - version: 8.28.0 + specifier: ~8.39 + version: 8.39.0 eslint-config-airbnb-typescript: specifier: ~17.0 - version: 17.0.0(@typescript-eslint/eslint-plugin@5.45.0)(@typescript-eslint/parser@5.45.0)(eslint-plugin-import@2.26.0)(eslint@8.28.0) + version: 17.0.0(@typescript-eslint/eslint-plugin@5.59.0)(@typescript-eslint/parser@5.59.0)(eslint-plugin-import@2.27.5)(eslint@8.39.0) eslint-config-prettier: - specifier: ~8.5 - version: 8.5.0(eslint@8.28.0) + specifier: ~8.8 + version: 8.8.0(eslint@8.39.0) eslint-import-resolver-typescript: specifier: ~3.5 - version: 3.5.2(eslint-plugin-import@2.26.0)(eslint@8.28.0) + version: 3.5.5(@typescript-eslint/parser@5.59.0)(eslint-plugin-import@2.27.5)(eslint@8.39.0) eslint-plugin-diff: specifier: ~2.0 - version: 2.0.1(eslint@8.28.0) + version: 2.0.1(eslint@8.39.0) eslint-plugin-import: - specifier: ~2.26 - version: 2.26.0(@typescript-eslint/parser@5.45.0)(eslint-import-resolver-typescript@3.5.2)(eslint@8.28.0) + specifier: ~2.27 + version: 2.27.5(@typescript-eslint/parser@5.59.0)(eslint-import-resolver-typescript@3.5.5)(eslint@8.39.0) eslint-plugin-n8n-local-rules: specifier: ~1.0 version: 1.0.0 eslint-plugin-prettier: specifier: ~4.2 - version: 4.2.1(eslint-config-prettier@8.5.0)(eslint@8.28.0)(prettier@2.8.3) + version: 4.2.1(eslint-config-prettier@8.8.0)(eslint@8.39.0)(prettier@2.8.3) + eslint-plugin-unicorn: + specifier: ~46.0 + version: 46.0.0(eslint@8.39.0) + eslint-plugin-unused-imports: + specifier: ~2.0 + version: 2.0.0(@typescript-eslint/eslint-plugin@5.59.0)(eslint@8.39.0) eslint-plugin-vue: specifier: ~7.17 - version: 7.17.0(eslint@8.28.0) + version: 7.17.0(eslint@8.39.0) packages/cli: dependencies: '@n8n_io/license-sdk': - specifier: ~2.1.0 - version: 2.1.0 + specifier: ~2.3.0 + version: 2.3.0 '@oclif/command': specifier: ^1.8.16 version: 1.8.18(@oclif/config@1.18.5)(supports-color@8.1.1) @@ -725,23 +735,26 @@ importers: specifier: ^2.0.9 version: 2.0.9(@fortawesome/fontawesome-svg-core@1.2.36)(vue@2.7.14) '@storybook/addon-actions': - specifier: ^7.0.0-beta.46 - version: 7.0.0-beta.46(react-dom@18.2.0)(react@17.0.2) + specifier: ^7.0.7 + version: 7.0.7(react-dom@18.2.0)(react@17.0.2) + '@storybook/addon-docs': + specifier: ^7.0.7 + version: 7.0.7(react-dom@18.2.0)(react@17.0.2) '@storybook/addon-essentials': - specifier: ^7.0.0-beta.46 - version: 7.0.0-beta.46(react-dom@18.2.0)(react@17.0.2) + specifier: ^7.0.7 + version: 7.0.7(react-dom@18.2.0)(react@17.0.2) '@storybook/addon-links': - specifier: ^7.0.0-beta.46 - version: 7.0.0-beta.46(react-dom@18.2.0)(react@17.0.2) + specifier: ^7.0.7 + version: 7.0.7(react-dom@18.2.0)(react@17.0.2) '@storybook/addon-postcss': specifier: ^3.0.0-alpha.1 version: 3.0.0-alpha.1(webpack@5.75.0) '@storybook/vue': - specifier: ^7.0.0-beta.46 - version: 7.0.0-beta.46(@babel/core@7.20.12)(babel-loader@9.1.2)(css-loader@6.7.3)(vue@2.7.14) + specifier: ^7.0.7 + version: 7.0.7(@babel/core@7.21.8)(babel-loader@9.1.2)(css-loader@6.7.3)(vue@2.7.14) '@storybook/vue-webpack5': - specifier: ^7.0.0-beta.46 - version: 7.0.0-beta.46(@babel/core@7.20.12)(@babel/preset-env@7.20.2)(babel-loader@9.1.2)(css-loader@6.7.3)(esbuild@0.16.17)(react-dom@18.2.0)(react@17.0.2)(typescript@5.0.3)(vue-loader@15.10.1)(vue-template-compiler@2.7.14)(vue@2.7.14) + specifier: ^7.0.7 + version: 7.0.7(@babel/core@7.21.8)(@babel/preset-env@7.21.5)(babel-loader@9.1.2)(css-loader@6.7.3)(esbuild@0.17.18)(react-dom@18.2.0)(react@17.0.2)(typescript@5.0.3)(vue-loader@15.10.1)(vue-template-compiler@2.7.14)(vue@2.7.14) '@testing-library/jest-dom': specifier: ^5.16.5 version: 5.16.5 @@ -788,11 +801,8 @@ importers: specifier: ^13.2.0 version: 13.2.0(sass@1.58.0)(webpack@5.75.0) storybook: - specifier: ^7.0.0-beta.46 - version: 7.0.0-beta.46 - storybook-addon-designs: - specifier: ^6.3.1 - version: 6.3.1(react@17.0.2) + specifier: ^7.0.7 + version: 7.0.7 storybook-addon-themes: specifier: ^6.1.0 version: 6.1.0(react-dom@18.2.0)(react@17.0.2)(vue@2.7.14) @@ -832,6 +842,15 @@ importers: '@codemirror/lang-javascript': specifier: ^6.1.2 version: 6.1.2 + '@codemirror/lang-json': + specifier: ^6.0.1 + version: 6.0.1 + '@codemirror/lang-python': + specifier: ^6.1.2 + version: 6.1.2(@codemirror/state@6.1.4)(@codemirror/view@6.5.1)(@lezer/common@1.0.1) + '@codemirror/lang-sql': + specifier: ^6.4.1 + version: 6.4.1(@codemirror/view@6.5.1)(@lezer/common@1.0.1) '@codemirror/language': specifier: ^6.2.1 version: 6.2.1 @@ -857,8 +876,8 @@ importers: specifier: ^5.15.3 version: 5.15.4 '@fortawesome/vue-fontawesome': - specifier: ^2.0.2 - version: 2.0.8(@fortawesome/fontawesome-svg-core@1.2.36)(vue@2.7.14) + specifier: ^2.0.10 + version: 2.0.10(@fortawesome/fontawesome-svg-core@1.2.36)(vue@2.7.14) '@jsplumb/browser-ui': specifier: ^5.13.2 version: 5.13.2 @@ -919,9 +938,6 @@ importers: luxon: specifier: ^3.3.0 version: 3.3.0 - monaco-editor: - specifier: ^0.33.0 - version: 0.33.0 n8n-design-system: specifier: workspace:* version: link:../design-system @@ -937,9 +953,6 @@ importers: prettier: specifier: ^2.8.3 version: 2.8.3 - prismjs: - specifier: ^1.17.1 - version: 1.29.0 stream-browserify: specifier: ^3.0.0 version: 3.0.0 @@ -970,9 +983,6 @@ importers: vue-json-pretty: specifier: 1.9.3 version: 1.9.3 - vue-prism-editor: - specifier: ^0.3.0 - version: 0.3.0 vue-router: specifier: ^3.6.5 version: 3.6.5(vue@2.7.14) @@ -1058,6 +1068,9 @@ importers: '@vitest/coverage-c8': specifier: ^0.28.5 version: 0.28.5(sass@1.55.0)(terser@5.16.1) + '@volar-plugins/eslint': + specifier: ^0.0.4 + version: 0.0.4(eslint@8.39.0) c8: specifier: ^7.12.0 version: 7.12.0 @@ -1079,9 +1092,6 @@ importers: vite: specifier: 4.0.4 version: 4.0.4(@types/node@16.18.12)(sass@1.55.0)(terser@5.16.1) - vite-plugin-monaco-editor: - specifier: ^1.0.10 - version: 1.1.0(monaco-editor@0.33.0) vitest: specifier: ^0.28.5 version: 0.28.5(sass@1.55.0)(terser@5.16.1) @@ -1389,6 +1399,9 @@ importers: promise-ftp: specifier: ^1.3.5 version: 1.3.5(promise-ftp-common@1.1.5) + pyodide: + specifier: ^0.22.1 + version: 0.22.1 redis: specifier: ^3.1.1 version: 3.1.2 @@ -1423,8 +1436,8 @@ importers: specifier: ~3.9.17 version: 3.9.17 xlsx: - specifier: ^0.17.0 - version: 0.17.5 + specifier: https://cdn.sheetjs.com/xlsx-0.19.3/xlsx-0.19.3.tgz + version: '@cdn.sheetjs.com/xlsx-0.19.3/xlsx-0.19.3.tgz' xml2js: specifier: ^0.5.0 version: 0.5.0 @@ -1638,7 +1651,7 @@ importers: version: 0.4.11 eslint-plugin-n8n-nodes-base: specifier: ^1.12.0 - version: 1.12.0(eslint@8.28.0)(typescript@5.0.3) + version: 1.12.0(eslint@8.39.0)(typescript@5.0.3) gulp: specifier: ^4.0.0 version: 4.0.2 @@ -2033,11 +2046,23 @@ packages: '@babel/highlight': 7.18.6 dev: true + /@babel/code-frame@7.21.4: + resolution: {integrity: sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/highlight': 7.18.6 + dev: true + /@babel/compat-data@7.20.10: resolution: {integrity: sha512-sEnuDPpOJR/fcafHMjpcpGN5M2jbUGUHwmuWKM/YdPzeEDJg8bgmbcWQFUfE32MQjti1koACvoPVsDe8Uq+idg==} engines: {node: '>=6.9.0'} dev: true + /@babel/compat-data@7.21.7: + resolution: {integrity: sha512-KYMqFYTaenzMK4yUtf4EW9wc4N9ef80FsbMtkwool5zpwl4YrT1SdWYSTRcT94KO4hannogdS+LxY7L+arP3gA==} + engines: {node: '>=6.9.0'} + dev: true + /@babel/core@7.20.12: resolution: {integrity: sha512-XsMfHovsUYHFMdrIHkZphTN/2Hzzi78R08NuHfDBehym2VsPDL6Zn/JAD/JQdnRvbSsbQc4mVaU1m6JgtTEElg==} engines: {node: '>=6.9.0'} @@ -2061,12 +2086,45 @@ packages: - supports-color dev: true + /@babel/core@7.21.8: + resolution: {integrity: sha512-YeM22Sondbo523Sz0+CirSPnbj9bG3P0CdHcBZdqUuaeOaYEFbOLoGU7lebvGP6P5J/WE9wOn7u7C4J9HvS1xQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@ampproject/remapping': 2.2.0 + '@babel/code-frame': 7.21.4 + '@babel/generator': 7.21.5 + '@babel/helper-compilation-targets': 7.21.5(@babel/core@7.21.8) + '@babel/helper-module-transforms': 7.21.5 + '@babel/helpers': 7.21.5 + '@babel/parser': 7.21.8 + '@babel/template': 7.20.7 + '@babel/traverse': 7.21.5 + '@babel/types': 7.21.5 + convert-source-map: 1.9.0 + debug: 4.3.4(supports-color@8.1.1) + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.0 + transitivePeerDependencies: + - supports-color + dev: true + /@babel/generator@7.20.7: resolution: {integrity: sha512-7wqMOJq8doJMZmP4ApXTzLxSr7+oO2jroJURrVEp6XShrQUObV8Tq/D0NCcoYg2uHqUrjzO0zwBjoYzelxK+sw==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.20.7 + '@babel/types': 7.21.5 + '@jridgewell/gen-mapping': 0.3.2 + jsesc: 2.5.2 + dev: true + + /@babel/generator@7.21.5: + resolution: {integrity: sha512-SrKK/sRv8GesIW1bDagf9cCG38IOMYZusoe1dfg0D8aiUe3Amvoj1QtjTPAWcfrZFvIwlleLb0gxzQidL9w14w==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.21.5 '@jridgewell/gen-mapping': 0.3.2 + '@jridgewell/trace-mapping': 0.3.17 jsesc: 2.5.2 dev: true @@ -2074,7 +2132,7 @@ packages: resolution: {integrity: sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.20.7 + '@babel/types': 7.21.5 dev: true /@babel/helper-builder-binary-assignment-operator-visitor@7.18.9: @@ -2082,7 +2140,7 @@ packages: engines: {node: '>=6.9.0'} dependencies: '@babel/helper-explode-assignable-expression': 7.18.6 - '@babel/types': 7.20.7 + '@babel/types': 7.21.5 dev: true /@babel/helper-compilation-targets@7.20.7(@babel/core@7.20.12): @@ -2099,6 +2157,34 @@ packages: semver: 6.3.0 dev: true + /@babel/helper-compilation-targets@7.21.5(@babel/core@7.20.12): + resolution: {integrity: sha512-1RkbFGUKex4lvsB9yhIfWltJM5cZKUftB2eNajaDv3dCMEp49iBG0K14uH8NnX9IPux2+mK7JGEOB0jn48/J6w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/compat-data': 7.21.7 + '@babel/core': 7.20.12 + '@babel/helper-validator-option': 7.21.0 + browserslist: 4.21.4 + lru-cache: 5.1.1 + semver: 6.3.0 + dev: true + + /@babel/helper-compilation-targets@7.21.5(@babel/core@7.21.8): + resolution: {integrity: sha512-1RkbFGUKex4lvsB9yhIfWltJM5cZKUftB2eNajaDv3dCMEp49iBG0K14uH8NnX9IPux2+mK7JGEOB0jn48/J6w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/compat-data': 7.21.7 + '@babel/core': 7.21.8 + '@babel/helper-validator-option': 7.21.0 + browserslist: 4.21.4 + lru-cache: 5.1.1 + semver: 6.3.0 + dev: true + /@babel/helper-create-class-features-plugin@7.20.12(@babel/core@7.20.12): resolution: {integrity: sha512-9OunRkbT0JQcednL0UFvbfXpAsUXiGjUk0a7sN8fUXX7Mue79cUSMjHGDRRi/Vz9vYlpIhLV5fMD5dKoMhhsNQ==} engines: {node: '>=6.9.0'} @@ -2107,8 +2193,27 @@ packages: dependencies: '@babel/core': 7.20.12 '@babel/helper-annotate-as-pure': 7.18.6 - '@babel/helper-environment-visitor': 7.18.9 - '@babel/helper-function-name': 7.19.0 + '@babel/helper-environment-visitor': 7.21.5 + '@babel/helper-function-name': 7.21.0 + '@babel/helper-member-expression-to-functions': 7.20.7 + '@babel/helper-optimise-call-expression': 7.18.6 + '@babel/helper-replace-supers': 7.20.7 + '@babel/helper-skip-transparent-expression-wrappers': 7.20.0 + '@babel/helper-split-export-declaration': 7.18.6 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helper-create-class-features-plugin@7.20.12(@babel/core@7.21.8): + resolution: {integrity: sha512-9OunRkbT0JQcednL0UFvbfXpAsUXiGjUk0a7sN8fUXX7Mue79cUSMjHGDRRi/Vz9vYlpIhLV5fMD5dKoMhhsNQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-annotate-as-pure': 7.18.6 + '@babel/helper-environment-visitor': 7.21.5 + '@babel/helper-function-name': 7.21.0 '@babel/helper-member-expression-to-functions': 7.20.7 '@babel/helper-optimise-call-expression': 7.18.6 '@babel/helper-replace-supers': 7.20.7 @@ -2118,6 +2223,26 @@ packages: - supports-color dev: true + /@babel/helper-create-class-features-plugin@7.21.8(@babel/core@7.21.8): + resolution: {integrity: sha512-+THiN8MqiH2AczyuZrnrKL6cAxFRRQDKW9h1YkBvbgKmAm6mwiacig1qT73DHIWMGo40GRnsEfN3LA+E6NtmSw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-annotate-as-pure': 7.18.6 + '@babel/helper-environment-visitor': 7.21.5 + '@babel/helper-function-name': 7.21.0 + '@babel/helper-member-expression-to-functions': 7.21.5 + '@babel/helper-optimise-call-expression': 7.18.6 + '@babel/helper-replace-supers': 7.21.5 + '@babel/helper-skip-transparent-expression-wrappers': 7.20.0 + '@babel/helper-split-export-declaration': 7.18.6 + semver: 6.3.0 + transitivePeerDependencies: + - supports-color + dev: true + /@babel/helper-create-regexp-features-plugin@7.20.5(@babel/core@7.20.12): resolution: {integrity: sha512-m68B1lkg3XDGX5yCvGO0kPx3v9WIYLnzjKfPcQiwntEQa5ZeRkPmo2X/ISJc8qxWGfwUr+kvZAeEzAwLec2r2w==} engines: {node: '>=6.9.0'} @@ -2129,14 +2254,41 @@ packages: regexpu-core: 5.2.2 dev: true + /@babel/helper-create-regexp-features-plugin@7.20.5(@babel/core@7.21.8): + resolution: {integrity: sha512-m68B1lkg3XDGX5yCvGO0kPx3v9WIYLnzjKfPcQiwntEQa5ZeRkPmo2X/ISJc8qxWGfwUr+kvZAeEzAwLec2r2w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-annotate-as-pure': 7.18.6 + regexpu-core: 5.2.2 + dev: true + /@babel/helper-define-polyfill-provider@0.3.3(@babel/core@7.20.12): resolution: {integrity: sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww==} peerDependencies: '@babel/core': ^7.4.0-0 dependencies: '@babel/core': 7.20.12 - '@babel/helper-compilation-targets': 7.20.7(@babel/core@7.20.12) - '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-compilation-targets': 7.21.5(@babel/core@7.20.12) + '@babel/helper-plugin-utils': 7.21.5 + debug: 4.3.4(supports-color@8.1.1) + lodash.debounce: 4.0.8 + resolve: 1.22.1 + semver: 6.3.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helper-define-polyfill-provider@0.3.3(@babel/core@7.21.8): + resolution: {integrity: sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww==} + peerDependencies: + '@babel/core': ^7.4.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-compilation-targets': 7.21.5(@babel/core@7.21.8) + '@babel/helper-plugin-utils': 7.21.5 debug: 4.3.4(supports-color@8.1.1) lodash.debounce: 4.0.8 resolve: 1.22.1 @@ -2145,8 +2297,8 @@ packages: - supports-color dev: true - /@babel/helper-environment-visitor@7.18.9: - resolution: {integrity: sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==} + /@babel/helper-environment-visitor@7.21.5: + resolution: {integrity: sha512-IYl4gZ3ETsWocUWgsFZLM5i1BYx9SoemminVEXadgLBa9TdeorzgLKm8wWLA6J1N/kT3Kch8XIk1laNzYoHKvQ==} engines: {node: '>=6.9.0'} dev: true @@ -2154,50 +2306,80 @@ packages: resolution: {integrity: sha512-eyAYAsQmB80jNfg4baAtLeWAQHfHFiR483rzFK+BhETlGZaQC9bsfrugfXDCbRHLQbIA7U5NxhhOxN7p/dWIcg==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.20.7 + '@babel/types': 7.21.5 dev: true - /@babel/helper-function-name@7.19.0: - resolution: {integrity: sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==} + /@babel/helper-function-name@7.21.0: + resolution: {integrity: sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg==} engines: {node: '>=6.9.0'} dependencies: '@babel/template': 7.20.7 - '@babel/types': 7.20.7 + '@babel/types': 7.21.5 dev: true /@babel/helper-hoist-variables@7.18.6: resolution: {integrity: sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.20.7 + '@babel/types': 7.21.5 dev: true /@babel/helper-member-expression-to-functions@7.20.7: resolution: {integrity: sha512-9J0CxJLq315fEdi4s7xK5TQaNYjZw+nDVpVqr1axNGKzdrdwYBD5b4uKv3n75aABG0rCCTK8Im8Ww7eYfMrZgw==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.20.7 + '@babel/types': 7.21.5 + dev: true + + /@babel/helper-member-expression-to-functions@7.21.5: + resolution: {integrity: sha512-nIcGfgwpH2u4n9GG1HpStW5Ogx7x7ekiFHbjjFRKXbn5zUvqO9ZgotCO4x1aNbKn/x/xOUaXEhyNHCwtFCpxWg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.21.5 dev: true /@babel/helper-module-imports@7.18.6: resolution: {integrity: sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.20.7 + '@babel/types': 7.21.5 + dev: true + + /@babel/helper-module-imports@7.21.4: + resolution: {integrity: sha512-orajc5T2PsRYUN3ZryCEFeMDYwyw09c/pZeaQEZPH0MpKzSvn3e0uXsDBu3k03VI+9DBiRo+l22BfKTpKwa/Wg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.21.5 dev: true /@babel/helper-module-transforms@7.20.11: resolution: {integrity: sha512-uRy78kN4psmji1s2QtbtcCSaj/LILFDp0f/ymhpQH5QY3nljUZCaNWz9X1dEj/8MBdBEFECs7yRhKn8i7NjZgg==} engines: {node: '>=6.9.0'} dependencies: - '@babel/helper-environment-visitor': 7.18.9 + '@babel/helper-environment-visitor': 7.21.5 '@babel/helper-module-imports': 7.18.6 '@babel/helper-simple-access': 7.20.2 '@babel/helper-split-export-declaration': 7.18.6 '@babel/helper-validator-identifier': 7.19.1 '@babel/template': 7.20.7 - '@babel/traverse': 7.20.12 - '@babel/types': 7.20.7 + '@babel/traverse': 7.21.5 + '@babel/types': 7.21.5 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helper-module-transforms@7.21.5: + resolution: {integrity: sha512-bI2Z9zBGY2q5yMHoBvJ2a9iX3ZOAzJPm7Q8Yz6YeoUjU/Cvhmi2G4QyTNyPBqqXSgTjUxRg3L0xV45HvkNWWBw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-environment-visitor': 7.21.5 + '@babel/helper-module-imports': 7.21.4 + '@babel/helper-simple-access': 7.21.5 + '@babel/helper-split-export-declaration': 7.18.6 + '@babel/helper-validator-identifier': 7.19.1 + '@babel/template': 7.20.7 + '@babel/traverse': 7.21.5 + '@babel/types': 7.21.5 transitivePeerDependencies: - supports-color dev: true @@ -2206,7 +2388,7 @@ packages: resolution: {integrity: sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.20.7 + '@babel/types': 7.21.5 dev: true /@babel/helper-plugin-utils@7.20.2: @@ -2214,6 +2396,11 @@ packages: engines: {node: '>=6.9.0'} dev: true + /@babel/helper-plugin-utils@7.21.5: + resolution: {integrity: sha512-0WDaIlXKOX/3KfBK/dwP1oQGiPh6rjMkT7HIRv7i5RR2VUMwrx5ZL0dwBkKx7+SW1zwNdgjHd34IMk5ZjTeHVg==} + engines: {node: '>=6.9.0'} + dev: true + /@babel/helper-remap-async-to-generator@7.18.9(@babel/core@7.20.12): resolution: {integrity: sha512-dI7q50YKd8BAv3VEfgg7PS7yD3Rtbi2J1XMXaalXO0W0164hYLnh8zpjRS0mte9MfVp/tltvr/cfdXPvJr1opA==} engines: {node: '>=6.9.0'} @@ -2222,9 +2409,24 @@ packages: dependencies: '@babel/core': 7.20.12 '@babel/helper-annotate-as-pure': 7.18.6 - '@babel/helper-environment-visitor': 7.18.9 + '@babel/helper-environment-visitor': 7.21.5 '@babel/helper-wrap-function': 7.20.5 - '@babel/types': 7.20.7 + '@babel/types': 7.21.5 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helper-remap-async-to-generator@7.18.9(@babel/core@7.21.8): + resolution: {integrity: sha512-dI7q50YKd8BAv3VEfgg7PS7yD3Rtbi2J1XMXaalXO0W0164hYLnh8zpjRS0mte9MfVp/tltvr/cfdXPvJr1opA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-annotate-as-pure': 7.18.6 + '@babel/helper-environment-visitor': 7.21.5 + '@babel/helper-wrap-function': 7.20.5 + '@babel/types': 7.21.5 transitivePeerDependencies: - supports-color dev: true @@ -2233,12 +2435,26 @@ packages: resolution: {integrity: sha512-vujDMtB6LVfNW13jhlCrp48QNslK6JXi7lQG736HVbHz/mbf4Dc7tIRh1Xf5C0rF7BP8iiSxGMCmY6Ci1ven3A==} engines: {node: '>=6.9.0'} dependencies: - '@babel/helper-environment-visitor': 7.18.9 + '@babel/helper-environment-visitor': 7.21.5 '@babel/helper-member-expression-to-functions': 7.20.7 '@babel/helper-optimise-call-expression': 7.18.6 '@babel/template': 7.20.7 - '@babel/traverse': 7.20.12 - '@babel/types': 7.20.7 + '@babel/traverse': 7.21.5 + '@babel/types': 7.21.5 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helper-replace-supers@7.21.5: + resolution: {integrity: sha512-/y7vBgsr9Idu4M6MprbOVUfH3vs7tsIfnVWv/Ml2xgwvyH6LTngdfbf5AdsKwkJy4zgy1X/kuNrEKvhhK28Yrg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-environment-visitor': 7.21.5 + '@babel/helper-member-expression-to-functions': 7.21.5 + '@babel/helper-optimise-call-expression': 7.18.6 + '@babel/template': 7.20.7 + '@babel/traverse': 7.21.5 + '@babel/types': 7.21.5 transitivePeerDependencies: - supports-color dev: true @@ -2247,25 +2463,32 @@ packages: resolution: {integrity: sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.20.7 + '@babel/types': 7.21.5 + dev: true + + /@babel/helper-simple-access@7.21.5: + resolution: {integrity: sha512-ENPDAMC1wAjR0uaCUwliBdiSl1KBJAVnMTzXqi64c2MG8MPR6ii4qf7bSXDqSFbr4W6W028/rf5ivoHop5/mkg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.21.5 dev: true /@babel/helper-skip-transparent-expression-wrappers@7.20.0: resolution: {integrity: sha512-5y1JYeNKfvnT8sZcK9DVRtpTbGiomYIHviSP3OQWmDPU3DeH4a1ZlT/N2lyQ5P8egjcRaT/Y9aNqUxK0WsnIIg==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.20.7 + '@babel/types': 7.21.5 dev: true /@babel/helper-split-export-declaration@7.18.6: resolution: {integrity: sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.20.7 + '@babel/types': 7.21.5 dev: true - /@babel/helper-string-parser@7.19.4: - resolution: {integrity: sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==} + /@babel/helper-string-parser@7.21.5: + resolution: {integrity: sha512-5pTUx3hAJaZIdW99sJ6ZUUgWq/Y+Hja7TowEnLNMm1VivRgZQL3vpBY3qUACVsvw+yQU6+YgfBVmcbLaZtrA1w==} engines: {node: '>=6.9.0'} /@babel/helper-validator-identifier@7.19.1: @@ -2277,14 +2500,19 @@ packages: engines: {node: '>=6.9.0'} dev: true + /@babel/helper-validator-option@7.21.0: + resolution: {integrity: sha512-rmL/B8/f0mKS2baE9ZpyTcTavvEuWhTTW8amjzXNvYG4AwBsqTLikfXsEofsJEfKHf+HQVQbFOHy6o+4cnC/fQ==} + engines: {node: '>=6.9.0'} + dev: true + /@babel/helper-wrap-function@7.20.5: resolution: {integrity: sha512-bYMxIWK5mh+TgXGVqAtnu5Yn1un+v8DDZtqyzKRLUzrh70Eal2O3aZ7aPYiMADO4uKlkzOiRiZ6GX5q3qxvW9Q==} engines: {node: '>=6.9.0'} dependencies: - '@babel/helper-function-name': 7.19.0 + '@babel/helper-function-name': 7.21.0 '@babel/template': 7.20.7 - '@babel/traverse': 7.20.12 - '@babel/types': 7.20.7 + '@babel/traverse': 7.21.5 + '@babel/types': 7.21.5 transitivePeerDependencies: - supports-color dev: true @@ -2294,8 +2522,19 @@ packages: engines: {node: '>=6.9.0'} dependencies: '@babel/template': 7.20.7 - '@babel/traverse': 7.20.12 - '@babel/types': 7.20.7 + '@babel/traverse': 7.21.5 + '@babel/types': 7.21.5 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helpers@7.21.5: + resolution: {integrity: sha512-BSY+JSlHxOmGsPTydUkPf1MdMQ3M81x5xGCOVgWM3G8XH77sJ292Y2oqcp0CbbgxhqBuI46iUz1tT7hqP7EfgA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/template': 7.20.7 + '@babel/traverse': 7.21.5 + '@babel/types': 7.21.5 transitivePeerDependencies: - supports-color dev: true @@ -2316,6 +2555,14 @@ packages: dependencies: '@babel/types': 7.20.7 + /@babel/parser@7.21.8: + resolution: {integrity: sha512-6zavDGdzG3gUqAdWvlLFfk+36RilI+Pwyuuh7HItyeScCWP3k6i8vKclAQ0bM/0y/Kz/xiwvxhMv9MgTJP5gmA==} + engines: {node: '>=6.0.0'} + hasBin: true + dependencies: + '@babel/types': 7.21.5 + dev: true + /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.18.6(@babel/core@7.20.12): resolution: {integrity: sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ==} engines: {node: '>=6.9.0'} @@ -2323,7 +2570,17 @@ packages: '@babel/core': ^7.0.0 dependencies: '@babel/core': 7.20.12 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-plugin-utils': 7.21.5 + dev: true + + /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.18.6(@babel/core@7.21.8): + resolution: {integrity: sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-plugin-utils': 7.21.5 dev: true /@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.20.7(@babel/core@7.20.12): @@ -2333,9 +2590,21 @@ packages: '@babel/core': ^7.13.0 dependencies: '@babel/core': 7.20.12 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-plugin-utils': 7.21.5 '@babel/helper-skip-transparent-expression-wrappers': 7.20.0 - '@babel/plugin-proposal-optional-chaining': 7.20.7(@babel/core@7.20.12) + '@babel/plugin-proposal-optional-chaining': 7.21.0(@babel/core@7.20.12) + dev: true + + /@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.20.7(@babel/core@7.21.8): + resolution: {integrity: sha512-sbr9+wNE5aXMBBFBICk01tt7sBf2Oc9ikRFEcem/ZORup9IMUdNhW7/wVLEbbtlWOsEubJet46mHAL2C8+2jKQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.13.0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-plugin-utils': 7.21.5 + '@babel/helper-skip-transparent-expression-wrappers': 7.20.0 + '@babel/plugin-proposal-optional-chaining': 7.21.0(@babel/core@7.21.8) dev: true /@babel/plugin-proposal-async-generator-functions@7.20.7(@babel/core@7.20.12): @@ -2345,14 +2614,29 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.20.12 - '@babel/helper-environment-visitor': 7.18.9 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-environment-visitor': 7.21.5 + '@babel/helper-plugin-utils': 7.21.5 '@babel/helper-remap-async-to-generator': 7.18.9(@babel/core@7.20.12) '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.20.12) transitivePeerDependencies: - supports-color dev: true + /@babel/plugin-proposal-async-generator-functions@7.20.7(@babel/core@7.21.8): + resolution: {integrity: sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-environment-visitor': 7.21.5 + '@babel/helper-plugin-utils': 7.21.5 + '@babel/helper-remap-async-to-generator': 7.18.9(@babel/core@7.21.8) + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.21.8) + transitivePeerDependencies: + - supports-color + dev: true + /@babel/plugin-proposal-class-properties@7.18.6(@babel/core@7.20.12): resolution: {integrity: sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==} engines: {node: '>=6.9.0'} @@ -2361,7 +2645,20 @@ packages: dependencies: '@babel/core': 7.20.12 '@babel/helper-create-class-features-plugin': 7.20.12(@babel/core@7.20.12) - '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-plugin-utils': 7.21.5 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-proposal-class-properties@7.18.6(@babel/core@7.21.8): + resolution: {integrity: sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-create-class-features-plugin': 7.20.12(@babel/core@7.21.8) + '@babel/helper-plugin-utils': 7.21.5 transitivePeerDependencies: - supports-color dev: true @@ -2374,12 +2671,26 @@ packages: dependencies: '@babel/core': 7.20.12 '@babel/helper-create-class-features-plugin': 7.20.12(@babel/core@7.20.12) - '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-plugin-utils': 7.21.5 '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.20.12) transitivePeerDependencies: - supports-color dev: true + /@babel/plugin-proposal-class-static-block@7.21.0(@babel/core@7.21.8): + resolution: {integrity: sha512-XP5G9MWNUskFuP30IfFSEFB0Z6HzLIUcjYM4bYOPHXl7eiJ9HFv8tWj6TXTN5QODiEhDZAeI4hLok2iHFFV4hw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.12.0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-create-class-features-plugin': 7.21.8(@babel/core@7.21.8) + '@babel/helper-plugin-utils': 7.21.5 + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.21.8) + transitivePeerDependencies: + - supports-color + dev: true + /@babel/plugin-proposal-dynamic-import@7.18.6(@babel/core@7.20.12): resolution: {integrity: sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==} engines: {node: '>=6.9.0'} @@ -2387,10 +2698,21 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.20.12 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-plugin-utils': 7.21.5 '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.20.12) dev: true + /@babel/plugin-proposal-dynamic-import@7.18.6(@babel/core@7.21.8): + resolution: {integrity: sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-plugin-utils': 7.21.5 + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.21.8) + dev: true + /@babel/plugin-proposal-export-namespace-from@7.18.9(@babel/core@7.20.12): resolution: {integrity: sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==} engines: {node: '>=6.9.0'} @@ -2398,10 +2720,21 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.20.12 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-plugin-utils': 7.21.5 '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.20.12) dev: true + /@babel/plugin-proposal-export-namespace-from@7.18.9(@babel/core@7.21.8): + resolution: {integrity: sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-plugin-utils': 7.21.5 + '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.21.8) + dev: true + /@babel/plugin-proposal-json-strings@7.18.6(@babel/core@7.20.12): resolution: {integrity: sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==} engines: {node: '>=6.9.0'} @@ -2409,19 +2742,41 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.20.12 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-plugin-utils': 7.21.5 '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.20.12) dev: true - /@babel/plugin-proposal-logical-assignment-operators@7.20.7(@babel/core@7.20.12): - resolution: {integrity: sha512-y7C7cZgpMIjWlKE5T7eJwp+tnRYM89HmRvWM5EQuB5BoHEONjmQ8lSNmBUwOyy/GFRsohJED51YBF79hE1djug==} + /@babel/plugin-proposal-json-strings@7.18.6(@babel/core@7.21.8): + resolution: {integrity: sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.12 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.20.12) + '@babel/core': 7.21.8 + '@babel/helper-plugin-utils': 7.21.5 + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.21.8) + dev: true + + /@babel/plugin-proposal-logical-assignment-operators@7.20.7(@babel/core@7.20.12): + resolution: {integrity: sha512-y7C7cZgpMIjWlKE5T7eJwp+tnRYM89HmRvWM5EQuB5BoHEONjmQ8lSNmBUwOyy/GFRsohJED51YBF79hE1djug==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.21.5 + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.20.12) + dev: true + + /@babel/plugin-proposal-logical-assignment-operators@7.20.7(@babel/core@7.21.8): + resolution: {integrity: sha512-y7C7cZgpMIjWlKE5T7eJwp+tnRYM89HmRvWM5EQuB5BoHEONjmQ8lSNmBUwOyy/GFRsohJED51YBF79hE1djug==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-plugin-utils': 7.21.5 + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.21.8) dev: true /@babel/plugin-proposal-nullish-coalescing-operator@7.18.6(@babel/core@7.20.12): @@ -2431,10 +2786,21 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.20.12 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-plugin-utils': 7.21.5 '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.20.12) dev: true + /@babel/plugin-proposal-nullish-coalescing-operator@7.18.6(@babel/core@7.21.8): + resolution: {integrity: sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-plugin-utils': 7.21.5 + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.21.8) + dev: true + /@babel/plugin-proposal-numeric-separator@7.18.6(@babel/core@7.20.12): resolution: {integrity: sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==} engines: {node: '>=6.9.0'} @@ -2442,22 +2808,47 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.20.12 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-plugin-utils': 7.21.5 '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.20.12) dev: true + /@babel/plugin-proposal-numeric-separator@7.18.6(@babel/core@7.21.8): + resolution: {integrity: sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-plugin-utils': 7.21.5 + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.21.8) + dev: true + /@babel/plugin-proposal-object-rest-spread@7.20.7(@babel/core@7.20.12): resolution: {integrity: sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/compat-data': 7.20.10 + '@babel/compat-data': 7.21.7 '@babel/core': 7.20.12 - '@babel/helper-compilation-targets': 7.20.7(@babel/core@7.20.12) - '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-compilation-targets': 7.21.5(@babel/core@7.20.12) + '@babel/helper-plugin-utils': 7.21.5 '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.20.12) - '@babel/plugin-transform-parameters': 7.20.7(@babel/core@7.20.12) + '@babel/plugin-transform-parameters': 7.21.3(@babel/core@7.20.12) + dev: true + + /@babel/plugin-proposal-object-rest-spread@7.20.7(@babel/core@7.21.8): + resolution: {integrity: sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/compat-data': 7.21.7 + '@babel/core': 7.21.8 + '@babel/helper-compilation-targets': 7.21.5(@babel/core@7.21.8) + '@babel/helper-plugin-utils': 7.21.5 + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.21.8) + '@babel/plugin-transform-parameters': 7.21.3(@babel/core@7.21.8) dev: true /@babel/plugin-proposal-optional-catch-binding@7.18.6(@babel/core@7.20.12): @@ -2467,10 +2858,21 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.20.12 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-plugin-utils': 7.21.5 '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.20.12) dev: true + /@babel/plugin-proposal-optional-catch-binding@7.18.6(@babel/core@7.21.8): + resolution: {integrity: sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-plugin-utils': 7.21.5 + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.21.8) + dev: true + /@babel/plugin-proposal-optional-chaining@7.20.7(@babel/core@7.20.12): resolution: {integrity: sha512-T+A7b1kfjtRM51ssoOfS1+wbyCVqorfyZhT99TvxxLMirPShD8CzKMRepMlCBGM5RpHMbn8s+5MMHnPstJH6mQ==} engines: {node: '>=6.9.0'} @@ -2478,11 +2880,47 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.20.12 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-plugin-utils': 7.21.5 '@babel/helper-skip-transparent-expression-wrappers': 7.20.0 '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.20.12) dev: true + /@babel/plugin-proposal-optional-chaining@7.20.7(@babel/core@7.21.8): + resolution: {integrity: sha512-T+A7b1kfjtRM51ssoOfS1+wbyCVqorfyZhT99TvxxLMirPShD8CzKMRepMlCBGM5RpHMbn8s+5MMHnPstJH6mQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-plugin-utils': 7.21.5 + '@babel/helper-skip-transparent-expression-wrappers': 7.20.0 + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.21.8) + dev: true + + /@babel/plugin-proposal-optional-chaining@7.21.0(@babel/core@7.20.12): + resolution: {integrity: sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.21.5 + '@babel/helper-skip-transparent-expression-wrappers': 7.20.0 + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.20.12) + dev: true + + /@babel/plugin-proposal-optional-chaining@7.21.0(@babel/core@7.21.8): + resolution: {integrity: sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-plugin-utils': 7.21.5 + '@babel/helper-skip-transparent-expression-wrappers': 7.20.0 + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.21.8) + dev: true + /@babel/plugin-proposal-private-methods@7.18.6(@babel/core@7.20.12): resolution: {integrity: sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==} engines: {node: '>=6.9.0'} @@ -2491,7 +2929,20 @@ packages: dependencies: '@babel/core': 7.20.12 '@babel/helper-create-class-features-plugin': 7.20.12(@babel/core@7.20.12) - '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-plugin-utils': 7.21.5 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-proposal-private-methods@7.18.6(@babel/core@7.21.8): + resolution: {integrity: sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-create-class-features-plugin': 7.20.12(@babel/core@7.21.8) + '@babel/helper-plugin-utils': 7.21.5 transitivePeerDependencies: - supports-color dev: true @@ -2505,12 +2956,27 @@ packages: '@babel/core': 7.20.12 '@babel/helper-annotate-as-pure': 7.18.6 '@babel/helper-create-class-features-plugin': 7.20.12(@babel/core@7.20.12) - '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-plugin-utils': 7.21.5 '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.20.12) transitivePeerDependencies: - supports-color dev: true + /@babel/plugin-proposal-private-property-in-object@7.21.0(@babel/core@7.21.8): + resolution: {integrity: sha512-ha4zfehbJjc5MmXBlHec1igel5TJXXLDDRbuJ4+XT2TJcyD9/V1919BA8gMvsdHcNMBy4WBUBiRb3nw/EQUtBw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-annotate-as-pure': 7.18.6 + '@babel/helper-create-class-features-plugin': 7.21.8(@babel/core@7.21.8) + '@babel/helper-plugin-utils': 7.21.5 + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.21.8) + transitivePeerDependencies: + - supports-color + dev: true + /@babel/plugin-proposal-unicode-property-regex@7.18.6(@babel/core@7.20.12): resolution: {integrity: sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==} engines: {node: '>=4'} @@ -2519,7 +2985,18 @@ packages: dependencies: '@babel/core': 7.20.12 '@babel/helper-create-regexp-features-plugin': 7.20.5(@babel/core@7.20.12) - '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-plugin-utils': 7.21.5 + dev: true + + /@babel/plugin-proposal-unicode-property-regex@7.18.6(@babel/core@7.21.8): + resolution: {integrity: sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==} + engines: {node: '>=4'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-create-regexp-features-plugin': 7.20.5(@babel/core@7.21.8) + '@babel/helper-plugin-utils': 7.21.5 dev: true /@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.20.12): @@ -2528,16 +3005,25 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.20.12 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-plugin-utils': 7.21.5 dev: true - /@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.20.12): + /@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.21.8): + resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-plugin-utils': 7.21.5 + dev: true + + /@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.21.8): resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.12 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/core': 7.21.8 + '@babel/helper-plugin-utils': 7.21.5 dev: true /@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.20.12): @@ -2546,7 +3032,16 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.20.12 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-plugin-utils': 7.21.5 + dev: true + + /@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.21.8): + resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-plugin-utils': 7.21.5 dev: true /@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.20.12): @@ -2556,7 +3051,17 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.20.12 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-plugin-utils': 7.21.5 + dev: true + + /@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.21.8): + resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-plugin-utils': 7.21.5 dev: true /@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.20.12): @@ -2565,7 +3070,16 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.20.12 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-plugin-utils': 7.21.5 + dev: true + + /@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.21.8): + resolution: {integrity: sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-plugin-utils': 7.21.5 dev: true /@babel/plugin-syntax-export-namespace-from@7.8.3(@babel/core@7.20.12): @@ -2574,17 +3088,26 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.20.12 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-plugin-utils': 7.21.5 + dev: true + + /@babel/plugin-syntax-export-namespace-from@7.8.3(@babel/core@7.21.8): + resolution: {integrity: sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-plugin-utils': 7.21.5 dev: true - /@babel/plugin-syntax-flow@7.18.6(@babel/core@7.20.12): + /@babel/plugin-syntax-flow@7.18.6(@babel/core@7.21.8): resolution: {integrity: sha512-LUbR+KNTBWCUAqRG9ex5Gnzu2IOkt8jRJbHHXFT9q+L9zm7M/QQbEqXyw1n1pohYvOyWC8CjeyjrSaIwiYjK7A==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.12 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/core': 7.21.8 + '@babel/helper-plugin-utils': 7.21.5 dev: true /@babel/plugin-syntax-import-assertions@7.20.0(@babel/core@7.20.12): @@ -2594,16 +3117,26 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.20.12 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-plugin-utils': 7.21.5 dev: true - /@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.20.12): + /@babel/plugin-syntax-import-assertions@7.20.0(@babel/core@7.21.8): + resolution: {integrity: sha512-IUh1vakzNoWalR8ch/areW7qFopR2AEw03JlG7BbrDqmQ4X3q9uuipQwSGrUn7oGiemKjtSLDhNtQHzMHr1JdQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-plugin-utils': 7.21.5 + dev: true + + /@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.21.8): resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.12 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/core': 7.21.8 + '@babel/helper-plugin-utils': 7.21.5 dev: true /@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.20.12): @@ -2612,7 +3145,16 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.20.12 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-plugin-utils': 7.21.5 + dev: true + + /@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.21.8): + resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-plugin-utils': 7.21.5 dev: true /@babel/plugin-syntax-jsx@7.18.6(@babel/core@7.20.12): @@ -2625,13 +3167,32 @@ packages: '@babel/helper-plugin-utils': 7.20.2 dev: true + /@babel/plugin-syntax-jsx@7.18.6(@babel/core@7.21.8): + resolution: {integrity: sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + /@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.20.12): resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.20.12 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-plugin-utils': 7.21.5 + dev: true + + /@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.21.8): + resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-plugin-utils': 7.21.5 dev: true /@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.20.12): @@ -2640,7 +3201,16 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.20.12 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-plugin-utils': 7.21.5 + dev: true + + /@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.21.8): + resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-plugin-utils': 7.21.5 dev: true /@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.20.12): @@ -2649,7 +3219,16 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.20.12 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-plugin-utils': 7.21.5 + dev: true + + /@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.21.8): + resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-plugin-utils': 7.21.5 dev: true /@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.20.12): @@ -2658,7 +3237,16 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.20.12 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-plugin-utils': 7.21.5 + dev: true + + /@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.21.8): + resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-plugin-utils': 7.21.5 dev: true /@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.20.12): @@ -2667,7 +3255,16 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.20.12 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-plugin-utils': 7.21.5 + dev: true + + /@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.21.8): + resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-plugin-utils': 7.21.5 dev: true /@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.20.12): @@ -2676,7 +3273,16 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.20.12 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-plugin-utils': 7.21.5 + dev: true + + /@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.21.8): + resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-plugin-utils': 7.21.5 dev: true /@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.20.12): @@ -2686,7 +3292,17 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.20.12 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-plugin-utils': 7.21.5 + dev: true + + /@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.21.8): + resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-plugin-utils': 7.21.5 dev: true /@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.20.12): @@ -2696,17 +3312,27 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.20.12 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-plugin-utils': 7.21.5 + dev: true + + /@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.21.8): + resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-plugin-utils': 7.21.5 dev: true - /@babel/plugin-syntax-typescript@7.20.0(@babel/core@7.20.12): + /@babel/plugin-syntax-typescript@7.20.0(@babel/core@7.21.8): resolution: {integrity: sha512-rd9TkG+u1CExzS4SM1BlMEhMXwFLKVjOAFFCDx9PbX5ycJWDoWMcwdJH9RhkPu1dOgn5TrxLot/Gx6lWFuAUNQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.12 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/core': 7.21.8 + '@babel/helper-plugin-utils': 7.21.5 dev: true /@babel/plugin-transform-arrow-functions@7.20.7(@babel/core@7.20.12): @@ -2716,7 +3342,17 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.20.12 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-plugin-utils': 7.21.5 + dev: true + + /@babel/plugin-transform-arrow-functions@7.21.5(@babel/core@7.21.8): + resolution: {integrity: sha512-wb1mhwGOCaXHDTcsRYMKF9e5bbMgqwxtqa2Y1ifH96dXJPwbuLX9qHy3clhrxVqgMz7nyNXs8VkxdH8UBcjKqA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-plugin-utils': 7.21.5 dev: true /@babel/plugin-transform-async-to-generator@7.20.7(@babel/core@7.20.12): @@ -2727,12 +3363,26 @@ packages: dependencies: '@babel/core': 7.20.12 '@babel/helper-module-imports': 7.18.6 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-plugin-utils': 7.21.5 '@babel/helper-remap-async-to-generator': 7.18.9(@babel/core@7.20.12) transitivePeerDependencies: - supports-color dev: true + /@babel/plugin-transform-async-to-generator@7.20.7(@babel/core@7.21.8): + resolution: {integrity: sha512-Uo5gwHPT9vgnSXQxqGtpdufUiWp96gk7yiP4Mp5bm1QMkEmLXBO7PAGYbKoJ6DhAwiNkcHFBol/x5zZZkL/t0Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-module-imports': 7.18.6 + '@babel/helper-plugin-utils': 7.21.5 + '@babel/helper-remap-async-to-generator': 7.18.9(@babel/core@7.21.8) + transitivePeerDependencies: + - supports-color + dev: true + /@babel/plugin-transform-block-scoped-functions@7.18.6(@babel/core@7.20.12): resolution: {integrity: sha512-ExUcOqpPWnliRcPqves5HJcJOvHvIIWfuS4sroBUenPuMdmW+SMHDakmtS7qOo13sVppmUijqeTv7qqGsvURpQ==} engines: {node: '>=6.9.0'} @@ -2740,7 +3390,17 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.20.12 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-plugin-utils': 7.21.5 + dev: true + + /@babel/plugin-transform-block-scoped-functions@7.18.6(@babel/core@7.21.8): + resolution: {integrity: sha512-ExUcOqpPWnliRcPqves5HJcJOvHvIIWfuS4sroBUenPuMdmW+SMHDakmtS7qOo13sVppmUijqeTv7qqGsvURpQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-plugin-utils': 7.21.5 dev: true /@babel/plugin-transform-block-scoping@7.20.11(@babel/core@7.20.12): @@ -2750,7 +3410,17 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.20.12 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-plugin-utils': 7.21.5 + dev: true + + /@babel/plugin-transform-block-scoping@7.21.0(@babel/core@7.21.8): + resolution: {integrity: sha512-Mdrbunoh9SxwFZapeHVrwFmri16+oYotcZysSzhNIVDwIAb1UV+kvnxULSYq9J3/q5MDG+4X6w8QVgD1zhBXNQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-plugin-utils': 7.21.5 dev: true /@babel/plugin-transform-classes@7.20.7(@babel/core@7.20.12): @@ -2761,11 +3431,31 @@ packages: dependencies: '@babel/core': 7.20.12 '@babel/helper-annotate-as-pure': 7.18.6 - '@babel/helper-compilation-targets': 7.20.7(@babel/core@7.20.12) - '@babel/helper-environment-visitor': 7.18.9 - '@babel/helper-function-name': 7.19.0 + '@babel/helper-compilation-targets': 7.21.5(@babel/core@7.20.12) + '@babel/helper-environment-visitor': 7.21.5 + '@babel/helper-function-name': 7.21.0 '@babel/helper-optimise-call-expression': 7.18.6 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-plugin-utils': 7.21.5 + '@babel/helper-replace-supers': 7.20.7 + '@babel/helper-split-export-declaration': 7.18.6 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-classes@7.21.0(@babel/core@7.21.8): + resolution: {integrity: sha512-RZhbYTCEUAe6ntPehC4hlslPWosNHDox+vAs4On/mCLRLfoDVHf6hVEd7kuxr1RnHwJmxFfUM3cZiZRmPxJPXQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-annotate-as-pure': 7.18.6 + '@babel/helper-compilation-targets': 7.21.5(@babel/core@7.21.8) + '@babel/helper-environment-visitor': 7.21.5 + '@babel/helper-function-name': 7.21.0 + '@babel/helper-optimise-call-expression': 7.18.6 + '@babel/helper-plugin-utils': 7.21.5 '@babel/helper-replace-supers': 7.20.7 '@babel/helper-split-export-declaration': 7.18.6 globals: 11.12.0 @@ -2780,18 +3470,39 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.20.12 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/template': 7.20.7 + '@babel/helper-plugin-utils': 7.21.5 + '@babel/template': 7.20.7 + dev: true + + /@babel/plugin-transform-computed-properties@7.21.5(@babel/core@7.21.8): + resolution: {integrity: sha512-TR653Ki3pAwxBxUe8srfF3e4Pe3FTA46uaNHYyQwIoM4oWKSoOZiDNyHJ0oIoDIUPSRQbQG7jzgVBX3FPVne1Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-plugin-utils': 7.21.5 + '@babel/template': 7.20.7 + dev: true + + /@babel/plugin-transform-destructuring@7.20.7(@babel/core@7.20.12): + resolution: {integrity: sha512-Xwg403sRrZb81IVB79ZPqNQME23yhugYVqgTxAhT99h485F4f+GMELFhhOsscDUB7HCswepKeCKLn/GZvUKoBA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.21.5 dev: true - /@babel/plugin-transform-destructuring@7.20.7(@babel/core@7.20.12): - resolution: {integrity: sha512-Xwg403sRrZb81IVB79ZPqNQME23yhugYVqgTxAhT99h485F4f+GMELFhhOsscDUB7HCswepKeCKLn/GZvUKoBA==} + /@babel/plugin-transform-destructuring@7.21.3(@babel/core@7.21.8): + resolution: {integrity: sha512-bp6hwMFzuiE4HqYEyoGJ/V2LeIWn+hLVKc4pnj++E5XQptwhtcGmSayM029d/j2X1bPKGTlsyPwAubuU22KhMA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.12 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/core': 7.21.8 + '@babel/helper-plugin-utils': 7.21.5 dev: true /@babel/plugin-transform-dotall-regex@7.18.6(@babel/core@7.20.12): @@ -2802,7 +3513,18 @@ packages: dependencies: '@babel/core': 7.20.12 '@babel/helper-create-regexp-features-plugin': 7.20.5(@babel/core@7.20.12) - '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-plugin-utils': 7.21.5 + dev: true + + /@babel/plugin-transform-dotall-regex@7.18.6(@babel/core@7.21.8): + resolution: {integrity: sha512-6S3jpun1eEbAxq7TdjLotAsl4WpQI9DxfkycRcKrjhQYzU87qpXdknpBg/e+TdcMehqGnLFi7tnFUBR02Vq6wg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-create-regexp-features-plugin': 7.20.5(@babel/core@7.21.8) + '@babel/helper-plugin-utils': 7.21.5 dev: true /@babel/plugin-transform-duplicate-keys@7.18.9(@babel/core@7.20.12): @@ -2812,7 +3534,17 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.20.12 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-plugin-utils': 7.21.5 + dev: true + + /@babel/plugin-transform-duplicate-keys@7.18.9(@babel/core@7.21.8): + resolution: {integrity: sha512-d2bmXCtZXYc59/0SanQKbiWINadaJXqtvIQIzd4+hNwkWBgyCd5F/2t1kXoUdvPMrxzPvhK6EMQRROxsue+mfw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-plugin-utils': 7.21.5 dev: true /@babel/plugin-transform-exponentiation-operator@7.18.6(@babel/core@7.20.12): @@ -2823,18 +3555,29 @@ packages: dependencies: '@babel/core': 7.20.12 '@babel/helper-builder-binary-assignment-operator-visitor': 7.18.9 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-plugin-utils': 7.21.5 + dev: true + + /@babel/plugin-transform-exponentiation-operator@7.18.6(@babel/core@7.21.8): + resolution: {integrity: sha512-wzEtc0+2c88FVR34aQmiz56dxEkxr2g8DQb/KfaFa1JYXOFVsbhvAonFN6PwVWj++fKmku8NP80plJ5Et4wqHw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-builder-binary-assignment-operator-visitor': 7.18.9 + '@babel/helper-plugin-utils': 7.21.5 dev: true - /@babel/plugin-transform-flow-strip-types@7.19.0(@babel/core@7.20.12): + /@babel/plugin-transform-flow-strip-types@7.19.0(@babel/core@7.21.8): resolution: {integrity: sha512-sgeMlNaQVbCSpgLSKP4ZZKfsJVnFnNQlUSk6gPYzR/q7tzCgQF2t8RBKAP6cKJeZdveei7Q7Jm527xepI8lNLg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.12 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/plugin-syntax-flow': 7.18.6(@babel/core@7.20.12) + '@babel/core': 7.21.8 + '@babel/helper-plugin-utils': 7.21.5 + '@babel/plugin-syntax-flow': 7.18.6(@babel/core@7.21.8) dev: true /@babel/plugin-transform-for-of@7.18.8(@babel/core@7.20.12): @@ -2844,7 +3587,17 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.20.12 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-plugin-utils': 7.21.5 + dev: true + + /@babel/plugin-transform-for-of@7.21.5(@babel/core@7.21.8): + resolution: {integrity: sha512-nYWpjKW/7j/I/mZkGVgHJXh4bA1sfdFnJoOXwJuj4m3Q2EraO/8ZyrkCau9P5tbHQk01RMSt6KYLCsW7730SXQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-plugin-utils': 7.21.5 dev: true /@babel/plugin-transform-function-name@7.18.9(@babel/core@7.20.12): @@ -2854,9 +3607,21 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.20.12 - '@babel/helper-compilation-targets': 7.20.7(@babel/core@7.20.12) - '@babel/helper-function-name': 7.19.0 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-compilation-targets': 7.21.5(@babel/core@7.20.12) + '@babel/helper-function-name': 7.21.0 + '@babel/helper-plugin-utils': 7.21.5 + dev: true + + /@babel/plugin-transform-function-name@7.18.9(@babel/core@7.21.8): + resolution: {integrity: sha512-WvIBoRPaJQ5yVHzcnJFor7oS5Ls0PYixlTYE63lCj2RtdQEl15M68FXQlxnG6wdraJIXRdR7KI+hQ7q/9QjrCQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-compilation-targets': 7.21.5(@babel/core@7.21.8) + '@babel/helper-function-name': 7.21.0 + '@babel/helper-plugin-utils': 7.21.5 dev: true /@babel/plugin-transform-literals@7.18.9(@babel/core@7.20.12): @@ -2866,7 +3631,17 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.20.12 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-plugin-utils': 7.21.5 + dev: true + + /@babel/plugin-transform-literals@7.18.9(@babel/core@7.21.8): + resolution: {integrity: sha512-IFQDSRoTPnrAIrI5zoZv73IFeZu2dhu6irxQjY9rNjTT53VmKg9fenjvoiOWOkJ6mm4jKVPtdMzBY98Fp4Z4cg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-plugin-utils': 7.21.5 dev: true /@babel/plugin-transform-member-expression-literals@7.18.6(@babel/core@7.20.12): @@ -2876,7 +3651,17 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.20.12 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-plugin-utils': 7.21.5 + dev: true + + /@babel/plugin-transform-member-expression-literals@7.18.6(@babel/core@7.21.8): + resolution: {integrity: sha512-qSF1ihLGO3q+/g48k85tUjD033C29TNTVB2paCwZPVmOsjn9pClvYYrM2VeJpBY2bcNkuny0YUyTNRyRxJ54KA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-plugin-utils': 7.21.5 dev: true /@babel/plugin-transform-modules-amd@7.20.11(@babel/core@7.20.12): @@ -2886,8 +3671,21 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.20.12 - '@babel/helper-module-transforms': 7.20.11 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-module-transforms': 7.21.5 + '@babel/helper-plugin-utils': 7.21.5 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-modules-amd@7.20.11(@babel/core@7.21.8): + resolution: {integrity: sha512-NuzCt5IIYOW0O30UvqktzHYR2ud5bOWbY0yaxWZ6G+aFzOMJvrs5YHNikrbdaT15+KNO31nPOy5Fim3ku6Zb5g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-module-transforms': 7.21.5 + '@babel/helper-plugin-utils': 7.21.5 transitivePeerDependencies: - supports-color dev: true @@ -2899,13 +3697,41 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.20.12 - '@babel/helper-module-transforms': 7.20.11 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-module-transforms': 7.21.5 + '@babel/helper-plugin-utils': 7.21.5 '@babel/helper-simple-access': 7.20.2 transitivePeerDependencies: - supports-color dev: true + /@babel/plugin-transform-modules-commonjs@7.20.11(@babel/core@7.21.8): + resolution: {integrity: sha512-S8e1f7WQ7cimJQ51JkAaDrEtohVEitXjgCGAS2N8S31Y42E+kWwfSz83LYz57QdBm7q9diARVqanIaH2oVgQnw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-module-transforms': 7.21.5 + '@babel/helper-plugin-utils': 7.21.5 + '@babel/helper-simple-access': 7.20.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-modules-commonjs@7.21.5(@babel/core@7.21.8): + resolution: {integrity: sha512-OVryBEgKUbtqMoB7eG2rs6UFexJi6Zj6FDXx+esBLPTCxCNxAY9o+8Di7IsUGJ+AVhp5ncK0fxWUBd0/1gPhrQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-module-transforms': 7.21.5 + '@babel/helper-plugin-utils': 7.21.5 + '@babel/helper-simple-access': 7.21.5 + transitivePeerDependencies: + - supports-color + dev: true + /@babel/plugin-transform-modules-systemjs@7.20.11(@babel/core@7.20.12): resolution: {integrity: sha512-vVu5g9BPQKSFEmvt2TA4Da5N+QVS66EX21d8uoOihC+OCpUoGvzVsXeqFdtAEfVa5BILAeFt+U7yVmLbQnAJmw==} engines: {node: '>=6.9.0'} @@ -2914,8 +3740,23 @@ packages: dependencies: '@babel/core': 7.20.12 '@babel/helper-hoist-variables': 7.18.6 - '@babel/helper-module-transforms': 7.20.11 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-module-transforms': 7.21.5 + '@babel/helper-plugin-utils': 7.21.5 + '@babel/helper-validator-identifier': 7.19.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-modules-systemjs@7.20.11(@babel/core@7.21.8): + resolution: {integrity: sha512-vVu5g9BPQKSFEmvt2TA4Da5N+QVS66EX21d8uoOihC+OCpUoGvzVsXeqFdtAEfVa5BILAeFt+U7yVmLbQnAJmw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-hoist-variables': 7.18.6 + '@babel/helper-module-transforms': 7.21.5 + '@babel/helper-plugin-utils': 7.21.5 '@babel/helper-validator-identifier': 7.19.1 transitivePeerDependencies: - supports-color @@ -2928,8 +3769,21 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.20.12 - '@babel/helper-module-transforms': 7.20.11 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-module-transforms': 7.21.5 + '@babel/helper-plugin-utils': 7.21.5 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-modules-umd@7.18.6(@babel/core@7.21.8): + resolution: {integrity: sha512-dcegErExVeXcRqNtkRU/z8WlBLnvD4MRnHgNs3MytRO1Mn1sHRyhbcpYbVMGclAqOjdW+9cfkdZno9dFdfKLfQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-module-transforms': 7.21.5 + '@babel/helper-plugin-utils': 7.21.5 transitivePeerDependencies: - supports-color dev: true @@ -2942,7 +3796,18 @@ packages: dependencies: '@babel/core': 7.20.12 '@babel/helper-create-regexp-features-plugin': 7.20.5(@babel/core@7.20.12) - '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-plugin-utils': 7.21.5 + dev: true + + /@babel/plugin-transform-named-capturing-groups-regex@7.20.5(@babel/core@7.21.8): + resolution: {integrity: sha512-mOW4tTzi5iTLnw+78iEq3gr8Aoq4WNRGpmSlrogqaiCBoR1HFhpU4JkpQFOHfeYx3ReVIFWOQJS4aZBRvuZ6mA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-create-regexp-features-plugin': 7.20.5(@babel/core@7.21.8) + '@babel/helper-plugin-utils': 7.21.5 dev: true /@babel/plugin-transform-new-target@7.18.6(@babel/core@7.20.12): @@ -2952,7 +3817,17 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.20.12 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-plugin-utils': 7.21.5 + dev: true + + /@babel/plugin-transform-new-target@7.18.6(@babel/core@7.21.8): + resolution: {integrity: sha512-DjwFA/9Iu3Z+vrAn+8pBUGcjhxKguSMlsFqeCKbhb9BAV756v0krzVK04CRDi/4aqmk8BsHb4a/gFcaA5joXRw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-plugin-utils': 7.21.5 dev: true /@babel/plugin-transform-object-super@7.18.6(@babel/core@7.20.12): @@ -2962,7 +3837,20 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.20.12 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-plugin-utils': 7.21.5 + '@babel/helper-replace-supers': 7.20.7 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-object-super@7.18.6(@babel/core@7.21.8): + resolution: {integrity: sha512-uvGz6zk+pZoS1aTZrOvrbj6Pp/kK2mp45t2B+bTDre2UgsZZ8EZLSJtUg7m/no0zOJUWgFONpB7Zv9W2tSaFlA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-plugin-utils': 7.21.5 '@babel/helper-replace-supers': 7.20.7 transitivePeerDependencies: - supports-color @@ -2975,7 +3863,27 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.20.12 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-plugin-utils': 7.21.5 + dev: true + + /@babel/plugin-transform-parameters@7.21.3(@babel/core@7.20.12): + resolution: {integrity: sha512-Wxc+TvppQG9xWFYatvCGPvZ6+SIUxQ2ZdiBP+PHYMIjnPXD+uThCshaz4NZOnODAtBjjcVQQ/3OKs9LW28purQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.21.5 + dev: true + + /@babel/plugin-transform-parameters@7.21.3(@babel/core@7.21.8): + resolution: {integrity: sha512-Wxc+TvppQG9xWFYatvCGPvZ6+SIUxQ2ZdiBP+PHYMIjnPXD+uThCshaz4NZOnODAtBjjcVQQ/3OKs9LW28purQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-plugin-utils': 7.21.5 dev: true /@babel/plugin-transform-property-literals@7.18.6(@babel/core@7.20.12): @@ -2985,7 +3893,17 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.20.12 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-plugin-utils': 7.21.5 + dev: true + + /@babel/plugin-transform-property-literals@7.18.6(@babel/core@7.21.8): + resolution: {integrity: sha512-cYcs6qlgafTud3PAzrrRNbQtfpQ8+y/+M5tKmksS9+M1ckbH6kzY8MrexEM9mcA6JDsukE19iIRvAyYl463sMg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-plugin-utils': 7.21.5 dev: true /@babel/plugin-transform-react-jsx@7.20.7(@babel/core@7.20.12): @@ -3009,7 +3927,18 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.20.12 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-plugin-utils': 7.21.5 + regenerator-transform: 0.15.1 + dev: true + + /@babel/plugin-transform-regenerator@7.21.5(@babel/core@7.21.8): + resolution: {integrity: sha512-ZoYBKDb6LyMi5yCsByQ5jmXsHAQDDYeexT1Szvlmui+lADvfSecr5Dxd/PkrTC3pAD182Fcju1VQkB4oCp9M+w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-plugin-utils': 7.21.5 regenerator-transform: 0.15.1 dev: true @@ -3020,7 +3949,17 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.20.12 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-plugin-utils': 7.21.5 + dev: true + + /@babel/plugin-transform-reserved-words@7.18.6(@babel/core@7.21.8): + resolution: {integrity: sha512-oX/4MyMoypzHjFrT1CdivfKZ+XvIPMFXwwxHp/r0Ddy2Vuomt4HDFGmft1TAY2yiTKiNSsh3kjBAzcM8kSdsjA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-plugin-utils': 7.21.5 dev: true /@babel/plugin-transform-shorthand-properties@7.18.6(@babel/core@7.20.12): @@ -3030,7 +3969,17 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.20.12 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-plugin-utils': 7.21.5 + dev: true + + /@babel/plugin-transform-shorthand-properties@7.18.6(@babel/core@7.21.8): + resolution: {integrity: sha512-eCLXXJqv8okzg86ywZJbRn19YJHU4XUa55oz2wbHhaQVn/MM+XhukiT7SYqp/7o00dg52Rj51Ny+Ecw4oyoygw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-plugin-utils': 7.21.5 dev: true /@babel/plugin-transform-spread@7.20.7(@babel/core@7.20.12): @@ -3040,7 +3989,18 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.20.12 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-plugin-utils': 7.21.5 + '@babel/helper-skip-transparent-expression-wrappers': 7.20.0 + dev: true + + /@babel/plugin-transform-spread@7.20.7(@babel/core@7.21.8): + resolution: {integrity: sha512-ewBbHQ+1U/VnH1fxltbJqDeWBU1oNLG8Dj11uIv3xVf7nrQu0bPGe5Rf716r7K5Qz+SqtAOVswoVunoiBtGhxw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-plugin-utils': 7.21.5 '@babel/helper-skip-transparent-expression-wrappers': 7.20.0 dev: true @@ -3051,7 +4011,17 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.20.12 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-plugin-utils': 7.21.5 + dev: true + + /@babel/plugin-transform-sticky-regex@7.18.6(@babel/core@7.21.8): + resolution: {integrity: sha512-kfiDrDQ+PBsQDO85yj1icueWMfGfJFKN1KCkndygtu/C9+XUfydLC8Iv5UYJqRwy4zk8EcplRxEOeLyjq1gm6Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-plugin-utils': 7.21.5 dev: true /@babel/plugin-transform-template-literals@7.18.9(@babel/core@7.20.12): @@ -3061,7 +4031,17 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.20.12 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-plugin-utils': 7.21.5 + dev: true + + /@babel/plugin-transform-template-literals@7.18.9(@babel/core@7.21.8): + resolution: {integrity: sha512-S8cOWfT82gTezpYOiVaGHrCbhlHgKhQt8XH5ES46P2XWmX92yisoZywf5km75wv5sYcXDUCLMmMxOLCtthDgMA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-plugin-utils': 7.21.5 dev: true /@babel/plugin-transform-typeof-symbol@7.18.9(@babel/core@7.20.12): @@ -3071,19 +4051,29 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.20.12 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-plugin-utils': 7.21.5 + dev: true + + /@babel/plugin-transform-typeof-symbol@7.18.9(@babel/core@7.21.8): + resolution: {integrity: sha512-SRfwTtF11G2aemAZWivL7PD+C9z52v9EvMqH9BuYbabyPuKUvSWks3oCg6041pT925L4zVFqaVBeECwsmlguEw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-plugin-utils': 7.21.5 dev: true - /@babel/plugin-transform-typescript@7.20.7(@babel/core@7.20.12): + /@babel/plugin-transform-typescript@7.20.7(@babel/core@7.21.8): resolution: {integrity: sha512-m3wVKEvf6SoszD8pu4NZz3PvfKRCMgk6D6d0Qi9hNnlM5M6CFS92EgF4EiHVLKbU0r/r7ty1hg7NPZwE7WRbYw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.12 - '@babel/helper-create-class-features-plugin': 7.20.12(@babel/core@7.20.12) - '@babel/helper-plugin-utils': 7.20.2 - '@babel/plugin-syntax-typescript': 7.20.0(@babel/core@7.20.12) + '@babel/core': 7.21.8 + '@babel/helper-create-class-features-plugin': 7.21.8(@babel/core@7.21.8) + '@babel/helper-plugin-utils': 7.21.5 + '@babel/plugin-syntax-typescript': 7.20.0(@babel/core@7.21.8) transitivePeerDependencies: - supports-color dev: true @@ -3095,7 +4085,17 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.20.12 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-plugin-utils': 7.21.5 + dev: true + + /@babel/plugin-transform-unicode-escapes@7.21.5(@babel/core@7.21.8): + resolution: {integrity: sha512-LYm/gTOwZqsYohlvFUe/8Tujz75LqqVC2w+2qPHLR+WyWHGCZPN1KBpJCJn+4Bk4gOkQy/IXKIge6az5MqwlOg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-plugin-utils': 7.21.5 dev: true /@babel/plugin-transform-unicode-regex@7.18.6(@babel/core@7.20.12): @@ -3106,7 +4106,18 @@ packages: dependencies: '@babel/core': 7.20.12 '@babel/helper-create-regexp-features-plugin': 7.20.5(@babel/core@7.20.12) - '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-plugin-utils': 7.21.5 + dev: true + + /@babel/plugin-transform-unicode-regex@7.18.6(@babel/core@7.21.8): + resolution: {integrity: sha512-gE7A6Lt7YLnNOL3Pb9BNeZvi+d8l7tcRrG4+pwJjK9hD2xX4mEvjlQW60G9EEmfXVYRPv9VRQcyegIVHCql/AA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-create-regexp-features-plugin': 7.20.5(@babel/core@7.21.8) + '@babel/helper-plugin-utils': 7.21.5 dev: true /@babel/preset-env@7.20.2(@babel/core@7.20.12): @@ -3185,7 +4196,7 @@ packages: '@babel/plugin-transform-unicode-escapes': 7.18.10(@babel/core@7.20.12) '@babel/plugin-transform-unicode-regex': 7.18.6(@babel/core@7.20.12) '@babel/preset-modules': 0.1.5(@babel/core@7.20.12) - '@babel/types': 7.20.7 + '@babel/types': 7.21.5 babel-plugin-polyfill-corejs2: 0.3.3(@babel/core@7.20.12) babel-plugin-polyfill-corejs3: 0.6.0(@babel/core@7.20.12) babel-plugin-polyfill-regenerator: 0.4.1(@babel/core@7.20.12) @@ -3195,16 +4206,103 @@ packages: - supports-color dev: true - /@babel/preset-flow@7.18.6(@babel/core@7.20.12): + /@babel/preset-env@7.21.5(@babel/core@7.21.8): + resolution: {integrity: sha512-wH00QnTTldTbf/IefEVyChtRdw5RJvODT/Vb4Vcxq1AZvtXj6T0YeX0cAcXhI6/BdGuiP3GcNIL4OQbI2DVNxg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/compat-data': 7.21.7 + '@babel/core': 7.21.8 + '@babel/helper-compilation-targets': 7.21.5(@babel/core@7.21.8) + '@babel/helper-plugin-utils': 7.21.5 + '@babel/helper-validator-option': 7.21.0 + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.18.6(@babel/core@7.21.8) + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.20.7(@babel/core@7.21.8) + '@babel/plugin-proposal-async-generator-functions': 7.20.7(@babel/core@7.21.8) + '@babel/plugin-proposal-class-properties': 7.18.6(@babel/core@7.21.8) + '@babel/plugin-proposal-class-static-block': 7.21.0(@babel/core@7.21.8) + '@babel/plugin-proposal-dynamic-import': 7.18.6(@babel/core@7.21.8) + '@babel/plugin-proposal-export-namespace-from': 7.18.9(@babel/core@7.21.8) + '@babel/plugin-proposal-json-strings': 7.18.6(@babel/core@7.21.8) + '@babel/plugin-proposal-logical-assignment-operators': 7.20.7(@babel/core@7.21.8) + '@babel/plugin-proposal-nullish-coalescing-operator': 7.18.6(@babel/core@7.21.8) + '@babel/plugin-proposal-numeric-separator': 7.18.6(@babel/core@7.21.8) + '@babel/plugin-proposal-object-rest-spread': 7.20.7(@babel/core@7.21.8) + '@babel/plugin-proposal-optional-catch-binding': 7.18.6(@babel/core@7.21.8) + '@babel/plugin-proposal-optional-chaining': 7.21.0(@babel/core@7.21.8) + '@babel/plugin-proposal-private-methods': 7.18.6(@babel/core@7.21.8) + '@babel/plugin-proposal-private-property-in-object': 7.21.0(@babel/core@7.21.8) + '@babel/plugin-proposal-unicode-property-regex': 7.18.6(@babel/core@7.21.8) + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.21.8) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.21.8) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.21.8) + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.21.8) + '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.21.8) + '@babel/plugin-syntax-import-assertions': 7.20.0(@babel/core@7.21.8) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.21.8) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.21.8) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.21.8) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.21.8) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.21.8) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.21.8) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.21.8) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.21.8) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.21.8) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.21.8) + '@babel/plugin-transform-arrow-functions': 7.21.5(@babel/core@7.21.8) + '@babel/plugin-transform-async-to-generator': 7.20.7(@babel/core@7.21.8) + '@babel/plugin-transform-block-scoped-functions': 7.18.6(@babel/core@7.21.8) + '@babel/plugin-transform-block-scoping': 7.21.0(@babel/core@7.21.8) + '@babel/plugin-transform-classes': 7.21.0(@babel/core@7.21.8) + '@babel/plugin-transform-computed-properties': 7.21.5(@babel/core@7.21.8) + '@babel/plugin-transform-destructuring': 7.21.3(@babel/core@7.21.8) + '@babel/plugin-transform-dotall-regex': 7.18.6(@babel/core@7.21.8) + '@babel/plugin-transform-duplicate-keys': 7.18.9(@babel/core@7.21.8) + '@babel/plugin-transform-exponentiation-operator': 7.18.6(@babel/core@7.21.8) + '@babel/plugin-transform-for-of': 7.21.5(@babel/core@7.21.8) + '@babel/plugin-transform-function-name': 7.18.9(@babel/core@7.21.8) + '@babel/plugin-transform-literals': 7.18.9(@babel/core@7.21.8) + '@babel/plugin-transform-member-expression-literals': 7.18.6(@babel/core@7.21.8) + '@babel/plugin-transform-modules-amd': 7.20.11(@babel/core@7.21.8) + '@babel/plugin-transform-modules-commonjs': 7.21.5(@babel/core@7.21.8) + '@babel/plugin-transform-modules-systemjs': 7.20.11(@babel/core@7.21.8) + '@babel/plugin-transform-modules-umd': 7.18.6(@babel/core@7.21.8) + '@babel/plugin-transform-named-capturing-groups-regex': 7.20.5(@babel/core@7.21.8) + '@babel/plugin-transform-new-target': 7.18.6(@babel/core@7.21.8) + '@babel/plugin-transform-object-super': 7.18.6(@babel/core@7.21.8) + '@babel/plugin-transform-parameters': 7.21.3(@babel/core@7.21.8) + '@babel/plugin-transform-property-literals': 7.18.6(@babel/core@7.21.8) + '@babel/plugin-transform-regenerator': 7.21.5(@babel/core@7.21.8) + '@babel/plugin-transform-reserved-words': 7.18.6(@babel/core@7.21.8) + '@babel/plugin-transform-shorthand-properties': 7.18.6(@babel/core@7.21.8) + '@babel/plugin-transform-spread': 7.20.7(@babel/core@7.21.8) + '@babel/plugin-transform-sticky-regex': 7.18.6(@babel/core@7.21.8) + '@babel/plugin-transform-template-literals': 7.18.9(@babel/core@7.21.8) + '@babel/plugin-transform-typeof-symbol': 7.18.9(@babel/core@7.21.8) + '@babel/plugin-transform-unicode-escapes': 7.21.5(@babel/core@7.21.8) + '@babel/plugin-transform-unicode-regex': 7.18.6(@babel/core@7.21.8) + '@babel/preset-modules': 0.1.5(@babel/core@7.21.8) + '@babel/types': 7.21.5 + babel-plugin-polyfill-corejs2: 0.3.3(@babel/core@7.21.8) + babel-plugin-polyfill-corejs3: 0.6.0(@babel/core@7.21.8) + babel-plugin-polyfill-regenerator: 0.4.1(@babel/core@7.21.8) + core-js-compat: 3.27.1 + semver: 6.3.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/preset-flow@7.18.6(@babel/core@7.21.8): resolution: {integrity: sha512-E7BDhL64W6OUqpuyHnSroLnqyRTcG6ZdOBl1OKI/QK/HJfplqK/S3sq1Cckx7oTodJ5yOXyfw7rEADJ6UjoQDQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.12 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/helper-validator-option': 7.18.6 - '@babel/plugin-transform-flow-strip-types': 7.19.0(@babel/core@7.20.12) + '@babel/core': 7.21.8 + '@babel/helper-plugin-utils': 7.21.5 + '@babel/helper-validator-option': 7.21.0 + '@babel/plugin-transform-flow-strip-types': 7.19.0(@babel/core@7.21.8) dev: true /@babel/preset-modules@0.1.5(@babel/core@7.20.12): @@ -3213,34 +4311,47 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.20.12 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-plugin-utils': 7.21.5 '@babel/plugin-proposal-unicode-property-regex': 7.18.6(@babel/core@7.20.12) '@babel/plugin-transform-dotall-regex': 7.18.6(@babel/core@7.20.12) - '@babel/types': 7.20.7 + '@babel/types': 7.21.5 + esutils: 2.0.3 + dev: true + + /@babel/preset-modules@0.1.5(@babel/core@7.21.8): + resolution: {integrity: sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-plugin-utils': 7.21.5 + '@babel/plugin-proposal-unicode-property-regex': 7.18.6(@babel/core@7.21.8) + '@babel/plugin-transform-dotall-regex': 7.18.6(@babel/core@7.21.8) + '@babel/types': 7.21.5 esutils: 2.0.3 dev: true - /@babel/preset-typescript@7.18.6(@babel/core@7.20.12): + /@babel/preset-typescript@7.18.6(@babel/core@7.21.8): resolution: {integrity: sha512-s9ik86kXBAnD760aybBucdpnLsAt0jK1xqJn2juOn9lkOvSHV60os5hxoVJsPzMQxvnUJFAlkont2DvvaYEBtQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.12 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/helper-validator-option': 7.18.6 - '@babel/plugin-transform-typescript': 7.20.7(@babel/core@7.20.12) + '@babel/core': 7.21.8 + '@babel/helper-plugin-utils': 7.21.5 + '@babel/helper-validator-option': 7.21.0 + '@babel/plugin-transform-typescript': 7.20.7(@babel/core@7.21.8) transitivePeerDependencies: - supports-color dev: true - /@babel/register@7.18.9(@babel/core@7.20.12): + /@babel/register@7.18.9(@babel/core@7.21.8): resolution: {integrity: sha512-ZlbnXDcNYHMR25ITwwNKT88JiaukkdVj/nG7r3wnuXkOTHc60Uy05PwMCPre0hSkY68E6zK3xz+vUJSP2jWmcw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.12 + '@babel/core': 7.21.8 clone-deep: 4.0.1 find-cache-dir: 2.1.0 make-dir: 2.1.0 @@ -3278,23 +4389,41 @@ packages: resolution: {integrity: sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==} engines: {node: '>=6.9.0'} dependencies: - '@babel/code-frame': 7.18.6 - '@babel/parser': 7.20.7 - '@babel/types': 7.20.7 + '@babel/code-frame': 7.21.4 + '@babel/parser': 7.21.8 + '@babel/types': 7.21.5 dev: true /@babel/traverse@7.20.12: resolution: {integrity: sha512-MsIbFN0u+raeja38qboyF8TIT7K0BFzz/Yd/77ta4MsUsmP2RAnidIlwq7d5HFQrH/OZJecGV6B71C4zAgpoSQ==} engines: {node: '>=6.9.0'} dependencies: - '@babel/code-frame': 7.18.6 - '@babel/generator': 7.20.7 - '@babel/helper-environment-visitor': 7.18.9 - '@babel/helper-function-name': 7.19.0 + '@babel/code-frame': 7.21.4 + '@babel/generator': 7.21.5 + '@babel/helper-environment-visitor': 7.21.5 + '@babel/helper-function-name': 7.21.0 '@babel/helper-hoist-variables': 7.18.6 '@babel/helper-split-export-declaration': 7.18.6 - '@babel/parser': 7.20.7 - '@babel/types': 7.20.7 + '@babel/parser': 7.21.8 + '@babel/types': 7.21.5 + debug: 4.3.4(supports-color@8.1.1) + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/traverse@7.21.5: + resolution: {integrity: sha512-AhQoI3YjWi6u/y/ntv7k48mcrCXmus0t79J9qPNlk/lAsFlCiJ047RmbfMOawySTHtywXhbXgpx/8nXMYd+oFw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.21.4 + '@babel/generator': 7.21.5 + '@babel/helper-environment-visitor': 7.21.5 + '@babel/helper-function-name': 7.21.0 + '@babel/helper-hoist-variables': 7.18.6 + '@babel/helper-split-export-declaration': 7.18.6 + '@babel/parser': 7.21.8 + '@babel/types': 7.21.5 debug: 4.3.4(supports-color@8.1.1) globals: 11.12.0 transitivePeerDependencies: @@ -3305,9 +4434,18 @@ packages: resolution: {integrity: sha512-69OnhBxSSgK0OzTJai4kyPDiKTIe3j+ctaHdIGVbRahTLAT7L3R9oeXHC2aVSuGYt3cVnoAMDmOCgJ2yaiLMvg==} engines: {node: '>=6.9.0'} dependencies: - '@babel/helper-string-parser': 7.19.4 + '@babel/helper-string-parser': 7.21.5 + '@babel/helper-validator-identifier': 7.19.1 + to-fast-properties: 2.0.0 + + /@babel/types@7.21.5: + resolution: {integrity: sha512-m4AfNvVF2mVC/F7fDEdH2El3HzUg9It/XsCxZiOTTA3m3qYfcSVSbTfM6Q9xG+hYDniZssYhlXKKUMD5m8tF4Q==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-string-parser': 7.21.5 '@babel/helper-validator-identifier': 7.19.1 to-fast-properties: 2.0.0 + dev: true /@bcoe/v8-coverage@0.2.3: resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} @@ -3360,6 +4498,38 @@ packages: '@lezer/javascript': 1.0.2 dev: false + /@codemirror/lang-json@6.0.1: + resolution: {integrity: sha512-+T1flHdgpqDDlJZ2Lkil/rLiRy684WMLc74xUnjJH48GQdfJo/pudlTRreZmKwzP8/tGdKf83wlbAdOCzlJOGQ==} + dependencies: + '@codemirror/language': 6.2.1 + '@lezer/json': 1.0.0 + dev: false + + /@codemirror/lang-python@6.1.2(@codemirror/state@6.1.4)(@codemirror/view@6.5.1)(@lezer/common@1.0.1): + resolution: {integrity: sha512-nbQfifLBZstpt6Oo4XxA2LOzlSp4b/7Bc5cmodG1R+Cs5PLLCTUvsMNWDnziiCfTOG/SW1rVzXq/GbIr6WXlcw==} + dependencies: + '@codemirror/autocomplete': 6.4.0(@codemirror/language@6.2.1)(@codemirror/state@6.1.4)(@codemirror/view@6.5.1)(@lezer/common@1.0.1) + '@codemirror/language': 6.2.1 + '@lezer/python': 1.1.5 + transitivePeerDependencies: + - '@codemirror/state' + - '@codemirror/view' + - '@lezer/common' + dev: false + + /@codemirror/lang-sql@6.4.1(@codemirror/view@6.5.1)(@lezer/common@1.0.1): + resolution: {integrity: sha512-PFB56L+A0WGY35uRya+Trt5g19V9k2V9X3c55xoFW4RgiATr/yLqWsbbnEsdxuMn5tLpuikp7Kmj9smRsqBXAg==} + dependencies: + '@codemirror/autocomplete': 6.4.0(@codemirror/language@6.2.1)(@codemirror/state@6.1.4)(@codemirror/view@6.5.1)(@lezer/common@1.0.1) + '@codemirror/language': 6.2.1 + '@codemirror/state': 6.1.4 + '@lezer/highlight': 1.1.1 + '@lezer/lr': 1.2.3 + transitivePeerDependencies: + - '@codemirror/view' + - '@lezer/common' + dev: false + /@codemirror/language@6.2.1: resolution: {integrity: sha512-MC3svxuvIj0MRpFlGHxLS6vPyIdbTr2KKPEW46kCoCXw2ktb4NTkpkPBI/lSP/FoNXLCBJ0mrnUi1OoZxtpW1Q==} dependencies: @@ -3475,6 +4645,14 @@ packages: dev: true optional: true + /@esbuild/android-arm64@0.17.18: + resolution: {integrity: sha512-/iq0aK0eeHgSC3z55ucMAHO05OIqmQehiGay8eP5l/5l+iEr4EIbh4/MI8xD9qRFjqzgkc0JkX0LculNC9mXBw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + dev: true + optional: true + /@esbuild/android-arm@0.16.17: resolution: {integrity: sha512-N9x1CMXVhtWEAMS7pNNONyA14f71VPQN9Cnavj1XQh6T7bskqiLLrSca4O0Vr8Wdcga943eThxnVp3JLnBMYtw==} engines: {node: '>=12'} @@ -3483,6 +4661,14 @@ packages: dev: true optional: true + /@esbuild/android-arm@0.17.18: + resolution: {integrity: sha512-EmwL+vUBZJ7mhFCs5lA4ZimpUH3WMAoqvOIYhVQwdIgSpHC8ImHdsRyhHAVxpDYUSm0lWvd63z0XH1IlImS2Qw==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + dev: true + optional: true + /@esbuild/android-x64@0.16.17: resolution: {integrity: sha512-a3kTv3m0Ghh4z1DaFEuEDfz3OLONKuFvI4Xqczqx4BqLyuFaFkuaG4j2MtA6fuWEFeC5x9IvqnX7drmRq/fyAQ==} engines: {node: '>=12'} @@ -3491,6 +4677,14 @@ packages: dev: true optional: true + /@esbuild/android-x64@0.17.18: + resolution: {integrity: sha512-x+0efYNBF3NPW2Xc5bFOSFW7tTXdAcpfEg2nXmxegm4mJuVeS+i109m/7HMiOQ6M12aVGGFlqJX3RhNdYM2lWg==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + dev: true + optional: true + /@esbuild/darwin-arm64@0.16.17: resolution: {integrity: sha512-/2agbUEfmxWHi9ARTX6OQ/KgXnOWfsNlTeLcoV7HSuSTv63E4DqtAc+2XqGw1KHxKMHGZgbVCZge7HXWX9Vn+w==} engines: {node: '>=12'} @@ -3499,6 +4693,14 @@ packages: dev: true optional: true + /@esbuild/darwin-arm64@0.17.18: + resolution: {integrity: sha512-6tY+djEAdF48M1ONWnQb1C+6LiXrKjmqjzPNPWXhu/GzOHTHX2nh8Mo2ZAmBFg0kIodHhciEgUBtcYCAIjGbjQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + dev: true + optional: true + /@esbuild/darwin-x64@0.16.17: resolution: {integrity: sha512-2By45OBHulkd9Svy5IOCZt376Aa2oOkiE9QWUK9fe6Tb+WDr8hXL3dpqi+DeLiMed8tVXspzsTAvd0jUl96wmg==} engines: {node: '>=12'} @@ -3507,6 +4709,14 @@ packages: dev: true optional: true + /@esbuild/darwin-x64@0.17.18: + resolution: {integrity: sha512-Qq84ykvLvya3dO49wVC9FFCNUfSrQJLbxhoQk/TE1r6MjHo3sFF2tlJCwMjhkBVq3/ahUisj7+EpRSz0/+8+9A==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + dev: true + optional: true + /@esbuild/freebsd-arm64@0.16.17: resolution: {integrity: sha512-mt+cxZe1tVx489VTb4mBAOo2aKSnJ33L9fr25JXpqQqzbUIw/yzIzi+NHwAXK2qYV1lEFp4OoVeThGjUbmWmdw==} engines: {node: '>=12'} @@ -3515,6 +4725,14 @@ packages: dev: true optional: true + /@esbuild/freebsd-arm64@0.17.18: + resolution: {integrity: sha512-fw/ZfxfAzuHfaQeMDhbzxp9mc+mHn1Y94VDHFHjGvt2Uxl10mT4CDavHm+/L9KG441t1QdABqkVYwakMUeyLRA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + dev: true + optional: true + /@esbuild/freebsd-x64@0.16.17: resolution: {integrity: sha512-8ScTdNJl5idAKjH8zGAsN7RuWcyHG3BAvMNpKOBaqqR7EbUhhVHOqXRdL7oZvz8WNHL2pr5+eIT5c65kA6NHug==} engines: {node: '>=12'} @@ -3523,6 +4741,14 @@ packages: dev: true optional: true + /@esbuild/freebsd-x64@0.17.18: + resolution: {integrity: sha512-FQFbRtTaEi8ZBi/A6kxOC0V0E9B/97vPdYjY9NdawyLd4Qk5VD5g2pbWN2VR1c0xhzcJm74HWpObPszWC+qTew==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + dev: true + optional: true + /@esbuild/linux-arm64@0.16.17: resolution: {integrity: sha512-7S8gJnSlqKGVJunnMCrXHU9Q8Q/tQIxk/xL8BqAP64wchPCTzuM6W3Ra8cIa1HIflAvDnNOt2jaL17vaW+1V0g==} engines: {node: '>=12'} @@ -3531,6 +4757,14 @@ packages: dev: true optional: true + /@esbuild/linux-arm64@0.17.18: + resolution: {integrity: sha512-R7pZvQZFOY2sxUG8P6A21eq6q+eBv7JPQYIybHVf1XkQYC+lT7nDBdC7wWKTrbvMXKRaGudp/dzZCwL/863mZQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + dev: true + optional: true + /@esbuild/linux-arm@0.16.17: resolution: {integrity: sha512-iihzrWbD4gIT7j3caMzKb/RsFFHCwqqbrbH9SqUSRrdXkXaygSZCZg1FybsZz57Ju7N/SHEgPyaR0LZ8Zbe9gQ==} engines: {node: '>=12'} @@ -3539,6 +4773,14 @@ packages: dev: true optional: true + /@esbuild/linux-arm@0.17.18: + resolution: {integrity: sha512-jW+UCM40LzHcouIaqv3e/oRs0JM76JfhHjCavPxMUti7VAPh8CaGSlS7cmyrdpzSk7A+8f0hiedHqr/LMnfijg==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + dev: true + optional: true + /@esbuild/linux-ia32@0.16.17: resolution: {integrity: sha512-kiX69+wcPAdgl3Lonh1VI7MBr16nktEvOfViszBSxygRQqSpzv7BffMKRPMFwzeJGPxcio0pdD3kYQGpqQ2SSg==} engines: {node: '>=12'} @@ -3547,6 +4789,14 @@ packages: dev: true optional: true + /@esbuild/linux-ia32@0.17.18: + resolution: {integrity: sha512-ygIMc3I7wxgXIxk6j3V00VlABIjq260i967Cp9BNAk5pOOpIXmd1RFQJQX9Io7KRsthDrQYrtcx7QCof4o3ZoQ==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + dev: true + optional: true + /@esbuild/linux-loong64@0.16.17: resolution: {integrity: sha512-dTzNnQwembNDhd654cA4QhbS9uDdXC3TKqMJjgOWsC0yNCbpzfWoXdZvp0mY7HU6nzk5E0zpRGGx3qoQg8T2DQ==} engines: {node: '>=12'} @@ -3555,6 +4805,14 @@ packages: dev: true optional: true + /@esbuild/linux-loong64@0.17.18: + resolution: {integrity: sha512-bvPG+MyFs5ZlwYclCG1D744oHk1Pv7j8psF5TfYx7otCVmcJsEXgFEhQkbhNW8otDHL1a2KDINW20cfCgnzgMQ==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + dev: true + optional: true + /@esbuild/linux-mips64el@0.16.17: resolution: {integrity: sha512-ezbDkp2nDl0PfIUn0CsQ30kxfcLTlcx4Foz2kYv8qdC6ia2oX5Q3E/8m6lq84Dj/6b0FrkgD582fJMIfHhJfSw==} engines: {node: '>=12'} @@ -3563,6 +4821,14 @@ packages: dev: true optional: true + /@esbuild/linux-mips64el@0.17.18: + resolution: {integrity: sha512-oVqckATOAGuiUOa6wr8TXaVPSa+6IwVJrGidmNZS1cZVx0HqkTMkqFGD2HIx9H1RvOwFeWYdaYbdY6B89KUMxA==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + dev: true + optional: true + /@esbuild/linux-ppc64@0.16.17: resolution: {integrity: sha512-dzS678gYD1lJsW73zrFhDApLVdM3cUF2MvAa1D8K8KtcSKdLBPP4zZSLy6LFZ0jYqQdQ29bjAHJDgz0rVbLB3g==} engines: {node: '>=12'} @@ -3571,6 +4837,14 @@ packages: dev: true optional: true + /@esbuild/linux-ppc64@0.17.18: + resolution: {integrity: sha512-3dLlQO+b/LnQNxgH4l9rqa2/IwRJVN9u/bK63FhOPB4xqiRqlQAU0qDU3JJuf0BmaH0yytTBdoSBHrb2jqc5qQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + dev: true + optional: true + /@esbuild/linux-riscv64@0.16.17: resolution: {integrity: sha512-ylNlVsxuFjZK8DQtNUwiMskh6nT0vI7kYl/4fZgV1llP5d6+HIeL/vmmm3jpuoo8+NuXjQVZxmKuhDApK0/cKw==} engines: {node: '>=12'} @@ -3579,6 +4853,14 @@ packages: dev: true optional: true + /@esbuild/linux-riscv64@0.17.18: + resolution: {integrity: sha512-/x7leOyDPjZV3TcsdfrSI107zItVnsX1q2nho7hbbQoKnmoeUWjs+08rKKt4AUXju7+3aRZSsKrJtaRmsdL1xA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + dev: true + optional: true + /@esbuild/linux-s390x@0.16.17: resolution: {integrity: sha512-gzy7nUTO4UA4oZ2wAMXPNBGTzZFP7mss3aKR2hH+/4UUkCOyqmjXiKpzGrY2TlEUhbbejzXVKKGazYcQTZWA/w==} engines: {node: '>=12'} @@ -3587,6 +4869,14 @@ packages: dev: true optional: true + /@esbuild/linux-s390x@0.17.18: + resolution: {integrity: sha512-cX0I8Q9xQkL/6F5zWdYmVf5JSQt+ZfZD2bJudZrWD+4mnUvoZ3TDDXtDX2mUaq6upMFv9FlfIh4Gfun0tbGzuw==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + dev: true + optional: true + /@esbuild/linux-x64@0.16.17: resolution: {integrity: sha512-mdPjPxfnmoqhgpiEArqi4egmBAMYvaObgn4poorpUaqmvzzbvqbowRllQ+ZgzGVMGKaPkqUmPDOOFQRUFDmeUw==} engines: {node: '>=12'} @@ -3595,6 +4885,14 @@ packages: dev: true optional: true + /@esbuild/linux-x64@0.17.18: + resolution: {integrity: sha512-66RmRsPlYy4jFl0vG80GcNRdirx4nVWAzJmXkevgphP1qf4dsLQCpSKGM3DUQCojwU1hnepI63gNZdrr02wHUA==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + dev: true + optional: true + /@esbuild/netbsd-x64@0.16.17: resolution: {integrity: sha512-/PzmzD/zyAeTUsduZa32bn0ORug+Jd1EGGAUJvqfeixoEISYpGnAezN6lnJoskauoai0Jrs+XSyvDhppCPoKOA==} engines: {node: '>=12'} @@ -3603,6 +4901,14 @@ packages: dev: true optional: true + /@esbuild/netbsd-x64@0.17.18: + resolution: {integrity: sha512-95IRY7mI2yrkLlTLb1gpDxdC5WLC5mZDi+kA9dmM5XAGxCME0F8i4bYH4jZreaJ6lIZ0B8hTrweqG1fUyW7jbg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + dev: true + optional: true + /@esbuild/openbsd-x64@0.16.17: resolution: {integrity: sha512-2yaWJhvxGEz2RiftSk0UObqJa/b+rIAjnODJgv2GbGGpRwAfpgzyrg1WLK8rqA24mfZa9GvpjLcBBg8JHkoodg==} engines: {node: '>=12'} @@ -3611,6 +4917,14 @@ packages: dev: true optional: true + /@esbuild/openbsd-x64@0.17.18: + resolution: {integrity: sha512-WevVOgcng+8hSZ4Q3BKL3n1xTv5H6Nb53cBrtzzEjDbbnOmucEVcZeGCsCOi9bAOcDYEeBZbD2SJNBxlfP3qiA==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + dev: true + optional: true + /@esbuild/sunos-x64@0.16.17: resolution: {integrity: sha512-xtVUiev38tN0R3g8VhRfN7Zl42YCJvyBhRKw1RJjwE1d2emWTVToPLNEQj/5Qxc6lVFATDiy6LjVHYhIPrLxzw==} engines: {node: '>=12'} @@ -3619,6 +4933,14 @@ packages: dev: true optional: true + /@esbuild/sunos-x64@0.17.18: + resolution: {integrity: sha512-Rzf4QfQagnwhQXVBS3BYUlxmEbcV7MY+BH5vfDZekU5eYpcffHSyjU8T0xucKVuOcdCsMo+Ur5wmgQJH2GfNrg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + dev: true + optional: true + /@esbuild/win32-arm64@0.16.17: resolution: {integrity: sha512-ga8+JqBDHY4b6fQAmOgtJJue36scANy4l/rL97W+0wYmijhxKetzZdKOJI7olaBaMhWt8Pac2McJdZLxXWUEQw==} engines: {node: '>=12'} @@ -3627,6 +4949,14 @@ packages: dev: true optional: true + /@esbuild/win32-arm64@0.17.18: + resolution: {integrity: sha512-Kb3Ko/KKaWhjeAm2YoT/cNZaHaD1Yk/pa3FTsmqo9uFh1D1Rfco7BBLIPdDOozrObj2sahslFuAQGvWbgWldAg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + dev: true + optional: true + /@esbuild/win32-ia32@0.16.17: resolution: {integrity: sha512-WnsKaf46uSSF/sZhwnqE4L/F89AYNMiD4YtEcYekBt9Q7nj0DiId2XH2Ng2PHM54qi5oPrQ8luuzGszqi/veig==} engines: {node: '>=12'} @@ -3635,6 +4965,14 @@ packages: dev: true optional: true + /@esbuild/win32-ia32@0.17.18: + resolution: {integrity: sha512-0/xUMIdkVHwkvxfbd5+lfG7mHOf2FRrxNbPiKWg9C4fFrB8H0guClmaM3BFiRUYrznVoyxTIyC/Ou2B7QQSwmw==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + dev: true + optional: true + /@esbuild/win32-x64@0.16.17: resolution: {integrity: sha512-y+EHuSchhL7FjHgvQL/0fnnFmO4T1bhvWANX6gcnqTjtnKWbTvUMCpGnv2+t+31d7RzyEAYAd4u2fnIhHL6N/Q==} engines: {node: '>=12'} @@ -3643,14 +4981,37 @@ packages: dev: true optional: true - /@eslint/eslintrc@1.3.3: - resolution: {integrity: sha512-uj3pT6Mg+3t39fvLrj8iuCIJ38zKO9FpGtJ4BBJebJhEwjoT+KLVNCcHT5QC9NGRIEi7fZ0ZR8YRb884auB4Lg==} + /@esbuild/win32-x64@0.17.18: + resolution: {integrity: sha512-qU25Ma1I3NqTSHJUOKi9sAH1/Mzuvlke0ioMJRthLXKm7JiSKVwFghlGbDLOO2sARECGhja4xYfRAZNPAkooYg==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + dev: true + optional: true + + /@eslint-community/eslint-utils@4.4.0(eslint@8.39.0): + resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + dependencies: + eslint: 8.39.0 + eslint-visitor-keys: 3.4.0 + dev: true + + /@eslint-community/regexpp@4.5.0: + resolution: {integrity: sha512-vITaYzIcNmjn5tF5uxcZ/ft7/RXGrMUIS9HalWckEOF6ESiwXKoMzAQf2UW0aVd6rnOeExTJVd5hmWXucBKGXQ==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + dev: true + + /@eslint/eslintrc@2.0.2: + resolution: {integrity: sha512-3W4f5tDUra+pA+FzgugqL2pRimUTDJWKr7BINqOpkZrC0uYI0NIc0/JFgBROCU07HR6GieA5m3/rsPIhDmCXTQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: ajv: 6.12.6 debug: 4.3.4(supports-color@8.1.1) - espree: 9.4.0 - globals: 13.17.0 + espree: 9.5.1 + globals: 13.20.0 ignore: 5.2.4 import-fresh: 3.3.0 js-yaml: 4.1.0 @@ -3660,6 +5021,11 @@ packages: - supports-color dev: true + /@eslint/js@8.39.0: + resolution: {integrity: sha512-kf9RB0Fg7NZfap83B3QOqOGg9QmD9yBudqQXzzOtn3i4y7ZUXe5ONeW34Gwi+TxhH4mvj72R1Zc300KUMa9Bng==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + /@faker-js/faker@7.6.0: resolution: {integrity: sha512-XK6BTq1NDMo9Xqw/YkYyGjSsg44fbNwYRx7QK2CuoQgyy+f1rrTDHoExVM5PsyXCtfl2vs2vVJ0MN0yN6LppRw==} engines: {node: '>=14.0.0', npm: '>=6.0.0'} @@ -3669,22 +5035,6 @@ packages: resolution: {integrity: sha512-cEee/Z+I12mZcFJshKcCqC8tuX5hG3s+d+9nZ3LabqKF1vKdF41B92pJVCBggjAGORAeOzyyDDKrZwIkLffeOQ==} dev: true - /@figspec/components@1.0.1: - resolution: {integrity: sha512-UvnEamPEAMh9HExViqpobWmX25g1+soA9kcJu+It3VerMa7CeVyaIbQydNf1Gys5v/rxJVdTDRgQ7OXW2zAAig==} - dependencies: - lit: 2.6.1 - dev: true - - /@figspec/react@1.0.3(react@17.0.2): - resolution: {integrity: sha512-r683qOko+5CbT48Ox280fMx2MNAtaFPgCNJvldOqN3YtmAzlcTT+YSxd3OahA+kjXGGrnzDbUgeTOX1cPLII+g==} - peerDependencies: - react: ^16.14.0 || ^17.0.0 || ^18.0.0 - dependencies: - '@figspec/components': 1.0.1 - '@lit-labs/react': 1.1.1 - react: 17.0.2 - dev: true - /@fontsource/open-sans@4.5.12: resolution: {integrity: sha512-WKCexsVbOECJUSOgG7GnrUxe+3ds4Sa1yhsTjSnszI+0TaJvMZnDnn5YDKwA/KwLbkZqCaV3nvMTH97jJuxWNA==} dev: false @@ -3717,8 +5067,8 @@ packages: dependencies: '@fortawesome/fontawesome-common-types': 0.2.36 - /@fortawesome/vue-fontawesome@2.0.8(@fortawesome/fontawesome-svg-core@1.2.36)(vue@2.7.14): - resolution: {integrity: sha512-SRmP0q9Ox4zq8ydDR/hrH+23TVU1bdwYVnugLVaAIwklOHbf56gx6JUGlwES7zjuNYqzKgl8e39iYf6ph8qSQw==} + /@fortawesome/vue-fontawesome@2.0.10(@fortawesome/fontawesome-svg-core@1.2.36)(vue@2.7.14): + resolution: {integrity: sha512-OTETSXz+3ygD2OK2/vy82cmUBpuJqeOAg4gfnnv+f2Rir1tDIhQg026Q3NQxznq83ZLz8iNqGG9XJm26inpDeg==} peerDependencies: '@fortawesome/fontawesome-svg-core': ~1 || ~6 vue: ~2 @@ -3752,8 +5102,8 @@ packages: '@hapi/hoek': 9.3.0 dev: true - /@humanwhocodes/config-array@0.11.6: - resolution: {integrity: sha512-jJr+hPTJYKyDILJfhNSHsjiwXYf26Flsz8DvNndOsHs5pwSnpGUEy8yzF0JYhCEvTDdV2vuOK5tt8BVhwO5/hg==} + /@humanwhocodes/config-array@0.11.8: + resolution: {integrity: sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==} engines: {node: '>=10.10.0'} dependencies: '@humanwhocodes/object-schema': 1.2.1 @@ -3956,13 +5306,6 @@ packages: - supports-color dev: true - /@jest/schemas@29.4.2: - resolution: {integrity: sha512-ZrGzGfh31NtdVH8tn0mgJw4khQuNHiKqdzJAFbCaERbyCP9tHlxWuL/mnMu8P7e/+k4puWjI1NOzi/sFsjce/g==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dependencies: - '@sinclair/typebox': 0.25.21 - dev: true - /@jest/schemas@29.4.3: resolution: {integrity: sha512-VLYKXQmtmuEz6IxJsrZwzG9NvtkQsWNnWMsKxqWNu3+CnfzJQhp0WDDKWLVV9hLKr0l3SLLFRqcYHjhtyuDVxg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -3999,29 +5342,6 @@ packages: slash: 3.0.0 dev: true - /@jest/transform@29.4.2: - resolution: {integrity: sha512-kf1v5iTJHn7p9RbOsBuc/lcwyPtJaZJt5885C98omWz79NIeD3PfoiiaPSu7JyCyFzNOIzKhmMhQLUhlTL9BvQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dependencies: - '@babel/core': 7.20.12 - '@jest/types': 29.5.0 - '@jridgewell/trace-mapping': 0.3.17 - babel-plugin-istanbul: 6.1.1 - chalk: 4.1.2 - convert-source-map: 2.0.0 - fast-json-stable-stringify: 2.1.0 - graceful-fs: 4.2.10 - jest-haste-map: 29.5.0 - jest-regex-util: 29.4.3 - jest-util: 29.5.0 - micromatch: 4.0.5 - pirates: 4.0.5 - slash: 3.0.0 - write-file-atomic: 4.0.2 - transitivePeerDependencies: - - supports-color - dev: true - /@jest/transform@29.5.0: resolution: {integrity: sha512-8vbeZWqLJOvHaDfeMuoHITGKSz5qWc9u04lnWrQE3VyuSw604PzQM824ZeX9XSjUCeDiE3GuxZe5UKa8J61NQw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -4203,25 +5523,25 @@ packages: '@lezer/lr': 1.2.3 dev: false + /@lezer/json@1.0.0: + resolution: {integrity: sha512-zbAuUY09RBzCoCA3lJ1+ypKw5WSNvLqGMtasdW6HvVOqZoCpPr8eWrsGnOVWGKGn8Rh21FnrKRVlJXrGAVUqRw==} + dependencies: + '@lezer/highlight': 1.1.1 + '@lezer/lr': 1.2.3 + dev: false + /@lezer/lr@1.2.3: resolution: {integrity: sha512-qpB7rBzH8f6Mzjv2AVZRahcm+2Cf7nbIH++uXbvVOL1yIRvVWQ3HAM/saeBLCyz/togB7LGo76qdJYL1uKQlqA==} dependencies: '@lezer/common': 1.0.1 dev: false - /@lit-labs/react@1.1.1: - resolution: {integrity: sha512-9TC+/ZWb6BJlWCyUr14FKFlaGnyKpeEDorufXozQgke/VoVrslUQNaL7nBmrAWdNrmzx5jWgi8lFmWwrxMjnlA==} - dev: true - - /@lit-labs/ssr-dom-shim@1.0.0: - resolution: {integrity: sha512-ic93MBXfApIFTrup4a70M/+ddD8xdt2zxxj9sRwHQzhS9ag/syqkD8JPdTXsc1gUy2K8TTirhlCqyTEM/sifNw==} - dev: true - - /@lit/reactive-element@1.6.1: - resolution: {integrity: sha512-va15kYZr7KZNNPZdxONGQzpUr+4sxVu7V/VG7a8mRfPPXUyhEYj5RzXCQmGrlP3tAh0L3HHm5AjBMFYRqlM9SA==} + /@lezer/python@1.1.5: + resolution: {integrity: sha512-h0DVr6IfrmKUbTc5PeetaC87IZYoHyn5JogsVYW5mRDpVRyEsvaLBMLyEN4Ufc2BKp1c9y2Pkr8ZNLxS8dTLsQ==} dependencies: - '@lit-labs/ssr-dom-shim': 1.0.0 - dev: true + '@lezer/highlight': 1.1.1 + '@lezer/lr': 1.2.3 + dev: false /@mapbox/node-pre-gyp@1.0.10: resolution: {integrity: sha512-4ySo4CjzStuprMwk35H5pPbkymjv1SF3jGLj6rAHp/xT/RF7TL7bd9CTm1xDY49K2qF7jmR/g7k+SkLETP6opA==} @@ -4297,8 +5617,8 @@ packages: dev: false optional: true - /@n8n_io/license-sdk@2.1.0: - resolution: {integrity: sha512-SwIm9b6a30/fAvl1aY0a6cgoSyQBgKHX44M4Ykesn45VSGBKlzO5uuIiIcEPdVjjLEelm7u6wLoDFdIVG37b7Q==} + /@n8n_io/license-sdk@2.3.0: + resolution: {integrity: sha512-1qOg4VEi2mZzhAJ5Uh9IT9Jn/b3xCaxyFbovYLtymzy3ObafUyWieUrSQri3BrCbW1dwQHz99DEVFxYCq1Je0Q==} engines: {node: '>=14.0.0', npm: '>=7.10.0'} dependencies: crypto-js: 4.1.1 @@ -4313,6 +5633,14 @@ packages: eslint-config-riot: 1.0.0 dev: false + /@ndelangen/get-tarball@3.0.7: + resolution: {integrity: sha512-NqGfTZIZpRFef1GoVaShSSRwDC3vde3ThtTeqFdcYd6ipKqnfEVhjK2hUeHjCQUcptyZr2TONqcloFXM+5QBrQ==} + dependencies: + gunzip-maybe: 1.4.2 + pump: 3.0.0 + tar-fs: 2.1.1 + dev: true + /@ngneat/falso@6.1.0: resolution: {integrity: sha512-eka5OxW65B1RphpLJ04Pd4PEkrmTab/Ut50K0OceAdM+O+MZA7YF9xo51uZgkxbhg8bJ5zEh5vucDRMSofcqsw==} dependencies: @@ -4758,8 +6086,8 @@ packages: resolution: {integrity: sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==} dev: false - /@storybook/addon-actions@7.0.0-beta.46(react-dom@18.2.0)(react@17.0.2): - resolution: {integrity: sha512-4n/J/MgKsBxt4/p1tsm+EaC8Sq2frNjSL4gkUrMpCB8LASK0onVttd/kSpc41Pck+R/nK812nSrfUhLkEsAGiA==} + /@storybook/addon-actions@7.0.7(react-dom@18.2.0)(react@17.0.2): + resolution: {integrity: sha512-WxsnSjAvdf6NhUfTqcwV+FJmsJV56gh2cY4QnGfqfwO5zoBWTUYnhz57TgxSMhJY0kspyX9Q1Kc//r1d5lt1qA==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -4769,14 +6097,14 @@ packages: react-dom: optional: true dependencies: - '@storybook/client-logger': 7.0.0-beta.46 - '@storybook/components': 7.0.0-beta.46(react-dom@18.2.0)(react@17.0.2) - '@storybook/core-events': 7.0.0-beta.46 + '@storybook/client-logger': 7.0.7 + '@storybook/components': 7.0.7(react-dom@18.2.0)(react@17.0.2) + '@storybook/core-events': 7.0.7 '@storybook/global': 5.0.0 - '@storybook/manager-api': 7.0.0-beta.46(react-dom@18.2.0)(react@17.0.2) - '@storybook/preview-api': 7.0.0-beta.46 - '@storybook/theming': 7.0.0-beta.46(react-dom@18.2.0)(react@17.0.2) - '@storybook/types': 7.0.0-beta.46 + '@storybook/manager-api': 7.0.7(react-dom@18.2.0)(react@17.0.2) + '@storybook/preview-api': 7.0.7 + '@storybook/theming': 7.0.7(react-dom@18.2.0)(react@17.0.2) + '@storybook/types': 7.0.7 dequal: 2.0.3 lodash: 4.17.21 polished: 4.2.2 @@ -4786,13 +6114,11 @@ packages: react-inspector: 6.0.1(react@17.0.2) telejson: 7.0.4 ts-dedent: 2.2.0 - uuid-browser: 3.1.0 - transitivePeerDependencies: - - supports-color + uuid: 9.0.0 dev: true - /@storybook/addon-backgrounds@7.0.0-beta.46(react-dom@18.2.0)(react@17.0.2): - resolution: {integrity: sha512-u2stsYYsEUOiZFDLmAurjemy2yKkGmOgHVoF3LjMdUVejJCCOf/9VyMwqCoe3jif4N5xI5WB/CKtwu3OC5V/5Q==} + /@storybook/addon-backgrounds@7.0.7(react-dom@18.2.0)(react@17.0.2): + resolution: {integrity: sha512-DhT32K1+ti7MXY9oqt36b9jlg7iY68IP0ZQbR3gjShcsIXZpFqh18TQo0vwDY1ldqnBvkTk6Jd5vcxA8tfyshw==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -4802,24 +6128,22 @@ packages: react-dom: optional: true dependencies: - '@storybook/client-logger': 7.0.0-beta.46 - '@storybook/components': 7.0.0-beta.46(react-dom@18.2.0)(react@17.0.2) - '@storybook/core-events': 7.0.0-beta.46 + '@storybook/client-logger': 7.0.7 + '@storybook/components': 7.0.7(react-dom@18.2.0)(react@17.0.2) + '@storybook/core-events': 7.0.7 '@storybook/global': 5.0.0 - '@storybook/manager-api': 7.0.0-beta.46(react-dom@18.2.0)(react@17.0.2) - '@storybook/preview-api': 7.0.0-beta.46 - '@storybook/theming': 7.0.0-beta.46(react-dom@18.2.0)(react@17.0.2) - '@storybook/types': 7.0.0-beta.46 + '@storybook/manager-api': 7.0.7(react-dom@18.2.0)(react@17.0.2) + '@storybook/preview-api': 7.0.7 + '@storybook/theming': 7.0.7(react-dom@18.2.0)(react@17.0.2) + '@storybook/types': 7.0.7 memoizerific: 1.11.3 react: 17.0.2 react-dom: 18.2.0(react@17.0.2) ts-dedent: 2.2.0 - transitivePeerDependencies: - - supports-color dev: true - /@storybook/addon-controls@7.0.0-beta.46(react-dom@18.2.0)(react@17.0.2): - resolution: {integrity: sha512-iKi86tbeWtBduV6bu/eeXrUGzPi2RE4omN552QjGwMFqP05hJtK2jdrrFi5gYtdbTt/kaIsJGzKPrj9l887lMQ==} + /@storybook/addon-controls@7.0.7(react-dom@18.2.0)(react@17.0.2): + resolution: {integrity: sha512-/QEzleKoWRQ3i7KB32QvqDGcGMw4kG2BxEf0d+ymxd2SjoeL6kX2eHE0b4OxFPXiWUyTfXBFwmcI2Re3fRUJnQ==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -4829,15 +6153,15 @@ packages: react-dom: optional: true dependencies: - '@storybook/blocks': 7.0.0-beta.46(react-dom@18.2.0)(react@17.0.2) - '@storybook/client-logger': 7.0.0-beta.46 - '@storybook/components': 7.0.0-beta.46(react-dom@18.2.0)(react@17.0.2) - '@storybook/core-common': 7.0.0-beta.46 - '@storybook/manager-api': 7.0.0-beta.46(react-dom@18.2.0)(react@17.0.2) - '@storybook/node-logger': 7.0.0-beta.46 - '@storybook/preview-api': 7.0.0-beta.46 - '@storybook/theming': 7.0.0-beta.46(react-dom@18.2.0)(react@17.0.2) - '@storybook/types': 7.0.0-beta.46 + '@storybook/blocks': 7.0.7(react-dom@18.2.0)(react@17.0.2) + '@storybook/client-logger': 7.0.7 + '@storybook/components': 7.0.7(react-dom@18.2.0)(react@17.0.2) + '@storybook/core-common': 7.0.7 + '@storybook/manager-api': 7.0.7(react-dom@18.2.0)(react@17.0.2) + '@storybook/node-logger': 7.0.7 + '@storybook/preview-api': 7.0.7 + '@storybook/theming': 7.0.7(react-dom@18.2.0)(react@17.0.2) + '@storybook/types': 7.0.7 lodash: 4.17.21 react: 17.0.2 react-dom: 18.2.0(react@17.0.2) @@ -4846,32 +6170,29 @@ packages: - supports-color dev: true - /@storybook/addon-docs@7.0.0-beta.46(react-dom@18.2.0)(react@17.0.2): - resolution: {integrity: sha512-ksdDLwsNxnGHMTlQORRd7LF1J7O2YxXEj9rmoETBPQlKSj0UUW8yRf91mB8oHEbp9wbGC6BVHDUubPJMx9PCCw==} + /@storybook/addon-docs@7.0.7(react-dom@18.2.0)(react@17.0.2): + resolution: {integrity: sha512-5PT7aiTD6QPH+4CZLcv4PiUgWucD9JNGHVMRbQMEyFW6qbs87dHmu1m1uXIvx3BF5h3mTo4FHNAf8IQIq5HH9w==} peerDependencies: - '@storybook/mdx1-csf': '>=1.0.0-0' react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - peerDependenciesMeta: - '@storybook/mdx1-csf': - optional: true dependencies: '@babel/core': 7.20.12 '@babel/plugin-transform-react-jsx': 7.20.7(@babel/core@7.20.12) - '@jest/transform': 29.4.2 + '@jest/transform': 29.5.0 '@mdx-js/react': 2.3.0(react@17.0.2) - '@storybook/blocks': 7.0.0-beta.46(react-dom@18.2.0)(react@17.0.2) - '@storybook/client-logger': 7.0.0-beta.46 - '@storybook/components': 7.0.0-beta.46(react-dom@18.2.0)(react@17.0.2) - '@storybook/csf-plugin': 7.0.0-beta.46 - '@storybook/csf-tools': 7.0.0-beta.46 + '@storybook/blocks': 7.0.7(react-dom@18.2.0)(react@17.0.2) + '@storybook/client-logger': 7.0.7 + '@storybook/components': 7.0.7(react-dom@18.2.0)(react@17.0.2) + '@storybook/csf-plugin': 7.0.7 + '@storybook/csf-tools': 7.0.7 '@storybook/global': 5.0.0 - '@storybook/mdx2-csf': 1.0.0-next.8 - '@storybook/node-logger': 7.0.0-beta.46 - '@storybook/postinstall': 7.0.0-beta.46 - '@storybook/preview-api': 7.0.0-beta.46 - '@storybook/theming': 7.0.0-beta.46(react-dom@18.2.0)(react@17.0.2) - '@storybook/types': 7.0.0-beta.46 + '@storybook/mdx2-csf': 1.0.0 + '@storybook/node-logger': 7.0.7 + '@storybook/postinstall': 7.0.7 + '@storybook/preview-api': 7.0.7 + '@storybook/react-dom-shim': 7.0.7(react-dom@18.2.0)(react@17.0.2) + '@storybook/theming': 7.0.7(react-dom@18.2.0)(react@17.0.2) + '@storybook/types': 7.0.7 fs-extra: 11.1.0 react: 17.0.2 react-dom: 18.2.0(react@17.0.2) @@ -4882,45 +6203,42 @@ packages: - supports-color dev: true - /@storybook/addon-essentials@7.0.0-beta.46(react-dom@18.2.0)(react@17.0.2): - resolution: {integrity: sha512-Chvj4gezsIHzAIplAjJIHhRG5WMoVl4AVCyrTD3E5h/8qqXXsLMDWN3sOl6pl/11OV1F1bhM4fFjgJxn9eGoEA==} + /@storybook/addon-essentials@7.0.7(react-dom@18.2.0)(react@17.0.2): + resolution: {integrity: sha512-uNx0BvN1XP7cNnk/L4oiFQlEB/KABqOeIyI8/mhfIyTvvwo9uAYIQAyiwWuz9MFmofCNm7CgLNOUaEwNDkM4CA==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: - '@storybook/addon-actions': 7.0.0-beta.46(react-dom@18.2.0)(react@17.0.2) - '@storybook/addon-backgrounds': 7.0.0-beta.46(react-dom@18.2.0)(react@17.0.2) - '@storybook/addon-controls': 7.0.0-beta.46(react-dom@18.2.0)(react@17.0.2) - '@storybook/addon-docs': 7.0.0-beta.46(react-dom@18.2.0)(react@17.0.2) - '@storybook/addon-highlight': 7.0.0-beta.46 - '@storybook/addon-measure': 7.0.0-beta.46(react-dom@18.2.0)(react@17.0.2) - '@storybook/addon-outline': 7.0.0-beta.46(react-dom@18.2.0)(react@17.0.2) - '@storybook/addon-toolbars': 7.0.0-beta.46(react-dom@18.2.0)(react@17.0.2) - '@storybook/addon-viewport': 7.0.0-beta.46(react-dom@18.2.0)(react@17.0.2) - '@storybook/core-common': 7.0.0-beta.46 - '@storybook/manager-api': 7.0.0-beta.46(react-dom@18.2.0)(react@17.0.2) - '@storybook/node-logger': 7.0.0-beta.46 - '@storybook/preview-api': 7.0.0-beta.46 + '@storybook/addon-actions': 7.0.7(react-dom@18.2.0)(react@17.0.2) + '@storybook/addon-backgrounds': 7.0.7(react-dom@18.2.0)(react@17.0.2) + '@storybook/addon-controls': 7.0.7(react-dom@18.2.0)(react@17.0.2) + '@storybook/addon-docs': 7.0.7(react-dom@18.2.0)(react@17.0.2) + '@storybook/addon-highlight': 7.0.7 + '@storybook/addon-measure': 7.0.7(react-dom@18.2.0)(react@17.0.2) + '@storybook/addon-outline': 7.0.7(react-dom@18.2.0)(react@17.0.2) + '@storybook/addon-toolbars': 7.0.7(react-dom@18.2.0)(react@17.0.2) + '@storybook/addon-viewport': 7.0.7(react-dom@18.2.0)(react@17.0.2) + '@storybook/core-common': 7.0.7 + '@storybook/manager-api': 7.0.7(react-dom@18.2.0)(react@17.0.2) + '@storybook/node-logger': 7.0.7 + '@storybook/preview-api': 7.0.7 react: 17.0.2 react-dom: 18.2.0(react@17.0.2) ts-dedent: 2.2.0 transitivePeerDependencies: - - '@storybook/mdx1-csf' - supports-color dev: true - /@storybook/addon-highlight@7.0.0-beta.46: - resolution: {integrity: sha512-bMwOdhIRofIG/MJvR5TxFWDWoBWhVC8xTgiytjSjzpo4HxWxUspCAC7mrriZ0QZvHGeTm8uk/P/3IRcb1EPIZA==} + /@storybook/addon-highlight@7.0.7: + resolution: {integrity: sha512-expme2GzzCXX7/lL7UjCDi1Tfj+4LeNsAdWiurVLH7glK7yKPPeXXkIldbLP/XjJv4NKlqCwnNRHQx0vDLlE6g==} dependencies: - '@storybook/core-events': 7.0.0-beta.46 + '@storybook/core-events': 7.0.7 '@storybook/global': 5.0.0 - '@storybook/preview-api': 7.0.0-beta.46 - transitivePeerDependencies: - - supports-color + '@storybook/preview-api': 7.0.7 dev: true - /@storybook/addon-links@7.0.0-beta.46(react-dom@18.2.0)(react@17.0.2): - resolution: {integrity: sha512-m5WZ/AVzkb4sHvu/25CQ4EjlHTr+T9enh5cHZD9c5frEpFX4gwOczcYirMKFzEI93dDkv6LX1Yo51hOOpJrhrg==} + /@storybook/addon-links@7.0.7(react-dom@18.2.0)(react@17.0.2): + resolution: {integrity: sha512-DEjDxjHb3mT8Sdnx4In5Ev9gJ/XdjlHOq4iuy0wnMyrCV4wnzTQnIeSCx8nkrXFb314zc33JPnCcrb5pQoD5GQ==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -4930,24 +6248,22 @@ packages: react-dom: optional: true dependencies: - '@storybook/client-logger': 7.0.0-beta.46 - '@storybook/core-events': 7.0.0-beta.46 - '@storybook/csf': 0.0.2-next.11 + '@storybook/client-logger': 7.0.7 + '@storybook/core-events': 7.0.7 + '@storybook/csf': 0.1.0 '@storybook/global': 5.0.0 - '@storybook/manager-api': 7.0.0-beta.46(react-dom@18.2.0)(react@17.0.2) - '@storybook/preview-api': 7.0.0-beta.46 - '@storybook/router': 7.0.0-beta.46(react-dom@18.2.0)(react@17.0.2) - '@storybook/types': 7.0.0-beta.46 + '@storybook/manager-api': 7.0.7(react-dom@18.2.0)(react@17.0.2) + '@storybook/preview-api': 7.0.7 + '@storybook/router': 7.0.7(react-dom@18.2.0)(react@17.0.2) + '@storybook/types': 7.0.7 prop-types: 15.8.1 react: 17.0.2 react-dom: 18.2.0(react@17.0.2) ts-dedent: 2.2.0 - transitivePeerDependencies: - - supports-color dev: true - /@storybook/addon-measure@7.0.0-beta.46(react-dom@18.2.0)(react@17.0.2): - resolution: {integrity: sha512-jvwvcvJ9YEhScRy51GUTSVDQSbNnhbMYQXTbvWjuzSV3RJolSx+lT+ILNblF6QptK5GKpM0GBKGLd1e5oXUlhA==} + /@storybook/addon-measure@7.0.7(react-dom@18.2.0)(react@17.0.2): + resolution: {integrity: sha512-lb4wEIvIVF+ePx1sC+n9rDI0+49sRa6MWbcvZ+BhbAoCeGcX7uACQFdW6HyXolmBuZASsTnzVQ4KqzzvY1dSWw==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -4957,21 +6273,19 @@ packages: react-dom: optional: true dependencies: - '@storybook/client-logger': 7.0.0-beta.46 - '@storybook/components': 7.0.0-beta.46(react-dom@18.2.0)(react@17.0.2) - '@storybook/core-events': 7.0.0-beta.46 + '@storybook/client-logger': 7.0.7 + '@storybook/components': 7.0.7(react-dom@18.2.0)(react@17.0.2) + '@storybook/core-events': 7.0.7 '@storybook/global': 5.0.0 - '@storybook/manager-api': 7.0.0-beta.46(react-dom@18.2.0)(react@17.0.2) - '@storybook/preview-api': 7.0.0-beta.46 - '@storybook/types': 7.0.0-beta.46 + '@storybook/manager-api': 7.0.7(react-dom@18.2.0)(react@17.0.2) + '@storybook/preview-api': 7.0.7 + '@storybook/types': 7.0.7 react: 17.0.2 react-dom: 18.2.0(react@17.0.2) - transitivePeerDependencies: - - supports-color dev: true - /@storybook/addon-outline@7.0.0-beta.46(react-dom@18.2.0)(react@17.0.2): - resolution: {integrity: sha512-hENg4a2lDeTVM9xuI0nHRsL/QMVTlmvtr2dOEOUul9gtwSjmssZ3Hr8LIeujp2WbSOL8xwx9s1NP15LCb367YQ==} + /@storybook/addon-outline@7.0.7(react-dom@18.2.0)(react@17.0.2): + resolution: {integrity: sha512-AxbNZ4N1fXBTeMYM9tFudfW+Gzq7UikCjPxn5ax3Pde+zZjaEMppUxv5EMz4g5GIJupLYRmKH5pN0YcYoRLY6w==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -4981,18 +6295,16 @@ packages: react-dom: optional: true dependencies: - '@storybook/client-logger': 7.0.0-beta.46 - '@storybook/components': 7.0.0-beta.46(react-dom@18.2.0)(react@17.0.2) - '@storybook/core-events': 7.0.0-beta.46 + '@storybook/client-logger': 7.0.7 + '@storybook/components': 7.0.7(react-dom@18.2.0)(react@17.0.2) + '@storybook/core-events': 7.0.7 '@storybook/global': 5.0.0 - '@storybook/manager-api': 7.0.0-beta.46(react-dom@18.2.0)(react@17.0.2) - '@storybook/preview-api': 7.0.0-beta.46 - '@storybook/types': 7.0.0-beta.46 + '@storybook/manager-api': 7.0.7(react-dom@18.2.0)(react@17.0.2) + '@storybook/preview-api': 7.0.7 + '@storybook/types': 7.0.7 react: 17.0.2 react-dom: 18.2.0(react@17.0.2) ts-dedent: 2.2.0 - transitivePeerDependencies: - - supports-color dev: true /@storybook/addon-postcss@3.0.0-alpha.1(webpack@5.75.0): @@ -5008,8 +6320,8 @@ packages: - webpack dev: true - /@storybook/addon-toolbars@7.0.0-beta.46(react-dom@18.2.0)(react@17.0.2): - resolution: {integrity: sha512-kC12SbWTOROE7IO82Ctl/wX/MXVhwkPia0zj9DbGul2xAwj59L5ZlWMdM+bG73VpSgbFDRIy3UZikSDgXUs/Ig==} + /@storybook/addon-toolbars@7.0.7(react-dom@18.2.0)(react@17.0.2): + resolution: {integrity: sha512-/NkYHhU1VAz5lXjWuV8+ADWB84HzktvZv4jfiKX7Zzu6JVzrBu7FotQSWh3pDqqVwCB50RClUGtcHmSSac9CAQ==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -5019,19 +6331,17 @@ packages: react-dom: optional: true dependencies: - '@storybook/client-logger': 7.0.0-beta.46 - '@storybook/components': 7.0.0-beta.46(react-dom@18.2.0)(react@17.0.2) - '@storybook/manager-api': 7.0.0-beta.46(react-dom@18.2.0)(react@17.0.2) - '@storybook/preview-api': 7.0.0-beta.46 - '@storybook/theming': 7.0.0-beta.46(react-dom@18.2.0)(react@17.0.2) + '@storybook/client-logger': 7.0.7 + '@storybook/components': 7.0.7(react-dom@18.2.0)(react@17.0.2) + '@storybook/manager-api': 7.0.7(react-dom@18.2.0)(react@17.0.2) + '@storybook/preview-api': 7.0.7 + '@storybook/theming': 7.0.7(react-dom@18.2.0)(react@17.0.2) react: 17.0.2 react-dom: 18.2.0(react@17.0.2) - transitivePeerDependencies: - - supports-color dev: true - /@storybook/addon-viewport@7.0.0-beta.46(react-dom@18.2.0)(react@17.0.2): - resolution: {integrity: sha512-vKINgaKSl1czI54Kh3Q7Rx9KsxjdCg1aeKW2cFxztBi1OlcKxjb85hnfhreyGlSphjsKRaQtX9fPGPRwdcaPTQ==} + /@storybook/addon-viewport@7.0.7(react-dom@18.2.0)(react@17.0.2): + resolution: {integrity: sha512-znqhd8JFEFoXcAdwYhz1CwrCpVAzhuSyUVBUNDsDs+mgBEfGth4D4abIdWWGcfP6+CmI5ebFHtk443cExZebag==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -5041,19 +6351,17 @@ packages: react-dom: optional: true dependencies: - '@storybook/client-logger': 7.0.0-beta.46 - '@storybook/components': 7.0.0-beta.46(react-dom@18.2.0)(react@17.0.2) - '@storybook/core-events': 7.0.0-beta.46 + '@storybook/client-logger': 7.0.7 + '@storybook/components': 7.0.7(react-dom@18.2.0)(react@17.0.2) + '@storybook/core-events': 7.0.7 '@storybook/global': 5.0.0 - '@storybook/manager-api': 7.0.0-beta.46(react-dom@18.2.0)(react@17.0.2) - '@storybook/preview-api': 7.0.0-beta.46 - '@storybook/theming': 7.0.0-beta.46(react-dom@18.2.0)(react@17.0.2) + '@storybook/manager-api': 7.0.7(react-dom@18.2.0)(react@17.0.2) + '@storybook/preview-api': 7.0.7 + '@storybook/theming': 7.0.7(react-dom@18.2.0)(react@17.0.2) memoizerific: 1.11.3 prop-types: 15.8.1 react: 17.0.2 react-dom: 18.2.0(react@17.0.2) - transitivePeerDependencies: - - supports-color dev: true /@storybook/addons@6.5.15(react-dom@18.2.0)(react@17.0.2): @@ -5077,19 +6385,17 @@ packages: regenerator-runtime: 0.13.11 dev: true - /@storybook/addons@7.0.0-beta.46(react-dom@18.2.0)(react@17.0.2): - resolution: {integrity: sha512-p2My3VkxPm3bzpV+giiM9zXt0fWnBN0UI4CXOroXlzPiTuxpJJcSXY65K4hHVuV4tvVRX023kQ0r+sEjxEaQSg==} + /@storybook/addons@7.0.7(react-dom@18.2.0)(react@17.0.2): + resolution: {integrity: sha512-it8NWXsdm3dhjc237d9jj7dGJf6eHDfuDv12nirV64J1dzWrnW+lONeZMPMgxxdLlgYfxH52fLgjcw/dAC/E+Q==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: - '@storybook/manager-api': 7.0.0-beta.46(react-dom@18.2.0)(react@17.0.2) - '@storybook/preview-api': 7.0.0-beta.46 - '@storybook/types': 7.0.0-beta.46 + '@storybook/manager-api': 7.0.7(react-dom@18.2.0)(react@17.0.2) + '@storybook/preview-api': 7.0.7 + '@storybook/types': 7.0.7 react: 17.0.2 react-dom: 18.2.0(react@17.0.2) - transitivePeerDependencies: - - supports-color dev: true /@storybook/api@6.5.15(react-dom@18.2.0)(react@17.0.2): @@ -5119,34 +6425,40 @@ packages: util-deprecate: 1.0.2 dev: true - /@storybook/api@7.0.0-beta.46(react-dom@18.2.0)(react@17.0.2): - resolution: {integrity: sha512-S42ZsT8uU2FNrCHVOtNEm5Wqebymu9S6iWXN/QSreQdrX2mvqcvbPbcFOa2nfNnUiBHrCjzF2ZySV1WnsJxtOQ==} + /@storybook/api@7.0.7(react-dom@18.2.0)(react@17.0.2): + resolution: {integrity: sha512-0++LcK6PX1Z2HsI9fyZyqvmeFrB5NDMcsbmIvJfA2NfK92UW8y7t6Ft2fq/2jUCJcWT8Jp3xpatUvYb28irfwg==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true dependencies: - '@storybook/client-logger': 7.0.0-beta.46 - '@storybook/manager-api': 7.0.0-beta.46(react-dom@18.2.0)(react@17.0.2) - transitivePeerDependencies: - - react - - react-dom - - supports-color + '@storybook/client-logger': 7.0.7 + '@storybook/manager-api': 7.0.7(react-dom@18.2.0)(react@17.0.2) + react: 17.0.2 + react-dom: 18.2.0(react@17.0.2) dev: true - /@storybook/blocks@7.0.0-beta.46(react-dom@18.2.0)(react@17.0.2): - resolution: {integrity: sha512-ziNMCAtJbZ2RnWx95iA7TAYRKugjDbt7/qp0yNHrEpwS5ddQOb3O6Yb7BbNDrAhhJ/oapDtyelSZJYM7BL6gSQ==} + /@storybook/blocks@7.0.7(react-dom@18.2.0)(react@17.0.2): + resolution: {integrity: sha512-ehR0hAFWNHHqmrmbwYPKhLpgbIBKtyMbeoGClTRSnrVBGONciYJdmxegkCTReUklCY+HBJjtlwNowT+7+5sSaw==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: - '@storybook/channels': 7.0.0-beta.46 - '@storybook/client-logger': 7.0.0-beta.46 - '@storybook/components': 7.0.0-beta.46(react-dom@18.2.0)(react@17.0.2) - '@storybook/core-events': 7.0.0-beta.46 - '@storybook/csf': 0.0.2-next.11 - '@storybook/docs-tools': 7.0.0-beta.46 + '@storybook/channels': 7.0.7 + '@storybook/client-logger': 7.0.7 + '@storybook/components': 7.0.7(react-dom@18.2.0)(react@17.0.2) + '@storybook/core-events': 7.0.7 + '@storybook/csf': 0.1.0 + '@storybook/docs-tools': 7.0.7 '@storybook/global': 5.0.0 - '@storybook/manager-api': 7.0.0-beta.46(react-dom@18.2.0)(react@17.0.2) - '@storybook/preview-api': 7.0.0-beta.46 - '@storybook/theming': 7.0.0-beta.46(react-dom@18.2.0)(react@17.0.2) - '@storybook/types': 7.0.0-beta.46 + '@storybook/manager-api': 7.0.7(react-dom@18.2.0)(react@17.0.2) + '@storybook/preview-api': 7.0.7 + '@storybook/theming': 7.0.7(react-dom@18.2.0)(react@17.0.2) + '@storybook/types': 7.0.7 '@types/lodash': 4.14.191 color-convert: 2.0.1 dequal: 2.0.3 @@ -5157,38 +6469,38 @@ packages: react: 17.0.2 react-colorful: 5.6.1(react-dom@18.2.0)(react@17.0.2) react-dom: 18.2.0(react@17.0.2) + telejson: 7.0.4 ts-dedent: 2.2.0 util-deprecate: 1.0.2 transitivePeerDependencies: - supports-color dev: true - /@storybook/builder-manager@7.0.0-beta.46: - resolution: {integrity: sha512-uQ4pAr2/gcdgZPmnxvA51VQxyZGh44TuDuoJB1IOcIenh/U5PPZ5VqCDrB8ONU46bKy32uzxz/Xpd9nHedIUiA==} + /@storybook/builder-manager@7.0.7: + resolution: {integrity: sha512-VI/0iEjAlzQDt1yKu8GXugNIz7t46IHIKgMNmltQ05KPypMgInUoMmbfP5AYOVddjLdSqjMLO7EK58pBLOInpw==} dependencies: '@fal-works/esbuild-plugin-global-externals': 2.1.2 - '@storybook/core-common': 7.0.0-beta.46 - '@storybook/manager': 7.0.0-beta.46 - '@storybook/node-logger': 7.0.0-beta.46 + '@storybook/core-common': 7.0.7 + '@storybook/manager': 7.0.7 + '@storybook/node-logger': 7.0.7 '@types/ejs': 3.1.1 '@types/find-cache-dir': 3.2.1 - '@yarnpkg/esbuild-plugin-pnp': 3.0.0-rc.15(esbuild@0.16.17) + '@yarnpkg/esbuild-plugin-pnp': 3.0.0-rc.15(esbuild@0.17.18) browser-assert: 1.2.1 ejs: 3.1.8 - esbuild: 0.16.17 + esbuild: 0.17.18 esbuild-plugin-alias: 0.2.1 express: 4.18.2 find-cache-dir: 3.3.2 fs-extra: 11.1.0 process: 0.11.10 - slash: 3.0.0 util: 0.12.5 transitivePeerDependencies: - supports-color dev: true - /@storybook/builder-webpack5@7.0.0-beta.46(esbuild@0.16.17)(react-dom@18.2.0)(react@17.0.2)(typescript@5.0.3)(vue-template-compiler@2.7.14): - resolution: {integrity: sha512-1pvo9IDlz836hiDFmALVttQxw4ehSd3UEhE6Yz0B+TMl4cAQuSRWze8w2sI474tiHtGeKGRoIIWmH5hSVQvFrQ==} + /@storybook/builder-webpack5@7.0.7(esbuild@0.17.18)(react-dom@18.2.0)(react@17.0.2)(typescript@5.0.3)(vue-template-compiler@2.7.14): + resolution: {integrity: sha512-VdPPgQau9150arge3IVW+CkVBNwkQgmtJlUst7pIe/7lTP/m+ClWLTfhdkZHZk5Qb24EAdq6rmEO3LlvRBMUAg==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -5198,25 +6510,25 @@ packages: optional: true dependencies: '@babel/core': 7.20.12 - '@storybook/addons': 7.0.0-beta.46(react-dom@18.2.0)(react@17.0.2) - '@storybook/api': 7.0.0-beta.46(react-dom@18.2.0)(react@17.0.2) - '@storybook/channel-postmessage': 7.0.0-beta.46 - '@storybook/channel-websocket': 7.0.0-beta.46 - '@storybook/channels': 7.0.0-beta.46 - '@storybook/client-api': 7.0.0-beta.46 - '@storybook/client-logger': 7.0.0-beta.46 - '@storybook/components': 7.0.0-beta.46(react-dom@18.2.0)(react@17.0.2) - '@storybook/core-common': 7.0.0-beta.46 - '@storybook/core-events': 7.0.0-beta.46 - '@storybook/core-webpack': 7.0.0-beta.46 + '@storybook/addons': 7.0.7(react-dom@18.2.0)(react@17.0.2) + '@storybook/api': 7.0.7(react-dom@18.2.0)(react@17.0.2) + '@storybook/channel-postmessage': 7.0.7 + '@storybook/channel-websocket': 7.0.7 + '@storybook/channels': 7.0.7 + '@storybook/client-api': 7.0.7 + '@storybook/client-logger': 7.0.7 + '@storybook/components': 7.0.7(react-dom@18.2.0)(react@17.0.2) + '@storybook/core-common': 7.0.7 + '@storybook/core-events': 7.0.7 + '@storybook/core-webpack': 7.0.7 '@storybook/global': 5.0.0 - '@storybook/manager-api': 7.0.0-beta.46(react-dom@18.2.0)(react@17.0.2) - '@storybook/node-logger': 7.0.0-beta.46 - '@storybook/preview': 7.0.0-beta.46 - '@storybook/preview-api': 7.0.0-beta.46 - '@storybook/router': 7.0.0-beta.46(react-dom@18.2.0)(react@17.0.2) - '@storybook/store': 7.0.0-beta.46 - '@storybook/theming': 7.0.0-beta.46(react-dom@18.2.0)(react@17.0.2) + '@storybook/manager-api': 7.0.7(react-dom@18.2.0)(react@17.0.2) + '@storybook/node-logger': 7.0.7 + '@storybook/preview': 7.0.7 + '@storybook/preview-api': 7.0.7 + '@storybook/router': 7.0.7(react-dom@18.2.0)(react@17.0.2) + '@storybook/store': 7.0.7 + '@storybook/theming': 7.0.7(react-dom@18.2.0)(react@17.0.2) '@types/node': 16.18.12 '@types/semver': 7.3.13 babel-loader: 9.1.2(@babel/core@7.20.12)(webpack@5.75.0) @@ -5233,14 +6545,13 @@ packages: react: 17.0.2 react-dom: 18.2.0(react@17.0.2) semver: 7.3.8 - slash: 3.0.0 style-loader: 3.3.1(webpack@5.75.0) - terser-webpack-plugin: 5.3.6(esbuild@0.16.17)(webpack@5.75.0) + terser-webpack-plugin: 5.3.6(esbuild@0.17.18)(webpack@5.75.0) ts-dedent: 2.2.0 typescript: 5.0.3 util: 0.12.5 util-deprecate: 1.0.2 - webpack: 5.75.0(esbuild@0.16.17) + webpack: 5.75.0(esbuild@0.17.18) webpack-dev-middleware: 5.3.3(webpack@5.75.0) webpack-hot-middleware: 2.25.3 webpack-virtual-modules: 0.4.6 @@ -5254,22 +6565,22 @@ packages: - webpack-cli dev: true - /@storybook/channel-postmessage@7.0.0-beta.46: - resolution: {integrity: sha512-bJ7/9J1vHrm4czBzB1gNV6N6eYVP2JQJjteMjyul1ujS6PhC243GB2PTlQDwrE9MuPGIP1Qv6dSgs9bMQeXNVg==} + /@storybook/channel-postmessage@7.0.7: + resolution: {integrity: sha512-XMtYfcaE0UoY/V7K1cTu9PcWETD4iyWb/Yswc4F9VrPw0Ui4UwGS1j4iaAu8DC06yyoJs4XvxYFBMlCQmKja6A==} dependencies: - '@storybook/channels': 7.0.0-beta.46 - '@storybook/client-logger': 7.0.0-beta.46 - '@storybook/core-events': 7.0.0-beta.46 + '@storybook/channels': 7.0.7 + '@storybook/client-logger': 7.0.7 + '@storybook/core-events': 7.0.7 '@storybook/global': 5.0.0 qs: 6.11.0 telejson: 7.0.4 dev: true - /@storybook/channel-websocket@7.0.0-beta.46: - resolution: {integrity: sha512-Y/d3aXpytu8yVws2sPrFyXPvtM+clAfo4WU5g3YvpPMO/K6bohfpVtucLrHIvq/lFDdoK3nWwi9AhqPe2E1YKg==} + /@storybook/channel-websocket@7.0.7: + resolution: {integrity: sha512-KDbLiQts4/dCow3qk5WJSPA6SlaX3iP9RhF0Fjj03hoG2TRskrvo+AkUiJr8gF6dpkPndfuCYUCRsO2Ml8B+AA==} dependencies: - '@storybook/channels': 7.0.0-beta.46 - '@storybook/client-logger': 7.0.0-beta.46 + '@storybook/channels': 7.0.7 + '@storybook/client-logger': 7.0.7 '@storybook/global': 5.0.0 telejson: 7.0.4 dev: true @@ -5282,23 +6593,24 @@ packages: util-deprecate: 1.0.2 dev: true - /@storybook/channels@7.0.0-beta.46: - resolution: {integrity: sha512-fNsUiWe+K+jnHyeG1WrUrJDGHmO/QLOek0Ly+5j2LhV7PG4S/33IUVShux4yfZIMJwtrVtaawM306q1uJofFaw==} + /@storybook/channels@7.0.7: + resolution: {integrity: sha512-Om4ovBLNw8pVrBu83MpOKgAuGO9Dpr1Coh2qp8t64WRPkejX1mxOY9IgH723//zH3igx8LCkf9rvBvcrsyaScQ==} dev: true - /@storybook/cli@7.0.0-beta.46: - resolution: {integrity: sha512-4JTTnW0GlQzp6KCPMRxNjKwpFoaywXAUn2pXfXaI44WCLdT7IjHEEvK5Z5kwAW9cc+2mGT7tMqy0Xwd+acvMDQ==} + /@storybook/cli@7.0.7: + resolution: {integrity: sha512-koTkWr7wlaHF14T5moRP/tYM44+Jf4GEzQ/rqx/Jfn7EbNlVUOibdLJj4JnseMGRc7ZP6tKYku2n+B8g7hJX4w==} hasBin: true dependencies: '@babel/core': 7.20.12 '@babel/preset-env': 7.20.2(@babel/core@7.20.12) - '@storybook/codemod': 7.0.0-beta.46 - '@storybook/core-common': 7.0.0-beta.46 - '@storybook/core-server': 7.0.0-beta.46 - '@storybook/csf-tools': 7.0.0-beta.46 - '@storybook/node-logger': 7.0.0-beta.46 - '@storybook/telemetry': 7.0.0-beta.46 - '@storybook/types': 7.0.0-beta.46 + '@ndelangen/get-tarball': 3.0.7 + '@storybook/codemod': 7.0.7 + '@storybook/core-common': 7.0.7 + '@storybook/core-server': 7.0.7 + '@storybook/csf-tools': 7.0.7 + '@storybook/node-logger': 7.0.7 + '@storybook/telemetry': 7.0.7 + '@storybook/types': 7.0.7 '@types/semver': 7.3.13 boxen: 5.1.2 chalk: 4.1.2 @@ -5310,11 +6622,13 @@ packages: express: 4.18.2 find-up: 5.0.0 fs-extra: 11.1.0 + get-npm-tarball-url: 2.0.3 get-port: 5.1.1 giget: 1.0.0 globby: 11.1.0 jscodeshift: 0.14.0(@babel/preset-env@7.20.2) leven: 3.1.0 + prettier: 2.8.3 prompts: 2.4.2 puppeteer-core: 2.1.1 read-pkg-up: 7.0.1 @@ -5332,13 +6646,11 @@ packages: - utf-8-validate dev: true - /@storybook/client-api@7.0.0-beta.46: - resolution: {integrity: sha512-cpCI6tMExvgfI+xfuzZItFrntz4PTTWSek9RG8E5SGZjKGg0vEuCaEBNIOK8PX5sRvqbFCfg6xqLZh8ZwW8Upg==} + /@storybook/client-api@7.0.7: + resolution: {integrity: sha512-O8HLBWR4TVuUifF8RBSi5++TjetSlQ6bgtGEWLkmPB9+135gczJuRYAZ7xbNw2xACzFa5KWka4OE30+MXOUiDA==} dependencies: - '@storybook/client-logger': 7.0.0-beta.46 - '@storybook/preview-api': 7.0.0-beta.46 - transitivePeerDependencies: - - supports-color + '@storybook/client-logger': 7.0.7 + '@storybook/preview-api': 7.0.7 dev: true /@storybook/client-logger@6.5.15: @@ -5348,29 +6660,28 @@ packages: global: 4.4.0 dev: true - /@storybook/client-logger@7.0.0-beta.46: - resolution: {integrity: sha512-dehsFk2DDrxY1l+HtsILCw4MQTw4/7XBpAWK01dR4xOagCZvmGJqVRouDVqZJBB3NAEw1uSKR5Jy+2teu9sCOA==} + /@storybook/client-logger@7.0.7: + resolution: {integrity: sha512-EclHjDs5HwHMKB4X2orn/KKA0DTIDmp4AXAUJGRfxb5ArpKEb7tXLHsgrRBlaoz1j5LAwKTmEyZOONh9G3etjg==} dependencies: '@storybook/global': 5.0.0 dev: true - /@storybook/codemod@7.0.0-beta.46: - resolution: {integrity: sha512-UZZIwogqqpsdkSNBWXZgpYjRIQN/cIJIkwL0Tt7fba6sx31HMCUdyiTBXQC6YvzoF/w2FWx/JRFBFqs9E7XpjA==} + /@storybook/codemod@7.0.7: + resolution: {integrity: sha512-VlkDlkvfbzLe+NOmzs5zGrGb4jnaeAFZqpvIkXxevr6aGcOwgeelNv8gTmgBAcy+xbGW4Pp0XA2BlMweIvKEKA==} dependencies: - '@babel/core': 7.20.12 - '@babel/preset-env': 7.20.2(@babel/core@7.20.12) - '@babel/types': 7.20.7 - '@storybook/csf': 0.0.2-next.11 - '@storybook/csf-tools': 7.0.0-beta.46 - '@storybook/node-logger': 7.0.0-beta.46 - '@storybook/types': 7.0.0-beta.46 + '@babel/core': 7.21.8 + '@babel/preset-env': 7.21.5(@babel/core@7.21.8) + '@babel/types': 7.21.5 + '@storybook/csf': 0.1.0 + '@storybook/csf-tools': 7.0.7 + '@storybook/node-logger': 7.0.7 + '@storybook/types': 7.0.7 cross-spawn: 7.0.3 globby: 11.1.0 - jscodeshift: 0.14.0(@babel/preset-env@7.20.2) + jscodeshift: 0.14.0(@babel/preset-env@7.21.5) lodash: 4.17.21 prettier: 2.8.3 recast: 0.23.1 - util: 0.12.5 transitivePeerDependencies: - supports-color dev: true @@ -5393,49 +6704,41 @@ packages: util-deprecate: 1.0.2 dev: true - /@storybook/components@7.0.0-beta.46(react-dom@18.2.0)(react@17.0.2): - resolution: {integrity: sha512-/rwNE7BHJ7RXDNC53yQCAb1JH56QJG7n258jxDJ0OSCAtTsaxfa2VPVJHDqEllnOh2VsbC1jF5msfGHoMbAV0A==} + /@storybook/components@7.0.7(react-dom@18.2.0)(react@17.0.2): + resolution: {integrity: sha512-6PLs9LMkBuhH/w4bSJ72tYgICMbOOIHuoB/fQdVlzhsdnXL2fM/v4RVW2N7v+Oz3lYXp/JtV8V9Ub8h6eDQKXg==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: - '@storybook/client-logger': 7.0.0-beta.46 - '@storybook/csf': 0.0.2-next.11 + '@storybook/client-logger': 7.0.7 + '@storybook/csf': 0.1.0 '@storybook/global': 5.0.0 - '@storybook/theming': 7.0.0-beta.46(react-dom@18.2.0)(react@17.0.2) - '@storybook/types': 7.0.0-beta.46 + '@storybook/theming': 7.0.7(react-dom@18.2.0)(react@17.0.2) + '@storybook/types': 7.0.7 memoizerific: 1.11.3 react: 17.0.2 react-dom: 18.2.0(react@17.0.2) use-resize-observer: 9.1.0(react-dom@18.2.0)(react@17.0.2) util-deprecate: 1.0.2 - transitivePeerDependencies: - - supports-color dev: true - /@storybook/core-client@7.0.0-beta.46: - resolution: {integrity: sha512-XEYjS6z+HL5tpriLFSoWPnJbZ04PdN1YNb3mn0GhPUZuCBWqjssH1T5gt5w5L2ZEbsiWBLHBbs34tQLWCGsnTg==} + /@storybook/core-client@7.0.7: + resolution: {integrity: sha512-eydcpR28qV3A3BwR5V6wsixoI1BRLA0SzFiwH/1ajrgX13inv+gV97gHv47Ojf/+YAZ3HqdVaUKFsUfMKwKieA==} dependencies: - '@storybook/client-logger': 7.0.0-beta.46 - '@storybook/preview-api': 7.0.0-beta.46 - transitivePeerDependencies: - - supports-color + '@storybook/client-logger': 7.0.7 + '@storybook/preview-api': 7.0.7 dev: true - /@storybook/core-common@7.0.0-beta.46: - resolution: {integrity: sha512-NerwE0237uqFpnLMWrsk0J0IAU3A/VD7GBiCWdySz4sgKFv2aFV9m/Rbucoo4jHhAbZs2B4a+q4vvVIZvse0Hw==} + /@storybook/core-common@7.0.7: + resolution: {integrity: sha512-c8T24wex9bnCYdZVZFNX4VV+wfhrp47OLzVONZDqxMhq6G//Bgv5zH4Awcx5UfWf/05VcP7KGF1VKj8ebRyEEA==} dependencies: - '@babel/core': 7.20.12 - '@storybook/node-logger': 7.0.0-beta.46 - '@storybook/types': 7.0.0-beta.46 - '@types/babel__core': 7.20.0 - '@types/express': 4.17.14 + '@storybook/node-logger': 7.0.7 + '@storybook/types': 7.0.7 '@types/node': 16.18.12 '@types/pretty-hrtime': 1.0.1 chalk: 4.1.2 - esbuild: 0.16.17 - esbuild-register: 3.4.2(esbuild@0.16.17) - express: 4.18.2 + esbuild: 0.17.18 + esbuild-register: 3.4.2(esbuild@0.17.18) file-system-cache: 2.0.2 find-up: 5.0.0 fs-extra: 11.1.0 @@ -5447,7 +6750,6 @@ packages: pkg-dir: 5.0.0 pretty-hrtime: 1.0.3 resolve-from: 5.0.0 - slash: 3.0.0 ts-dedent: 2.2.0 transitivePeerDependencies: - supports-color @@ -5459,27 +6761,27 @@ packages: core-js: 3.27.2 dev: true - /@storybook/core-events@7.0.0-beta.46: - resolution: {integrity: sha512-ogC+bHeMsmpZuEiKWCUdgncATgDsnPTDqumgV99fXDWrYpt2KUePsZSBSTaI97lTZqko3WN1GTBbqqZ0OrerHw==} + /@storybook/core-events@7.0.7: + resolution: {integrity: sha512-XNsR2RgaL2vBwuqsu+KA1DzGmB1UFfrAhpxhmyWTKDCniwtTLlaXgfKbqwcrOrPu/o1YswgIup/9UHepRHaf4A==} dev: true - /@storybook/core-server@7.0.0-beta.46: - resolution: {integrity: sha512-QKEfI1Aoi69fnP52UzZfG6DEtc+gWKqJjbTiIq/RA+4OXEQiRne/Z8I2NopSCrTKJTwidEJ2K0OcVimhFxHERQ==} + /@storybook/core-server@7.0.7: + resolution: {integrity: sha512-PB4zoClH7aKG4XeJhxx43iK9n/C9gctXubNN5DSN6thPm4UITOas+/q4N7AHbCPyRbcMyoW7M31KtpzZu4Fjew==} dependencies: '@aw-web-design/x-default-browser': 1.4.88 '@discoveryjs/json-ext': 0.5.7 - '@storybook/builder-manager': 7.0.0-beta.46 - '@storybook/core-common': 7.0.0-beta.46 - '@storybook/core-events': 7.0.0-beta.46 - '@storybook/csf': 0.0.2-next.11 - '@storybook/csf-tools': 7.0.0-beta.46 - '@storybook/docs-mdx': 0.0.1-next.7 + '@storybook/builder-manager': 7.0.7 + '@storybook/core-common': 7.0.7 + '@storybook/core-events': 7.0.7 + '@storybook/csf': 0.1.0 + '@storybook/csf-tools': 7.0.7 + '@storybook/docs-mdx': 0.1.0 '@storybook/global': 5.0.0 - '@storybook/manager': 7.0.0-beta.46 - '@storybook/node-logger': 7.0.0-beta.46 - '@storybook/preview-api': 7.0.0-beta.46 - '@storybook/telemetry': 7.0.0-beta.46 - '@storybook/types': 7.0.0-beta.46 + '@storybook/manager': 7.0.7 + '@storybook/node-logger': 7.0.7 + '@storybook/preview-api': 7.0.7 + '@storybook/telemetry': 7.0.7 + '@storybook/types': 7.0.7 '@types/detect-port': 1.3.2 '@types/node': 16.18.12 '@types/node-fetch': 2.6.2 @@ -5503,7 +6805,6 @@ packages: read-pkg-up: 7.0.1 semver: 7.3.8 serve-favicon: 2.5.0 - slash: 3.0.0 telejson: 7.0.4 ts-dedent: 2.2.0 util-deprecate: 1.0.2 @@ -5516,33 +6817,36 @@ packages: - utf-8-validate dev: true - /@storybook/core-webpack@7.0.0-beta.46: - resolution: {integrity: sha512-YpPLECzWcZQ1+U78FkX5Ec9bOpx7tOqoBpBgQP4gGShZZA6v3oe8Q8UDAe84SzNxkH/d/AWsIpu0bIhK/WWgtg==} + /@storybook/core-webpack@7.0.7: + resolution: {integrity: sha512-6s2PN4yoaIubs/Lv02/un9zQd/OSxFmx+dLStz3nI6JWzHjboIkIwjU3aryJuhM0OrQ/3Sxmv1WxgMiCnSCagg==} dependencies: - '@storybook/core-common': 7.0.0-beta.46 - '@storybook/node-logger': 7.0.0-beta.46 - '@storybook/types': 7.0.0-beta.46 + '@storybook/core-common': 7.0.7 + '@storybook/node-logger': 7.0.7 + '@storybook/types': 7.0.7 '@types/node': 16.18.12 ts-dedent: 2.2.0 transitivePeerDependencies: - supports-color dev: true - /@storybook/csf-plugin@7.0.0-beta.46: - resolution: {integrity: sha512-8FQKNajJQWCF1NA8MGZI0+GtlulLSBRmj4e7cP+0NCVs49ypslRpWQRFMRaTibYgMlOvCsGpZLgYMNghRbiJSg==} + /@storybook/csf-plugin@7.0.7: + resolution: {integrity: sha512-uhf2g077gXA6ZEMXIPQ0RnX+IoOTBJbj+6+VQfT7K5tvJeop1z0Fvk0FoknNXcUe7aUA0nzA/cUQ1v4vXqbY3Q==} dependencies: - '@storybook/csf-tools': 7.0.0-beta.46 + '@storybook/csf-tools': 7.0.7 unplugin: 0.10.2 transitivePeerDependencies: - supports-color dev: true - /@storybook/csf-tools@7.0.0-beta.46: - resolution: {integrity: sha512-H7zXfL1wf/1jWi5MaFISt/taxE41fgpV/uLfi5CHcHLX9ZgeQs2B/2utpUgwvBsxiL+E/jKAt5cLeuZCIvglMg==} + /@storybook/csf-tools@7.0.7: + resolution: {integrity: sha512-KbO5K2RS0oFm94eR49bAPvoyXY3Q6+ozvBek/F05RP7iAV790icQc59Xci9YDM1ONgb3afS+gSJGFBsE0h4pmg==} dependencies: - '@babel/types': 7.20.7 - '@storybook/csf': 0.0.2-next.11 - '@storybook/types': 7.0.0-beta.46 + '@babel/generator': 7.21.5 + '@babel/parser': 7.21.8 + '@babel/traverse': 7.21.5 + '@babel/types': 7.21.5 + '@storybook/csf': 0.1.0 + '@storybook/types': 7.0.7 fs-extra: 11.1.0 recast: 0.23.1 ts-dedent: 2.2.0 @@ -5556,23 +6860,23 @@ packages: lodash: 4.17.21 dev: true - /@storybook/csf@0.0.2-next.11: - resolution: {integrity: sha512-xGt0YSVxZb43sKmEf1GIQD8xEbo+c+S6khDEL7Qu/pYA0gh5z3WUuhOlovnelYj/YJod+XRsfVvk23AaRfUJ4Q==} + /@storybook/csf@0.1.0: + resolution: {integrity: sha512-uk+jMXCZ8t38jSTHk2o5btI+aV2Ksbvl6DoOv3r6VaCM1KZqeuMwtwywIQdflkA8/6q/dKT8z8L+g8hC4GC3VQ==} dependencies: type-fest: 2.19.0 dev: true - /@storybook/docs-mdx@0.0.1-next.7: - resolution: {integrity: sha512-JbgBf/EMBtx65iXtB3pOiX3818UeL9jZ+KAY241OAPqJVXjMQ5KaVOdg/57MSmd508HDIGx7CiImOMEmWwQ9/g==} + /@storybook/docs-mdx@0.1.0: + resolution: {integrity: sha512-JDaBR9lwVY4eSH5W8EGHrhODjygPd6QImRbwjAuJNEnY0Vw4ie3bPkeGfnacB3OBW6u/agqPv2aRlR46JcAQLg==} dev: true - /@storybook/docs-tools@7.0.0-beta.46: - resolution: {integrity: sha512-lTZyjz6PspoCEpelIKznEfoHCQIchxhtHAtU/OJIbZIk1Hu2AuMOOJiD7yEC8lD2yhlO4XNR/ZegxPidzdPfeA==} + /@storybook/docs-tools@7.0.7: + resolution: {integrity: sha512-VB4Qa33DYNxXALWcXyDid246r9Q6SGM+Q+pGWOuEJsxRxDmrUspXHaHG0CO1NIjMWfbqpOoz61vquZO0GZoAAg==} dependencies: '@babel/core': 7.20.12 - '@storybook/core-common': 7.0.0-beta.46 - '@storybook/preview-api': 7.0.0-beta.46 - '@storybook/types': 7.0.0-beta.46 + '@storybook/core-common': 7.0.7 + '@storybook/preview-api': 7.0.7 + '@storybook/types': 7.0.7 '@types/doctrine': 0.0.3 doctrine: 3.0.0 lodash: 4.17.21 @@ -5584,20 +6888,20 @@ packages: resolution: {integrity: sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ==} dev: true - /@storybook/manager-api@7.0.0-beta.46(react-dom@18.2.0)(react@17.0.2): - resolution: {integrity: sha512-9GvE1h/jbQ2rkMXahJ4oHlPynyffXwIlKiDFgdDyauZquh7uLDc+ANLLoppzf4EGD7QVcGJ31SU9EUhTjqYQCw==} + /@storybook/manager-api@7.0.7(react-dom@18.2.0)(react@17.0.2): + resolution: {integrity: sha512-QTd/P72peAhofKqK+8yzIO9iWAEfPn8WUGGveV2KGaTlSlgbr87RLHEKilcXMZcYhBWC9izFRmjKum9ROdskrQ==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: - '@storybook/channels': 7.0.0-beta.46 - '@storybook/client-logger': 7.0.0-beta.46 - '@storybook/core-events': 7.0.0-beta.46 - '@storybook/csf': 0.0.2-next.11 + '@storybook/channels': 7.0.7 + '@storybook/client-logger': 7.0.7 + '@storybook/core-events': 7.0.7 + '@storybook/csf': 0.1.0 '@storybook/global': 5.0.0 - '@storybook/router': 7.0.0-beta.46(react-dom@18.2.0)(react@17.0.2) - '@storybook/theming': 7.0.0-beta.46(react-dom@18.2.0)(react@17.0.2) - '@storybook/types': 7.0.0-beta.46 + '@storybook/router': 7.0.7(react-dom@18.2.0)(react@17.0.2) + '@storybook/theming': 7.0.7(react-dom@18.2.0)(react@17.0.2) + '@storybook/types': 7.0.7 dequal: 2.0.3 lodash: 4.17.21 memoizerific: 1.11.3 @@ -5607,16 +6911,14 @@ packages: store2: 2.14.2 telejson: 7.0.4 ts-dedent: 2.2.0 - transitivePeerDependencies: - - supports-color dev: true - /@storybook/manager@7.0.0-beta.46: - resolution: {integrity: sha512-0Tsm47YM3SU9rvPpXxp6/toQ1DDUrIbZt1pXcj72szLZvi7U/fXTMpsBX9gOB1MNVYIYRqS2V+jcO8UjFd4qyQ==} + /@storybook/manager@7.0.7: + resolution: {integrity: sha512-FhquwUpUOHsjZROf6E6kzUnJ6EmMeJ9b+HYg6yYPyIMYVMjAhnkRKbIj0phGx2lhgKFlmxik+3pgchK5SLdcZA==} dev: true - /@storybook/mdx2-csf@1.0.0-next.8: - resolution: {integrity: sha512-t2O5s/HHTH5evZVHgVtCWTZgMZ/CaqDu3xVGgjVbKeTvpPAbi0Waab5SSX8T9PG5jNDei/x+jpAVCcNMOHoWzg==} + /@storybook/mdx2-csf@1.0.0: + resolution: {integrity: sha512-dBAnEL4HfxxJmv7LdEYUoZlQbWj9APZNIbOaq0tgF8XkxiIbzqvgB0jhL/9UOrysSDbQWBiCRTu2wOVxedGfmw==} dev: true /@storybook/node-logger@6.5.15: @@ -5629,8 +6931,8 @@ packages: pretty-hrtime: 1.0.3 dev: true - /@storybook/node-logger@7.0.0-beta.46: - resolution: {integrity: sha512-EEf9apXHZuYRrwuFckwg/0InAr/TRTllqJjo5E+fH5UWDRr3aRSrDFb57C84FwP8cY/aKMf+quPQZ7/LoDqpzQ==} + /@storybook/node-logger@7.0.7: + resolution: {integrity: sha512-5Y4LLgKeCStq1ktCKZ5eNPzQQSQ+CYZAlkEdzQ3Pp//0KXaZvVxEvGtaYhAymP2HatLpI8Oneo4lHrJioRfgww==} dependencies: '@types/npmlog': 4.1.4 chalk: 4.1.2 @@ -5638,12 +6940,12 @@ packages: pretty-hrtime: 1.0.3 dev: true - /@storybook/postinstall@7.0.0-beta.46: - resolution: {integrity: sha512-tQ6hv57SPVxyOYPQzhlrhkuKs3Nk4Efa1DN9bzYg5jEzXbeZ9uK4jmV9TDQdWv0QeAvK81SD1YNI2OtzbLPVgA==} + /@storybook/postinstall@7.0.7: + resolution: {integrity: sha512-APcZ2KaR7z1aJje3pID4Ywmt1/aVcP3Sc4ltzNdH9mCkEsuq0fZHHQrYSa9Ya1IPRmSeLZ5/23q1iyqmGU3zoQ==} dev: true - /@storybook/preset-vue-webpack@7.0.0-beta.46(@babel/core@7.20.12)(@babel/preset-env@7.20.2)(babel-loader@9.1.2)(css-loader@6.7.3)(esbuild@0.16.17)(typescript@5.0.3)(vue-loader@15.10.1)(vue-template-compiler@2.7.14)(vue@2.7.14): - resolution: {integrity: sha512-X40UKuFlXV5lQuVrdP4jBEkPwf+9whXqJwNnBMlQICU/SEwC5oE2sqi7cFU5sZ8DBVzqDunxCE0rfYETeliyyw==} + /@storybook/preset-vue-webpack@7.0.7(@babel/core@7.21.8)(@babel/preset-env@7.21.5)(babel-loader@9.1.2)(css-loader@6.7.3)(esbuild@0.17.18)(typescript@5.0.3)(vue-loader@15.10.1)(vue-template-compiler@2.7.14)(vue@2.7.14): + resolution: {integrity: sha512-iuCWRlvc1lRa6s6KB39MCCxVFBp40BJAip+rluSuvjZyDva/9L6+TIB2tkz3d4dqRHee8Y8C2KSnx9OT8Ue6Iw==} engines: {node: '>=16.0.0'} peerDependencies: '@babel/core': '*' @@ -5653,19 +6955,19 @@ packages: vue-loader: ^15.7.0 vue-template-compiler: ^2.6.14 dependencies: - '@babel/core': 7.20.12 - '@storybook/core-webpack': 7.0.0-beta.46 - '@storybook/docs-tools': 7.0.0-beta.46 + '@babel/core': 7.21.8 + '@storybook/core-webpack': 7.0.7 + '@storybook/docs-tools': 7.0.7 '@types/node': 16.18.12 - babel-loader: 9.1.2(@babel/core@7.20.12)(webpack@5.75.0) + babel-loader: 9.1.2(@babel/core@7.21.8)(webpack@5.75.0) css-loader: 6.7.3(webpack@5.75.0) ts-loader: 9.4.2(typescript@5.0.3)(webpack@5.75.0) vue: 2.7.14 vue-docgen-api: 4.56.4(vue@2.7.14) - vue-docgen-loader: 1.5.1(@babel/preset-env@7.20.2)(vue-docgen-api@4.56.4)(webpack@5.75.0) + vue-docgen-loader: 1.5.1(@babel/preset-env@7.21.5)(vue-docgen-api@4.56.4)(webpack@5.75.0) vue-loader: 15.10.1(css-loader@6.7.3)(react-dom@18.2.0)(react@17.0.2)(vue-template-compiler@2.7.14)(webpack@5.75.0) vue-template-compiler: 2.7.14 - webpack: 5.75.0(esbuild@0.16.17) + webpack: 5.75.0(esbuild@0.17.18) transitivePeerDependencies: - '@babel/preset-env' - '@swc/core' @@ -5676,31 +6978,38 @@ packages: - webpack-cli dev: true - /@storybook/preview-api@7.0.0-beta.46: - resolution: {integrity: sha512-sEfdUk9rMcIrCKYA9q6/735VUGRaQvGaUpATjy468WOhXwUgASFjBvRk6w0xu/lpfcnIcPwoDVpbYR17kKvzTg==} + /@storybook/preview-api@7.0.7: + resolution: {integrity: sha512-R5pmGTodpu6hbwEg2RM2ulWtW3d426YzsisHrZJ+FT9lecWauN1y9xHCz7HdNzEFhT8r4YOa24L9ZS3mosZ7hA==} dependencies: - '@storybook/channel-postmessage': 7.0.0-beta.46 - '@storybook/channels': 7.0.0-beta.46 - '@storybook/client-logger': 7.0.0-beta.46 - '@storybook/core-events': 7.0.0-beta.46 - '@storybook/csf': 0.0.2-next.11 + '@storybook/channel-postmessage': 7.0.7 + '@storybook/channels': 7.0.7 + '@storybook/client-logger': 7.0.7 + '@storybook/core-events': 7.0.7 + '@storybook/csf': 0.1.0 '@storybook/global': 5.0.0 - '@storybook/types': 7.0.0-beta.46 + '@storybook/types': 7.0.7 '@types/qs': 6.9.7 dequal: 2.0.3 lodash: 4.17.21 memoizerific: 1.11.3 qs: 6.11.0 - slash: 3.0.0 synchronous-promise: 2.0.16 ts-dedent: 2.2.0 util-deprecate: 1.0.2 - transitivePeerDependencies: - - supports-color dev: true - /@storybook/preview@7.0.0-beta.46: - resolution: {integrity: sha512-4k62e85sR6Cyl0O7ZeCct/pE0v+tjIjGhHDs9U0AZbfBJlSppazRAP51q6v1QdF2eWDgdRbn3oTQnW8rdCBeMg==} + /@storybook/preview@7.0.7: + resolution: {integrity: sha512-uL3ZcFao6UvxiSxCIcXKFakxEr9Nn0lvu0zzC2yQCVepzA7a+GDr1cK5VbZ6Mez38CnOvBmb5pkCbgRqSf/oug==} + dev: true + + /@storybook/react-dom-shim@7.0.7(react-dom@18.2.0)(react@17.0.2): + resolution: {integrity: sha512-INGwFeu9M+RzpvktSKuwy8Rk/70mXGqxxsb9lPtq7phmETvfpNX7GnLJqiVazTaQiB1DkB0iAPUsK2MNbBu+Kw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + dependencies: + react: 17.0.2 + react-dom: 18.2.0(react@17.0.2) dev: true /@storybook/router@6.5.15(react-dom@18.2.0)(react@17.0.2): @@ -5718,13 +7027,13 @@ packages: regenerator-runtime: 0.13.11 dev: true - /@storybook/router@7.0.0-beta.46(react-dom@18.2.0)(react@17.0.2): - resolution: {integrity: sha512-W0mrFxILTrOvM2nca2WfXenHHnblX9gPOEfXBrw+0ZQ2eyuXPCPCgNMHfz+ZXVIVOTJHQ5NCgq5Fbpwrrz+P1A==} + /@storybook/router@7.0.7(react-dom@18.2.0)(react@17.0.2): + resolution: {integrity: sha512-/lM8/NHQKeshfnC3ayFuO8Y9TCSHnCAPRhIsVxvanBzcj+ILbCIyZ+TspvB3hT4MbX/Ez+JR8VrMbjXIGwmH8w==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: - '@storybook/client-logger': 7.0.0-beta.46 + '@storybook/client-logger': 7.0.7 memoizerific: 1.11.3 qs: 6.11.0 react: 17.0.2 @@ -5740,20 +7049,18 @@ packages: find-up: 4.1.0 dev: true - /@storybook/store@7.0.0-beta.46: - resolution: {integrity: sha512-gGe6RuBp4ZFYKzvZrYaxc8rNbVWWKaq8mFfKzL2NJ7o0o9zrcW7GVT6Klfr4JP3+slPhPt0KQTH9FL33/6T8VA==} + /@storybook/store@7.0.7: + resolution: {integrity: sha512-yXVuh6DF9kOcpMGeT7lZ1kQV8bKkeGMZMRFwCfy7TeusXqUXZLCPYfgqm8gvd05dP+XZegDAXVsm/9d/NVLcwA==} dependencies: - '@storybook/client-logger': 7.0.0-beta.46 - '@storybook/preview-api': 7.0.0-beta.46 - transitivePeerDependencies: - - supports-color + '@storybook/client-logger': 7.0.7 + '@storybook/preview-api': 7.0.7 dev: true - /@storybook/telemetry@7.0.0-beta.46: - resolution: {integrity: sha512-sDdE0GZDOYzyoZ2Z4o4ei1WVyJp3Ac5u3ZPldZKYxen4mFhM+vVbDvpwkvp8aXrO59L9I8aleyEMjmTFqJWhjw==} + /@storybook/telemetry@7.0.7: + resolution: {integrity: sha512-Ka6pwWr3sWs3A/6WQ0wsoSYzXx3Mhr7eByNZZKuuCu9jnw3I8AbIOqQX2iOVzaQBLZsvXEeqvYY8iZ+GuRbbGQ==} dependencies: - '@storybook/client-logger': 7.0.0-beta.46 - '@storybook/core-common': 7.0.0-beta.46 + '@storybook/client-logger': 7.0.7 + '@storybook/core-common': 7.0.7 chalk: 4.1.2 detect-package-manager: 2.0.1 fetch-retry: 5.0.3 @@ -5780,35 +7087,31 @@ packages: regenerator-runtime: 0.13.11 dev: true - /@storybook/theming@7.0.0-beta.46(react-dom@18.2.0)(react@17.0.2): - resolution: {integrity: sha512-Zhiu8gEWgUoHYP8VMCXuI1TyjG+MwMDLqooFGoNoaX/p0Fg+orf+Y4u1J8eXUMv75tlPEoBvmAv8pa8REvxYGw==} + /@storybook/theming@7.0.7(react-dom@18.2.0)(react@17.0.2): + resolution: {integrity: sha512-InTZe+Sgco1NsxgiG+cyUKWQe3GsjlIyU/o5qDdtOTXcZ64HzyBuAZlAequSddqfDeMDqxRFPc2w1J28MAUHxA==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: '@emotion/use-insertion-effect-with-fallbacks': 1.0.0(react@17.0.2) - '@storybook/client-logger': 7.0.0-beta.46 + '@storybook/client-logger': 7.0.7 '@storybook/global': 5.0.0 memoizerific: 1.11.3 react: 17.0.2 react-dom: 18.2.0(react@17.0.2) dev: true - /@storybook/types@7.0.0-beta.46: - resolution: {integrity: sha512-I5nf8aDYmkM3FqlBAqr2SxnIp16iydvjSegShHEeraXDs9wSqbyAlYuw8m2WcN+bGA2qqhGUGEWbJv5bY3WaTQ==} + /@storybook/types@7.0.7: + resolution: {integrity: sha512-v9piuwp8FvTiHXIOOi5lEyTEJKhnbcbhVxgJ3VFhhXYFd0DTz6Bst0XIIgkgs21ITb3xhkfPbCRUueMcbXO1MA==} dependencies: - '@babel/core': 7.20.12 - '@storybook/channels': 7.0.0-beta.46 + '@storybook/channels': 7.0.7 '@types/babel__core': 7.20.0 '@types/express': 4.17.14 - express: 4.18.2 file-system-cache: 2.0.2 - transitivePeerDependencies: - - supports-color dev: true - /@storybook/vue-webpack5@7.0.0-beta.46(@babel/core@7.20.12)(@babel/preset-env@7.20.2)(babel-loader@9.1.2)(css-loader@6.7.3)(esbuild@0.16.17)(react-dom@18.2.0)(react@17.0.2)(typescript@5.0.3)(vue-loader@15.10.1)(vue-template-compiler@2.7.14)(vue@2.7.14): - resolution: {integrity: sha512-PJJo/NGiWL1raOfHpJTb3X6LmLAC2TgI3EV2nDlkPigxj29O+zeMkyvfDky7eWpgOIbVqqtNVUYGH1GwbL7jqw==} + /@storybook/vue-webpack5@7.0.7(@babel/core@7.21.8)(@babel/preset-env@7.21.5)(babel-loader@9.1.2)(css-loader@6.7.3)(esbuild@0.17.18)(react-dom@18.2.0)(react@17.0.2)(typescript@5.0.3)(vue-loader@15.10.1)(vue-template-compiler@2.7.14)(vue@2.7.14): + resolution: {integrity: sha512-SNxmyIxZl7kkCWhJfyvyIkhb11muKYHAcXMJTSlcIAKRQN37ft0jhPBciQZ4GLOLorqmIm14NLIdzInBXcm++A==} engines: {node: '>=16.0.0'} peerDependencies: '@babel/core': '*' @@ -5820,13 +7123,13 @@ packages: vue-loader: ^15.7.0 vue-template-compiler: ^2.6.8 dependencies: - '@babel/core': 7.20.12 - '@storybook/builder-webpack5': 7.0.0-beta.46(esbuild@0.16.17)(react-dom@18.2.0)(react@17.0.2)(typescript@5.0.3)(vue-template-compiler@2.7.14) - '@storybook/core-common': 7.0.0-beta.46 - '@storybook/preset-vue-webpack': 7.0.0-beta.46(@babel/core@7.20.12)(@babel/preset-env@7.20.2)(babel-loader@9.1.2)(css-loader@6.7.3)(esbuild@0.16.17)(typescript@5.0.3)(vue-loader@15.10.1)(vue-template-compiler@2.7.14)(vue@2.7.14) - '@storybook/vue': 7.0.0-beta.46(@babel/core@7.20.12)(babel-loader@9.1.2)(css-loader@6.7.3)(vue@2.7.14) + '@babel/core': 7.21.8 + '@storybook/builder-webpack5': 7.0.7(esbuild@0.17.18)(react-dom@18.2.0)(react@17.0.2)(typescript@5.0.3)(vue-template-compiler@2.7.14) + '@storybook/core-common': 7.0.7 + '@storybook/preset-vue-webpack': 7.0.7(@babel/core@7.21.8)(@babel/preset-env@7.21.5)(babel-loader@9.1.2)(css-loader@6.7.3)(esbuild@0.17.18)(typescript@5.0.3)(vue-loader@15.10.1)(vue-template-compiler@2.7.14)(vue@2.7.14) + '@storybook/vue': 7.0.7(@babel/core@7.21.8)(babel-loader@9.1.2)(css-loader@6.7.3)(vue@2.7.14) '@types/node': 16.18.12 - babel-loader: 9.1.2(@babel/core@7.20.12)(webpack@5.75.0) + babel-loader: 9.1.2(@babel/core@7.21.8)(webpack@5.75.0) css-loader: 6.7.3(webpack@5.75.0) react: 17.0.2 react-dom: 18.2.0(react@17.0.2) @@ -5844,8 +7147,8 @@ packages: - webpack-cli dev: true - /@storybook/vue@7.0.0-beta.46(@babel/core@7.20.12)(babel-loader@9.1.2)(css-loader@6.7.3)(vue@2.7.14): - resolution: {integrity: sha512-ULUZ78BuHKbz98VmmdqstDOh+rBSDAoxUlwr3hTOfoK84HyxpXizab8BK4lCK+DkYvxeUduDcHn9aT3VbjGbsA==} + /@storybook/vue@7.0.7(@babel/core@7.21.8)(babel-loader@9.1.2)(css-loader@6.7.3)(vue@2.7.14): + resolution: {integrity: sha512-xz4yU+iIy3lmlPsfqhtYWZC2kVvgwMtL/gNvROTnTGgxjAcr0TK0GiIvCoJDnY+8CE83Ek+/8Qmf26/VtpzJCw==} engines: {node: '>=16.0.0'} peerDependencies: '@babel/core': '*' @@ -5856,14 +7159,14 @@ packages: babel-loader: optional: true dependencies: - '@babel/core': 7.20.12 - '@storybook/client-logger': 7.0.0-beta.46 - '@storybook/core-client': 7.0.0-beta.46 - '@storybook/docs-tools': 7.0.0-beta.46 + '@babel/core': 7.21.8 + '@storybook/client-logger': 7.0.7 + '@storybook/core-client': 7.0.7 + '@storybook/docs-tools': 7.0.7 '@storybook/global': 5.0.0 - '@storybook/preview-api': 7.0.0-beta.46 - '@storybook/types': 7.0.0-beta.46 - babel-loader: 9.1.2(@babel/core@7.20.12)(webpack@5.75.0) + '@storybook/preview-api': 7.0.7 + '@storybook/types': 7.0.7 + babel-loader: 9.1.2(@babel/core@7.21.8)(webpack@5.75.0) css-loader: 6.7.3(webpack@5.75.0) ts-dedent: 2.2.0 type-fest: 2.19.0 @@ -5975,8 +7278,8 @@ packages: /@types/babel__core@7.20.0: resolution: {integrity: sha512-+n8dL/9GWblDO0iU6eZAwEIJVr5DWigtle+Q6HLOrh/pdbXOhOtqzq8VPPE2zvNJzSKY4vH/z3iT3tn0A3ypiQ==} dependencies: - '@babel/parser': 7.20.7 - '@babel/types': 7.20.7 + '@babel/parser': 7.21.8 + '@babel/types': 7.21.5 '@types/babel__generator': 7.6.4 '@types/babel__template': 7.4.1 '@types/babel__traverse': 7.18.2 @@ -5985,20 +7288,20 @@ packages: /@types/babel__generator@7.6.4: resolution: {integrity: sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==} dependencies: - '@babel/types': 7.20.7 + '@babel/types': 7.21.5 dev: true /@types/babel__template@7.4.1: resolution: {integrity: sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==} dependencies: - '@babel/parser': 7.20.7 - '@babel/types': 7.20.7 + '@babel/parser': 7.21.8 + '@babel/types': 7.21.5 dev: true /@types/babel__traverse@7.18.2: resolution: {integrity: sha512-FcFaxOr2V5KZCviw1TnutEMVUVsGt4D2hP1TAfXZAMKuHYW3xQhe3jTxNPWutgCJ3/X1c5yX8ZoGVEItxKbwBg==} dependencies: - '@babel/types': 7.20.7 + '@babel/types': 7.21.5 dev: true /@types/basic-auth@1.1.3: @@ -6135,12 +7438,12 @@ packages: /@types/eslint-scope@3.7.4: resolution: {integrity: sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==} dependencies: - '@types/eslint': 8.4.6 + '@types/eslint': 8.37.0 '@types/estree': 1.0.0 dev: true - /@types/eslint@8.4.6: - resolution: {integrity: sha512-/fqTbjxyFUaYNO7VcW5g+4npmqVACz1bB7RTHYuLj+PRjw9hrCwrUXVQFpChUS0JsyEFvMZ7U/PfmvWgxJhI9g==} + /@types/eslint@8.37.0: + resolution: {integrity: sha512-Piet7dG2JBuDIfohBngQ3rCt7MgO9xCO4xIMKxBThCq5PNRB91IjlJ10eJVwfoNtvTErmxLzwBZ7rHZtbOMmFQ==} dependencies: '@types/estree': 1.0.0 '@types/json-schema': 7.0.11 @@ -6954,10 +8257,6 @@ packages: resolution: {integrity: sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==} dev: true - /@types/trusted-types@2.0.2: - resolution: {integrity: sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==} - dev: true - /@types/tunnel@0.0.3: resolution: {integrity: sha512-sOUTGn6h1SfQ+gbgqC364jLFBw2lnFqkgF3q0WovEHRLMrVD1sd5aufqi/aJObLekJO+Aq5z646U4Oxy6shXMA==} dependencies: @@ -7042,8 +8341,8 @@ packages: dev: true optional: true - /@typescript-eslint/eslint-plugin@5.45.0(@typescript-eslint/parser@5.45.0)(eslint@8.28.0)(typescript@5.0.3): - resolution: {integrity: sha512-CXXHNlf0oL+Yg021cxgOdMHNTXD17rHkq7iW6RFHoybdFgQBjU3yIXhhcPpGwr1CjZlo6ET8C6tzX5juQoXeGA==} + /@typescript-eslint/eslint-plugin@5.59.0(patch_hash=tk3n6hvmqwfzrfqe3awfxnqtuy)(@typescript-eslint/parser@5.59.0)(eslint@8.39.0)(typescript@5.0.3): + resolution: {integrity: sha512-p0QgrEyrxAWBecR56gyn3wkG15TJdI//eetInP3zYRewDh0XS+DhB3VUAd3QqvziFsfaQIoIuZMxZRB7vXYaYw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: '@typescript-eslint/parser': ^5.0.0 @@ -7053,24 +8352,26 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/parser': 5.45.0(eslint@8.28.0)(typescript@5.0.3) - '@typescript-eslint/scope-manager': 5.45.0 - '@typescript-eslint/type-utils': 5.45.0(eslint@8.28.0)(typescript@5.0.3) - '@typescript-eslint/utils': 5.45.0(eslint@8.28.0)(typescript@5.0.3) + '@eslint-community/regexpp': 4.5.0 + '@typescript-eslint/parser': 5.59.0(eslint@8.39.0)(typescript@5.0.3) + '@typescript-eslint/scope-manager': 5.59.0 + '@typescript-eslint/type-utils': 5.59.0(eslint@8.39.0)(typescript@5.0.3) + '@typescript-eslint/utils': 5.59.0(eslint@8.39.0)(typescript@5.0.3) debug: 4.3.4(supports-color@8.1.1) - eslint: 8.28.0 - ignore: 5.2.0 + eslint: 8.39.0 + grapheme-splitter: 1.0.4 + ignore: 5.2.4 natural-compare-lite: 1.4.0 - regexpp: 3.2.0 semver: 7.3.8 tsutils: 3.21.0(typescript@5.0.3) typescript: 5.0.3 transitivePeerDependencies: - supports-color dev: true + patched: true - /@typescript-eslint/parser@5.45.0(eslint@8.28.0)(typescript@5.0.3): - resolution: {integrity: sha512-brvs/WSM4fKUmF5Ot/gEve6qYiCMjm6w4HkHPfS6ZNmxTS0m0iNN4yOChImaCkqc1hRwFGqUyanMXuGal6oyyQ==} + /@typescript-eslint/parser@5.59.0(eslint@8.39.0)(typescript@5.0.3): + resolution: {integrity: sha512-qK9TZ70eJtjojSUMrrEwA9ZDQ4N0e/AuoOIgXuNBorXYcBDk397D2r5MIe1B3cok/oCtdNC5j+lUUpVB+Dpb+w==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -7079,11 +8380,11 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/scope-manager': 5.45.0 - '@typescript-eslint/types': 5.45.0 - '@typescript-eslint/typescript-estree': 5.45.0(typescript@5.0.3) + '@typescript-eslint/scope-manager': 5.59.0 + '@typescript-eslint/types': 5.59.0 + '@typescript-eslint/typescript-estree': 5.59.0(typescript@5.0.3) debug: 4.3.4(supports-color@8.1.1) - eslint: 8.28.0 + eslint: 8.39.0 typescript: 5.0.3 transitivePeerDependencies: - supports-color @@ -7097,8 +8398,16 @@ packages: '@typescript-eslint/visitor-keys': 5.45.0 dev: true - /@typescript-eslint/type-utils@5.45.0(eslint@8.28.0)(typescript@5.0.3): - resolution: {integrity: sha512-DY7BXVFSIGRGFZ574hTEyLPRiQIvI/9oGcN8t1A7f6zIs6ftbrU0nhyV26ZW//6f85avkwrLag424n+fkuoJ1Q==} + /@typescript-eslint/scope-manager@5.59.0: + resolution: {integrity: sha512-tsoldKaMh7izN6BvkK6zRMINj4Z2d6gGhO2UsI8zGZY3XhLq1DndP3Ycjhi1JwdwPRwtLMW4EFPgpuKhbCGOvQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + '@typescript-eslint/types': 5.59.0 + '@typescript-eslint/visitor-keys': 5.59.0 + dev: true + + /@typescript-eslint/type-utils@5.59.0(eslint@8.39.0)(typescript@5.0.3): + resolution: {integrity: sha512-d/B6VSWnZwu70kcKQSCqjcXpVH+7ABKH8P1KNn4K7j5PXXuycZTPXF44Nui0TEm6rbWGi8kc78xRgOC4n7xFgA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: '*' @@ -7107,10 +8416,10 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/typescript-estree': 5.45.0(typescript@5.0.3) - '@typescript-eslint/utils': 5.45.0(eslint@8.28.0)(typescript@5.0.3) + '@typescript-eslint/typescript-estree': 5.59.0(typescript@5.0.3) + '@typescript-eslint/utils': 5.59.0(eslint@8.39.0)(typescript@5.0.3) debug: 4.3.4(supports-color@8.1.1) - eslint: 8.28.0 + eslint: 8.39.0 tsutils: 3.21.0(typescript@5.0.3) typescript: 5.0.3 transitivePeerDependencies: @@ -7122,6 +8431,11 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true + /@typescript-eslint/types@5.59.0: + resolution: {integrity: sha512-yR2h1NotF23xFFYKHZs17QJnB51J/s+ud4PYU4MqdZbzeNxpgUr05+dNeCN/bb6raslHvGdd6BFCkVhpPk/ZeA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + /@typescript-eslint/typescript-estree@5.45.0(typescript@5.0.3): resolution: {integrity: sha512-maRhLGSzqUpFcZgXxg1qc/+H0bT36lHK4APhp0AEUVrpSwXiRAomm/JGjSG+kNUio5kAa3uekCYu/47cnGn5EQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -7143,7 +8457,28 @@ packages: - supports-color dev: true - /@typescript-eslint/utils@5.45.0(eslint@8.28.0)(typescript@5.0.3): + /@typescript-eslint/typescript-estree@5.59.0(typescript@5.0.3): + resolution: {integrity: sha512-sUNnktjmI8DyGzPdZ8dRwW741zopGxltGs/SAPgGL/AAgDpiLsCFLcMNSpbfXfmnNeHmK9h3wGmCkGRGAoUZAg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/types': 5.59.0 + '@typescript-eslint/visitor-keys': 5.59.0 + debug: 4.3.4(supports-color@8.1.1) + globby: 11.1.0 + is-glob: 4.0.3 + semver: 7.3.8 + tsutils: 3.21.0(typescript@5.0.3) + typescript: 5.0.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/utils@5.45.0(eslint@8.39.0)(typescript@5.0.3): resolution: {integrity: sha512-OUg2JvsVI1oIee/SwiejTot2OxwU8a7UfTFMOdlhD2y+Hl6memUSL4s98bpUTo8EpVEr0lmwlU7JSu/p2QpSvA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -7154,9 +8489,29 @@ packages: '@typescript-eslint/scope-manager': 5.45.0 '@typescript-eslint/types': 5.45.0 '@typescript-eslint/typescript-estree': 5.45.0(typescript@5.0.3) - eslint: 8.28.0 + eslint: 8.39.0 + eslint-scope: 5.1.1 + eslint-utils: 3.0.0(eslint@8.39.0) + semver: 7.3.8 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + + /@typescript-eslint/utils@5.59.0(eslint@8.39.0)(typescript@5.0.3): + resolution: {integrity: sha512-GGLFd+86drlHSvPgN/el6dRQNYYGOvRSDVydsUaQluwIW3HvbXuxyuD5JETvBt/9qGYe+lOrDk6gRrWOHb/FvA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.39.0) + '@types/json-schema': 7.0.11 + '@types/semver': 7.3.13 + '@typescript-eslint/scope-manager': 5.59.0 + '@typescript-eslint/types': 5.59.0 + '@typescript-eslint/typescript-estree': 5.59.0(typescript@5.0.3) + eslint: 8.39.0 eslint-scope: 5.1.1 - eslint-utils: 3.0.0(eslint@8.28.0) semver: 7.3.8 transitivePeerDependencies: - supports-color @@ -7168,7 +8523,15 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: '@typescript-eslint/types': 5.45.0 - eslint-visitor-keys: 3.3.0 + eslint-visitor-keys: 3.4.0 + dev: true + + /@typescript-eslint/visitor-keys@5.59.0: + resolution: {integrity: sha512-qZ3iXxQhanchCeaExlKPV3gDQFxMUmU35xfd5eCXB6+kUw1TUAbIy2n7QIrwz9s98DQLzNWyHp61fY0da4ZcbA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + '@typescript-eslint/types': 5.59.0 + eslint-visitor-keys: 3.4.0 dev: true /@vitejs/plugin-legacy@3.0.1(terser@5.16.1)(vite@4.0.4): @@ -7272,6 +8635,15 @@ packages: pretty-format: 27.5.1 dev: true + /@volar-plugins/eslint@0.0.4(eslint@8.39.0): + resolution: {integrity: sha512-CjvOHSPOyfMv4mgjFk0JOsnq0yWj4LaHb7n4bjNWqqhdwPWmaUEK5AbOloBRxix0736Z3OimH8GMjHNlMcbeiw==} + peerDependencies: + eslint: '*' + dependencies: + '@volar/shared': 1.0.24 + eslint: 8.39.0 + dev: true + /@volar/language-core@1.0.24: resolution: {integrity: sha512-vTN+alJiWwK0Pax6POqrmevbtFW2dXhjwWiW/MW4f48eDYPLdyURWcr8TixO7EN/nHsUBj2udT7igFKPtjyAKg==} dependencies: @@ -7279,6 +8651,13 @@ packages: muggle-string: 0.1.0 dev: true + /@volar/shared@1.0.24: + resolution: {integrity: sha512-30mqmNsw49xlGhziL59z6kP6/TlBatkeOzMImUSWmn1QtqV7r2onDGgNNdCqSa1esTo4UtGup6yqqM2oUwrMSQ==} + dependencies: + typesafe-path: 0.2.2 + vscode-uri: 3.0.7 + dev: true + /@volar/source-map@1.0.24: resolution: {integrity: sha512-Qsv/tkplx18pgBr8lKAbM1vcDqgkGKQzbChg6NW+v0CZc3G7FLmK+WrqEPzKlN7Cwdc6XVL559Nod8WKAfKr4A==} dependencies: @@ -7314,7 +8693,7 @@ packages: /@vue/compiler-core@3.2.45: resolution: {integrity: sha512-rcMj7H+PYe5wBV3iYeUgbCglC+pbpN8hBLTJvRiK2eKQiWqu+fG9F+8sW99JdL4LQi7Re178UOxn09puSXvn4A==} dependencies: - '@babel/parser': 7.20.7 + '@babel/parser': 7.21.8 '@vue/shared': 3.2.45 estree-walker: 2.0.2 source-map: 0.6.1 @@ -7428,7 +8807,7 @@ packages: /@vue/devtools-api@6.4.5: resolution: {integrity: sha512-JD5fcdIuFxU4fQyXUu3w2KpAJHzTVdN+p4iOX2lMWSHMOoQdMAcpFLZzm9Z/2nmsoZ1a96QEhZ26e50xLBsgOQ==} - /@vue/eslint-config-typescript@8.0.0(@typescript-eslint/eslint-plugin@5.45.0)(@typescript-eslint/parser@5.45.0)(eslint-plugin-vue@7.17.0)(eslint@8.28.0)(typescript@5.0.3): + /@vue/eslint-config-typescript@8.0.0(@typescript-eslint/eslint-plugin@5.59.0)(@typescript-eslint/parser@5.59.0)(eslint-plugin-vue@7.17.0)(eslint@8.39.0)(typescript@5.0.3): resolution: {integrity: sha512-8u8Qpg4qfjJoNeRMdHlxif9BcGy4iYSSK4YYW5AFPPRtkBJiCqtoyT72l4F3ZeZII09ax2N6yQeHbQ0CXQi1bA==} engines: {node: ^10.12.0 || >=12.0.0} peerDependencies: @@ -7441,12 +8820,12 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/eslint-plugin': 5.45.0(@typescript-eslint/parser@5.45.0)(eslint@8.28.0)(typescript@5.0.3) - '@typescript-eslint/parser': 5.45.0(eslint@8.28.0)(typescript@5.0.3) - eslint: 8.28.0 - eslint-plugin-vue: 7.17.0(eslint@8.28.0) + '@typescript-eslint/eslint-plugin': 5.59.0(patch_hash=tk3n6hvmqwfzrfqe3awfxnqtuy)(@typescript-eslint/parser@5.59.0)(eslint@8.39.0)(typescript@5.0.3) + '@typescript-eslint/parser': 5.59.0(eslint@8.39.0)(typescript@5.0.3) + eslint: 8.39.0 + eslint-plugin-vue: 7.17.0(eslint@8.39.0) typescript: 5.0.3 - vue-eslint-parser: 7.11.0(eslint@8.28.0) + vue-eslint-parser: 7.11.0(eslint@8.39.0) transitivePeerDependencies: - supports-color dev: true @@ -7454,7 +8833,7 @@ packages: /@vue/reactivity-transform@3.2.45: resolution: {integrity: sha512-BHVmzYAvM7vcU5WmuYqXpwaBHjsS8T63jlKGWVtHxAHIoMIlmaMyurUSEs1Zcg46M4AYT5MtB1U274/2aNzjJQ==} dependencies: - '@babel/parser': 7.20.7 + '@babel/parser': 7.21.8 '@vue/compiler-core': 3.2.45 '@vue/shared': 3.2.45 estree-walker: 2.0.2 @@ -7603,13 +8982,13 @@ packages: resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} dev: true - /@yarnpkg/esbuild-plugin-pnp@3.0.0-rc.15(esbuild@0.16.17): + /@yarnpkg/esbuild-plugin-pnp@3.0.0-rc.15(esbuild@0.17.18): resolution: {integrity: sha512-kYzDJO5CA9sy+on/s2aIW0411AklfCi8Ck/4QDivOqsMKpStZA2SsR+X27VTggGwpStWaLrjJcDcdDMowtG8MA==} engines: {node: '>=14.15.0'} peerDependencies: esbuild: '>=0.10.0' dependencies: - esbuild: 0.16.17 + esbuild: 0.17.18 tslib: 2.5.0 dev: true @@ -7687,20 +9066,6 @@ packages: engines: {node: '>= 10.0.0'} dev: true - /adler-32@1.2.0: - resolution: {integrity: sha512-/vUqU/UY4MVeFsg+SsK6c+/05RZXIHZMGJA+PX5JyWI0ZRcBpupnRuPLU/NXXoFwMYCPCoxIfElM2eS+DUXCqQ==} - engines: {node: '>=0.8'} - hasBin: true - dependencies: - exit-on-epipe: 1.0.1 - printj: 1.1.2 - dev: false - - /adler-32@1.3.1: - resolution: {integrity: sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==} - engines: {node: '>=0.8'} - dev: false - /agent-base@5.1.1: resolution: {integrity: sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g==} engines: {node: '>= 6.0.0'} @@ -8007,13 +9372,13 @@ packages: /array-flatten@1.1.1: resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} - /array-includes@3.1.5: - resolution: {integrity: sha512-iSDYZMMyTPkiFasVqfuAQnWAYcvO/SeBSCGKePoEthjp4LEMTe4uLc7b025o4jAZpHhihh8xPo99TNWUWWkGDQ==} + /array-includes@3.1.6: + resolution: {integrity: sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==} engines: {node: '>= 0.4'} dependencies: call-bind: 1.0.2 define-properties: 1.1.4 - es-abstract: 1.20.4 + es-abstract: 1.21.1 get-intrinsic: 1.1.3 is-string: 1.0.7 dev: true @@ -8064,13 +9429,23 @@ packages: engines: {node: '>=0.10.0'} dev: true - /array.prototype.flat@1.3.0: - resolution: {integrity: sha512-12IUEkHsAhA4DY5s0FPgNXIdc8VRSqD9Zp78a5au9abH/SOBrsp082JOWFNTjkMozh8mqcdiKuaLGhPeYztxSw==} + /array.prototype.flat@1.3.1: + resolution: {integrity: sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.1.4 + es-abstract: 1.21.1 + es-shim-unscopables: 1.0.0 + dev: true + + /array.prototype.flatmap@1.3.1: + resolution: {integrity: sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ==} engines: {node: '>= 0.4'} dependencies: call-bind: 1.0.2 define-properties: 1.1.4 - es-abstract: 1.20.4 + es-abstract: 1.21.1 es-shim-unscopables: 1.0.0 dev: true @@ -8080,7 +9455,7 @@ packages: dependencies: call-bind: 1.0.2 define-properties: 1.1.4 - es-abstract: 1.20.4 + es-abstract: 1.21.1 es-array-method-boxes-properly: 1.0.0 is-string: 1.0.7 dev: false @@ -8309,29 +9684,29 @@ packages: - debug dev: false - /babel-core@7.0.0-bridge.0(@babel/core@7.20.12): + /babel-core@7.0.0-bridge.0(@babel/core@7.21.8): resolution: {integrity: sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.12 + '@babel/core': 7.21.8 dev: true /babel-helper-vue-jsx-merge-props@2.0.3: resolution: {integrity: sha512-gsLiKK7Qrb7zYJNgiXKpXblxbV5ffSwR0f5whkPAaBAR4fhi6bwRZxX9wBlIc5M/v8CCkXUbXZL4N/nSE97cqg==} dev: false - /babel-jest@29.5.0(@babel/core@7.20.12): + /babel-jest@29.5.0(@babel/core@7.21.8): resolution: {integrity: sha512-mA4eCDh5mSo2EcA9xQjVTpmbbNk32Zb3Q3QFQsNhaK56Q+yoXowzFodLux30HRgyOho5rsQ6B0P9QpMkvvnJ0Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: '@babel/core': ^7.8.0 dependencies: - '@babel/core': 7.20.12 + '@babel/core': 7.21.8 '@jest/transform': 29.5.0 '@types/babel__core': 7.20.0 babel-plugin-istanbul: 6.1.1 - babel-preset-jest: 29.5.0(@babel/core@7.20.12) + babel-preset-jest: 29.5.0(@babel/core@7.21.8) chalk: 4.1.2 graceful-fs: 4.2.10 slash: 3.0.0 @@ -8349,7 +9724,20 @@ packages: '@babel/core': 7.20.12 find-cache-dir: 3.3.2 schema-utils: 4.0.0 - webpack: 5.75.0(esbuild@0.16.17) + webpack: 5.75.0(esbuild@0.17.18) + dev: true + + /babel-loader@9.1.2(@babel/core@7.21.8)(webpack@5.75.0): + resolution: {integrity: sha512-mN14niXW43tddohGl8HPu5yfQq70iUThvFL/4QzESA7GcZoC0eVOhvWdQ8+3UlSjaDE9MVtsW9mxDY07W7VpVA==} + engines: {node: '>= 14.15.0'} + peerDependencies: + '@babel/core': ^7.12.0 + webpack: '>=5' + dependencies: + '@babel/core': 7.21.8 + find-cache-dir: 3.3.2 + schema-utils: 4.0.0 + webpack: 5.75.0(esbuild@0.17.18) dev: true /babel-plugin-istanbul@6.1.1: @@ -8370,7 +9758,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@babel/template': 7.20.7 - '@babel/types': 7.20.7 + '@babel/types': 7.21.5 '@types/babel__core': 7.20.0 '@types/babel__traverse': 7.18.2 dev: true @@ -8384,7 +9772,7 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/compat-data': 7.20.10 + '@babel/compat-data': 7.21.7 '@babel/core': 7.20.12 '@babel/helper-define-polyfill-provider': 0.3.3(@babel/core@7.20.12) semver: 6.3.0 @@ -8392,6 +9780,19 @@ packages: - supports-color dev: true + /babel-plugin-polyfill-corejs2@0.3.3(@babel/core@7.21.8): + resolution: {integrity: sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/compat-data': 7.21.7 + '@babel/core': 7.21.8 + '@babel/helper-define-polyfill-provider': 0.3.3(@babel/core@7.21.8) + semver: 6.3.0 + transitivePeerDependencies: + - supports-color + dev: true + /babel-plugin-polyfill-corejs3@0.6.0(@babel/core@7.20.12): resolution: {integrity: sha512-+eHqR6OPcBhJOGgsIar7xoAB1GcSwVUA3XjAd7HJNzOXT4wv6/H7KIdA/Nc60cvUlDbKApmqNvD1B1bzOt4nyA==} peerDependencies: @@ -8404,6 +9805,18 @@ packages: - supports-color dev: true + /babel-plugin-polyfill-corejs3@0.6.0(@babel/core@7.21.8): + resolution: {integrity: sha512-+eHqR6OPcBhJOGgsIar7xoAB1GcSwVUA3XjAd7HJNzOXT4wv6/H7KIdA/Nc60cvUlDbKApmqNvD1B1bzOt4nyA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-define-polyfill-provider': 0.3.3(@babel/core@7.21.8) + core-js-compat: 3.27.1 + transitivePeerDependencies: + - supports-color + dev: true + /babel-plugin-polyfill-regenerator@0.4.1(@babel/core@7.20.12): resolution: {integrity: sha512-NtQGmyQDXjQqQ+IzRkBVwEOz9lQ4zxAQZgoAYEtU9dJjnl1Oc98qnN7jcp+bE7O7aYzVpavXE3/VKXNzUbh7aw==} peerDependencies: @@ -8415,35 +9828,46 @@ packages: - supports-color dev: true - /babel-preset-current-node-syntax@1.0.1(@babel/core@7.20.12): - resolution: {integrity: sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==} + /babel-plugin-polyfill-regenerator@0.4.1(@babel/core@7.21.8): + resolution: {integrity: sha512-NtQGmyQDXjQqQ+IzRkBVwEOz9lQ4zxAQZgoAYEtU9dJjnl1Oc98qnN7jcp+bE7O7aYzVpavXE3/VKXNzUbh7aw==} peerDependencies: - '@babel/core': ^7.0.0 + '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.20.12 - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.20.12) - '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.20.12) - '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.20.12) - '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.20.12) - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.20.12) - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.20.12) - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.20.12) - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.20.12) - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.20.12) - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.20.12) - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.20.12) - '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.20.12) + '@babel/core': 7.21.8 + '@babel/helper-define-polyfill-provider': 0.3.3(@babel/core@7.21.8) + transitivePeerDependencies: + - supports-color dev: true - /babel-preset-jest@29.5.0(@babel/core@7.20.12): + /babel-preset-current-node-syntax@1.0.1(@babel/core@7.21.8): + resolution: {integrity: sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.21.8 + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.21.8) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.21.8) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.21.8) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.21.8) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.21.8) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.21.8) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.21.8) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.21.8) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.21.8) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.21.8) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.21.8) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.21.8) + dev: true + + /babel-preset-jest@29.5.0(@babel/core@7.21.8): resolution: {integrity: sha512-JOMloxOqdiBSxMAzjRaH023/vvcaSaec49zvg+2LmNsktC7ei39LTJGw02J+9uUtTZUq6xbLyJ4dxe9sSmIuAg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.20.12 + '@babel/core': 7.21.8 babel-plugin-jest-hoist: 29.5.0 - babel-preset-current-node-syntax: 1.0.1(@babel/core@7.20.12) + babel-preset-current-node-syntax: 1.0.1(@babel/core@7.21.8) dev: true /babel-runtime@6.26.0: @@ -8457,7 +9881,7 @@ packages: resolution: {integrity: sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==} engines: {node: '>= 10.0.0'} dependencies: - '@babel/types': 7.20.7 + '@babel/types': 7.21.5 dev: true /bach@1.2.0: @@ -8478,6 +9902,10 @@ packages: /balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + /base-64@1.0.0: + resolution: {integrity: sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==} + dev: false + /base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -8671,6 +10099,12 @@ packages: engines: {'0': node} dev: false + /browserify-zlib@0.1.4: + resolution: {integrity: sha512-19OEpq7vWgsH6WkvkBJQDFvJS1uPcbFOQ4v9CU839dO+ZZXUZO6XpE6hNCqvlIIj+4fZvRiJ6DsAQ382GwiyTQ==} + dependencies: + pako: 0.2.9 + dev: true + /browserslist@4.21.4: resolution: {integrity: sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -8753,6 +10187,11 @@ packages: dev: false optional: true + /builtin-modules@3.3.0: + resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} + engines: {node: '>=6'} + dev: true + /bull@3.29.3: resolution: {integrity: sha512-MOqV1dKLy1YQgP9m3lFolyMxaU+1+o4afzYYf0H4wNM+x/S0I1QPQfkgGlLiH00EyFrvSmeubeCYFP47rTfpjg==} engines: {node: '>=10'} @@ -8949,14 +10388,6 @@ packages: /caseless@0.12.0: resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==} - /cfb@1.2.2: - resolution: {integrity: sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==} - engines: {node: '>=0.8'} - dependencies: - adler-32: 1.3.1 - crc-32: 1.2.2 - dev: false - /chai@4.3.7: resolution: {integrity: sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==} engines: {node: '>=4'} @@ -9130,6 +10561,13 @@ packages: source-map: 0.6.1 dev: true + /clean-regexp@1.0.0: + resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==} + engines: {node: '>=4'} + dependencies: + escape-string-regexp: 1.0.5 + dev: true + /clean-stack@2.2.0: resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} engines: {node: '>=6'} @@ -9372,11 +10810,6 @@ packages: - '@lezer/common' dev: false - /codepage@1.15.0: - resolution: {integrity: sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==} - engines: {node: '>=0.8'} - dev: false - /collect-v8-coverage@1.0.1: resolution: {integrity: sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==} dev: true @@ -9491,18 +10924,10 @@ packages: resolution: {integrity: sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==} dev: true - /component-props@1.1.1: - resolution: {integrity: sha512-69pIRJs9fCCHRqCz3390YF2LV1Lu6iEMZ5zuVqqUn+G20V9BNXlMs0cWawWeW9g4Ynmg29JmkG6R7/lUJoGd1Q==} - dev: false - /component-type@1.2.1: resolution: {integrity: sha512-Kgy+2+Uwr75vAi6ChWXgHuLvd+QLD7ssgpaRq2zCvt80ptvAfMc/hijcJxXkBa2wMlEZcJvC2H8Ubo+A9ATHIg==} dev: false - /component-xor@0.0.4: - resolution: {integrity: sha512-ZIt6sla8gfo+AFVRZoZOertcnD5LJaY2T9CKE2j13NJxQt/mUafD69Bl7/Y4AnpI2LGjiXH7cOfJDx/n2G9edA==} - dev: false - /compressible@2.0.18: resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==} engines: {node: '>= 0.6'} @@ -9775,9 +11200,9 @@ packages: /constantinople@4.0.1: resolution: {integrity: sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==} - dependencies: - '@babel/parser': 7.20.7 - '@babel/types': 7.20.7 + dependencies: + '@babel/parser': 7.21.8 + '@babel/types': 7.21.5 dev: true /content-disposition@0.5.4: @@ -9911,12 +11336,6 @@ packages: dev: false optional: true - /crc-32@1.2.2: - resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} - engines: {node: '>=0.8'} - hasBin: true - dev: false - /crelt@1.0.5: resolution: {integrity: sha512-+BO9wPPi+DWTDcNYhr/W90myha8ptzftZT+LwcmUbbok0rcP/fequmFYCw8NMoH7pkAZQzU78b3kYrlua5a9eA==} dev: false @@ -10017,7 +11436,7 @@ packages: postcss-value-parser: 4.2.0 schema-utils: 2.7.1 semver: 6.3.0 - webpack: 5.75.0(esbuild@0.16.17) + webpack: 5.75.0(esbuild@0.17.18) dev: true /css-loader@6.7.3(webpack@5.75.0): @@ -10034,7 +11453,7 @@ packages: postcss-modules-values: 4.0.0(postcss@8.4.21) postcss-value-parser: 4.2.0 semver: 7.3.8 - webpack: 5.75.0(esbuild@0.16.17) + webpack: 5.75.0(esbuild@0.17.18) dev: true /css-select@4.3.0: @@ -10562,13 +11981,6 @@ packages: resolution: {integrity: sha512-jNCX+uNJ3v38BKvPbpki6j5ItVlnSqVV6vDWGS6rExzCMjsc39frLjm1n91o6YaKK6AZl0wLloItW6C6mr61BQ==} dev: true - /dom-iterator@1.0.0: - resolution: {integrity: sha512-7dsMOQI07EMU98gQM8NSB3GsAiIeBYIPKpnxR3c9xOvdvBjChAcOM0iJ222I3p5xyiZO9e5oggkNaCusuTdYig==} - dependencies: - component-props: 1.1.1 - component-xor: 0.0.4 - dev: false - /dom-serializer@0.2.2: resolution: {integrity: sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==} dependencies: @@ -10802,8 +12214,8 @@ packages: dependencies: once: 1.4.0 - /enhanced-resolve@5.10.0: - resolution: {integrity: sha512-T0yTFjdpldGY8PmuXXR0PyQ1ufZpEGiHVrp7zHKB7jdR4qlmZHhONVM5AQOAWXuF/w3dnHbEQVrNptJgt7F+cQ==} + /enhanced-resolve@5.13.0: + resolution: {integrity: sha512-eyV8f0y1+bzyfh8xAwW/WTSZpLbjhqc4ne9eGSH4Zo2ejdyiNG9pU6mf9DG8a7+Auk6MFTlNOT4Y2y/9k8GKVg==} engines: {node: '>=10.13.0'} dependencies: graceful-fs: 4.2.10 @@ -10855,35 +12267,6 @@ packages: is-arrayish: 0.2.1 dev: true - /es-abstract@1.20.4: - resolution: {integrity: sha512-0UtvRN79eMe2L+UNEF1BwRe364sj/DXhQ/k5FmivgoSdpM90b8Jc0mDzKMGo7QS0BVbOP/bTwBKNnDc9rNzaPA==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.2 - es-to-primitive: 1.2.1 - function-bind: 1.1.1 - function.prototype.name: 1.1.5 - get-intrinsic: 1.1.3 - get-symbol-description: 1.0.0 - has: 1.0.3 - has-property-descriptors: 1.0.0 - has-symbols: 1.0.3 - internal-slot: 1.0.4 - is-callable: 1.2.7 - is-negative-zero: 2.0.2 - is-regex: 1.1.4 - is-shared-array-buffer: 1.0.2 - is-string: 1.0.7 - is-weakref: 1.0.2 - object-inspect: 1.12.3 - object-keys: 1.1.1 - object.assign: 4.1.4 - regexp.prototype.flags: 1.4.3 - safe-regex-test: 1.0.0 - string.prototype.trimend: 1.0.5 - string.prototype.trimstart: 1.0.5 - unbox-primitive: 1.0.2 - /es-abstract@1.21.1: resolution: {integrity: sha512-QudMsPOz86xYz/1dG1OuGBKOELjCh99IIWHLzy5znUB6j8xG2yMA7bfTV86VSqKF+Y/H08vQPR+9jyXpuC6hfg==} engines: {node: '>= 0.4'} @@ -10927,7 +12310,7 @@ packages: engines: {node: '>= 0.4'} dependencies: define-properties: 1.1.4 - es-abstract: 1.20.4 + es-abstract: 1.21.1 function-bind: 1.1.1 functions-have-names: 1.2.3 get-intrinsic: 1.1.3 @@ -11024,13 +12407,13 @@ packages: resolution: {integrity: sha512-jyfL/pwPqaFXyKnj8lP8iLk6Z0m099uXR45aSN8Av1XD4vhvQutxxPzgA2bTcAwQpa1zCXDcWOlhFgyP3GKqhQ==} dev: true - /esbuild-register@3.4.2(esbuild@0.16.17): + /esbuild-register@3.4.2(esbuild@0.17.18): resolution: {integrity: sha512-kG/XyTDyz6+YDuyfB9ZoSIOOmgyFCH+xPRtsCa8W85HLRV5Csp+o3jWVbOSHgSLfyLc5DmP+KFDNwty4mEjC+Q==} peerDependencies: esbuild: '>=0.12 <1' dependencies: debug: 4.3.4(supports-color@8.1.1) - esbuild: 0.16.17 + esbuild: 0.17.18 transitivePeerDependencies: - supports-color dev: true @@ -11064,6 +12447,35 @@ packages: '@esbuild/win32-x64': 0.16.17 dev: true + /esbuild@0.17.18: + resolution: {integrity: sha512-z1lix43jBs6UKjcZVKOw2xx69ffE2aG0PygLL5qJ9OS/gy0Ewd1gW/PUQIOIQGXBHWNywSc0floSKoMFF8aK2w==} + engines: {node: '>=12'} + hasBin: true + optionalDependencies: + '@esbuild/android-arm': 0.17.18 + '@esbuild/android-arm64': 0.17.18 + '@esbuild/android-x64': 0.17.18 + '@esbuild/darwin-arm64': 0.17.18 + '@esbuild/darwin-x64': 0.17.18 + '@esbuild/freebsd-arm64': 0.17.18 + '@esbuild/freebsd-x64': 0.17.18 + '@esbuild/linux-arm': 0.17.18 + '@esbuild/linux-arm64': 0.17.18 + '@esbuild/linux-ia32': 0.17.18 + '@esbuild/linux-loong64': 0.17.18 + '@esbuild/linux-mips64el': 0.17.18 + '@esbuild/linux-ppc64': 0.17.18 + '@esbuild/linux-riscv64': 0.17.18 + '@esbuild/linux-s390x': 0.17.18 + '@esbuild/linux-x64': 0.17.18 + '@esbuild/netbsd-x64': 0.17.18 + '@esbuild/openbsd-x64': 0.17.18 + '@esbuild/sunos-x64': 0.17.18 + '@esbuild/win32-arm64': 0.17.18 + '@esbuild/win32-ia32': 0.17.18 + '@esbuild/win32-x64': 0.17.18 + dev: true + /escalade@3.1.1: resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} engines: {node: '>=6'} @@ -11110,7 +12522,7 @@ packages: source-map: 0.6.1 dev: true - /eslint-config-airbnb-base@15.0.0(eslint-plugin-import@2.26.0)(eslint@8.28.0): + /eslint-config-airbnb-base@15.0.0(eslint-plugin-import@2.27.5)(eslint@8.39.0): resolution: {integrity: sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig==} engines: {node: ^10.12.0 || >=12.0.0} peerDependencies: @@ -11118,14 +12530,14 @@ packages: eslint-plugin-import: ^2.25.2 dependencies: confusing-browser-globals: 1.0.11 - eslint: 8.28.0 - eslint-plugin-import: 2.26.0(@typescript-eslint/parser@5.45.0)(eslint-import-resolver-typescript@3.5.2)(eslint@8.28.0) + eslint: 8.39.0 + eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.59.0)(eslint-import-resolver-typescript@3.5.5)(eslint@8.39.0) object.assign: 4.1.4 object.entries: 1.1.5 semver: 6.3.0 dev: true - /eslint-config-airbnb-typescript@17.0.0(@typescript-eslint/eslint-plugin@5.45.0)(@typescript-eslint/parser@5.45.0)(eslint-plugin-import@2.26.0)(eslint@8.28.0): + /eslint-config-airbnb-typescript@17.0.0(@typescript-eslint/eslint-plugin@5.59.0)(@typescript-eslint/parser@5.59.0)(eslint-plugin-import@2.27.5)(eslint@8.39.0): resolution: {integrity: sha512-elNiuzD0kPAPTXjFWg+lE24nMdHMtuxgYoD30OyMD6yrW1AhFZPAg27VX7d3tzOErw+dgJTNWfRSDqEcXb4V0g==} peerDependencies: '@typescript-eslint/eslint-plugin': ^5.13.0 @@ -11133,56 +12545,61 @@ packages: eslint: ^7.32.0 || ^8.2.0 eslint-plugin-import: ^2.25.3 dependencies: - '@typescript-eslint/eslint-plugin': 5.45.0(@typescript-eslint/parser@5.45.0)(eslint@8.28.0)(typescript@5.0.3) - '@typescript-eslint/parser': 5.45.0(eslint@8.28.0)(typescript@5.0.3) - eslint: 8.28.0 - eslint-config-airbnb-base: 15.0.0(eslint-plugin-import@2.26.0)(eslint@8.28.0) - eslint-plugin-import: 2.26.0(@typescript-eslint/parser@5.45.0)(eslint-import-resolver-typescript@3.5.2)(eslint@8.28.0) + '@typescript-eslint/eslint-plugin': 5.59.0(patch_hash=tk3n6hvmqwfzrfqe3awfxnqtuy)(@typescript-eslint/parser@5.59.0)(eslint@8.39.0)(typescript@5.0.3) + '@typescript-eslint/parser': 5.59.0(eslint@8.39.0)(typescript@5.0.3) + eslint: 8.39.0 + eslint-config-airbnb-base: 15.0.0(eslint-plugin-import@2.27.5)(eslint@8.39.0) + eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.59.0)(eslint-import-resolver-typescript@3.5.5)(eslint@8.39.0) dev: true - /eslint-config-prettier@8.5.0(eslint@8.28.0): - resolution: {integrity: sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q==} + /eslint-config-prettier@8.8.0(eslint@8.39.0): + resolution: {integrity: sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA==} hasBin: true peerDependencies: eslint: '>=7.0.0' dependencies: - eslint: 8.28.0 + eslint: 8.39.0 dev: true /eslint-config-riot@1.0.0: resolution: {integrity: sha512-NB/L/1Y30qyJcG5xZxCJKW/+bqyj+llbcCwo9DEz8bESIP0SLTOQ8T1DWCCFc+wJ61AMEstj4511PSScqMMfCw==} dev: false - /eslint-import-resolver-node@0.3.6: - resolution: {integrity: sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw==} + /eslint-import-resolver-node@0.3.7: + resolution: {integrity: sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA==} dependencies: debug: 3.2.7(supports-color@8.1.1) + is-core-module: 2.11.0 resolve: 1.22.1 transitivePeerDependencies: - supports-color dev: true - /eslint-import-resolver-typescript@3.5.2(eslint-plugin-import@2.26.0)(eslint@8.28.0): - resolution: {integrity: sha512-zX4ebnnyXiykjhcBvKIf5TNvt8K7yX6bllTRZ14MiurKPjDpCAZujlszTdB8pcNXhZcOf+god4s9SjQa5GnytQ==} + /eslint-import-resolver-typescript@3.5.5(@typescript-eslint/parser@5.59.0)(eslint-plugin-import@2.27.5)(eslint@8.39.0): + resolution: {integrity: sha512-TdJqPHs2lW5J9Zpe17DZNQuDnox4xo2o+0tE7Pggain9Rbc19ik8kFtXdxZ250FVx2kF4vlt2RSf4qlUpG7bhw==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: eslint: '*' eslint-plugin-import: '*' dependencies: debug: 4.3.4(supports-color@8.1.1) - enhanced-resolve: 5.10.0 - eslint: 8.28.0 - eslint-plugin-import: 2.26.0(@typescript-eslint/parser@5.45.0)(eslint-import-resolver-typescript@3.5.2)(eslint@8.28.0) - get-tsconfig: 4.2.0 + enhanced-resolve: 5.13.0 + eslint: 8.39.0 + eslint-module-utils: 2.7.4(@typescript-eslint/parser@5.59.0)(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.5.5)(eslint@8.39.0) + eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.59.0)(eslint-import-resolver-typescript@3.5.5)(eslint@8.39.0) + get-tsconfig: 4.5.0 globby: 13.1.3 is-core-module: 2.11.0 is-glob: 4.0.3 - synckit: 0.8.4 + synckit: 0.8.5 transitivePeerDependencies: + - '@typescript-eslint/parser' + - eslint-import-resolver-node + - eslint-import-resolver-webpack - supports-color dev: true - /eslint-module-utils@2.7.4(@typescript-eslint/parser@5.45.0)(eslint-import-resolver-node@0.3.6)(eslint-import-resolver-typescript@3.5.2)(eslint@8.28.0): + /eslint-module-utils@2.7.4(@typescript-eslint/parser@5.59.0)(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.5.5)(eslint@8.39.0): resolution: {integrity: sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA==} engines: {node: '>=4'} peerDependencies: @@ -11203,26 +12620,26 @@ packages: eslint-import-resolver-webpack: optional: true dependencies: - '@typescript-eslint/parser': 5.45.0(eslint@8.28.0)(typescript@5.0.3) + '@typescript-eslint/parser': 5.59.0(eslint@8.39.0)(typescript@5.0.3) debug: 3.2.7(supports-color@8.1.1) - eslint: 8.28.0 - eslint-import-resolver-node: 0.3.6 - eslint-import-resolver-typescript: 3.5.2(eslint-plugin-import@2.26.0)(eslint@8.28.0) + eslint: 8.39.0 + eslint-import-resolver-node: 0.3.7 + eslint-import-resolver-typescript: 3.5.5(@typescript-eslint/parser@5.59.0)(eslint-plugin-import@2.27.5)(eslint@8.39.0) transitivePeerDependencies: - supports-color dev: true - /eslint-plugin-diff@2.0.1(eslint@8.28.0): + /eslint-plugin-diff@2.0.1(eslint@8.39.0): resolution: {integrity: sha512-qqbvwaaO1cfkUprliqiRojRsD0qGsvzmJNqNrb9s0h15sDVzZMXYdu0TUFpUwauLeU28etSsfWIp0Uu+OAcXXw==} engines: {node: '>=14.0.0'} peerDependencies: eslint: '>=6.7.0' dependencies: - eslint: 8.28.0 + eslint: 8.39.0 dev: true - /eslint-plugin-import@2.26.0(@typescript-eslint/parser@5.45.0)(eslint-import-resolver-typescript@3.5.2)(eslint@8.28.0): - resolution: {integrity: sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==} + /eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.59.0)(eslint-import-resolver-typescript@3.5.5)(eslint@8.39.0): + resolution: {integrity: sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==} engines: {node: '>=4'} peerDependencies: '@typescript-eslint/parser': '*' @@ -11231,20 +12648,22 @@ packages: '@typescript-eslint/parser': optional: true dependencies: - '@typescript-eslint/parser': 5.45.0(eslint@8.28.0)(typescript@5.0.3) - array-includes: 3.1.5 - array.prototype.flat: 1.3.0 - debug: 2.6.9 + '@typescript-eslint/parser': 5.59.0(eslint@8.39.0)(typescript@5.0.3) + array-includes: 3.1.6 + array.prototype.flat: 1.3.1 + array.prototype.flatmap: 1.3.1 + debug: 3.2.7(supports-color@8.1.1) doctrine: 2.1.0 - eslint: 8.28.0 - eslint-import-resolver-node: 0.3.6 - eslint-module-utils: 2.7.4(@typescript-eslint/parser@5.45.0)(eslint-import-resolver-node@0.3.6)(eslint-import-resolver-typescript@3.5.2)(eslint@8.28.0) + eslint: 8.39.0 + eslint-import-resolver-node: 0.3.7 + eslint-module-utils: 2.7.4(@typescript-eslint/parser@5.59.0)(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.5.5)(eslint@8.39.0) has: 1.0.3 is-core-module: 2.11.0 is-glob: 4.0.3 minimatch: 3.1.2 - object.values: 1.1.5 + object.values: 1.1.6 resolve: 1.22.1 + semver: 6.3.0 tsconfig-paths: 3.14.1 transitivePeerDependencies: - eslint-import-resolver-typescript @@ -11256,10 +12675,10 @@ packages: resolution: {integrity: sha512-qe6sVFDP1Vj5eXlqZxYZpIjwYvhuqXlI0P8OfPyhiPOhMkFtr0TpFphD8/6WCzkm7LJCvG1eJEzURCtMIsFTAg==} dev: true - /eslint-plugin-n8n-nodes-base@1.12.0(eslint@8.28.0)(typescript@5.0.3): + /eslint-plugin-n8n-nodes-base@1.12.0(eslint@8.39.0)(typescript@5.0.3): resolution: {integrity: sha512-AotXR6IsxLNnxp4OxhD33xcmRFwVq7ZImBd0mTgpirV3VX8pCJDdiDlI2zCAICcICZxtOdbVtHOMhhnMjTh71A==} dependencies: - '@typescript-eslint/utils': 5.45.0(eslint@8.28.0)(typescript@5.0.3) + '@typescript-eslint/utils': 5.45.0(eslint@8.39.0)(typescript@5.0.3) camel-case: 4.1.2 indefinite: 2.4.1 pascal-case: 3.1.2 @@ -11273,7 +12692,7 @@ packages: - typescript dev: true - /eslint-plugin-prettier@4.2.1(eslint-config-prettier@8.5.0)(eslint@8.28.0)(prettier@2.8.3): + /eslint-plugin-prettier@4.2.1(eslint-config-prettier@8.8.0)(eslint@8.39.0)(prettier@2.8.3): resolution: {integrity: sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==} engines: {node: '>=12.0.0'} peerDependencies: @@ -11284,27 +12703,72 @@ packages: eslint-config-prettier: optional: true dependencies: - eslint: 8.28.0 - eslint-config-prettier: 8.5.0(eslint@8.28.0) + eslint: 8.39.0 + eslint-config-prettier: 8.8.0(eslint@8.39.0) prettier: 2.8.3 prettier-linter-helpers: 1.0.0 dev: true - /eslint-plugin-vue@7.17.0(eslint@8.28.0): + /eslint-plugin-unicorn@46.0.0(eslint@8.39.0): + resolution: {integrity: sha512-j07WkC+PFZwk8J33LYp6JMoHa1lXc1u6R45pbSAipjpfpb7KIGr17VE2D685zCxR5VL4cjrl65kTJflziQWMDA==} + engines: {node: '>=14.18'} + peerDependencies: + eslint: '>=8.28.0' + dependencies: + '@babel/helper-validator-identifier': 7.19.1 + '@eslint-community/eslint-utils': 4.4.0(eslint@8.39.0) + ci-info: 3.7.1 + clean-regexp: 1.0.0 + eslint: 8.39.0 + esquery: 1.5.0 + indent-string: 4.0.0 + is-builtin-module: 3.2.1 + jsesc: 3.0.2 + lodash: 4.17.21 + pluralize: 8.0.0 + read-pkg-up: 7.0.1 + regexp-tree: 0.1.25 + regjsparser: 0.9.1 + safe-regex: 2.1.1 + semver: 7.3.8 + strip-indent: 3.0.0 + dev: true + + /eslint-plugin-unused-imports@2.0.0(@typescript-eslint/eslint-plugin@5.59.0)(eslint@8.39.0): + resolution: {integrity: sha512-3APeS/tQlTrFa167ThtP0Zm0vctjr4M44HMpeg1P4bK6wItarumq0Ma82xorMKdFsWpphQBlRPzw/pxiVELX1A==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + '@typescript-eslint/eslint-plugin': ^5.0.0 + eslint: ^8.0.0 + peerDependenciesMeta: + '@typescript-eslint/eslint-plugin': + optional: true + dependencies: + '@typescript-eslint/eslint-plugin': 5.59.0(patch_hash=tk3n6hvmqwfzrfqe3awfxnqtuy)(@typescript-eslint/parser@5.59.0)(eslint@8.39.0)(typescript@5.0.3) + eslint: 8.39.0 + eslint-rule-composer: 0.3.0 + dev: true + + /eslint-plugin-vue@7.17.0(eslint@8.39.0): resolution: {integrity: sha512-Rq5R2QetDCgC+kBFQw1+aJ5B93tQ4xqZvoCUxuIzwTonngNArsdP8ChM8PowIzsJvRtWl4ltGh/bZcN3xhFWSw==} engines: {node: '>=8.10'} peerDependencies: eslint: ^6.2.0 || ^7.0.0 || ^8.0.0-0 dependencies: - eslint: 8.28.0 + eslint: 8.39.0 eslint-utils: 2.1.0 natural-compare: 1.4.0 semver: 6.3.0 - vue-eslint-parser: 7.11.0(eslint@8.28.0) + vue-eslint-parser: 7.11.0(eslint@8.39.0) transitivePeerDependencies: - supports-color dev: true + /eslint-rule-composer@0.3.0: + resolution: {integrity: sha512-bt+Sh8CtDmn2OajxvNO+BX7Wn4CIWMpTRm3MaiKPCQcnnlm0CS2mhui6QaoeQugs+3Kj2ESKEEGJUdVafwhiCg==} + engines: {node: '>=4.0.0'} + dev: true + /eslint-scope@5.1.1: resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} engines: {node: '>=8.0.0'} @@ -11313,8 +12777,8 @@ packages: estraverse: 4.3.0 dev: true - /eslint-scope@7.1.1: - resolution: {integrity: sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==} + /eslint-scope@7.2.0: + resolution: {integrity: sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: esrecurse: 4.3.0 @@ -11328,13 +12792,13 @@ packages: eslint-visitor-keys: 1.3.0 dev: true - /eslint-utils@3.0.0(eslint@8.28.0): + /eslint-utils@3.0.0(eslint@8.39.0): resolution: {integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==} engines: {node: ^10.0.0 || ^12.0.0 || >= 14.0.0} peerDependencies: eslint: '>=5' dependencies: - eslint: 8.28.0 + eslint: 8.39.0 eslint-visitor-keys: 2.1.0 dev: true @@ -11348,18 +12812,21 @@ packages: engines: {node: '>=10'} dev: true - /eslint-visitor-keys@3.3.0: - resolution: {integrity: sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==} + /eslint-visitor-keys@3.4.0: + resolution: {integrity: sha512-HPpKPUBQcAsZOsHAFwTtIKcYlCje62XB7SEAcxjtmW6TD1WVpkS6i6/hOVtTZIl4zGj/mBqpFVGvaDneik+VoQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true - /eslint@8.28.0: - resolution: {integrity: sha512-S27Di+EVyMxcHiwDrFzk8dJYAaD+/5SoWKxL1ri/71CRHsnJnRDPNt2Kzj24+MT9FDupf4aqqyqPrvI8MvQ4VQ==} + /eslint@8.39.0: + resolution: {integrity: sha512-mwiok6cy7KTW7rBpo05k6+p4YVZByLNjAZ/ACB9DRCu4YDRwjXI01tWHp6KAUWelsBetTxKK/2sHB0vdS8Z2Og==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} hasBin: true dependencies: - '@eslint/eslintrc': 1.3.3 - '@humanwhocodes/config-array': 0.11.6 + '@eslint-community/eslint-utils': 4.4.0(eslint@8.39.0) + '@eslint-community/regexpp': 4.5.0 + '@eslint/eslintrc': 2.0.2 + '@eslint/js': 8.39.0 + '@humanwhocodes/config-array': 0.11.8 '@humanwhocodes/module-importer': 1.0.1 '@nodelib/fs.walk': 1.2.8 ajv: 6.12.6 @@ -11368,17 +12835,16 @@ packages: debug: 4.3.4(supports-color@8.1.1) doctrine: 3.0.0 escape-string-regexp: 4.0.0 - eslint-scope: 7.1.1 - eslint-utils: 3.0.0(eslint@8.28.0) - eslint-visitor-keys: 3.3.0 - espree: 9.4.0 - esquery: 1.4.0 + eslint-scope: 7.2.0 + eslint-visitor-keys: 3.4.0 + espree: 9.5.1 + esquery: 1.5.0 esutils: 2.0.3 fast-deep-equal: 3.1.3 file-entry-cache: 6.0.1 find-up: 5.0.0 glob-parent: 6.0.2 - globals: 13.17.0 + globals: 13.20.0 grapheme-splitter: 1.0.4 ignore: 5.2.4 import-fresh: 3.3.0 @@ -11393,7 +12859,6 @@ packages: minimatch: 3.1.2 natural-compare: 1.4.0 optionator: 0.9.1 - regexpp: 3.2.0 strip-ansi: 6.0.1 strip-json-comments: 3.1.1 text-table: 0.2.0 @@ -11410,13 +12875,13 @@ packages: eslint-visitor-keys: 1.3.0 dev: true - /espree@9.4.0: - resolution: {integrity: sha512-DQmnRpLj7f6TgN/NYb0MTzJXL+vJF9h3pHy4JhCIs3zwcgez8xmGg3sXHcEO97BrmO2OSvCwMdfdlyl+E9KjOw==} + /espree@9.5.1: + resolution: {integrity: sha512-5yxtHSZXRSW5pvv3hAlXM5+/Oswi1AUFqBmbibKb5s6bp3rGIDkyXU6xCoyuuLhijr4SFwPrXRoZjz0AZDN9tg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: acorn: 8.8.1 acorn-jsx: 5.3.2(acorn@8.8.1) - eslint-visitor-keys: 3.3.0 + eslint-visitor-keys: 3.4.0 dev: true /esprima-next@5.8.4: @@ -11443,6 +12908,13 @@ packages: estraverse: 5.3.0 dev: true + /esquery@1.5.0: + resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} + engines: {node: '>=0.10'} + dependencies: + estraverse: 5.3.0 + dev: true + /esrecurse@4.3.0: resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} engines: {node: '>=4.0'} @@ -11551,11 +13023,6 @@ packages: pify: 2.3.0 dev: true - /exit-on-epipe@1.0.1: - resolution: {integrity: sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw==} - engines: {node: '>=0.8'} - dev: false - /exit@0.1.2: resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} engines: {node: '>= 0.8.0'} @@ -12139,7 +13606,7 @@ packages: tapable: 1.1.3 typescript: 5.0.3 vue-template-compiler: 2.7.14 - webpack: 5.75.0(esbuild@0.16.17) + webpack: 5.75.0(esbuild@0.17.18) dev: true /form-data@2.3.3: @@ -12201,11 +13668,6 @@ packages: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} - /frac@1.1.2: - resolution: {integrity: sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==} - engines: {node: '>=0.8'} - dev: false - /fraction.js@4.2.0: resolution: {integrity: sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==} dev: true @@ -12376,6 +13838,11 @@ packages: has: 1.0.3 has-symbols: 1.0.3 + /get-npm-tarball-url@2.0.3: + resolution: {integrity: sha512-R/PW6RqyaBQNWYaSyfrh54/qtcnOp22FHCCiRhSSZj0FP3KQWCsxxt0DzIdVTbwTqe9CtQfvl/FPD4UIPt4pqw==} + engines: {node: '>=12.17'} + dev: true + /get-package-type@0.1.0: resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} engines: {node: '>=8.0.0'} @@ -12413,8 +13880,8 @@ packages: engines: {node: '>8.0.0'} dev: false - /get-tsconfig@4.2.0: - resolution: {integrity: sha512-X8u8fREiYOE6S8hLbq99PeykTDoLVnxvF4DjWKJmz9xy2nNRdUcV8ZN9tniJFeKyTU3qnC9lL8n4Chd6LmVKHg==} + /get-tsconfig@4.5.0: + resolution: {integrity: sha512-MjhiaIWCJ1sAU4pIQ5i5OfOuHHxVo1oYeNsWTON7jxYkod8pHocXeh+SSbmu5OZZZK73B6cbJ2XADzXehLyovQ==} dev: true /get-uri@3.0.2: @@ -12602,8 +14069,8 @@ packages: engines: {node: '>=4'} dev: true - /globals@13.17.0: - resolution: {integrity: sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw==} + /globals@13.20.0: + resolution: {integrity: sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==} engines: {node: '>=8'} dependencies: type-fest: 0.20.2 @@ -12636,7 +14103,7 @@ packages: dependencies: dir-glob: 3.0.1 fast-glob: 3.2.12 - ignore: 5.2.0 + ignore: 5.2.4 merge2: 1.4.1 slash: 4.0.0 dev: true @@ -12731,6 +14198,18 @@ packages: glogg: 1.0.2 dev: true + /gunzip-maybe@1.4.2: + resolution: {integrity: sha512-4haO1M4mLO91PW57BMsDFf75UmwoRX0GkdD+Faw+Lr+r/OZrOCS0pIBwOL1xCKQqnQzbNFGgK2V2CpBUPeFNTw==} + hasBin: true + dependencies: + browserify-zlib: 0.1.4 + is-deflate: 1.0.0 + is-gzip: 1.0.0 + peek-stream: 1.1.3 + pumpify: 1.5.1 + through2: 2.0.5 + dev: true + /handlebars@4.7.7: resolution: {integrity: sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==} engines: {node: '>=0.4.7'} @@ -12935,7 +14414,7 @@ packages: lodash: 4.17.21 pretty-error: 4.0.0 tapable: 2.2.1 - webpack: 5.75.0(esbuild@0.16.17) + webpack: 5.75.0(esbuild@0.17.18) dev: true /htmlparser2@3.8.3: @@ -13121,11 +14600,6 @@ packages: resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==} dev: true - /ignore@5.2.0: - resolution: {integrity: sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==} - engines: {node: '>= 4'} - dev: true - /ignore@5.2.4: resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} engines: {node: '>= 4'} @@ -13375,6 +14849,13 @@ packages: /is-buffer@1.1.6: resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==} + /is-builtin-module@3.2.1: + resolution: {integrity: sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==} + engines: {node: '>=6'} + dependencies: + builtin-modules: 3.3.0 + dev: true + /is-callable@1.2.7: resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} engines: {node: '>= 0.4'} @@ -13411,6 +14892,10 @@ packages: dependencies: has-tostringtag: 1.0.0 + /is-deflate@1.0.0: + resolution: {integrity: sha512-YDoFpuZWu1VRXlsnlYMzKyVRITXj7Ej/V9gXQ2/pAe7X1J7M/RNOqaIYi6qUn+B7nGyB9pDXrv02dsB58d2ZAQ==} + dev: true + /is-descriptor@0.1.6: resolution: {integrity: sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==} engines: {node: '>=0.10.0'} @@ -13504,6 +14989,11 @@ packages: dependencies: is-extglob: 2.1.1 + /is-gzip@1.0.0: + resolution: {integrity: sha512-rcfALRIb1YewtnksfRIHGcIY93QnK8BIQ/2c9yDYcG/Y6+vRoJuTWBmmSEbyLLYtXm7q35pHOHbZFQBaLrhlWQ==} + engines: {node: '>=0.10.0'} + dev: true + /is-installed-globally@0.4.0: resolution: {integrity: sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==} engines: {node: '>=10'} @@ -13773,8 +15263,8 @@ packages: resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==} engines: {node: '>=8'} dependencies: - '@babel/core': 7.20.12 - '@babel/parser': 7.20.7 + '@babel/core': 7.21.8 + '@babel/parser': 7.21.8 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.0 semver: 6.3.0 @@ -13896,11 +15386,11 @@ packages: ts-node: optional: true dependencies: - '@babel/core': 7.20.12 + '@babel/core': 7.21.8 '@jest/test-sequencer': 29.5.0 '@jest/types': 29.5.0 '@types/node': 16.18.12 - babel-jest: 29.5.0(@babel/core@7.20.12) + babel-jest: 29.5.0(@babel/core@7.21.8) chalk: 4.1.2 ci-info: 3.7.1 deepmerge: 4.2.2 @@ -14082,7 +15572,7 @@ packages: resolution: {integrity: sha512-lMJTbgNcDm5z+6KDxWtqOFWlGQxD6XaYwBqHR8kmpkP+WWWG90I35kdtQHY67Ay5CSuydkTBbJG+tH9JShFCyA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@babel/code-frame': 7.18.6 + '@babel/code-frame': 7.21.4 '@jest/types': 29.5.0 '@types/stack-utils': 2.0.1 chalk: 4.1.2 @@ -14112,7 +15602,7 @@ packages: resolution: {integrity: sha512-Kijeg9Dag6CKtIDA7O21zNTACqD5MD/8HfIV8pdD94vFyFuer52SigdC3IQMhab3vACxXMiFk+yMHNdbqtyTGA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@babel/code-frame': 7.18.6 + '@babel/code-frame': 7.21.4 '@jest/types': 29.5.0 '@types/stack-utils': 2.0.1 chalk: 4.1.2 @@ -14248,18 +15738,18 @@ packages: resolution: {integrity: sha512-x7Wolra5V0tt3wRs3/ts3S6ciSQVypgGQlJpz2rsdQYoUKxMxPNaoHMGJN6qAuPJqS+2iQ1ZUn5kl7HCyls84g==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@babel/core': 7.20.12 - '@babel/generator': 7.20.7 - '@babel/plugin-syntax-jsx': 7.18.6(@babel/core@7.20.12) - '@babel/plugin-syntax-typescript': 7.20.0(@babel/core@7.20.12) - '@babel/traverse': 7.20.12 - '@babel/types': 7.20.7 + '@babel/core': 7.21.8 + '@babel/generator': 7.21.5 + '@babel/plugin-syntax-jsx': 7.18.6(@babel/core@7.21.8) + '@babel/plugin-syntax-typescript': 7.20.0(@babel/core@7.21.8) + '@babel/traverse': 7.21.5 + '@babel/types': 7.21.5 '@jest/expect-utils': 29.5.0 '@jest/transform': 29.5.0 '@jest/types': 29.5.0 '@types/babel__traverse': 7.18.2 '@types/prettier': 2.7.1 - babel-preset-current-node-syntax: 1.0.1(@babel/core@7.20.12) + babel-preset-current-node-syntax: 1.0.1(@babel/core@7.21.8) chalk: 4.1.2 expect: 29.5.0 graceful-fs: 4.2.10 @@ -14434,23 +15924,23 @@ packages: /jsbn@0.1.1: resolution: {integrity: sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==} - /jscodeshift@0.13.1(@babel/preset-env@7.20.2): + /jscodeshift@0.13.1(@babel/preset-env@7.21.5): resolution: {integrity: sha512-lGyiEbGOvmMRKgWk4vf+lUrCWO/8YR8sUR3FKF1Cq5fovjZDlIcw3Hu5ppLHAnEXshVffvaM0eyuY/AbOeYpnQ==} hasBin: true peerDependencies: '@babel/preset-env': ^7.1.6 dependencies: - '@babel/core': 7.20.12 - '@babel/parser': 7.20.7 - '@babel/plugin-proposal-class-properties': 7.18.6(@babel/core@7.20.12) - '@babel/plugin-proposal-nullish-coalescing-operator': 7.18.6(@babel/core@7.20.12) - '@babel/plugin-proposal-optional-chaining': 7.20.7(@babel/core@7.20.12) - '@babel/plugin-transform-modules-commonjs': 7.20.11(@babel/core@7.20.12) - '@babel/preset-env': 7.20.2(@babel/core@7.20.12) - '@babel/preset-flow': 7.18.6(@babel/core@7.20.12) - '@babel/preset-typescript': 7.18.6(@babel/core@7.20.12) - '@babel/register': 7.18.9(@babel/core@7.20.12) - babel-core: 7.0.0-bridge.0(@babel/core@7.20.12) + '@babel/core': 7.21.8 + '@babel/parser': 7.21.8 + '@babel/plugin-proposal-class-properties': 7.18.6(@babel/core@7.21.8) + '@babel/plugin-proposal-nullish-coalescing-operator': 7.18.6(@babel/core@7.21.8) + '@babel/plugin-proposal-optional-chaining': 7.20.7(@babel/core@7.21.8) + '@babel/plugin-transform-modules-commonjs': 7.20.11(@babel/core@7.21.8) + '@babel/preset-env': 7.21.5(@babel/core@7.21.8) + '@babel/preset-flow': 7.18.6(@babel/core@7.21.8) + '@babel/preset-typescript': 7.18.6(@babel/core@7.21.8) + '@babel/register': 7.18.9(@babel/core@7.21.8) + babel-core: 7.0.0-bridge.0(@babel/core@7.21.8) chalk: 4.1.2 flow-parser: 0.197.0 graceful-fs: 4.2.10 @@ -14470,17 +15960,47 @@ packages: peerDependencies: '@babel/preset-env': ^7.1.6 dependencies: - '@babel/core': 7.20.12 - '@babel/parser': 7.20.7 - '@babel/plugin-proposal-class-properties': 7.18.6(@babel/core@7.20.12) - '@babel/plugin-proposal-nullish-coalescing-operator': 7.18.6(@babel/core@7.20.12) - '@babel/plugin-proposal-optional-chaining': 7.20.7(@babel/core@7.20.12) - '@babel/plugin-transform-modules-commonjs': 7.20.11(@babel/core@7.20.12) + '@babel/core': 7.21.8 + '@babel/parser': 7.21.8 + '@babel/plugin-proposal-class-properties': 7.18.6(@babel/core@7.21.8) + '@babel/plugin-proposal-nullish-coalescing-operator': 7.18.6(@babel/core@7.21.8) + '@babel/plugin-proposal-optional-chaining': 7.20.7(@babel/core@7.21.8) + '@babel/plugin-transform-modules-commonjs': 7.20.11(@babel/core@7.21.8) '@babel/preset-env': 7.20.2(@babel/core@7.20.12) - '@babel/preset-flow': 7.18.6(@babel/core@7.20.12) - '@babel/preset-typescript': 7.18.6(@babel/core@7.20.12) - '@babel/register': 7.18.9(@babel/core@7.20.12) - babel-core: 7.0.0-bridge.0(@babel/core@7.20.12) + '@babel/preset-flow': 7.18.6(@babel/core@7.21.8) + '@babel/preset-typescript': 7.18.6(@babel/core@7.21.8) + '@babel/register': 7.18.9(@babel/core@7.21.8) + babel-core: 7.0.0-bridge.0(@babel/core@7.21.8) + chalk: 4.1.2 + flow-parser: 0.197.0 + graceful-fs: 4.2.10 + micromatch: 4.0.5 + neo-async: 2.6.2 + node-dir: 0.1.17 + recast: 0.21.5 + temp: 0.8.4 + write-file-atomic: 2.4.3 + transitivePeerDependencies: + - supports-color + dev: true + + /jscodeshift@0.14.0(@babel/preset-env@7.21.5): + resolution: {integrity: sha512-7eCC1knD7bLUPuSCwXsMZUH51O8jIcoVyKtI6P0XM0IVzlGjckPy3FIwQlorzbN0Sg79oK+RlohN32Mqf/lrYA==} + hasBin: true + peerDependencies: + '@babel/preset-env': ^7.1.6 + dependencies: + '@babel/core': 7.21.8 + '@babel/parser': 7.21.8 + '@babel/plugin-proposal-class-properties': 7.18.6(@babel/core@7.21.8) + '@babel/plugin-proposal-nullish-coalescing-operator': 7.18.6(@babel/core@7.21.8) + '@babel/plugin-proposal-optional-chaining': 7.20.7(@babel/core@7.21.8) + '@babel/plugin-transform-modules-commonjs': 7.20.11(@babel/core@7.21.8) + '@babel/preset-env': 7.21.5(@babel/core@7.21.8) + '@babel/preset-flow': 7.18.6(@babel/core@7.21.8) + '@babel/preset-typescript': 7.18.6(@babel/core@7.21.8) + '@babel/register': 7.18.9(@babel/core@7.21.8) + babel-core: 7.0.0-bridge.0(@babel/core@7.21.8) chalk: 4.1.2 flow-parser: 0.197.0 graceful-fs: 4.2.10 @@ -14591,7 +16111,6 @@ packages: resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==} engines: {node: '>=6'} hasBin: true - dev: false /jshint@2.13.5: resolution: {integrity: sha512-dB2n1w3OaQ35PLcBGIWXlszjbPZwsgZoxsg6G8PtNf2cFMC1l0fObkYLUuXqTTdi6tKw4sAjfUseTdmDMHQRcg==} @@ -14646,13 +16165,6 @@ packages: /json-stringify-safe@5.0.1: resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} - /json5@1.0.1: - resolution: {integrity: sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==} - hasBin: true - dependencies: - minimist: 1.2.7 - dev: true - /json5@1.0.2: resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} hasBin: true @@ -14994,27 +16506,6 @@ packages: wrap-ansi: 7.0.0 dev: true - /lit-element@3.2.2: - resolution: {integrity: sha512-6ZgxBR9KNroqKb6+htkyBwD90XGRiqKDHVrW/Eh0EZ+l+iC+u+v+w3/BA5NGi4nizAVHGYvQBHUDuSmLjPp7NQ==} - dependencies: - '@lit/reactive-element': 1.6.1 - lit-html: 2.6.1 - dev: true - - /lit-html@2.6.1: - resolution: {integrity: sha512-Z3iw+E+3KKFn9t2YKNjsXNEu/LRLI98mtH/C6lnFg7kvaqPIzPn124Yd4eT/43lyqrejpc5Wb6BHq3fdv4S8Rw==} - dependencies: - '@types/trusted-types': 2.0.2 - dev: true - - /lit@2.6.1: - resolution: {integrity: sha512-DT87LD64f8acR7uVp7kZfhLRrHkfC/N4BVzAtnw9Yg8087mbBJ//qedwdwX0kzDbxgPccWRW6mFwGbRQIxy0pw==} - dependencies: - '@lit/reactive-element': 1.6.1 - lit-element: 3.2.2 - lit-html: 2.6.1 - dev: true - /load-json-file@1.1.0: resolution: {integrity: sha512-cy7ZdNRXdablkXYNI049pthVeXFurRyb9+hA/dZzerZ0pGTx42z+y+ssxBaVV2l70t1muq5IdKhn4UtcoGUY9A==} engines: {node: '>=0.10.0'} @@ -15965,9 +17456,6 @@ packages: /moment@2.29.4: resolution: {integrity: sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==} - /monaco-editor@0.33.0: - resolution: {integrity: sha512-VcRWPSLIUEgQJQIE0pVT8FcGBIgFoxz7jtqctE+IiCxWugD0DwgyQBcZBhdSrdMC84eumoqMZsGl2GTreOzwqw==} - /mongodb-connection-string-url@2.5.4: resolution: {integrity: sha512-SeAxuWs0ez3iI3vvmLk/j2y+zHwigTDKQhtdxTgt5ZCOQQS5+HW4g45/Xw5vzzbn7oQXCNQ24Z40AkJsizEy7w==} dependencies: @@ -16610,7 +18098,7 @@ packages: dependencies: call-bind: 1.0.2 define-properties: 1.1.4 - es-abstract: 1.20.4 + es-abstract: 1.21.1 dev: true /object.getownpropertydescriptors@2.1.4: @@ -16620,7 +18108,7 @@ packages: array.prototype.reduce: 1.0.4 call-bind: 1.0.2 define-properties: 1.1.4 - es-abstract: 1.20.4 + es-abstract: 1.21.1 dev: false /object.map@1.0.1: @@ -16646,13 +18134,13 @@ packages: make-iterator: 1.0.1 dev: true - /object.values@1.1.5: - resolution: {integrity: sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg==} + /object.values@1.1.6: + resolution: {integrity: sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==} engines: {node: '>= 0.4'} dependencies: call-bind: 1.0.2 define-properties: 1.1.4 - es-abstract: 1.20.4 + es-abstract: 1.21.1 dev: true /on-finished@2.4.1: @@ -16886,6 +18374,10 @@ packages: resolution: {integrity: sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==} dev: false + /pako@0.2.9: + resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==} + dev: true + /pako@1.0.11: resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} dev: false @@ -16937,7 +18429,7 @@ packages: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} dependencies: - '@babel/code-frame': 7.18.6 + '@babel/code-frame': 7.21.4 error-ex: 1.3.2 json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 @@ -17151,6 +18643,14 @@ packages: engines: {node: '>=8'} dev: false + /peek-stream@1.1.3: + resolution: {integrity: sha512-FhJ+YbOSBb9/rIl2ZeE/QHEsWn7PqNYt8ARAY3kIgNGOk13g9FGyIY6JIl/xB/3TFRVoTv5as0l11weORrTekA==} + dependencies: + buffer-from: 1.1.2 + duplexify: 3.7.1 + through2: 2.0.5 + dev: true + /pend@1.2.0: resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} dev: true @@ -17420,7 +18920,7 @@ packages: postcss: 7.0.39 schema-utils: 3.1.1 semver: 7.3.8 - webpack: 5.75.0(esbuild@0.16.17) + webpack: 5.75.0(esbuild@0.17.18) dev: true /postcss-modules-extract-imports@2.0.0: @@ -17617,7 +19117,7 @@ packages: resolution: {integrity: sha512-qKlHR8yFVCbcEWba0H0TOC8dnLlO4vPlyEjRPw31FZ2Rupy9nLa8ZLbYny8gWEl8CkEhJqAE6IzdNELTBVcBEg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/schemas': 29.4.2 + '@jest/schemas': 29.4.3 ansi-styles: 5.2.0 react-is: 18.2.0 dev: true @@ -17645,17 +19145,6 @@ packages: js-beautify: 1.14.7 dev: true - /printj@1.1.2: - resolution: {integrity: sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ==} - engines: {node: '>=0.8'} - hasBin: true - dev: false - - /prismjs@1.29.0: - resolution: {integrity: sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==} - engines: {node: '>=6'} - dev: false - /process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} @@ -17714,7 +19203,7 @@ packages: dependencies: call-bind: 1.0.2 define-properties: 1.1.4 - es-abstract: 1.20.4 + es-abstract: 1.21.1 dev: false /promise@1.3.0: @@ -17948,6 +19437,18 @@ packages: resolution: {integrity: sha512-t+x1zEHDjBwkDGY5v5ApnZ/utcd4XYDiJsaQQoptTXgUXX95sDg1elCdJghzicm7n2mbCBJ3uYWr6M22SO19rg==} dev: true + /pyodide@0.22.1: + resolution: {integrity: sha512-6+PkFLTC+kcBKtFQxYBxR44J5IBxLm8UGkobLgZv1SxzV9qOU2rb0YYf0qDtlnfDiN/IQd2uckf+D8Zwe88Mqg==} + dependencies: + base-64: 1.0.0 + node-fetch: 2.6.8 + ws: 8.12.0 + transitivePeerDependencies: + - bufferutil + - encoding + - utf-8-validate + dev: false + /python-struct@1.1.3: resolution: {integrity: sha512-UsI/mNvk25jRpGKYI38Nfbv84z48oiIWwG67DLVvjRhy8B/0aIK+5Ju5WOHgw/o9rnEmbAS00v4rgKFQeC332Q==} dependencies: @@ -18322,6 +19823,11 @@ packages: safe-regex: 1.1.0 dev: true + /regexp-tree@0.1.25: + resolution: {integrity: sha512-szcL3aqw+vEeuxhL1AMYRyeMP+goYF5I/guaH10uJX5xbGyeQeNPPneaj3ZWVmGLCDxrVaaYekkr5R12gk4dJw==} + hasBin: true + dev: true + /regexp.prototype.flags@1.4.3: resolution: {integrity: sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==} engines: {node: '>= 0.4'} @@ -18330,11 +19836,6 @@ packages: define-properties: 1.1.4 functions-have-names: 1.2.3 - /regexpp@3.2.0: - resolution: {integrity: sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==} - engines: {node: '>=8'} - dev: true - /regexpu-core@5.2.2: resolution: {integrity: sha512-T0+1Zp2wjF/juXMrMxHxidqGYn8U4R+zleSJhX9tQ1PUsS8a9UtYfbsF9LdiVgNX3kiX8RNaKM42nfSgvFJjmw==} engines: {node: '>=4'} @@ -18715,6 +20216,12 @@ packages: ret: 0.1.15 dev: true + /safe-regex@2.1.1: + resolution: {integrity: sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==} + dependencies: + regexp-tree: 0.1.25 + dev: true + /safe-stable-stringify@2.4.0: resolution: {integrity: sha512-eehKHKpab6E741ud7ZIMcXhKcP6TSIezPkNZhy5U8xC6+VvrRdUA2tMgxGxaGl4cz7c2Ew5+mg5+wNB16KQqrA==} engines: {node: '>=10'} @@ -18779,7 +20286,7 @@ packages: sass: 1.55.0 schema-utils: 3.1.1 semver: 7.3.8 - webpack: 5.75.0(esbuild@0.16.17) + webpack: 5.75.0(esbuild@0.17.18) dev: true /sass-loader@13.2.0(sass@1.58.0)(webpack@5.75.0): @@ -18804,7 +20311,7 @@ packages: klona: 2.0.6 neo-async: 2.6.2 sass: 1.58.0 - webpack: 5.75.0(esbuild@0.16.17) + webpack: 5.75.0(esbuild@0.17.18) dev: true /sass@1.55.0: @@ -19452,13 +20959,6 @@ packages: resolution: {integrity: sha512-I539Tc0gyDTQ2QCSg4v78Flxo/UbqR9x7JoyPcqaPtwo+qzeOw/fF+aPSbk0xTvBQAAAZk7Dlkc8K1bum5GUnw==} dev: false - /ssf@0.11.2: - resolution: {integrity: sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==} - engines: {node: '>=0.8'} - dependencies: - frac: 1.1.2 - dev: false - /ssh2-sftp-client@7.2.3: resolution: {integrity: sha512-Bmq4Uewu3e0XOwu5bnPbiS5KRQYv+dff5H6+85V4GZrPrt0Fkt1nUH+uXanyAkoNxUpzjnAPEEoLdOaBO9c3xw==} engines: {node: '>=10.24.1'} @@ -19583,14 +21083,6 @@ packages: resolution: {integrity: sha512-siT1RiqlfQnGqgT/YzXVUNsom9S0H1OX+dpdGN1xkyYATo4I6sep5NmsRD/40s3IIOvlCq6akxkqG82urIZW1w==} dev: true - /storybook-addon-designs@6.3.1(react@17.0.2): - resolution: {integrity: sha512-QCHZp4KuUikOq52MPiMfU8QifYTfhHar5vWlbcfkFDz1YrgGMy+QAEt5Y3Vdnffl4GKSK1lAsLuvTuzqTBRvnw==} - dependencies: - '@figspec/react': 1.0.3(react@17.0.2) - transitivePeerDependencies: - - react - dev: true - /storybook-addon-themes@6.1.0(react-dom@18.2.0)(react@17.0.2)(vue@2.7.14): resolution: {integrity: sha512-ZT8aNgrwFVNEOmOPBLNS0WBacjvMFo/bZ83P8MmsJ3Ewqt0AbmPioghTZccARUn/EQ+LrDxyh2D0QgmLaKo07Q==} peerDependencies: @@ -19619,11 +21111,11 @@ packages: - react-dom dev: true - /storybook@7.0.0-beta.46: - resolution: {integrity: sha512-xMMCSrnfU2JLQVH7G8eJQVTwCwx3ABA/WeDxPCkNESR1PBVWA/OipUxE4wTD3z/1XLq0sfwknZcJZgz1mhqaAQ==} + /storybook@7.0.7: + resolution: {integrity: sha512-MaFAhpPm/KsaoIQfGzapnRyXNh1VbS8l38BNZR5ZD97ejGkLukJ7TO4fFS87Hyy6whAXo6tTdtqeCByMQ9gRFA==} hasBin: true dependencies: - '@storybook/cli': 7.0.0-beta.46 + '@storybook/cli': 7.0.7 transitivePeerDependencies: - bufferutil - encoding @@ -19725,16 +21217,9 @@ packages: resolution: {integrity: sha512-VHhsDkuf8gsw4JNRK9cIZjYe6r7PsVUutVohaBhqYAoPaRADoQH+mMgUg7Cs/TgQeDGEvI+PzPEMOdvdsCMvpg==} dependencies: define-properties: 1.1.4 - es-abstract: 1.20.4 + es-abstract: 1.21.1 dev: false - /string.prototype.trimend@1.0.5: - resolution: {integrity: sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==} - dependencies: - call-bind: 1.0.2 - define-properties: 1.1.4 - es-abstract: 1.20.4 - /string.prototype.trimend@1.0.6: resolution: {integrity: sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==} dependencies: @@ -19742,13 +21227,6 @@ packages: define-properties: 1.1.4 es-abstract: 1.21.1 - /string.prototype.trimstart@1.0.5: - resolution: {integrity: sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==} - dependencies: - call-bind: 1.0.2 - define-properties: 1.1.4 - es-abstract: 1.20.4 - /string.prototype.trimstart@1.0.6: resolution: {integrity: sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==} dependencies: @@ -19863,7 +21341,7 @@ packages: dependencies: loader-utils: 2.0.4 schema-utils: 2.7.1 - webpack: 5.75.0(esbuild@0.16.17) + webpack: 5.75.0(esbuild@0.17.18) dev: true /style-loader@3.3.1(webpack@5.75.0): @@ -19872,7 +21350,7 @@ packages: peerDependencies: webpack: ^5.0.0 dependencies: - webpack: 5.75.0(esbuild@0.16.17) + webpack: 5.75.0(esbuild@0.17.18) dev: true /style-mod@4.0.0: @@ -19973,8 +21451,8 @@ packages: resolution: {integrity: sha512-qImOD23aDfnIDNqlG1NOehdB9IYsn1V9oByPjKY1nakv2MQYCEMyX033/q+aEtYCpmYK1cv2+NTmlH+ra6GA5A==} dev: true - /synckit@0.8.4: - resolution: {integrity: sha512-Dn2ZkzMdSX827QbowGbU/4yjWuvNaCoScLLoMo/yKbu+P4GBR6cRGKZH27k6a9bRzdqcyd1DE96pQtQ6uNkmyw==} + /synckit@0.8.5: + resolution: {integrity: sha512-L1dapNV6vu2s/4Sputv8xGsCdAVlb5nRDMFU/E27D44l5U6cw1g0dGd45uLc+OXjNMmF4ntiMdCimzcjFKQI8Q==} engines: {node: ^14.18.0 || >=16.0.0} dependencies: '@pkgr/utils': 2.3.1 @@ -20104,7 +21582,7 @@ packages: unique-string: 2.0.0 dev: true - /terser-webpack-plugin@5.3.6(esbuild@0.16.17)(webpack@5.75.0): + /terser-webpack-plugin@5.3.6(esbuild@0.17.18)(webpack@5.75.0): resolution: {integrity: sha512-kfLFk+PoLUQIbLmB1+PZDMRSZS99Mp+/MHqDNmMA6tOItzRt+Npe3E+fsMs5mfcM0wCtrrdU387UnV+vnSffXQ==} engines: {node: '>= 10.13.0'} peerDependencies: @@ -20121,12 +21599,12 @@ packages: optional: true dependencies: '@jridgewell/trace-mapping': 0.3.17 - esbuild: 0.16.17 + esbuild: 0.17.18 jest-worker: 27.5.1 schema-utils: 3.1.1 serialize-javascript: 6.0.1 terser: 5.16.1 - webpack: 5.75.0(esbuild@0.16.17) + webpack: 5.75.0(esbuild@0.17.18) dev: true /terser@5.16.1: @@ -20428,7 +21906,7 @@ packages: resolution: {integrity: sha512-e4g0EJtAjk64xgnFPD6kTBUtpnMVzDrMb12N1YZV0VvSlhnVT3SGxiYTLdGy8Q5cYHOIC/FAHmZ10eGrAguicQ==} dev: false - /ts-jest@29.1.0(@babel/core@7.20.12)(jest@29.5.0)(typescript@5.0.3): + /ts-jest@29.1.0(@babel/core@7.21.8)(jest@29.5.0)(typescript@5.0.3): resolution: {integrity: sha512-ZhNr7Z4PcYa+JjMl62ir+zPiNJfXJN6E8hSLnaUKhOgqcn8vb3e537cpkd0FuAfRK3sR1LSqM1MOhliXNgOFPA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -20449,7 +21927,7 @@ packages: esbuild: optional: true dependencies: - '@babel/core': 7.20.12 + '@babel/core': 7.21.8 bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 jest: 29.5.0 @@ -20470,11 +21948,11 @@ packages: webpack: ^5.0.0 dependencies: chalk: 4.1.2 - enhanced-resolve: 5.10.0 + enhanced-resolve: 5.13.0 micromatch: 4.0.5 semver: 7.3.8 typescript: 5.0.3 - webpack: 5.75.0(esbuild@0.16.17) + webpack: 5.75.0(esbuild@0.17.18) dev: true /ts-map@1.0.3: @@ -20511,7 +21989,7 @@ packages: resolution: {integrity: sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==} dependencies: '@types/json5': 0.0.29 - json5: 1.0.1 + json5: 1.0.2 minimist: 1.2.7 strip-bom: 3.0.0 dev: true @@ -20776,6 +22254,10 @@ packages: - supports-color dev: false + /typesafe-path@0.2.2: + resolution: {integrity: sha512-OJabfkAg1WLZSqJAJ0Z6Sdt3utnbzr/jh+NAHoyWHJe8CMSy79Gm085094M9nvTPy22KzTVn5Zq5mbapCI/hPA==} + dev: true + /typescript@5.0.3: resolution: {integrity: sha512-xv8mOEDnigb/tN9PSMTwSEqAnUvkoXMQlicOb0IUVDBSQCgBSaAAROUZYy2IcUy5qU6XajK5jjjO7TMWqBTKZA==} engines: {node: '>=12.20'} @@ -21150,10 +22632,6 @@ packages: resolution: {integrity: sha512-yEEhCuCi5wRV7Z5ZVf9iV2gWMvUZqKJhAs1ecFdKJ0qzbyaVelmsE3QjYAamehfp9FKLiZbKldd+jklG3O0LfA==} dev: false - /uuid-browser@3.1.0: - resolution: {integrity: sha512-dsNgbLaTrd6l3MMxTtouOCFw4CBFc/3a+GgYA2YyrJvyQ1u6q4pcu3ktLoUZ/VN/Aw9WsauazbgsgdfVWgAKQg==} - dev: true - /uuid@3.4.0: resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==} deprecated: Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details. @@ -21172,7 +22650,6 @@ packages: /uuid@9.0.0: resolution: {integrity: sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==} hasBin: true - dev: false /v-click-outside@3.2.0: resolution: {integrity: sha512-QD0bDy38SHJXQBjgnllmkI/rbdiwmq9RC+/+pvrFjYJKTn8dtp7Penf9q1lLBta280fYG2q53mgLhQ+3l3z74w==} @@ -21294,14 +22771,6 @@ packages: - terser dev: true - /vite-plugin-monaco-editor@1.1.0(monaco-editor@0.33.0): - resolution: {integrity: sha512-IvtUqZotrRoVqwT0PBBDIZPNraya3BxN/bfcNfnxZ5rkJiGcNtO5eAOWWSgT7zullIAEqQwxMU83yL9J5k7gww==} - peerDependencies: - monaco-editor: '>=0.33.0' - dependencies: - monaco-editor: 0.33.0 - dev: true - /vite@4.0.4(@types/node@16.18.12)(sass@1.55.0)(terser@5.16.1): resolution: {integrity: sha512-xevPU7M8FU0i/80DMR+YhgrzR5KS2ORy1B4xcX/cXLsvnUWvfHuqMmVU6N0YiJ4JWGRJJsLCgjEzKjG9/GKoSw==} engines: {node: ^14.18.0 || >=16.0.0} @@ -21463,6 +22932,10 @@ packages: engines: {node: '>=0.10.0'} dev: true + /vscode-uri@3.0.7: + resolution: {integrity: sha512-eOpPHogvorZRobNqJGhapa0JdwaxpjVvyBp0QIUMRMSf8ZAlqOdEquKuRmw9Qwu0qXtJIWqFtMkmvJjUZmMjVA==} + dev: true + /vue-agile@2.0.0: resolution: {integrity: sha512-5xkSLJQNRdQ7qpEnXj5FgLg33XKRHaTZKGP5qkvteOc/uGJX89MYCjPSgdNqJ1GYFGfdGAp0jvhihW8OMuXS3g==} dependencies: @@ -21504,8 +22977,8 @@ packages: /vue-docgen-api@4.56.4(vue@2.7.14): resolution: {integrity: sha512-Z59qNUYZNHMZL0QITNbqobGKJgmqyOs4OiAdzr1Ug7ys1gkFBGewiuwPURTsoM2zi4GWZWy2eW4B5FevwT7gpA==} dependencies: - '@babel/parser': 7.20.7 - '@babel/types': 7.20.7 + '@babel/parser': 7.21.8 + '@babel/types': 7.21.5 '@vue/compiler-dom': 3.2.45 '@vue/compiler-sfc': 3.2.45 ast-types: 0.14.2 @@ -21519,7 +22992,7 @@ packages: - vue dev: true - /vue-docgen-loader@1.5.1(@babel/preset-env@7.20.2)(vue-docgen-api@4.56.4)(webpack@5.75.0): + /vue-docgen-loader@1.5.1(@babel/preset-env@7.21.5)(vue-docgen-api@4.56.4)(webpack@5.75.0): resolution: {integrity: sha512-coMmQYsg+fy18SVtBNU7/tztdqEyrneFfwQFLmx8O7jaJ11VZ//9tRWXlwGzJM07cPRwMHDKMlAdWrpuw3U46A==} engines: {node: '>= 8.16'} peerDependencies: @@ -21527,24 +23000,24 @@ packages: webpack: '>=4' dependencies: clone: 2.1.2 - jscodeshift: 0.13.1(@babel/preset-env@7.20.2) + jscodeshift: 0.13.1(@babel/preset-env@7.21.5) loader-utils: 1.4.2 querystring: 0.2.1 vue-docgen-api: 4.56.4(vue@2.7.14) - webpack: 5.75.0(esbuild@0.16.17) + webpack: 5.75.0(esbuild@0.17.18) transitivePeerDependencies: - '@babel/preset-env' - supports-color dev: true - /vue-eslint-parser@7.11.0(eslint@8.28.0): + /vue-eslint-parser@7.11.0(eslint@8.39.0): resolution: {integrity: sha512-qh3VhDLeh773wjgNTl7ss0VejY9bMMa0GoDG2fQVyDzRFdiU3L7fw74tWZDHNQXdZqxO3EveQroa9ct39D2nqg==} engines: {node: '>=8.10'} peerDependencies: eslint: '>=5.0.0' dependencies: debug: 4.3.4(supports-color@8.1.1) - eslint: 8.28.0 + eslint: 8.39.0 eslint-scope: 5.1.1 eslint-visitor-keys: 1.3.0 espree: 6.2.1 @@ -21619,7 +23092,7 @@ packages: vue-hot-reload-api: 2.3.4 vue-style-loader: 4.1.3 vue-template-compiler: 2.7.14 - webpack: 5.75.0(esbuild@0.16.17) + webpack: 5.75.0(esbuild@0.17.18) transitivePeerDependencies: - arc-templates - atpl @@ -21676,14 +23149,6 @@ packages: - whiskers dev: true - /vue-prism-editor@0.3.0: - resolution: {integrity: sha512-yNSuwql/xHMJrWghn/OhZ5WPBKdhx7FkvFjgq2uDm99jHSJhuGwhcgPyuoGzpm6w8DRDzi85lgerKCu8DTDWWg==} - dependencies: - dom-iterator: 1.0.0 - escape-html: 1.0.3 - unescape: 1.0.1 - dev: false - /vue-property-decorator@9.1.2(vue-class-component@7.2.6)(vue@2.7.14): resolution: {integrity: sha512-xYA8MkZynPBGd/w5QFJ2d/NM0z/YeegMqYTphy7NJQXbZcuU6FC6AOdUAcy4SXP+YnkerC6AfH+ldg7PDk9ESQ==} peerDependencies: @@ -21828,7 +23293,7 @@ packages: mime-types: 2.1.35 range-parser: 1.2.1 schema-utils: 4.0.0 - webpack: 5.75.0(esbuild@0.16.17) + webpack: 5.75.0(esbuild@0.17.18) dev: true /webpack-hot-middleware@2.25.3: @@ -21852,7 +23317,7 @@ packages: resolution: {integrity: sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==} dev: true - /webpack@5.75.0(esbuild@0.16.17): + /webpack@5.75.0(esbuild@0.17.18): resolution: {integrity: sha512-piaIaoVJlqMsPtX/+3KTTO6jfvrSYgauFVdt8cr9LTHKmcq/AMd4mhzsiP7ZF/PGRNPGA8336jldh9l2Kt2ogQ==} engines: {node: '>=10.13.0'} hasBin: true @@ -21871,7 +23336,7 @@ packages: acorn-import-assertions: 1.8.0(acorn@8.8.1) browserslist: 4.21.4 chrome-trace-event: 1.0.3 - enhanced-resolve: 5.10.0 + enhanced-resolve: 5.13.0 es-module-lexer: 0.9.3 eslint-scope: 5.1.1 events: 3.3.0 @@ -21883,7 +23348,7 @@ packages: neo-async: 2.6.2 schema-utils: 3.1.1 tapable: 2.2.1 - terser-webpack-plugin: 5.3.6(esbuild@0.16.17)(webpack@5.75.0) + terser-webpack-plugin: 5.3.6(esbuild@0.17.18)(webpack@5.75.0) watchpack: 2.4.0 webpack-sources: 3.2.3 transitivePeerDependencies: @@ -22023,26 +23488,16 @@ packages: resolution: {integrity: sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==} engines: {node: '>= 10.0.0'} dependencies: - '@babel/parser': 7.20.7 - '@babel/types': 7.20.7 + '@babel/parser': 7.21.8 + '@babel/types': 7.21.5 assert-never: 1.2.1 babel-walk: 3.0.0-canary-5 dev: true - /wmf@1.0.2: - resolution: {integrity: sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==} - engines: {node: '>=0.8'} - dev: false - /word-wrap@1.2.3: resolution: {integrity: sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==} engines: {node: '>=0.10.0'} - /word@0.3.0: - resolution: {integrity: sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==} - engines: {node: '>=0.8'} - dev: false - /wordwrap@1.0.0: resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} @@ -22158,20 +23613,6 @@ packages: utf-8-validate: optional: true - /xlsx@0.17.5: - resolution: {integrity: sha512-lXNU0TuYsvElzvtI6O7WIVb9Zar1XYw7Xb3VAx2wn8N/n0whBYrCnHMxtFyIiUU1Wjf09WzmLALDfBO5PqTb1g==} - engines: {node: '>=0.8'} - hasBin: true - dependencies: - adler-32: 1.2.0 - cfb: 1.2.2 - codepage: 1.15.0 - crc-32: 1.2.2 - ssf: 0.11.2 - wmf: 1.0.2 - word: 0.3.0 - dev: false - /xml-crypto@3.0.1: resolution: {integrity: sha512-7XrwB3ujd95KCO6+u9fidb8ajvRJvIfGNWD0XLJoTWlBKz+tFpUzEYxsN+Il/6/gHtEs1RgRh2RH+TzhcWBZUw==} engines: {node: '>=0.4.0'} @@ -22438,3 +23879,11 @@ packages: optionalDependencies: commander: 2.20.3 dev: true + + '@cdn.sheetjs.com/xlsx-0.19.3/xlsx-0.19.3.tgz': + resolution: {tarball: https://cdn.sheetjs.com/xlsx-0.19.3/xlsx-0.19.3.tgz} + name: xlsx + version: 0.19.3 + engines: {node: '>=0.8'} + hasBin: true + dev: false