diff --git a/.github/scripts/trim-fe-packageJson.js b/.github/scripts/trim-fe-packageJson.js index 791e483a2d..50f456d2a9 100644 --- a/.github/scripts/trim-fe-packageJson.js +++ b/.github/scripts/trim-fe-packageJson.js @@ -7,12 +7,12 @@ const trimPackageJson = (packageName) => { const { scripts, peerDependencies, devDependencies, dependencies, ...packageJson } = require( filePath, ); - if (packageName === '@n8n/chat') { + if (packageName === 'frontend/@n8n/chat') { packageJson.dependencies = dependencies; } writeFileSync(filePath, JSON.stringify(packageJson, null, 2) + '\n', 'utf-8'); }; -trimPackageJson('@n8n/chat'); -trimPackageJson('design-system'); -trimPackageJson('editor-ui'); +trimPackageJson('frontend/@n8n/chat'); +trimPackageJson('frontend/@n8n/design-system'); +trimPackageJson('frontend/editor-ui'); diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml index 06bc4e4465..c38f038105 100644 --- a/.github/workflows/release-publish.yml +++ b/.github/workflows/release-publish.yml @@ -51,14 +51,20 @@ jobs: - name: Dry-run publishing run: pnpm publish -r --no-git-checks --dry-run - - name: Publish to NPM + - name: Pre publishing changes run: | echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc node .github/scripts/trim-fe-packageJson.js node .github/scripts/ensure-provenance-fields.mjs + cp README.md packages/cli/README.md sed -i "s/default: 'dev'/default: 'stable'/g" packages/cli/dist/config/schema.js - pnpm publish -r --publish-branch ${{github.event.pull_request.base.ref}} --access public --tag rc --no-git-checks - npm dist-tag rm n8n rc + + - name: Publish to NPM + run: pnpm publish -r --publish-branch ${{github.event.pull_request.base.ref}} --access public --tag rc --no-git-checks + + - name: Cleanup rc tag + run: npm dist-tag rm n8n rc + continue-on-error: true - id: set-release run: echo "release=${{ env.RELEASE }}" >> $GITHUB_OUTPUT @@ -68,7 +74,7 @@ jobs: needs: [publish-to-npm] runs-on: ubuntu-latest if: github.event.pull_request.merged == true - timeout-minutes: 10 + timeout-minutes: 15 steps: - name: Checkout @@ -103,6 +109,7 @@ jobs: context: ./docker/images/n8n build-args: | N8N_VERSION=${{ needs.publish-to-npm.outputs.release }} + N8N_RELEASE_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") platforms: linux/amd64,linux/arm64 provenance: false push: true @@ -155,7 +162,7 @@ jobs: with: projects: ${{ secrets.SENTRY_FRONTEND_PROJECT }} version: ${{ needs.publish-to-npm.outputs.release }} - sourcemaps: packages/editor-ui/dist + sourcemaps: packages/frontend/editor-ui/dist - name: Create a backend release uses: getsentry/action-release@v1.7.0 diff --git a/.github/workflows/release-push-to-channel.yml b/.github/workflows/release-push-to-channel.yml index 74219e51c4..2f4a9170a7 100644 --- a/.github/workflows/release-push-to-channel.yml +++ b/.github/workflows/release-push-to-channel.yml @@ -11,10 +11,10 @@ on: description: 'Release channel' required: true type: choice - default: 'next' + default: 'beta' options: - - next - - latest + - beta + - stable jobs: release-to-npm: @@ -25,9 +25,18 @@ jobs: - uses: actions/setup-node@v4.2.0 with: node-version: 20.x - - run: | - echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc - npm dist-tag add n8n@${{ github.event.inputs.version }} ${{ github.event.inputs.release-channel }} + + - run: echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc + + - if: github.event.inputs.release-channel == 'beta' + run: | + npm dist-tag add n8n@${{ github.event.inputs.version }} next + npm dist-tag add n8n@${{ github.event.inputs.version }} beta + + - if: github.event.inputs.release-channel == 'stable' + run: | + npm dist-tag add n8n@${{ github.event.inputs.version }} latest + npm dist-tag add n8n@${{ github.event.inputs.version }} stable release-to-docker-hub: name: Release to DockerHub @@ -39,7 +48,15 @@ jobs: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - - run: docker buildx imagetools create -t ${{ secrets.DOCKER_USERNAME }}/n8n:${{ github.event.inputs.release-channel }} ${{ secrets.DOCKER_USERNAME }}/n8n:${{ github.event.inputs.version }} + - if: github.event.inputs.release-channel == 'stable' + run: | + docker buildx imagetools create -t ${{ secrets.DOCKER_USERNAME }}/n8n:stable ${{ secrets.DOCKER_USERNAME }}/n8n:${{ github.event.inputs.version }} + docker buildx imagetools create -t ${{ secrets.DOCKER_USERNAME }}/n8n:latest ${{ secrets.DOCKER_USERNAME }}/n8n:${{ github.event.inputs.version }} + + - if: github.event.inputs.release-channel == 'beta' + run: | + docker buildx imagetools create -t ${{ secrets.DOCKER_USERNAME }}/n8n:beta ${{ secrets.DOCKER_USERNAME }}/n8n:${{ github.event.inputs.version }} + docker buildx imagetools create -t ${{ secrets.DOCKER_USERNAME }}/n8n:next ${{ secrets.DOCKER_USERNAME }}/n8n:${{ github.event.inputs.version }} release-to-github-container-registry: name: Release to GitHub Container Registry @@ -52,7 +69,15 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - run: docker buildx imagetools create -t ghcr.io/${{ github.repository_owner }}/n8n:${{ github.event.inputs.release-channel }} ghcr.io/${{ github.repository_owner }}/n8n:${{ github.event.inputs.version }} + - if: github.event.inputs.release-channel == 'stable' + run: | + docker buildx imagetools create -t ghcr.io/${{ github.repository_owner }}/n8n:stable ghcr.io/${{ github.repository_owner }}/n8n:${{ github.event.inputs.version }} + docker buildx imagetools create -t ghcr.io/${{ github.repository_owner }}/n8n:latest ghcr.io/${{ github.repository_owner }}/n8n:${{ github.event.inputs.version }} + + - if: github.event.inputs.release-channel == 'beta' + run: | + docker buildx imagetools create -t ghcr.io/${{ github.repository_owner }}/n8n:beta ghcr.io/${{ github.repository_owner }}/n8n:${{ github.event.inputs.version }} + docker buildx imagetools create -t ghcr.io/${{ github.repository_owner }}/n8n:next ghcr.io/${{ github.repository_owner }}/n8n:${{ github.event.inputs.version }} update-docs: name: Update latest and next in the docs diff --git a/.prettierignore b/.prettierignore index 192edfec2d..d34c856f88 100644 --- a/.prettierignore +++ b/.prettierignore @@ -2,7 +2,7 @@ coverage dist package.json pnpm-lock.yaml -packages/editor-ui/index.html +packages/frontend/editor-ui/index.html packages/nodes-base/nodes/**/test packages/cli/templates/form-trigger.handlebars packages/cli/templates/form-trigger-completion.handlebars diff --git a/CHANGELOG.md b/CHANGELOG.md index c90cde6a33..18175ee50e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,67 @@ +# [1.81.0](https://github.com/n8n-io/n8n/compare/n8n@1.80.0...n8n@1.81.0) (2025-02-24) + + +### Bug Fixes + +* Always clear popupWindowState before showing popup from form trigger ([#13363](https://github.com/n8n-io/n8n/issues/13363)) ([b7f1265](https://github.com/n8n-io/n8n/commit/b7f12650f1f42c0ff15c1da3e5ade350fb1e23d2)) +* **Code Node:** Fix `$items` in Code node when using task runner ([#13368](https://github.com/n8n-io/n8n/issues/13368)) ([87b3c50](https://github.com/n8n-io/n8n/commit/87b3c508b3d5a7d6f3b9f8377de66567a04fa970)) +* **core:** Avoid renewing the license on init to prevent excessive duplicate renewal calls ([#13347](https://github.com/n8n-io/n8n/issues/13347)) ([1e1f528](https://github.com/n8n-io/n8n/commit/1e1f52846641515ad4479ab1088e78a9266e452d)) +* **core:** Ensure that 'workflow-post-execute' event has userId whenever it's available ([#13326](https://github.com/n8n-io/n8n/issues/13326)) ([f41e353](https://github.com/n8n-io/n8n/commit/f41e353887fef4269510d25fa87b73da4cf925f9)) +* **core:** Fix DB migrations for MySQL ([#13261](https://github.com/n8n-io/n8n/issues/13261)) ([d0968a1](https://github.com/n8n-io/n8n/commit/d0968a10d56ac5c97974129742ba8f8a85997dac)) +* **core:** Fix resuming executions on test webhooks from Wait forms ([#13410](https://github.com/n8n-io/n8n/issues/13410)) ([8ffd316](https://github.com/n8n-io/n8n/commit/8ffd3167d58d30f087fd31010e6f79f1398d8f49)) +* **core:** Handle connections for missing nodes in a workflow ([#13362](https://github.com/n8n-io/n8n/issues/13362)) ([1e5feb1](https://github.com/n8n-io/n8n/commit/1e5feb195d50054939f85c9e1b5a32885c579901)) +* **core:** Make sure middleware works with legacy API Keys ([#13390](https://github.com/n8n-io/n8n/issues/13390)) ([ca76ef4](https://github.com/n8n-io/n8n/commit/ca76ef4bc248a3bcde844bc8378d38eed269f032)) +* **core:** Return original hooks errors to the frontend ([#13365](https://github.com/n8n-io/n8n/issues/13365)) ([5439181](https://github.com/n8n-io/n8n/commit/5439181e92f20fef1423575cabec7acbe1740b26)) +* **editor:** Correctly close node creator when selecting/deselecting a node ([#13338](https://github.com/n8n-io/n8n/issues/13338)) ([c3dc66e](https://github.com/n8n-io/n8n/commit/c3dc66ee7372927fcfd6baac3b9d853690e39c99)) +* **editor:** Do not show credential details popup for users without necessary scopes with direct link ([#13264](https://github.com/n8n-io/n8n/issues/13264)) ([a5401d0](https://github.com/n8n-io/n8n/commit/a5401d06a58ef026f44499d05b42a8d0dbe2520e)) +* **editor:** Do not show project settings for users without permission with direct link ([#13246](https://github.com/n8n-io/n8n/issues/13246)) ([fa488f1](https://github.com/n8n-io/n8n/commit/fa488f15619f798a0360c96492f2928ac661d9ee)) +* **editor:** Don't open form popup window if different trigger node is used ([#13391](https://github.com/n8n-io/n8n/issues/13391)) ([57a9a5b](https://github.com/n8n-io/n8n/commit/57a9a5b15f55aae0301851e93847ed87feb081e8)) +* **editor:** Fix configurable node description margins and text alignment ([#13318](https://github.com/n8n-io/n8n/issues/13318)) ([c881ea2](https://github.com/n8n-io/n8n/commit/c881ea2c7b43a4fb610533dd553520a6de51f22d)) +* **editor:** Fix workflow moving E2E tests ([#13396](https://github.com/n8n-io/n8n/issues/13396)) ([073b05b](https://github.com/n8n-io/n8n/commit/073b05b10c81e3a0451c310bc0bde25170e1591e)) +* **editor:** Optionally share credentials used by the workflow when moving the workflow between projects ([#12524](https://github.com/n8n-io/n8n/issues/12524)) ([7bd83d7](https://github.com/n8n-io/n8n/commit/7bd83d7d330b6f01b5798461f2218254a9964d87)) +* **editor:** Polyfill `Array.prototype.toSorted` (no-chanhelog) ([#13463](https://github.com/n8n-io/n8n/issues/13463)) ([f2b15ea](https://github.com/n8n-io/n8n/commit/f2b15ea086fcc541a5a584998985d712335210ec)) +* **editor:** Register/unregister keybindings on window focus/blur ([#13419](https://github.com/n8n-io/n8n/issues/13419)) ([7a504dc](https://github.com/n8n-io/n8n/commit/7a504dc30fcf0c7641528ed469835811f82bb098)) +* **editor:** Switch back to selection mode on window blur ([#13341](https://github.com/n8n-io/n8n/issues/13341)) ([415e25b](https://github.com/n8n-io/n8n/commit/415e25b5d524b0d3c391403f129468e57bbb918e)) +* Prevent flicker during paginated workflow navigation ([#13348](https://github.com/n8n-io/n8n/issues/13348)) ([d277e0b](https://github.com/n8n-io/n8n/commit/d277e0ba0e5d87500457538b4b0f1363e267f071)) + + +### Features + +* **core:** Hackmation - Add last activity metric ([#13237](https://github.com/n8n-io/n8n/issues/13237)) ([272f55b](https://github.com/n8n-io/n8n/commit/272f55b80f1d4576d1675040bd2775210c4ab5e9)) +* **editor:** Change rename node keyboard shortcut to Space on new canvas ([#11872](https://github.com/n8n-io/n8n/issues/11872)) ([c90d0d9](https://github.com/n8n-io/n8n/commit/c90d0d9161ec161cd1afd6aa5b56345c1611f9c9)) +* **editor:** Implement breadcrumbs component ([#13317](https://github.com/n8n-io/n8n/issues/13317)) ([db297f1](https://github.com/n8n-io/n8n/commit/db297f107d81738d57e298135a9c279ad83345dc)) +* **editor:** Implement folder navigation in workflows list ([#13370](https://github.com/n8n-io/n8n/issues/13370)) ([0eae14e](https://github.com/n8n-io/n8n/commit/0eae14e27ab4fab3229750d6b2a32868db1e8dd4)) +* **editor:** Rename 'Text' fields on AI nodes to 'Prompt' ([#13416](https://github.com/n8n-io/n8n/issues/13416)) ([4fa666b](https://github.com/n8n-io/n8n/commit/4fa666b976423365299e915130384e10c8e12528)) +* Enable partial exections v2 by default ([#13344](https://github.com/n8n-io/n8n/issues/13344)) ([29ae239](https://github.com/n8n-io/n8n/commit/29ae2396c99d54d8f3db71e6370516f0dc354d00)) +* **n8n Form Node:** Limit wait time parameters ([#13160](https://github.com/n8n-io/n8n/issues/13160)) ([14b6f8b](https://github.com/n8n-io/n8n/commit/14b6f8b97275e38ba4a4c1819e8e32b711de21ba)) + + + +# [1.80.0](https://github.com/n8n-io/n8n/compare/n8n@1.79.0...n8n@1.80.0) (2025-02-17) + + +### Bug Fixes + +* **AI Agent Node:** Move model retrieval into try/catch to fix continueOnFail handling ([#13165](https://github.com/n8n-io/n8n/issues/13165)) ([47c5688](https://github.com/n8n-io/n8n/commit/47c5688618001a51c9412c5d07fd25d85b8d1b8d)) +* **Code Tool Node:** Fix Input Schema Parameter not hiding correctly ([#13245](https://github.com/n8n-io/n8n/issues/13245)) ([8e15ebf](https://github.com/n8n-io/n8n/commit/8e15ebf8333d06b5fe4d5bf8ee39f285b31332d7)) +* **core:** Redact credentials ([#13263](https://github.com/n8n-io/n8n/issues/13263)) ([052f177](https://github.com/n8n-io/n8n/commit/052f17744d072cd16ce90ea94fa9873b4ea2ffed)) +* **core:** Reduce risk of race condition during workflow activation loop ([#13186](https://github.com/n8n-io/n8n/issues/13186)) ([64c5b6e](https://github.com/n8n-io/n8n/commit/64c5b6e0604ce9da6b19dd5f04e61e38209b3153)) +* **core:** Run full manual execution when a trigger is executed even if run data exists ([#13194](https://github.com/n8n-io/n8n/issues/13194)) ([66acb1b](https://github.com/n8n-io/n8n/commit/66acb1bcb68926526ed98a5fe5b89bdaa74148d6)) +* Display correct editor URL ([#13251](https://github.com/n8n-io/n8n/issues/13251)) ([67a4ed1](https://github.com/n8n-io/n8n/commit/67a4ed18a13cb2bc54b3472b9a8beb2f274c2bd2)) +* **editor:** Add template id to metadata when saving workflows from json ([#13172](https://github.com/n8n-io/n8n/issues/13172)) ([2a92032](https://github.com/n8n-io/n8n/commit/2a92032704ebc4e0cdd11aa59b6834a9d891ffb0)) +* **editor:** Fix page size resetting when filters are reset on workflows page ([#13265](https://github.com/n8n-io/n8n/issues/13265)) ([b4380d0](https://github.com/n8n-io/n8n/commit/b4380d05087e1213641ee322875cf51bf706d2f5)) +* **editor:** Open autocompletion when starting an expression ([#13249](https://github.com/n8n-io/n8n/issues/13249)) ([6377635](https://github.com/n8n-io/n8n/commit/6377635bf03387c8d0ae5d54848113258bbabacc)) +* **editor:** Prevent pagination setting from being overwritten in URL ([#13266](https://github.com/n8n-io/n8n/issues/13266)) ([d1e65a1](https://github.com/n8n-io/n8n/commit/d1e65a1cd5841f1d4e815f8da36713cdb18281a4)) +* **editor:** Propagate isReadOnly to ResourceMapper `Attempt to Convert Types` switch ([#13216](https://github.com/n8n-io/n8n/issues/13216)) ([617f841](https://github.com/n8n-io/n8n/commit/617f841e0d82f2b40fcf9ac4bf2cb6a8010b517f)) +* **editor:** Render assignments without ID correctly ([#13252](https://github.com/n8n-io/n8n/issues/13252)) ([d116f12](https://github.com/n8n-io/n8n/commit/d116f121e351e3d81e1b5d6c52eb3e5c3b68ae43)) + + +### Features + +* **editor:** Add pagination to the workflows list ([#13100](https://github.com/n8n-io/n8n/issues/13100)) ([8e37088](https://github.com/n8n-io/n8n/commit/8e370882490d569ff85bba6b7f0a1320fab5eb91)) + + + # [1.79.0](https://github.com/n8n-io/n8n/compare/n8n@1.78.0...n8n@1.79.0) (2025-02-13) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5e495fda9c..ef062a12a0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -49,8 +49,8 @@ The most important directories: execution, active webhooks and workflows. **Contact n8n before starting on any changes here** -- [/packages/design-system](/packages/design-system) - Vue frontend components -- [/packages/editor-ui](/packages/editor-ui) - Vue frontend workflow editor +- [/packages/frontend/@n8n/design-system](/packages/design-system) - Vue frontend components +- [/packages/frontend/editor-ui](/packages/editor-ui) - Vue frontend workflow editor - [/packages/node-dev](/packages/node-dev) - CLI to create new n8n-nodes - [/packages/nodes-base](/packages/nodes-base) - Base n8n nodes - [/packages/workflow](/packages/workflow) - Workflow code with interfaces which diff --git a/codecov.yml b/codecov.yml index dfcd2b0d9c..1d61749328 100644 --- a/codecov.yml +++ b/codecov.yml @@ -42,10 +42,7 @@ component_management: - component_id: frontend_packages name: Frontend paths: - - packages/@n8n/chat/** - packages/@n8n/codemirror-lang/** - - packages/design-system/** - - packages/editor-ui/** - packages/frontend/** - component_id: nodes_packages name: Nodes diff --git a/cypress/.eslintrc.js b/cypress/.eslintrc.js index 36fc0af7e2..4d42f23154 100644 --- a/cypress/.eslintrc.js +++ b/cypress/.eslintrc.js @@ -1,10 +1,10 @@ -const sharedOptions = require('@n8n_io/eslint-config/shared'); +const sharedOptions = require('@n8n/eslint-config/shared'); /** * @type {import('@types/eslint').ESLint.ConfigData} */ module.exports = { - extends: ['@n8n_io/eslint-config/base', 'plugin:cypress/recommended'], + extends: ['@n8n/eslint-config/base', 'plugin:cypress/recommended'], ...sharedOptions(__dirname), diff --git a/cypress/composables/ndv.ts b/cypress/composables/ndv.ts index c70e0b20e8..36ae10669b 100644 --- a/cypress/composables/ndv.ts +++ b/cypress/composables/ndv.ts @@ -206,6 +206,10 @@ export function clickWorkflowCardContent(workflowName: string) { getWorkflowCardContent(workflowName).click(); } +export function clickAssignmentCollectionAdd() { + cy.getByTestId('assignment-collection-drop-area').click(); +} + export function assertNodeOutputHintExists() { getNodeOutputHint().should('exist'); } diff --git a/cypress/composables/workflow.ts b/cypress/composables/workflow.ts index 7d783c1d3c..ff9788349b 100644 --- a/cypress/composables/workflow.ts +++ b/cypress/composables/workflow.ts @@ -356,5 +356,5 @@ export function openContextMenu( } export function clickContextMenuAction(action: string) { - getContextMenuAction(action).click(); + getContextMenuAction(action).click({ force: true }); } diff --git a/cypress/constants.ts b/cypress/constants.ts index cbbf838530..8186b23db0 100644 --- a/cypress/constants.ts +++ b/cypress/constants.ts @@ -52,7 +52,7 @@ export const PIPEDRIVE_NODE_NAME = 'Pipedrive'; export const HTTP_REQUEST_NODE_NAME = 'HTTP Request'; export const AGENT_NODE_NAME = 'AI Agent'; export const BASIC_LLM_CHAIN_NODE_NAME = 'Basic LLM Chain'; -export const AI_MEMORY_WINDOW_BUFFER_MEMORY_NODE_NAME = 'Window Buffer Memory'; +export const AI_MEMORY_WINDOW_BUFFER_MEMORY_NODE_NAME = 'Simple Memory'; export const AI_TOOL_CALCULATOR_NODE_NAME = 'Calculator'; export const AI_TOOL_CODE_NODE_NAME = 'Code Tool'; export const AI_TOOL_WIKIPEDIA_NODE_NAME = 'Wikipedia'; diff --git a/cypress/cypress.config.js b/cypress/cypress.config.js index 63913af7f8..e7b953d6ca 100644 --- a/cypress/cypress.config.js +++ b/cypress/cypress.config.js @@ -14,6 +14,8 @@ module.exports = defineConfig({ experimentalMemoryManagement: true, e2e: { baseUrl: BASE_URL, + viewportWidth: 1536, + viewportHeight: 960, video: true, screenshotOnRunFailure: true, experimentalInteractiveRunEvents: true, diff --git a/cypress/e2e/1-workflows.cy.ts b/cypress/e2e/1-workflows.cy.ts index 82d1b9b90b..022d4ec73b 100644 --- a/cypress/e2e/1-workflows.cy.ts +++ b/cypress/e2e/1-workflows.cy.ts @@ -1,9 +1,12 @@ +import { WorkflowSharingModal } from '../pages'; +import { successToast } from '../pages/notifications'; import { WorkflowPage as WorkflowPageClass } from '../pages/workflow'; import { WorkflowsPage as WorkflowsPageClass } from '../pages/workflows'; import { getUniqueWorkflowName } from '../utils/workflowUtils'; const WorkflowsPage = new WorkflowsPageClass(); const WorkflowPage = new WorkflowPageClass(); +const workflowSharingModal = new WorkflowSharingModal(); const multipleWorkflowsCount = 5; @@ -62,14 +65,12 @@ describe('Workflows', () => { it('should delete all the workflows', () => { WorkflowsPage.getters.workflowCards().should('have.length', multipleWorkflowsCount + 1); - WorkflowsPage.getters.workflowCards().each(($el) => { - const workflowName = $el.find('[data-test-id="workflow-card-name"]').text(); - - WorkflowsPage.getters.workflowCardActions(workflowName).click(); + for (let i = 0; i < multipleWorkflowsCount + 1; i++) { + cy.getByTestId('workflow-card-actions').first().click(); WorkflowsPage.getters.workflowDeleteButton().click(); - cy.get('button').contains('delete').click(); - }); + successToast().should('be.visible'); + } WorkflowsPage.getters.newWorkflowButtonCard().should('be.visible'); }); @@ -138,4 +139,10 @@ describe('Workflows', () => { cy.url().should('include', 'sort=lastCreated'); cy.url().should('include', 'pageSize=25'); }); + + it('should be able to share workflows from workflows list', () => { + WorkflowsPage.getters.workflowCardActions('Empty State Card Workflow').click(); + WorkflowsPage.getters.workflowActionItem('share').click(); + workflowSharingModal.getters.modal().should('be.visible'); + }); }); diff --git a/cypress/e2e/10-undo-redo.cy.ts b/cypress/e2e/10-undo-redo.cy.ts index 2931897f03..bad159b1cb 100644 --- a/cypress/e2e/10-undo-redo.cy.ts +++ b/cypress/e2e/10-undo-redo.cy.ts @@ -22,7 +22,11 @@ describe('Undo/Redo', () => { it('should undo/redo deleting node using context menu', () => { WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); - WorkflowPage.actions.deleteNodeFromContextMenu(CODE_NODE_NAME); + WorkflowPage.actions.zoomToFit(); + WorkflowPage.actions.deleteNodeFromContextMenu(CODE_NODE_NAME, { + method: 'right-click', + anchor: 'topLeft', + }); WorkflowPage.getters.canvasNodes().should('have.have.length', 1); WorkflowPage.getters.nodeConnections().should('have.length', 0); WorkflowPage.actions.hitUndo(); diff --git a/cypress/e2e/12-canvas.cy.ts b/cypress/e2e/12-canvas.cy.ts index be423344fb..3035a61ca2 100644 --- a/cypress/e2e/12-canvas.cy.ts +++ b/cypress/e2e/12-canvas.cy.ts @@ -17,6 +17,7 @@ import { openContextMenu, } from '../composables/workflow'; import { NDV, WorkflowExecutionsTab } from '../pages'; +import { clearNotifications, successToast } from '../pages/notifications'; import { WorkflowPage as WorkflowPageClass } from '../pages/workflow'; const WorkflowPage = new WorkflowPageClass(); @@ -235,7 +236,11 @@ describe('Canvas Node Manipulation and Navigation', () => { it('should delete node using context menu', () => { WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); - WorkflowPage.actions.deleteNodeFromContextMenu(CODE_NODE_NAME); + WorkflowPage.actions.zoomToFit(); + WorkflowPage.actions.deleteNodeFromContextMenu(CODE_NODE_NAME, { + method: 'right-click', + anchor: 'topLeft', + }); WorkflowPage.getters.canvasNodes().should('have.length', 1); WorkflowPage.getters.nodeConnections().should('have.length', 0); }); @@ -379,6 +384,7 @@ describe('Canvas Node Manipulation and Navigation', () => { WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); + WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); // At this point last added node should be off-screen WorkflowPage.getters.canvasNodes().last().should('not.be.visible'); WorkflowPage.getters.zoomToFitButton().click(); @@ -485,6 +491,9 @@ describe('Canvas Node Manipulation and Navigation', () => { WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); WorkflowPage.actions.executeWorkflow(); + successToast().should('contain.text', 'Workflow executed successfully'); + clearNotifications(); + ExecutionsTab.actions.switchToExecutionsTab(); ExecutionsTab.getters.successfulExecutionListItems().should('have.length', 1); diff --git a/cypress/e2e/19-execution.cy.ts b/cypress/e2e/19-execution.cy.ts index 7563cc4827..23172f8fda 100644 --- a/cypress/e2e/19-execution.cy.ts +++ b/cypress/e2e/19-execution.cy.ts @@ -489,7 +489,11 @@ describe('Execution', () => { cy.wait('@workflowRun').then((interception) => { expect(interception.request.body).to.have.property('runData').that.is.an('object'); - const expectedKeys = ['When clicking ‘Test workflow’', 'fetch 5 random users']; + const expectedKeys = [ + 'When clicking ‘Test workflow’', + 'fetch 5 random users', + 'do something with them', + ]; const { runData } = interception.request.body as Record; expect(Object.keys(runData)).to.have.lengthOf(expectedKeys.length); diff --git a/cypress/e2e/20-workflow-executions.cy.ts b/cypress/e2e/20-workflow-executions.cy.ts index 5788af171c..618334e2b6 100644 --- a/cypress/e2e/20-workflow-executions.cy.ts +++ b/cypress/e2e/20-workflow-executions.cy.ts @@ -27,8 +27,8 @@ describe('Workflow Executions', () => { executionsTab.getters.executionsList().scrollTo(0, 500).wait(0); - executionsTab.getters.executionListItems().should('have.length', 11); - executionsTab.getters.successfulExecutionListItems().should('have.length', 9); + executionsTab.getters.executionListItems().should('have.length', 30); + executionsTab.getters.successfulExecutionListItems().should('have.length', 28); executionsTab.getters.failedExecutionListItems().should('have.length', 2); executionsTab.getters .executionListItems() @@ -185,8 +185,9 @@ describe('Workflow Executions', () => { .invoke('attr', 'title') .should('eq', newWorkflowName); }); - - it('should load items and auto scroll after filter change', () => { + // This should be a component test. Abstracting this away into to ensure our lists work. + // eslint-disable-next-line n8n-local-rules/no-skipped-tests + it.skip('should load items and auto scroll after filter change', () => { createMockExecutions(); createMockExecutions(); cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions'); @@ -289,15 +290,20 @@ describe('Workflow Executions', () => { }); const createMockExecutions = () => { - executionsTab.actions.createManualExecutions(5); + executionsTab.actions.createManualExecutions(15); + // This wait is added to allow time for the notifications to expire + cy.wait(2000); // Make some failed executions by enabling Code node with syntax error executionsTab.actions.toggleNodeEnabled('Error'); workflowPage.getters.disabledNodes().should('have.length', 0); executionsTab.actions.createManualExecutions(2); + // This wait is added to allow time for the notifications to expire + cy.wait(2000); + // Then add some more successful ones executionsTab.actions.toggleNodeEnabled('Error'); workflowPage.getters.disabledNodes().should('have.length', 1); - executionsTab.actions.createManualExecutions(4); + executionsTab.actions.createManualExecutions(15); }; const checkMainHeaderELements = () => { diff --git a/cypress/e2e/39-projects.cy.ts b/cypress/e2e/39-projects.cy.ts index b506040c87..d8c618539f 100644 --- a/cypress/e2e/39-projects.cy.ts +++ b/cypress/e2e/39-projects.cy.ts @@ -54,11 +54,7 @@ describe('Projects', { disableAutoLogin: true }, () => { cy.changeQuota('maxTeamProjects', -1); }); - /** - * @TODO: New Canvas - Fix this test - */ - // eslint-disable-next-line n8n-local-rules/no-skipped-tests - it.skip('should filter credentials by project ID when creating new workflow or hard reloading an opened workflow', () => { + it('should filter credentials by project ID when creating new workflow or hard reloading an opened workflow', () => { cy.signinAsOwner(); cy.visit(workflowsPage.url); @@ -225,10 +221,12 @@ describe('Projects', { disableAutoLogin: true }, () => { }); it('should move resources between projects', () => { + cy.intercept('GET', /\/rest\/(workflows|credentials).*/).as('getResources'); + cy.signinAsOwner(); cy.visit(workflowsPage.url); - // Create a workflow and a credential in the Home project + cy.log('Create a workflow and a credential in the Home project'); workflowsPage.getters.workflowCards().should('not.have.length'); workflowsPage.getters.newWorkflowButtonCard().click(); projects.createWorkflow('Test_workflow_1.json', 'Workflow in Home project'); @@ -238,12 +236,12 @@ describe('Projects', { disableAutoLogin: true }, () => { projects.getProjectTabCredentials().should('be.visible').click(); credentialsPage.getters.emptyListCreateCredentialButton().click(); projects.createCredential('Credential in Home project'); - clearNotifications(); - // Create a project and add a credential and a workflow to it + cy.log('Create a project and add a credential and a workflow to it'); projects.createProject('Project 1'); clearNotifications(); + projects.getProjectTabCredentials().click(); credentialsPage.getters.emptyListCreateCredentialButton().click(); projects.createCredential('Credential in Project 1'); @@ -252,12 +250,12 @@ describe('Projects', { disableAutoLogin: true }, () => { projects.getProjectTabWorkflows().click(); workflowsPage.getters.newWorkflowButtonCard().click(); projects.createWorkflow('Test_workflow_1.json', 'Workflow in Project 1'); - clearNotifications(); - // Create another project and add a credential and a workflow to it + cy.log('Create another project and add a credential and a workflow to it'); projects.createProject('Project 2'); clearNotifications(); + projects.getProjectTabCredentials().click(); credentialsPage.getters.emptyListCreateCredentialButton().click(); projects.createCredential('Credential in Project 2'); @@ -268,13 +266,10 @@ describe('Projects', { disableAutoLogin: true }, () => { projects.createWorkflow('Test_workflow_1.json', 'Workflow in Project 2'); clearNotifications(); - // Move the workflow Personal from Home to Project 1 + cy.log('Move the workflow Personal from Home to Project 1'); projects.getHomeButton().click(); - workflowsPage.getters - .workflowCards() - .should('have.length', 3) - .filter(':contains("Personal")') - .should('exist'); + workflowsPage.getters.workflowCards().should('have.length', 3); + workflowsPage.getters.workflowCards().filter(':contains("Personal")').should('exist'); workflowsPage.getters.workflowCardActions('Workflow in Home project').click(); workflowsPage.getters.workflowMoveButton().click(); @@ -284,21 +279,16 @@ describe('Projects', { disableAutoLogin: true }, () => { .contains('button', 'Move workflow') .should('be.disabled'); projects.getProjectMoveSelect().click(); - getVisibleSelect() - .find('li') - .should('have.length', 5) - .filter(':contains("Project 1")') - .click(); + getVisibleSelect().find('li').should('have.length', 5); + getVisibleSelect().find('li').filter(':contains("Project 1")').click(); projects.getResourceMoveModal().contains('button', 'Move workflow').click(); clearNotifications(); + cy.wait('@getResources'); - workflowsPage.getters - .workflowCards() - .should('have.length', 3) - .filter(':contains("Personal")') - .should('not.exist'); + workflowsPage.getters.workflowCards().should('have.length', 3); + workflowsPage.getters.workflowCards().filter(':contains("Personal")').should('not.exist'); - // Move the workflow from Project 1 to Project 2 + cy.log('Move the workflow from Project 1 to Project 2'); projects.getMenuItems().first().click(); workflowsPage.getters.workflowCards().should('have.length', 2); workflowsPage.getters.workflowCardActions('Workflow in Home project').click(); @@ -310,19 +300,16 @@ describe('Projects', { disableAutoLogin: true }, () => { .contains('button', 'Move workflow') .should('be.disabled'); projects.getProjectMoveSelect().click(); - getVisibleSelect() - .find('li') - .should('have.length', 5) - .filter(':contains("Project 2")') - .click(); + getVisibleSelect().find('li').should('have.length', 5); + getVisibleSelect().find('li').filter(':contains("Project 2")').click(); projects.getResourceMoveModal().contains('button', 'Move workflow').click(); + clearNotifications(); - // Move the workflow from Project 2 to a member user + cy.log('Move the workflow from Project 2 to a member user'); projects.getMenuItems().last().click(); workflowsPage.getters.workflowCards().should('have.length', 2); workflowsPage.getters.workflowCardActions('Workflow in Home project').click(); workflowsPage.getters.workflowMoveButton().click(); - clearNotifications(); projects .getResourceMoveModal() @@ -330,20 +317,20 @@ describe('Projects', { disableAutoLogin: true }, () => { .contains('button', 'Move workflow') .should('be.disabled'); projects.getProjectMoveSelect().click(); - getVisibleSelect() - .find('li') - .should('have.length', 5) - .filter(`:contains("${INSTANCE_MEMBERS[0].email}")`) - .click(); + getVisibleSelect().find('li').should('have.length', 5); + getVisibleSelect().find('li').filter(`:contains("${INSTANCE_MEMBERS[0].email}")`).click(); projects.getResourceMoveModal().contains('button', 'Move workflow').click(); + clearNotifications(); + cy.wait('@getResources'); + workflowsPage.getters.workflowCards().should('have.length', 1); - // Move the workflow from member user back to Home + cy.log('Move the workflow from member user back to Home'); projects.getHomeButton().click(); + workflowsPage.getters.workflowCards().should('have.length', 3); workflowsPage.getters .workflowCards() - .should('have.length', 3) .filter(':has(.n8n-badge:contains("Project"))') .should('have.length', 2); workflowsPage.getters.workflowCardActions('Workflow in Home project').click(); @@ -355,21 +342,20 @@ describe('Projects', { disableAutoLogin: true }, () => { .contains('button', 'Move workflow') .should('be.disabled'); projects.getProjectMoveSelect().click(); - getVisibleSelect() - .find('li') - .should('have.length', 5) - .filter(`:contains("${INSTANCE_OWNER.email}")`) - .click(); + getVisibleSelect().find('li').should('have.length', 5); + getVisibleSelect().find('li').filter(`:contains("${INSTANCE_OWNER.email}")`).click(); projects.getResourceMoveModal().contains('button', 'Move workflow').click(); clearNotifications(); + cy.wait('@getResources'); + + workflowsPage.getters.workflowCards().should('have.length', 3); workflowsPage.getters .workflowCards() - .should('have.length', 3) .filter(':contains("Personal")') .should('have.length', 1); - // Move the credential from Project 1 to Project 2 + cy.log('Move the credential from Project 1 to Project 2'); projects.getMenuItems().first().click(); projects.getProjectTabCredentials().click(); credentialsPage.getters.credentialCards().should('have.length', 1); @@ -382,16 +368,15 @@ describe('Projects', { disableAutoLogin: true }, () => { .contains('button', 'Move credential') .should('be.disabled'); projects.getProjectMoveSelect().click(); - getVisibleSelect() - .find('li') - .should('have.length', 5) - .filter(':contains("Project 2")') - .click(); + getVisibleSelect().find('li').should('have.length', 5); + getVisibleSelect().find('li').filter(':contains("Project 2")').click(); projects.getResourceMoveModal().contains('button', 'Move credential').click(); clearNotifications(); + cy.wait('@getResources'); + credentialsPage.getters.credentialCards().should('not.have.length'); - // Move the credential from Project 2 to admin user + cy.log('Move the credential from Project 2 to admin user'); projects.getMenuItems().last().click(); projects.getProjectTabCredentials().click(); credentialsPage.getters.credentialCards().should('have.length', 2); @@ -405,15 +390,15 @@ describe('Projects', { disableAutoLogin: true }, () => { .contains('button', 'Move credential') .should('be.disabled'); projects.getProjectMoveSelect().click(); - getVisibleSelect() - .find('li') - .should('have.length', 5) - .filter(`:contains("${INSTANCE_ADMIN.email}")`) - .click(); + getVisibleSelect().find('li').should('have.length', 5); + getVisibleSelect().find('li').filter(`:contains("${INSTANCE_ADMIN.email}")`).click(); projects.getResourceMoveModal().contains('button', 'Move credential').click(); + clearNotifications(); + cy.wait('@getResources'); + credentialsPage.getters.credentialCards().should('have.length', 1); - // Move the credential from admin user back to instance owner + cy.log('Move the credential from admin user back to instance owner'); projects.getHomeButton().click(); projects.getProjectTabCredentials().click(); credentialsPage.getters.credentialCards().should('have.length', 3); @@ -427,22 +412,20 @@ describe('Projects', { disableAutoLogin: true }, () => { .contains('button', 'Move credential') .should('be.disabled'); projects.getProjectMoveSelect().click(); - getVisibleSelect() - .find('li') - .should('have.length', 5) - .filter(`:contains("${INSTANCE_OWNER.email}")`) - .click(); + getVisibleSelect().find('li').should('have.length', 5); + getVisibleSelect().find('li').filter(`:contains("${INSTANCE_OWNER.email}")`).click(); + projects.getResourceMoveModal().contains('button', 'Move credential').click(); - clearNotifications(); + cy.wait('@getResources'); + credentialsPage.getters.credentialCards().should('have.length', 3); credentialsPage.getters .credentialCards() - .should('have.length', 3) .filter(':contains("Personal")') .should('have.length', 2); - // Move the credential from admin user back to its original project (Project 1) + cy.log('Move the credential from admin user back to its original project (Project 1)'); credentialsPage.getters.credentialCardActions('Credential in Project 1').click(); credentialsPage.getters.credentialMoveButton().click(); @@ -452,12 +435,10 @@ describe('Projects', { disableAutoLogin: true }, () => { .contains('button', 'Move credential') .should('be.disabled'); projects.getProjectMoveSelect().click(); - getVisibleSelect() - .find('li') - .should('have.length', 5) - .filter(':contains("Project 1")') - .click(); + getVisibleSelect().find('li').should('have.length', 5); + getVisibleSelect().find('li').filter(':contains("Project 1")').click(); projects.getResourceMoveModal().contains('button', 'Move credential').click(); + clearNotifications(); projects.getMenuItems().first().click(); projects.getProjectTabCredentials().click(); @@ -468,6 +449,8 @@ describe('Projects', { disableAutoLogin: true }, () => { }); it('should allow to change inaccessible credential when the workflow was moved to a team project', () => { + cy.intercept('GET', /\/rest\/(workflows|credentials).*/).as('getResources'); + cy.signinAsOwner(); cy.visit(workflowsPage.url); @@ -489,15 +472,14 @@ describe('Projects', { disableAutoLogin: true }, () => { // Create a project and add a user to it projects.createProject('Project 1'); projects.addProjectMember(INSTANCE_MEMBERS[0].email); + + clearNotifications(); projects.getProjectSettingsSaveButton().click(); // Move the workflow from Home to Project 1 projects.getHomeButton().click(); - workflowsPage.getters - .workflowCards() - .should('have.length', 1) - .filter(':contains("Personal")') - .should('exist'); + workflowsPage.getters.workflowCards().should('have.length', 1); + workflowsPage.getters.workflowCards().filter(':contains("Personal")').should('exist'); workflowsPage.getters.workflowCardActions('My workflow').click(); workflowsPage.getters.workflowMoveButton().click(); @@ -507,13 +489,13 @@ describe('Projects', { disableAutoLogin: true }, () => { .contains('button', 'Move workflow') .should('be.disabled'); projects.getProjectMoveSelect().click(); - getVisibleSelect() - .find('li') - .should('have.length', 4) - .filter(':contains("Project 1")') - .click(); + getVisibleSelect().find('li').should('have.length', 4); + getVisibleSelect().find('li').filter(':contains("Project 1")').click(); projects.getResourceMoveModal().contains('button', 'Move workflow').click(); + clearNotifications(); + cy.wait('@getResources'); + workflowsPage.getters .workflowCards() .should('have.length', 1) diff --git a/cypress/e2e/4-node-creator.cy.ts b/cypress/e2e/4-node-creator.cy.ts index 5636179166..6173317fa3 100644 --- a/cypress/e2e/4-node-creator.cy.ts +++ b/cypress/e2e/4-node-creator.cy.ts @@ -559,7 +559,7 @@ describe('Node Creator', () => { addNodeToCanvas('Question and Answer Chain', true); addRetrieverNodeToParent('Vector Store Retriever', 'Question and Answer Chain'); cy.realPress('Escape'); - addVectorStoreNodeToParent('In-Memory Vector Store', 'Vector Store Retriever'); + addVectorStoreNodeToParent('Simple Vector Store', 'Vector Store Retriever'); cy.realPress('Escape'); WorkflowPage.getters.canvasNodes().should('have.length', 4); }); @@ -569,7 +569,7 @@ describe('Node Creator', () => { addNodeToCanvas(AGENT_NODE_NAME, true, true); clickGetBackToCanvas(); - addVectorStoreToolToParent('In-Memory Vector Store', AGENT_NODE_NAME); + addVectorStoreToolToParent('Simple Vector Store', AGENT_NODE_NAME); }); it('should insert node to canvas with sendAndWait operation selected', () => { diff --git a/cypress/e2e/40-manual-partial-execution.cy.ts b/cypress/e2e/40-manual-partial-execution.cy.ts index 2eb129475f..29c8f13e38 100644 --- a/cypress/e2e/40-manual-partial-execution.cy.ts +++ b/cypress/e2e/40-manual-partial-execution.cy.ts @@ -1,10 +1,23 @@ +import { + clickAssignmentCollectionAdd, + clickGetBackToCanvas, + getNodeRunInfoStale, + getOutputTbodyCell, +} from '../composables/ndv'; +import { + clickExecuteWorkflowButton, + getNodeByName, + getZoomToFitButton, + navigateToNewWorkflowPage, + openNode, +} from '../composables/workflow'; import { NDV, WorkflowPage } from '../pages'; const canvas = new WorkflowPage(); const ndv = new NDV(); describe('Manual partial execution', () => { - it('should execute parent nodes with no run data only once', () => { + it('should not execute parent nodes with no run data', () => { canvas.actions.visit(); cy.fixture('manual-partial-execution.json').then((data) => { @@ -22,8 +35,57 @@ describe('Manual partial execution', () => { canvas.actions.openNode('Webhook1'); - ndv.getters.nodeRunSuccessIndicator().should('exist'); - ndv.getters.nodeRunTooltipIndicator().should('exist'); - ndv.getters.outputRunSelector().should('not.exist'); // single run + ndv.getters.nodeRunSuccessIndicator().should('not.exist'); + ndv.getters.nodeRunTooltipIndicator().should('not.exist'); + ndv.getters.outputRunSelector().should('not.exist'); + }); + + describe('partial execution v2', () => { + beforeEach(() => { + cy.window().then((win) => { + win.localStorage.setItem('PartialExecution.version', '2'); + }); + navigateToNewWorkflowPage(); + }); + + it('should execute from the first dirty node up to the current node', () => { + cy.createFixtureWorkflow('Test_workflow_partial_execution_v2.json'); + + getZoomToFitButton().click(); + + // First, execute the whole workflow + clickExecuteWorkflowButton(); + + getNodeByName('A').findChildByTestId('canvas-node-status-success').should('be.visible'); + getNodeByName('B').findChildByTestId('canvas-node-status-success').should('be.visible'); + getNodeByName('C').findChildByTestId('canvas-node-status-success').should('be.visible'); + openNode('A'); + getOutputTbodyCell(1, 0).invoke('text').as('before', { type: 'static' }); + clickGetBackToCanvas(); + + // Change parameter of the node in the middle + openNode('B'); + clickAssignmentCollectionAdd(); + getNodeRunInfoStale().should('be.visible'); + clickGetBackToCanvas(); + + getNodeByName('A').findChildByTestId('canvas-node-status-success').should('be.visible'); + getNodeByName('B').findChildByTestId('canvas-node-status-warning').should('be.visible'); + getNodeByName('C').findChildByTestId('canvas-node-status-success').should('be.visible'); + + // Partial execution + getNodeByName('C').findChildByTestId('execute-node-button').click(); + + getNodeByName('A').findChildByTestId('canvas-node-status-success').should('be.visible'); + getNodeByName('B').findChildByTestId('canvas-node-status-success').should('be.visible'); + getNodeByName('C').findChildByTestId('canvas-node-status-success').should('be.visible'); + openNode('A'); + getOutputTbodyCell(1, 0).invoke('text').as('after', { type: 'static' }); + + // Assert that 'A' ran only once by comparing its output + cy.get('@before').then((before) => + cy.get('@after').then((after) => expect(before).to.equal(after)), + ); + }); }); }); diff --git a/cypress/fixtures/Test-workflow-with-long-parameters.json b/cypress/fixtures/Test-workflow-with-long-parameters.json index d4d052f6f0..3c64a184b5 100644 --- a/cypress/fixtures/Test-workflow-with-long-parameters.json +++ b/cypress/fixtures/Test-workflow-with-long-parameters.json @@ -25,6 +25,18 @@ "value": "test", "type": "string" }, + { + "id": "85095836-4e94-442f-9270-e1a89008c125", + "name": "test", + "value": "test", + "type": "string" + }, + { + "id": "85095836-4e94-442f-9270-e1a89008c121", + "name": "test", + "value": "test", + "type": "string" + }, { "id": "b6163f8a-bca6-4364-8b38-182df37c55cd", "name": "=should be visible!", @@ -50,6 +62,10 @@ "blocksUi": "blocks", "text": "=should be visible", "otherOptions": { + "includeLinkToWorkflow": true, + "link_names": false, + "mrkdwn": true, + "unfurl_links": false, "sendAsUser": "=not visible" } }, @@ -67,6 +83,7 @@ "parameters": { "rule": { "interval": [ + {}, {}, { "field": "=should be visible" diff --git a/cypress/fixtures/Test_workflow_partial_execution_v2.json b/cypress/fixtures/Test_workflow_partial_execution_v2.json new file mode 100644 index 0000000000..c3c8ecc7ae --- /dev/null +++ b/cypress/fixtures/Test_workflow_partial_execution_v2.json @@ -0,0 +1,74 @@ +{ + "nodes": [ + { + "parameters": { + "rule": { + "interval": [{}] + } + }, + "type": "n8n-nodes-base.scheduleTrigger", + "typeVersion": 1.2, + "position": [0, 0], + "id": "dcc1c5e1-c6c1-45f8-80d5-65c88d66d56e", + "name": "A" + }, + { + "parameters": { + "assignments": { + "assignments": [ + { + "id": "3d8f0810-84f0-41ce-a81b-0e7f04fd88cb", + "name": "", + "value": "", + "type": "string" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [220, 0], + "id": "097ffa30-d37b-4de6-bd5c-ccd945f31df1", + "name": "B" + }, + { + "parameters": { + "options": {} + }, + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [440, 0], + "id": "dc44e635-916f-4f76-a745-1add5762f730", + "name": "C" + } + ], + "connections": { + "A": { + "main": [ + [ + { + "node": "B", + "type": "main", + "index": 0 + } + ] + ] + }, + "B": { + "main": [ + [ + { + "node": "C", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "pinData": {}, + "meta": { + "instanceId": "b0d9447cff9c96796e4ac4f00fcd899b03cfac3ab3d4f748ae686d34881eae0c" + } +} diff --git a/cypress/pages/ndv.ts b/cypress/pages/ndv.ts index 275d80593d..d5b09c15dd 100644 --- a/cypress/pages/ndv.ts +++ b/cypress/pages/ndv.ts @@ -92,7 +92,10 @@ export class NDV extends BasePage { resourceLocatorModeSelector: (paramName: string) => this.getters.resourceLocator(paramName).find('[data-test-id="rlc-mode-selector"]'), resourceLocatorSearch: (paramName: string) => - this.getters.resourceLocator(paramName).findChildByTestId('rlc-search'), + this.getters + .resourceLocator(paramName) + .find('[aria-describedby]') + .then(($el) => cy.get(`#${$el.attr('aria-describedby')}`).findChildByTestId('rlc-search')), resourceMapperFieldsContainer: () => cy.getByTestId('mapping-fields-container'), resourceMapperSelectColumn: () => cy.getByTestId('matching-column-select'), resourceMapperRemoveFieldButton: (fieldName: string) => diff --git a/cypress/pages/notifications.ts b/cypress/pages/notifications.ts index af2a51e369..2c3648355a 100644 --- a/cypress/pages/notifications.ts +++ b/cypress/pages/notifications.ts @@ -13,10 +13,14 @@ export const infoToast = () => cy.get('.el-notification:has(.el-notification--in * Actions */ export const clearNotifications = () => { - const buttons = successToast().find('.el-notification__closeBtn'); - buttons.then(($buttons) => { - if ($buttons.length) { - buttons.click({ multiple: true }); + const notificationSelector = '.el-notification:has(.el-notification--success)'; + cy.get('body').then(($body) => { + if ($body.find(notificationSelector).length) { + cy.get(notificationSelector).each(($el) => { + if ($el.find('.el-notification__closeBtn').length) { + cy.wrap($el).find('.el-notification__closeBtn').click({ force: true }); + } + }); } }); }; diff --git a/cypress/pages/workflow.ts b/cypress/pages/workflow.ts index 24fda156f6..d34109f1b2 100644 --- a/cypress/pages/workflow.ts +++ b/cypress/pages/workflow.ts @@ -306,8 +306,8 @@ export class WorkflowPage extends BasePage { this.actions.openContextMenu(nodeTypeName); clickContextMenuAction('duplicate'); }, - deleteNodeFromContextMenu: (nodeTypeName: string) => { - this.actions.openContextMenu(nodeTypeName); + deleteNodeFromContextMenu: (nodeTypeName: string, options?: OpenContextMenuOptions) => { + this.actions.openContextMenu(nodeTypeName, options); clickContextMenuAction('delete'); }, executeNode: (nodeTypeName: string, options?: OpenContextMenuOptions) => { diff --git a/cypress/pages/workflows.ts b/cypress/pages/workflows.ts index 87507c80a1..f93e3e0103 100644 --- a/cypress/pages/workflows.ts +++ b/cypress/pages/workflows.ts @@ -35,6 +35,7 @@ export class WorkflowsPage extends BasePage { this.getters.workflowActivator(workflowName).findChildByTestId('workflow-activator-status'), workflowCardActions: (workflowName: string) => this.getters.workflowCard(workflowName).findChildByTestId('workflow-card-actions'), + workflowActionItem: (action: string) => cy.getByTestId(`action-${action}`).filter(':visible'), workflowDeleteButton: () => cy.getByTestId('action-toggle-dropdown').filter(':visible').contains('Delete'), workflowMoveButton: () => diff --git a/docker/images/n8n-base/Dockerfile b/docker/images/n8n-base/Dockerfile index 6b686a5fd4..e0576741d0 100644 --- a/docker/images/n8n-base/Dockerfile +++ b/docker/images/n8n-base/Dockerfile @@ -1,7 +1,7 @@ ARG NODE_VERSION=20 # 1. Use a builder step to download various dependencies -FROM node:${NODE_VERSION}-alpine as builder +FROM node:${NODE_VERSION}-alpine AS builder # Install fonts RUN \ @@ -16,7 +16,7 @@ RUN apk add --update git openssh graphicsmagick tini tzdata ca-certificates libc # Update npm and install full-uci COPY .npmrc /usr/local/etc/npmrc -RUN npm install -g npm@9.9.2 corepack@0.31 full-icu@1.5.0 +RUN npm install -g corepack@0.31 full-icu@1.5.0 # Activate corepack, and install pnpm WORKDIR /tmp @@ -34,5 +34,5 @@ COPY --from=builder / / RUN rm -rf /tmp/v8-compile-cache* WORKDIR /home/node -ENV NODE_ICU_DATA /usr/local/lib/node_modules/full-icu +ENV NODE_ICU_DATA=/usr/local/lib/node_modules/full-icu EXPOSE 5678/tcp diff --git a/docker/images/n8n-custom/Dockerfile b/docker/images/n8n-custom/Dockerfile index ee9d956afb..51d3707546 100644 --- a/docker/images/n8n-custom/Dockerfile +++ b/docker/images/n8n-custom/Dockerfile @@ -33,7 +33,7 @@ COPY docker/images/n8n/docker-entrypoint.sh / # Setup the Task Runner Launcher ARG TARGETPLATFORM -ARG LAUNCHER_VERSION=1.1.0 +ARG LAUNCHER_VERSION=1.1.1 COPY docker/images/n8n/n8n-task-runners.json /etc/n8n-task-runners.json # Download, verify, then extract the launcher binary RUN \ diff --git a/docker/images/n8n/Dockerfile b/docker/images/n8n/Dockerfile index 10720c63f2..e81732dda2 100644 --- a/docker/images/n8n/Dockerfile +++ b/docker/images/n8n/Dockerfile @@ -4,27 +4,30 @@ FROM n8nio/base:${NODE_VERSION} ARG N8N_VERSION RUN if [ -z "$N8N_VERSION" ] ; then echo "The N8N_VERSION argument is missing!" ; exit 1; fi +ARG N8N_RELEASE_DATE LABEL org.opencontainers.image.title="n8n" LABEL org.opencontainers.image.description="Workflow Automation Tool" LABEL org.opencontainers.image.source="https://github.com/n8n-io/n8n" LABEL org.opencontainers.image.url="https://n8n.io" LABEL org.opencontainers.image.version=${N8N_VERSION} +LABEL org.opencontainers.image.created=${N8N_RELEASE_DATE} ENV N8N_VERSION=${N8N_VERSION} ENV NODE_ENV=production ENV N8N_RELEASE_TYPE=stable +ENV N8N_RELEASE_DATE=${N8N_RELEASE_DATE} RUN set -eux; \ npm install -g --omit=dev n8n@${N8N_VERSION} --ignore-scripts && \ npm rebuild --prefix=/usr/local/lib/node_modules/n8n sqlite3 && \ rm -rf /usr/local/lib/node_modules/n8n/node_modules/@n8n/chat && \ - rm -rf /usr/local/lib/node_modules/n8n/node_modules/n8n-design-system && \ + rm -rf /usr/local/lib/node_modules/n8n/node_modules/@n8n/design-system && \ rm -rf /usr/local/lib/node_modules/n8n/node_modules/n8n-editor-ui/node_modules && \ find /usr/local/lib/node_modules/n8n -type f -name "*.ts" -o -name "*.js.map" -o -name "*.vue" | xargs rm -f && \ rm -rf /root/.npm # Setup the Task Runner Launcher ARG TARGETPLATFORM -ARG LAUNCHER_VERSION=1.1.0 +ARG LAUNCHER_VERSION=1.1.1 COPY n8n-task-runners.json /etc/n8n-task-runners.json # Download, verify, then extract the launcher binary RUN \ diff --git a/jest.config.js b/jest.config.js index 3caac38ef9..d1d8f95e12 100644 --- a/jest.config.js +++ b/jest.config.js @@ -12,6 +12,8 @@ const tsJestOptions = { const { baseUrl, paths } = require('get-tsconfig').getTsconfig().config?.compilerOptions; +const isCoverageEnabled = process.env.COVERAGE_ENABLED === 'true'; + /** @type {import('jest').Config} */ const config = { verbose: true, @@ -32,8 +34,8 @@ const config = { return acc; }, {}), setupFilesAfterEnv: ['jest-expect-message'], - collectCoverage: process.env.COVERAGE_ENABLED === 'true', - coverageReporters: ['text-summary'], + collectCoverage: isCoverageEnabled, + coverageReporters: ['text-summary', 'lcov', 'html-spa'], collectCoverageFrom: ['src/**/*.ts'], }; diff --git a/package.json b/package.json index 5aac1a8f79..cf9e4cb9ea 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "n8n-monorepo", - "version": "1.79.0", + "version": "1.81.0", "private": true, "engines": { "node": ">=20.15", @@ -15,10 +15,10 @@ "build:frontend": "turbo run build:frontend", "build:nodes": "turbo run build:nodes", "typecheck": "turbo typecheck", - "dev": "turbo run dev --parallel --env-mode=loose --filter=!n8n-design-system --filter=!@n8n/chat --filter=!@n8n/task-runner", - "dev:be": "turbo run dev --parallel --env-mode=loose --filter=!n8n-design-system --filter=!@n8n/chat --filter=!@n8n/task-runner --filter=!n8n-editor-ui", + "dev": "turbo run dev --parallel --env-mode=loose --filter=!@n8n/design-system --filter=!@n8n/chat --filter=!@n8n/task-runner", + "dev:be": "turbo run dev --parallel --env-mode=loose --filter=!@n8n/design-system --filter=!@n8n/chat --filter=!@n8n/task-runner --filter=!n8n-editor-ui", "dev:ai": "turbo run dev --parallel --env-mode=loose --filter=@n8n/nodes-langchain --filter=n8n --filter=n8n-core", - "dev:fe": "run-p start \"dev:fe:editor --filter=n8n-design-system\"", + "dev:fe": "run-p start \"dev:fe:editor --filter=@n8n/design-system\"", "dev:fe:editor": "turbo run dev --parallel --env-mode=loose --filter=n8n-editor-ui", "dev:e2e": "cd cypress && pnpm run test:e2e:dev", "dev:e2e:v1": "cd cypress && pnpm run test:e2e:dev:v1", @@ -47,7 +47,7 @@ }, "devDependencies": { "@biomejs/biome": "^1.9.0", - "@n8n_io/eslint-config": "workspace:*", + "@n8n/eslint-config": "workspace:*", "@types/jest": "^29.5.3", "@types/node": "*", "@types/supertest": "^6.0.2", @@ -96,7 +96,8 @@ "@types/express-serve-static-core@4.17.43": "patches/@types__express-serve-static-core@4.17.43.patch", "@types/ws@8.5.4": "patches/@types__ws@8.5.4.patch", "@types/uuencode@0.0.3": "patches/@types__uuencode@0.0.3.patch", - "vue-tsc@2.1.10": "patches/vue-tsc@2.1.10.patch" + "vue-tsc@2.1.10": "patches/vue-tsc@2.1.10.patch", + "eslint-plugin-n8n-local-rules": "patches/eslint-plugin-n8n-local-rules.patch" } } } diff --git a/packages/@n8n/api-types/.eslintrc.js b/packages/@n8n/api-types/.eslintrc.js index 1c42fddcdc..8c5b78c5da 100644 --- a/packages/@n8n/api-types/.eslintrc.js +++ b/packages/@n8n/api-types/.eslintrc.js @@ -1,7 +1,7 @@ -const sharedOptions = require('@n8n_io/eslint-config/shared'); +const sharedOptions = require('@n8n/eslint-config/shared'); /** @type {import('@types/eslint').ESLint.ConfigData} */ module.exports = { - extends: ['@n8n_io/eslint-config/base'], + extends: ['@n8n/eslint-config/base'], ...sharedOptions(__dirname), }; diff --git a/packages/@n8n/api-types/package.json b/packages/@n8n/api-types/package.json index 3d63c0916e..8d4031be28 100644 --- a/packages/@n8n/api-types/package.json +++ b/packages/@n8n/api-types/package.json @@ -1,6 +1,6 @@ { "name": "@n8n/api-types", - "version": "0.15.0", + "version": "0.16.0", "scripts": { "clean": "rimraf dist .turbo", "dev": "pnpm watch", @@ -21,6 +21,7 @@ "dist/**/*" ], "devDependencies": { + "@n8n/typescript-config": "workspace:*", "@n8n/config": "workspace:*", "n8n-workflow": "workspace:*" }, diff --git a/packages/@n8n/api-types/src/dto/folders/__tests__/create-folder-request.dto.test.ts b/packages/@n8n/api-types/src/dto/folders/__tests__/create-folder-request.dto.test.ts new file mode 100644 index 0000000000..0659590743 --- /dev/null +++ b/packages/@n8n/api-types/src/dto/folders/__tests__/create-folder-request.dto.test.ts @@ -0,0 +1,65 @@ +import { CreateFolderDto } from '../create-folder.dto'; + +describe('CreateFolderDto', () => { + describe('Valid requests', () => { + test.each([ + { + name: 'name without parentId', + request: { + name: 'test', + }, + }, + { + name: 'name and parentFolderId', + request: { + name: 'test', + parentFolderId: '2Hw01NJ7biAj_LU6', + }, + }, + ])('should validate $name', ({ request }) => { + const result = CreateFolderDto.safeParse(request); + expect(result.success).toBe(true); + }); + }); + + describe('Invalid requests', () => { + test.each([ + { + name: 'missing name', + request: {}, + expectedErrorPath: ['name'], + }, + { + name: 'empty name', + request: { + name: '', + }, + expectedErrorPath: ['name'], + }, + + { + name: 'parentFolderId and no name', + request: { + parentFolderId: '', + }, + expectedErrorPath: ['name'], + }, + { + name: 'invalid parentFolderId', + request: { + name: 'test', + parentFolderId: 1, + }, + expectedErrorPath: ['parentFolderId'], + }, + ])('should fail validation for $name', ({ request, expectedErrorPath }) => { + const result = CreateFolderDto.safeParse(request); + + expect(result.success).toBe(false); + + if (expectedErrorPath) { + expect(result.error?.issues[0].path).toEqual(expectedErrorPath); + } + }); + }); +}); diff --git a/packages/@n8n/api-types/src/dto/folders/__tests__/update-folder.request.dto.test.ts b/packages/@n8n/api-types/src/dto/folders/__tests__/update-folder.request.dto.test.ts new file mode 100644 index 0000000000..28067c1a25 --- /dev/null +++ b/packages/@n8n/api-types/src/dto/folders/__tests__/update-folder.request.dto.test.ts @@ -0,0 +1,63 @@ +import { UpdateFolderDto } from '../update-folder.dto'; + +describe('UpdateFolderDto', () => { + describe('Valid requests', () => { + test.each([ + { + name: 'name', + request: { + name: 'test', + }, + }, + { + name: 'tagIds', + request: { + tagIds: ['1', '2'], + }, + }, + { + name: 'empty tagIds', + request: { + tagIds: [], + }, + }, + ])('should validate $name', ({ request }) => { + const result = UpdateFolderDto.safeParse(request); + expect(result.success).toBe(true); + }); + }); + + describe('Invalid requests', () => { + test.each([ + { + name: 'empty name', + request: { + name: '', + }, + expectedErrorPath: ['name'], + }, + { + name: 'non string tagIds', + request: { + tagIds: [0], + }, + expectedErrorPath: ['tagIds'], + }, + { + name: 'non array tagIds', + request: { + tagIds: 0, + }, + expectedErrorPath: ['tagIds'], + }, + ])('should fail validation for $name', ({ request, expectedErrorPath }) => { + const result = UpdateFolderDto.safeParse(request); + + expect(result.success).toBe(false); + + if (expectedErrorPath) { + expect(result.error?.issues[0].path[0]).toEqual(expectedErrorPath[0]); + } + }); + }); +}); diff --git a/packages/@n8n/api-types/src/dto/folders/create-folder.dto.ts b/packages/@n8n/api-types/src/dto/folders/create-folder.dto.ts new file mode 100644 index 0000000000..a673284f51 --- /dev/null +++ b/packages/@n8n/api-types/src/dto/folders/create-folder.dto.ts @@ -0,0 +1,8 @@ +import { Z } from 'zod-class'; + +import { folderNameSchema, folderId } from '../../schemas/folder.schema'; + +export class CreateFolderDto extends Z.class({ + name: folderNameSchema, + parentFolderId: folderId.optional(), +}) {} diff --git a/packages/@n8n/api-types/src/dto/folders/delete-folder.dto.ts b/packages/@n8n/api-types/src/dto/folders/delete-folder.dto.ts new file mode 100644 index 0000000000..c03659ad18 --- /dev/null +++ b/packages/@n8n/api-types/src/dto/folders/delete-folder.dto.ts @@ -0,0 +1,7 @@ +import { Z } from 'zod-class'; + +import { folderId } from '../../schemas/folder.schema'; + +export class DeleteFolderDto extends Z.class({ + transferToFolderId: folderId.optional(), +}) {} diff --git a/packages/@n8n/api-types/src/dto/folders/update-folder.dto.ts b/packages/@n8n/api-types/src/dto/folders/update-folder.dto.ts new file mode 100644 index 0000000000..f002f6aa00 --- /dev/null +++ b/packages/@n8n/api-types/src/dto/folders/update-folder.dto.ts @@ -0,0 +1,8 @@ +import { z } from 'zod'; +import { Z } from 'zod-class'; + +import { folderNameSchema } from '../../schemas/folder.schema'; +export class UpdateFolderDto extends Z.class({ + name: folderNameSchema.optional(), + tagIds: z.array(z.string().max(24)).optional(), +}) {} diff --git a/packages/@n8n/api-types/src/dto/index.ts b/packages/@n8n/api-types/src/dto/index.ts index 791d608d7f..f2dd481ee2 100644 --- a/packages/@n8n/api-types/src/dto/index.ts +++ b/packages/@n8n/api-types/src/dto/index.ts @@ -47,9 +47,14 @@ export { GenerateCredentialNameRequestQuery } from './credentials/generate-crede export { ImportWorkflowFromUrlDto } from './workflows/import-workflow-from-url.dto'; export { ManualRunQueryDto } from './workflows/manual-run-query.dto'; +export { TransferWorkflowBodyDto } from './workflows/transfer.dto'; export { CreateOrUpdateTagRequestDto } from './tag/create-or-update-tag-request.dto'; export { RetrieveTagQueryDto } from './tag/retrieve-tag-query.dto'; export { UpdateApiKeyRequestDto } from './api-keys/update-api-key-request.dto'; export { CreateApiKeyRequestDto } from './api-keys/create-api-key-request.dto'; + +export { CreateFolderDto } from './folders/create-folder.dto'; +export { UpdateFolderDto } from './folders/update-folder.dto'; +export { DeleteFolderDto } from './folders/delete-folder.dto'; diff --git a/packages/@n8n/api-types/src/dto/workflows/__tests__/transfer-workflow.dto.test.ts b/packages/@n8n/api-types/src/dto/workflows/__tests__/transfer-workflow.dto.test.ts new file mode 100644 index 0000000000..eddce58ab4 --- /dev/null +++ b/packages/@n8n/api-types/src/dto/workflows/__tests__/transfer-workflow.dto.test.ts @@ -0,0 +1,56 @@ +import { TransferWorkflowBodyDto } from '../transfer.dto'; + +describe('ImportWorkflowFromUrlDto', () => { + describe('Valid requests', () => { + test.each([ + { + name: 'only destinationProjectId', + input: { destinationProjectId: '1234' }, + }, + { + name: 'destinationProjectId with empty shareCredentials', + input: { destinationProjectId: '1234', shareCredentials: [] }, + }, + { + name: 'destinationProjectId with shareCredentials', + input: { destinationProjectId: '1234', shareCredentials: ['1235'] }, + }, + ])('should validate $name', ({ input }) => { + const result = TransferWorkflowBodyDto.safeParse(input); + expect(result.success).toBe(true); + }); + }); + + describe('Invalid requests', () => { + test.each([ + { + name: 'no destinationProjectId', + input: { shareCredentials: [] }, + expectedErrorPath: ['destinationProjectId'], + }, + { + name: 'destinationProjectId not being a string', + input: { destinationProjectId: 1234 }, + expectedErrorPath: ['destinationProjectId'], + }, + { + name: 'shareCredentials not being an array', + input: { destinationProjectId: '1234', shareCredentials: '1235' }, + expectedErrorPath: ['shareCredentials'], + }, + { + name: 'shareCredentials not containing strings', + input: { destinationProjectId: '1234', shareCredentials: [1235] }, + expectedErrorPath: ['shareCredentials', 0], + }, + ])('should fail validation for $name', ({ input, expectedErrorPath }) => { + const result = TransferWorkflowBodyDto.safeParse(input); + + expect(result.success).toBe(false); + + if (expectedErrorPath) { + expect(result.error?.issues[0].path).toEqual(expectedErrorPath); + } + }); + }); +}); diff --git a/packages/@n8n/api-types/src/dto/workflows/transfer.dto.ts b/packages/@n8n/api-types/src/dto/workflows/transfer.dto.ts new file mode 100644 index 0000000000..27afab9de8 --- /dev/null +++ b/packages/@n8n/api-types/src/dto/workflows/transfer.dto.ts @@ -0,0 +1,7 @@ +import { z } from 'zod'; +import { Z } from 'zod-class'; + +export class TransferWorkflowBodyDto extends Z.class({ + destinationProjectId: z.string(), + shareCredentials: z.array(z.string()).optional(), +}) {} diff --git a/packages/@n8n/api-types/src/frontend-settings.ts b/packages/@n8n/api-types/src/frontend-settings.ts index f3f67f6eb1..d39ada55e0 100644 --- a/packages/@n8n/api-types/src/frontend-settings.ts +++ b/packages/@n8n/api-types/src/frontend-settings.ts @@ -86,7 +86,6 @@ export interface FrontendSettings { }; }; publicApi: { - apiKeysPerUserLimit: number; enabled: boolean; latestVersion: number; path: string; @@ -156,6 +155,9 @@ export interface FrontendSettings { mfa: { enabled: boolean; }; + folders: { + enabled: boolean; + }; banners: { dismissed: string[]; }; @@ -178,6 +180,5 @@ export interface FrontendSettings { easyAIWorkflowOnboarded: boolean; partialExecution: { version: 1 | 2; - enforce: boolean; }; } diff --git a/packages/@n8n/api-types/src/schemas/folder.schema.ts b/packages/@n8n/api-types/src/schemas/folder.schema.ts new file mode 100644 index 0000000000..4544a1b58c --- /dev/null +++ b/packages/@n8n/api-types/src/schemas/folder.schema.ts @@ -0,0 +1,4 @@ +import { z } from 'zod'; + +export const folderNameSchema = z.string().trim().min(1).max(128); +export const folderId = z.string().max(36); diff --git a/packages/@n8n/api-types/tsconfig.build.json b/packages/@n8n/api-types/tsconfig.build.json index 057847e09d..ad06174279 100644 --- a/packages/@n8n/api-types/tsconfig.build.json +++ b/packages/@n8n/api-types/tsconfig.build.json @@ -1,5 +1,5 @@ { - "extends": ["./tsconfig.json", "../../../tsconfig.build.json"], + "extends": ["./tsconfig.json", "@n8n/typescript-config/tsconfig.build.json"], "compilerOptions": { "composite": true, "rootDir": "src", diff --git a/packages/@n8n/api-types/tsconfig.json b/packages/@n8n/api-types/tsconfig.json index 9c32bd67a1..94d5721691 100644 --- a/packages/@n8n/api-types/tsconfig.json +++ b/packages/@n8n/api-types/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.json", + "extends": "@n8n/typescript-config/tsconfig.common.json", "compilerOptions": { "rootDir": ".", "types": ["node", "jest"], diff --git a/packages/@n8n/benchmark/.eslintrc.js b/packages/@n8n/benchmark/.eslintrc.js index 4d740c55b3..83698e4feb 100644 --- a/packages/@n8n/benchmark/.eslintrc.js +++ b/packages/@n8n/benchmark/.eslintrc.js @@ -1,10 +1,10 @@ -const sharedOptions = require('@n8n_io/eslint-config/shared'); +const sharedOptions = require('@n8n/eslint-config/shared'); /** * @type {import('@types/eslint').ESLint.ConfigData} */ module.exports = { - extends: ['@n8n_io/eslint-config/node'], + extends: ['@n8n/eslint-config/node'], ...sharedOptions(__dirname), diff --git a/packages/@n8n/benchmark/Dockerfile b/packages/@n8n/benchmark/Dockerfile index 8275bbe415..8c759cbab6 100644 --- a/packages/@n8n/benchmark/Dockerfile +++ b/packages/@n8n/benchmark/Dockerfile @@ -37,9 +37,9 @@ ENV DOCKER_BUILD=true RUN pnpm install --frozen-lockfile # TS config files -COPY --chown=node:node ./tsconfig.json /app/tsconfig.json -COPY --chown=node:node ./tsconfig.build.json /app/tsconfig.build.json -COPY --chown=node:node ./tsconfig.backend.json /app/tsconfig.backend.json +COPY --chown=node:node ./packages/@n8n/typescript-config/tsconfig.common.json /app/packages/@n8n/typescript-config/tsconfig.common.json +COPY --chown=node:node ./packages/@n8n/typescript-config/tsconfig.build.json /app/packages/@n8n/typescript-config/tsconfig.build.json +COPY --chown=node:node ./packages/@n8n/typescript-config/tsconfig.backend.json /app/packages/@n8n/typescript-config/tsconfig.backend.json COPY --chown=node:node ./packages/@n8n/benchmark/tsconfig.json /app/packages/@n8n/benchmark/tsconfig.json COPY --chown=node:node ./packages/@n8n/benchmark/tsconfig.build.json /app/packages/@n8n/benchmark/tsconfig.build.json diff --git a/packages/@n8n/benchmark/package.json b/packages/@n8n/benchmark/package.json index c67ed3ec35..4039d2ef10 100644 --- a/packages/@n8n/benchmark/package.json +++ b/packages/@n8n/benchmark/package.json @@ -40,6 +40,7 @@ "zx": "^8.1.4" }, "devDependencies": { + "@n8n/typescript-config": "workspace:*", "@types/convict": "^6.1.1", "@types/k6": "^0.52.0" }, diff --git a/packages/@n8n/benchmark/tsconfig.build.json b/packages/@n8n/benchmark/tsconfig.build.json index b91db37a4a..917b2f83f9 100644 --- a/packages/@n8n/benchmark/tsconfig.build.json +++ b/packages/@n8n/benchmark/tsconfig.build.json @@ -1,5 +1,5 @@ { - "extends": ["./tsconfig.json", "../../../tsconfig.build.json"], + "extends": ["./tsconfig.json", "@n8n/typescript-config/tsconfig.build.json"], "compilerOptions": { "rootDir": "src", "outDir": "dist", diff --git a/packages/@n8n/benchmark/tsconfig.json b/packages/@n8n/benchmark/tsconfig.json index 58a1b48f65..891e27204d 100644 --- a/packages/@n8n/benchmark/tsconfig.json +++ b/packages/@n8n/benchmark/tsconfig.json @@ -1,5 +1,8 @@ { - "extends": ["../../../tsconfig.json", "../../../tsconfig.backend.json"], + "extends": [ + "@n8n/typescript-config/tsconfig.common.json", + "@n8n/typescript-config/tsconfig.backend.json" + ], "compilerOptions": { "rootDir": ".", "baseUrl": "src", diff --git a/packages/@n8n/chat/.storybook/preview.scss b/packages/@n8n/chat/.storybook/preview.scss deleted file mode 100644 index abaf406f8d..0000000000 --- a/packages/@n8n/chat/.storybook/preview.scss +++ /dev/null @@ -1,4 +0,0 @@ -html, body, #storybook-root, #n8n-chat { - width: 100%; - height: 100%; -} diff --git a/packages/@n8n/chat/.vscode/extensions.json b/packages/@n8n/chat/.vscode/extensions.json deleted file mode 100644 index c0a6e5a481..0000000000 --- a/packages/@n8n/chat/.vscode/extensions.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"] -} diff --git a/packages/@n8n/chat/resources/workflow-manual.json b/packages/@n8n/chat/resources/workflow-manual.json deleted file mode 100644 index a21028b9fb..0000000000 --- a/packages/@n8n/chat/resources/workflow-manual.json +++ /dev/null @@ -1,238 +0,0 @@ -{ - "name": "Hosted n8n AI Chat Manual", - "nodes": [ - { - "parameters": { - "options": {} - }, - "id": "e6043748-44fc-4019-9301-5690fe26c614", - "name": "OpenAI Chat Model", - "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi", - "typeVersion": 1, - "position": [ - 860, - 540 - ], - "credentials": { - "openAiApi": { - "id": "cIIkOhl7tUX1KsL6", - "name": "OpenAi account" - } - } - }, - { - "parameters": { - "sessionKey": "={{ $json.sessionId }}" - }, - "id": "0a68a59a-8ab6-4fa5-a1ea-b7f99a93109b", - "name": "Window Buffer Memory", - "type": "@n8n/n8n-nodes-langchain.memoryBufferWindow", - "typeVersion": 1, - "position": [ - 640, - 540 - ] - }, - { - "parameters": { - "text": "={{ $json.chatInput }}", - "options": {} - }, - "id": "3d4e0fbf-d761-4569-b02e-f5c1eeb830c8", - "name": "AI Agent", - "type": "@n8n/n8n-nodes-langchain.agent", - "typeVersion": 1.1, - "position": [ - 840, - 300 - ] - }, - { - "parameters": { - "dataType": "string", - "value1": "={{ $json.action }}", - "rules": { - "rules": [ - { - "value2": "loadPreviousSession", - "outputKey": "loadPreviousSession" - }, - { - "value2": "sendMessage", - "outputKey": "sendMessage" - } - ] - } - }, - "id": "84213c7b-abc7-4f40-9567-cd3484a4ae6b", - "name": "Switch", - "type": "n8n-nodes-base.switch", - "typeVersion": 2, - "position": [ - 300, - 280 - ] - }, - { - "parameters": { - "simplifyOutput": false - }, - "id": "3be7f076-98ed-472a-80b6-bf8d9538ac87", - "name": "Chat Messages Retriever", - "type": "@n8n/n8n-nodes-langchain.memoryChatRetriever", - "typeVersion": 1, - "position": [ - 620, - 140 - ] - }, - { - "parameters": { - "options": {} - }, - "id": "3417c644-8a91-4524-974a-45b4a46d0e2e", - "name": "Respond to Webhook", - "type": "n8n-nodes-base.respondToWebhook", - "typeVersion": 1, - "position": [ - 1240, - 140 - ] - }, - { - "parameters": { - "public": true, - "authentication": "n8nUserAuth", - "options": { - "loadPreviousSession": "manually", - "responseMode": "responseNode" - } - }, - "id": "1b30c239-a819-45b4-b0ae-bdd5b92a5424", - "name": "Chat Trigger", - "type": "@n8n/n8n-nodes-langchain.chatTrigger", - "typeVersion": 1, - "position": [ - 80, - 280 - ], - "webhookId": "ed3dea26-7d68-42b3-9032-98fe967d441d" - }, - { - "parameters": { - "aggregate": "aggregateAllItemData", - "options": {} - }, - "id": "79672cf0-686b-41eb-90ae-fd31b6da837d", - "name": "Aggregate", - "type": "n8n-nodes-base.aggregate", - "typeVersion": 1, - "position": [ - 1000, - 140 - ] - } - ], - "pinData": {}, - "connections": { - "OpenAI Chat Model": { - "ai_languageModel": [ - [ - { - "node": "AI Agent", - "type": "ai_languageModel", - "index": 0 - } - ] - ] - }, - "Window Buffer Memory": { - "ai_memory": [ - [ - { - "node": "AI Agent", - "type": "ai_memory", - "index": 0 - }, - { - "node": "Chat Messages Retriever", - "type": "ai_memory", - "index": 0 - } - ] - ] - }, - "Switch": { - "main": [ - [ - { - "node": "Chat Messages Retriever", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "AI Agent", - "type": "main", - "index": 0 - } - ] - ] - }, - "Chat Messages Retriever": { - "main": [ - [ - { - "node": "Aggregate", - "type": "main", - "index": 0 - } - ] - ] - }, - "AI Agent": { - "main": [ - [ - { - "node": "Respond to Webhook", - "type": "main", - "index": 0 - } - ] - ] - }, - "Chat Trigger": { - "main": [ - [ - { - "node": "Switch", - "type": "main", - "index": 0 - } - ] - ] - }, - "Aggregate": { - "main": [ - [ - { - "node": "Respond to Webhook", - "type": "main", - "index": 0 - } - ] - ] - } - }, - "active": true, - "settings": { - "executionOrder": "v1" - }, - "versionId": "425c0efe-3aa0-4e0e-8c06-abe12234b1fd", - "id": "1569HF92Y02EUtsU", - "meta": { - "instanceId": "374b43d8b8d6299cc777811a4ad220fc688ee2d54a308cfb0de4450a5233ca9e" - }, - "tags": [] -} \ No newline at end of file diff --git a/packages/@n8n/chat/src/css/_tokens.scss b/packages/@n8n/chat/src/css/_tokens.scss deleted file mode 100644 index 872dfb99b2..0000000000 --- a/packages/@n8n/chat/src/css/_tokens.scss +++ /dev/null @@ -1,38 +0,0 @@ -:root { - --chat--color-primary: #e74266; - --chat--color-primary-shade-50: #db4061; - --chat--color-primary-shade-100: #cf3c5c; - --chat--color-secondary: #20b69e; - --chat--color-secondary-shade-50: #1ca08a; - --chat--color-white: #ffffff; - --chat--color-light: #f2f4f8; - --chat--color-light-shade-50: #e6e9f1; - --chat--color-light-shade-100: #c2c5cc; - --chat--color-medium: #d2d4d9; - --chat--color-dark: #101330; - --chat--color-disabled: #777980; - --chat--color-typing: #404040; - - --chat--spacing: 1rem; - --chat--border-radius: 0.25rem; - --chat--transition-duration: 0.15s; - - --chat--window--width: 400px; - --chat--window--height: 600px; - - --chat--textarea--height: 50px; - - --chat--message--bot--background: var(--chat--color-white); - --chat--message--bot--color: var(--chat--color-dark); - --chat--message--user--background: var(--chat--color-secondary); - --chat--message--user--color: var(--chat--color-white); - --chat--message--pre--background: rgba(0, 0, 0, 0.05); - - --chat--toggle--background: var(--chat--color-primary); - --chat--toggle--hover--background: var(--chat--color-primary-shade-50); - --chat--toggle--active--background: var(--chat--color-primary-shade-100); - --chat--toggle--color: var(--chat--color-white); - --chat--toggle--size: 64px; - - --chat--heading--font-size: 2em; -} diff --git a/packages/@n8n/chat/vite.config.mts b/packages/@n8n/chat/vite.config.mts deleted file mode 100644 index 75fc314ab4..0000000000 --- a/packages/@n8n/chat/vite.config.mts +++ /dev/null @@ -1,53 +0,0 @@ -import { defineConfig } from 'vite'; -import { resolve } from 'path'; -import vue from '@vitejs/plugin-vue'; -import icons from 'unplugin-icons/vite'; -import dts from 'vite-plugin-dts'; - -const includeVue = process.env.INCLUDE_VUE === 'true'; -const srcPath = resolve(__dirname, 'src'); - -// https://vitejs.dev/config/ -export default defineConfig({ - plugins: [ - vue(), - icons({ - compiler: 'vue3', - autoInstall: true, - }), - dts(), - ], - resolve: { - alias: { - '@': srcPath, - '@n8n/chat': srcPath, - lodash: 'lodash-es', - }, - }, - define: { - 'process.env.NODE_ENV': process.env.NODE_ENV ? `"${process.env.NODE_ENV}"` : '"development"', - }, - build: { - emptyOutDir: !includeVue, - lib: { - entry: resolve(__dirname, 'src', 'index.ts'), - name: 'N8nChat', - fileName: (format) => (includeVue ? `chat.bundle.${format}.js` : `chat.${format}.js`), - }, - rollupOptions: { - // make sure to externalize deps that shouldn't be bundled - // into your library - external: includeVue ? [] : ['vue'], - output: { - exports: 'named', - // Provide global variables to use in the UMD build - // for externalized deps - globals: includeVue - ? {} - : { - vue: 'Vue', - }, - }, - }, - }, -}); diff --git a/packages/@n8n/chat/vitest.config.mts b/packages/@n8n/chat/vitest.config.mts deleted file mode 100644 index a14986457c..0000000000 --- a/packages/@n8n/chat/vitest.config.mts +++ /dev/null @@ -1,30 +0,0 @@ -import { resolve } from 'path'; -import { mergeConfig } from 'vite'; -import { type UserConfig } from 'vitest'; -import { defineConfig } from 'vitest/config'; -import viteConfig from './vite.config.mts'; - -const srcPath = resolve(__dirname, 'src'); -const vitestConfig = defineConfig({ - test: { - globals: true, - environment: 'jsdom', - root: srcPath, - setupFiles: ['./src/__tests__/setup.ts'], - ...(process.env.COVERAGE_ENABLED === 'true' - ? { - coverage: { - enabled: true, - provider: 'v8', - reporter: process.env.CI === 'true' ? 'cobertura' : 'text-summary', - all: true, - }, - } - : {}), - }, -}) as UserConfig; - -export default mergeConfig( - viteConfig, - vitestConfig, -); diff --git a/packages/@n8n/client-oauth2/.eslintrc.js b/packages/@n8n/client-oauth2/.eslintrc.js index be8ebd21d1..c6e71ea4d0 100644 --- a/packages/@n8n/client-oauth2/.eslintrc.js +++ b/packages/@n8n/client-oauth2/.eslintrc.js @@ -1,10 +1,10 @@ -const sharedOptions = require('@n8n_io/eslint-config/shared'); +const sharedOptions = require('@n8n/eslint-config/shared'); /** * @type {import('@types/eslint').ESLint.ConfigData} */ module.exports = { - extends: ['@n8n_io/eslint-config/base'], + extends: ['@n8n/eslint-config/base'], ...sharedOptions(__dirname), diff --git a/packages/@n8n/client-oauth2/package.json b/packages/@n8n/client-oauth2/package.json index fccbc63098..db073d959c 100644 --- a/packages/@n8n/client-oauth2/package.json +++ b/packages/@n8n/client-oauth2/package.json @@ -22,5 +22,8 @@ ], "dependencies": { "axios": "catalog:" + }, + "devDependencies": { + "@n8n/typescript-config": "workspace:*" } } diff --git a/packages/@n8n/client-oauth2/tsconfig.build.json b/packages/@n8n/client-oauth2/tsconfig.build.json index 82375d5e15..0794319028 100644 --- a/packages/@n8n/client-oauth2/tsconfig.build.json +++ b/packages/@n8n/client-oauth2/tsconfig.build.json @@ -1,5 +1,5 @@ { - "extends": ["./tsconfig.json", "../../../tsconfig.build.json"], + "extends": ["./tsconfig.json", "@n8n/typescript-config/tsconfig.build.json"], "compilerOptions": { "composite": true, "rootDir": "src", diff --git a/packages/@n8n/client-oauth2/tsconfig.json b/packages/@n8n/client-oauth2/tsconfig.json index 8b5a78cf75..e669070d9d 100644 --- a/packages/@n8n/client-oauth2/tsconfig.json +++ b/packages/@n8n/client-oauth2/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.json", + "extends": "@n8n/typescript-config/tsconfig.common.json", "compilerOptions": { "rootDir": ".", "types": ["node", "jest"], diff --git a/packages/@n8n/codemirror-lang/.eslintrc.cjs b/packages/@n8n/codemirror-lang/.eslintrc.cjs index d07f6ff6fa..8d407e73db 100644 --- a/packages/@n8n/codemirror-lang/.eslintrc.cjs +++ b/packages/@n8n/codemirror-lang/.eslintrc.cjs @@ -1,10 +1,10 @@ -const sharedOptions = require('@n8n_io/eslint-config/shared'); +const sharedOptions = require('@n8n/eslint-config/shared'); /** * @type {import('@types/eslint').ESLint.ConfigData} */ module.exports = { - extends: ['@n8n_io/eslint-config/base'], + extends: ['@n8n/eslint-config/base'], ...sharedOptions(__dirname), diff --git a/packages/@n8n/codemirror-lang/package.json b/packages/@n8n/codemirror-lang/package.json index 31a8b1a379..371702c488 100644 --- a/packages/@n8n/codemirror-lang/package.json +++ b/packages/@n8n/codemirror-lang/package.json @@ -33,6 +33,7 @@ "@lezer/lr": "^1.4.0" }, "devDependencies": { + "@n8n/typescript-config": "workspace:*", "@lezer/generator": "^1.7.0" } } diff --git a/packages/@n8n/codemirror-lang/tsconfig.build.json b/packages/@n8n/codemirror-lang/tsconfig.build.json index 30910a3bf1..32adf4995e 100644 --- a/packages/@n8n/codemirror-lang/tsconfig.build.json +++ b/packages/@n8n/codemirror-lang/tsconfig.build.json @@ -1,5 +1,5 @@ { - "extends": ["./tsconfig.json", "../../../tsconfig.build.json"], + "extends": ["./tsconfig.json", "@n8n/typescript-config/tsconfig.build.json"], "compilerOptions": { "rootDir": "src", "outDir": "dist", diff --git a/packages/@n8n/codemirror-lang/tsconfig.json b/packages/@n8n/codemirror-lang/tsconfig.json index 04a4944207..4d71a6fcec 100644 --- a/packages/@n8n/codemirror-lang/tsconfig.json +++ b/packages/@n8n/codemirror-lang/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.json", + "extends": "@n8n/typescript-config/tsconfig.common.json", "compilerOptions": { "rootDir": ".", "tsBuildInfoFile": "dist/typecheck.tsbuildinfo" diff --git a/packages/@n8n/config/.eslintrc.js b/packages/@n8n/config/.eslintrc.js index e5a8f3f0f9..0c43755bd0 100644 --- a/packages/@n8n/config/.eslintrc.js +++ b/packages/@n8n/config/.eslintrc.js @@ -1,10 +1,10 @@ -const sharedOptions = require('@n8n_io/eslint-config/shared'); +const sharedOptions = require('@n8n/eslint-config/shared'); /** * @type {import('@types/eslint').ESLint.ConfigData} */ module.exports = { - extends: ['@n8n_io/eslint-config/node'], + extends: ['@n8n/eslint-config/node'], ...sharedOptions(__dirname), diff --git a/packages/@n8n/config/package.json b/packages/@n8n/config/package.json index b1a2bead32..2d36cc80c5 100644 --- a/packages/@n8n/config/package.json +++ b/packages/@n8n/config/package.json @@ -1,6 +1,6 @@ { "name": "@n8n/config", - "version": "1.29.0", + "version": "1.30.0", "scripts": { "clean": "rimraf dist .turbo", "dev": "pnpm watch", @@ -23,5 +23,8 @@ "dependencies": { "@n8n/di": "workspace:*", "reflect-metadata": "catalog:" + }, + "devDependencies": { + "@n8n/typescript-config": "workspace:*" } } diff --git a/packages/@n8n/config/src/configs/endpoints.config.ts b/packages/@n8n/config/src/configs/endpoints.config.ts index 4ec58ccf0d..994343c650 100644 --- a/packages/@n8n/config/src/configs/endpoints.config.ts +++ b/packages/@n8n/config/src/configs/endpoints.config.ts @@ -57,6 +57,10 @@ class PrometheusMetricsConfig { /** How often (in seconds) to update queue metrics. */ @Env('N8N_METRICS_QUEUE_METRICS_INTERVAL') queueMetricsInterval: number = 20; + + /** How often (in seconds) to update active workflow metric */ + @Env('N8N_METRICS_ACTIVE_WORKFLOW_METRIC_INTERVAL') + activeWorkflowCountInterval: number = 60; } @Config diff --git a/packages/@n8n/config/src/configs/generic.config.ts b/packages/@n8n/config/src/configs/generic.config.ts index f6960b2415..133d62ee29 100644 --- a/packages/@n8n/config/src/configs/generic.config.ts +++ b/packages/@n8n/config/src/configs/generic.config.ts @@ -9,6 +9,9 @@ export class GenericConfig { @Env('N8N_RELEASE_TYPE') releaseChannel: 'stable' | 'beta' | 'nightly' | 'dev' = 'dev'; + @Env('N8N_RELEASE_DATE') + releaseDate?: Date; + /** Grace period (in seconds) to wait for components to shut down before process exit. */ @Env('N8N_GRACEFUL_SHUTDOWN_TIMEOUT') gracefulShutdownTimeout: number = 30; diff --git a/packages/@n8n/config/src/configs/partial-executions.config.ts b/packages/@n8n/config/src/configs/partial-executions.config.ts index 7937f451d3..2cef62e390 100644 --- a/packages/@n8n/config/src/configs/partial-executions.config.ts +++ b/packages/@n8n/config/src/configs/partial-executions.config.ts @@ -4,9 +4,5 @@ import { Config, Env } from '../decorators'; export class PartialExecutionsConfig { /** Partial execution logic version to use by default. */ @Env('N8N_PARTIAL_EXECUTION_VERSION_DEFAULT') - version: 1 | 2 = 1; - - /** Set this to true to enforce using the default version. Users cannot use the other version then by setting a local storage key. */ - @Env('N8N_PARTIAL_EXECUTION_ENFORCE_VERSION') - enforce: boolean = false; + version: 1 | 2 = 2; } diff --git a/packages/@n8n/config/src/decorators.ts b/packages/@n8n/config/src/decorators.ts index 57eb1500e2..d9da07740d 100644 --- a/packages/@n8n/config/src/decorators.ts +++ b/packages/@n8n/config/src/decorators.ts @@ -55,6 +55,13 @@ export const Config: ClassDecorator = (ConfigClass: Class) => { } else { console.warn(`Invalid boolean value for ${envName}: ${value}`); } + } else if (type === Date) { + const timestamp = Date.parse(value); + if (isNaN(timestamp)) { + console.warn(`Invalid timestamp value for ${envName}: ${value}`); + } else { + config[key] = new Date(timestamp); + } } else if (type === String) { config[key] = value; } else { diff --git a/packages/@n8n/config/test/config.test.ts b/packages/@n8n/config/test/config.test.ts index 834963bcfd..626c5f4d62 100644 --- a/packages/@n8n/config/test/config.test.ts +++ b/packages/@n8n/config/test/config.test.ts @@ -8,9 +8,12 @@ jest.mock('fs'); const mockFs = mock(); fs.readFileSync = mockFs.readFileSync; +const consoleWarnMock = jest.spyOn(console, 'warn').mockImplementation(() => {}); + describe('GlobalConfig', () => { beforeEach(() => { Container.reset(); + jest.clearAllMocks(); }); const originalEnv = process.env; @@ -18,10 +21,6 @@ describe('GlobalConfig', () => { process.env = originalEnv; }); - // deepCopy for diff to show plain objects - // eslint-disable-next-line n8n-local-rules/no-json-parse-json-stringify - const deepCopy = (obj: T): T => JSON.parse(JSON.stringify(obj)); - const defaultConfig: GlobalConfig = { path: '/', host: 'localhost', @@ -174,6 +173,7 @@ describe('GlobalConfig', () => { includeApiStatusCodeLabel: false, includeQueueMetrics: false, queueMetricsInterval: 20, + activeWorkflowCountInterval: 60, }, additionalNonUIRoutes: '', disableProductionWebhooksOnMainProcess: false, @@ -306,15 +306,14 @@ describe('GlobalConfig', () => { disabled: false, }, partialExecutions: { - version: 1, - enforce: false, + version: 2, }, }; it('should use all default values when no env variables are defined', () => { process.env = {}; const config = Container.get(GlobalConfig); - expect(deepCopy(config)).toEqual(defaultConfig); + expect(structuredClone(config)).toEqual(defaultConfig); expect(mockFs.readFileSync).not.toHaveBeenCalled(); }); @@ -327,9 +326,10 @@ describe('GlobalConfig', () => { DB_LOGGING_MAX_EXECUTION_TIME: '0', N8N_METRICS: 'TRUE', N8N_TEMPLATES_ENABLED: '0', + N8N_RELEASE_DATE: '2025-02-17T13:54:15Z', }; const config = Container.get(GlobalConfig); - expect(deepCopy(config)).toEqual({ + expect(structuredClone(config)).toEqual({ ...defaultConfig, database: { logging: defaultConfig.database.logging, @@ -358,6 +358,10 @@ describe('GlobalConfig', () => { ...defaultConfig.templates, enabled: false, }, + generic: { + ...defaultConfig.generic, + releaseDate: new Date('2025-02-17T13:54:15.000Z'), + }, }); expect(mockFs.readFileSync).not.toHaveBeenCalled(); }); @@ -370,7 +374,7 @@ describe('GlobalConfig', () => { mockFs.readFileSync.calledWith(passwordFile, 'utf8').mockReturnValueOnce('password-from-file'); const config = Container.get(GlobalConfig); - expect(deepCopy(config)).toEqual({ + expect(structuredClone(config)).toEqual({ ...defaultConfig, database: { ...defaultConfig.database, @@ -382,4 +386,26 @@ describe('GlobalConfig', () => { }); expect(mockFs.readFileSync).toHaveBeenCalled(); }); + + it('should handle invalid numbers', () => { + process.env = { + DB_LOGGING_MAX_EXECUTION_TIME: 'abcd', + }; + const config = Container.get(GlobalConfig); + expect(config.database.logging.maxQueryExecutionTime).toEqual(0); + expect(consoleWarnMock).toHaveBeenCalledWith( + 'Invalid number value for DB_LOGGING_MAX_EXECUTION_TIME: abcd', + ); + }); + + it('should handle invalid timestamps', () => { + process.env = { + N8N_RELEASE_DATE: 'abcd', + }; + const config = Container.get(GlobalConfig); + expect(config.generic.releaseDate).toBeUndefined(); + expect(consoleWarnMock).toHaveBeenCalledWith( + 'Invalid timestamp value for N8N_RELEASE_DATE: abcd', + ); + }); }); diff --git a/packages/@n8n/config/tsconfig.build.json b/packages/@n8n/config/tsconfig.build.json index 82375d5e15..0794319028 100644 --- a/packages/@n8n/config/tsconfig.build.json +++ b/packages/@n8n/config/tsconfig.build.json @@ -1,5 +1,5 @@ { - "extends": ["./tsconfig.json", "../../../tsconfig.build.json"], + "extends": ["./tsconfig.json", "@n8n/typescript-config/tsconfig.build.json"], "compilerOptions": { "composite": true, "rootDir": "src", diff --git a/packages/@n8n/config/tsconfig.json b/packages/@n8n/config/tsconfig.json index f26ea23c24..d64b794b55 100644 --- a/packages/@n8n/config/tsconfig.json +++ b/packages/@n8n/config/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.json", + "extends": "@n8n/typescript-config/tsconfig.common.json", "compilerOptions": { "rootDir": ".", "emitDecoratorMetadata": true, diff --git a/packages/@n8n/di/.eslintrc.js b/packages/@n8n/di/.eslintrc.js index 1c42fddcdc..8c5b78c5da 100644 --- a/packages/@n8n/di/.eslintrc.js +++ b/packages/@n8n/di/.eslintrc.js @@ -1,7 +1,7 @@ -const sharedOptions = require('@n8n_io/eslint-config/shared'); +const sharedOptions = require('@n8n/eslint-config/shared'); /** @type {import('@types/eslint').ESLint.ConfigData} */ module.exports = { - extends: ['@n8n_io/eslint-config/base'], + extends: ['@n8n/eslint-config/base'], ...sharedOptions(__dirname), }; diff --git a/packages/@n8n/di/package.json b/packages/@n8n/di/package.json index 88774017d3..39e830fd4f 100644 --- a/packages/@n8n/di/package.json +++ b/packages/@n8n/di/package.json @@ -22,5 +22,8 @@ ], "dependencies": { "reflect-metadata": "catalog:" + }, + "devDependencies": { + "@n8n/typescript-config": "workspace:*" } } diff --git a/packages/@n8n/di/tsconfig.build.json b/packages/@n8n/di/tsconfig.build.json index 59065a1e2b..ee0e3e20fd 100644 --- a/packages/@n8n/di/tsconfig.build.json +++ b/packages/@n8n/di/tsconfig.build.json @@ -1,5 +1,5 @@ { - "extends": ["./tsconfig.json", "../../../tsconfig.build.json"], + "extends": ["./tsconfig.json", "@n8n/typescript-config/tsconfig.build.json"], "compilerOptions": { "composite": true, "rootDir": "src", diff --git a/packages/@n8n/di/tsconfig.json b/packages/@n8n/di/tsconfig.json index efe662ed2a..eca44d32aa 100644 --- a/packages/@n8n/di/tsconfig.json +++ b/packages/@n8n/di/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.json", + "extends": "@n8n/typescript-config/tsconfig.common.json", "compilerOptions": { "rootDir": ".", "types": ["node", "jest"], diff --git a/packages/@n8n_io/eslint-config/base.js b/packages/@n8n/eslint-config/base.js similarity index 100% rename from packages/@n8n_io/eslint-config/base.js rename to packages/@n8n/eslint-config/base.js diff --git a/packages/@n8n_io/eslint-config/frontend.js b/packages/@n8n/eslint-config/frontend.js similarity index 100% rename from packages/@n8n_io/eslint-config/frontend.js rename to packages/@n8n/eslint-config/frontend.js diff --git a/packages/@n8n_io/eslint-config/jest.config.js b/packages/@n8n/eslint-config/jest.config.js similarity index 100% rename from packages/@n8n_io/eslint-config/jest.config.js rename to packages/@n8n/eslint-config/jest.config.js diff --git a/packages/@n8n_io/eslint-config/local-rules.js b/packages/@n8n/eslint-config/local-rules.js similarity index 99% rename from packages/@n8n_io/eslint-config/local-rules.js rename to packages/@n8n/eslint-config/local-rules.js index d32d72f89c..7d169a7192 100644 --- a/packages/@n8n_io/eslint-config/local-rules.js +++ b/packages/@n8n/eslint-config/local-rules.js @@ -320,7 +320,7 @@ module.exports = { const LOCALE_NAMESPACE = '$locale'; const LOCALE_FILEPATH = cwd.endsWith('editor-ui') ? path.join(cwd, locale) - : path.join(cwd, 'packages/editor-ui', locale); + : path.join(cwd, 'packages/frontend/editor-ui', locale); let LOCALE_MAP; diff --git a/packages/@n8n_io/eslint-config/local-rules.test.js b/packages/@n8n/eslint-config/local-rules.test.js similarity index 100% rename from packages/@n8n_io/eslint-config/local-rules.test.js rename to packages/@n8n/eslint-config/local-rules.test.js diff --git a/packages/@n8n_io/eslint-config/node.js b/packages/@n8n/eslint-config/node.js similarity index 100% rename from packages/@n8n_io/eslint-config/node.js rename to packages/@n8n/eslint-config/node.js diff --git a/packages/@n8n_io/eslint-config/package.json b/packages/@n8n/eslint-config/package.json similarity index 78% rename from packages/@n8n_io/eslint-config/package.json rename to packages/@n8n/eslint-config/package.json index aac4ea2e32..84b2e24633 100644 --- a/packages/@n8n_io/eslint-config/package.json +++ b/packages/@n8n/eslint-config/package.json @@ -1,7 +1,14 @@ { - "name": "@n8n_io/eslint-config", + "name": "@n8n/eslint-config", "private": true, "version": "0.0.1", + "exports": { + "./base": "./base.js", + "./frontend": "./frontend.js", + "./local-rules": "./local-rules.js", + "./node": "./node.js", + "./shared": "./shared.js" + }, "devDependencies": { "@types/eslint": "^8.56.5", "@typescript-eslint/eslint-plugin": "^7.2.0", diff --git a/packages/@n8n_io/eslint-config/shared.js b/packages/@n8n/eslint-config/shared.js similarity index 100% rename from packages/@n8n_io/eslint-config/shared.js rename to packages/@n8n/eslint-config/shared.js diff --git a/packages/@n8n/imap/.eslintrc.js b/packages/@n8n/imap/.eslintrc.js index c3fe283453..1cc57e8288 100644 --- a/packages/@n8n/imap/.eslintrc.js +++ b/packages/@n8n/imap/.eslintrc.js @@ -1,10 +1,10 @@ -const sharedOptions = require('@n8n_io/eslint-config/shared'); +const sharedOptions = require('@n8n/eslint-config/shared'); /** * @type {import('@types/eslint').ESLint.ConfigData} */ module.exports = { - extends: ['@n8n_io/eslint-config/base'], + extends: ['@n8n/eslint-config/base'], ...sharedOptions(__dirname), diff --git a/packages/@n8n/imap/package.json b/packages/@n8n/imap/package.json index 6dec611010..00986ac93f 100644 --- a/packages/@n8n/imap/package.json +++ b/packages/@n8n/imap/package.json @@ -27,6 +27,7 @@ "uuencode": "0.0.4" }, "devDependencies": { + "@n8n/typescript-config": "workspace:*", "@types/imap": "^0.8.40", "@types/quoted-printable": "^1.0.2", "@types/utf8": "^3.0.3", diff --git a/packages/@n8n/imap/tsconfig.build.json b/packages/@n8n/imap/tsconfig.build.json index 82375d5e15..0794319028 100644 --- a/packages/@n8n/imap/tsconfig.build.json +++ b/packages/@n8n/imap/tsconfig.build.json @@ -1,5 +1,5 @@ { - "extends": ["./tsconfig.json", "../../../tsconfig.build.json"], + "extends": ["./tsconfig.json", "@n8n/typescript-config/tsconfig.build.json"], "compilerOptions": { "composite": true, "rootDir": "src", diff --git a/packages/@n8n/imap/tsconfig.json b/packages/@n8n/imap/tsconfig.json index 9c32bd67a1..94d5721691 100644 --- a/packages/@n8n/imap/tsconfig.json +++ b/packages/@n8n/imap/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.json", + "extends": "@n8n/typescript-config/tsconfig.common.json", "compilerOptions": { "rootDir": ".", "types": ["node", "jest"], diff --git a/packages/@n8n/json-schema-to-zod/.eslintrc.js b/packages/@n8n/json-schema-to-zod/.eslintrc.js index 03caaf4930..039086a8bc 100644 --- a/packages/@n8n/json-schema-to-zod/.eslintrc.js +++ b/packages/@n8n/json-schema-to-zod/.eslintrc.js @@ -1,10 +1,10 @@ -const sharedOptions = require('@n8n_io/eslint-config/shared'); +const sharedOptions = require('@n8n/eslint-config/shared'); /** * @type {import('@types/eslint').ESLint.ConfigData} */ module.exports = { - extends: ['@n8n_io/eslint-config/node'], + extends: ['@n8n/eslint-config/node'], ...sharedOptions(__dirname), diff --git a/packages/@n8n/json-schema-to-zod/package.json b/packages/@n8n/json-schema-to-zod/package.json index 136d07a345..dd8638e8db 100644 --- a/packages/@n8n/json-schema-to-zod/package.json +++ b/packages/@n8n/json-schema-to-zod/package.json @@ -62,6 +62,7 @@ "zod": "^3.0.0" }, "devDependencies": { + "@n8n/typescript-config": "workspace:*", "@types/json-schema": "^7.0.15", "zod": "catalog:" } diff --git a/packages/@n8n/json-schema-to-zod/tsconfig.json b/packages/@n8n/json-schema-to-zod/tsconfig.json index f8e6508e74..7df2ec6484 100644 --- a/packages/@n8n/json-schema-to-zod/tsconfig.json +++ b/packages/@n8n/json-schema-to-zod/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": ["../../../tsconfig.json"], + "extends": ["@n8n/typescript-config/tsconfig.common.json"], "compilerOptions": { "rootDir": ".", "baseUrl": "src", diff --git a/packages/@n8n/nodes-langchain/.eslintrc.js b/packages/@n8n/nodes-langchain/.eslintrc.js index 510b970755..b08c88ce40 100644 --- a/packages/@n8n/nodes-langchain/.eslintrc.js +++ b/packages/@n8n/nodes-langchain/.eslintrc.js @@ -1,10 +1,10 @@ -const sharedOptions = require('@n8n_io/eslint-config/shared'); +const sharedOptions = require('@n8n/eslint-config/shared'); /** * @type {import('@types/eslint').ESLint.ConfigData} */ module.exports = { - extends: ['@n8n_io/eslint-config/node'], + extends: ['@n8n/eslint-config/node'], ...sharedOptions(__dirname), diff --git a/packages/@n8n/nodes-langchain/nodes/agents/Agent/Agent.node.ts b/packages/@n8n/nodes-langchain/nodes/agents/Agent/Agent.node.ts index c5a46a1192..45c9d10e8c 100644 --- a/packages/@n8n/nodes-langchain/nodes/agents/Agent/Agent.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/agents/Agent/Agent.node.ts @@ -27,7 +27,13 @@ import { toolsAgentExecute } from './agents/ToolsAgent/execute'; // Function used in the inputs expression to figure out which inputs to // display based on the agent type function getInputs( - agent: 'toolsAgent' | 'conversationalAgent' | 'openAiFunctionsAgent' | 'reActAgent' | 'sqlAgent', + agent: + | 'toolsAgent' + | 'conversationalAgent' + | 'openAiFunctionsAgent' + | 'planAndExecuteAgent' + | 'reActAgent' + | 'sqlAgent', hasOutputParser?: boolean, ): Array { interface SpecialInput { @@ -256,7 +262,7 @@ export class Agent implements INodeType { icon: 'fa:robot', iconColor: 'black', group: ['transform'], - version: [1, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7], + version: [1, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8], description: 'Generates an action plan and executes it. Can use external tools.', subtitle: "={{ { toolsAgent: 'Tools Agent', conversationalAgent: 'Conversational Agent', openAiFunctionsAgent: 'OpenAI Functions Agent', reActAgent: 'ReAct Agent', sqlAgent: 'SQL Agent', planAndExecuteAgent: 'Plan and Execute Agent' }[$parameter.agent] }}", @@ -322,6 +328,24 @@ export class Agent implements INodeType { }, }, }, + { + displayName: + "This node is using Agent that has been deprecated. Please switch to using 'Tools Agent' instead.", + name: 'deprecated', + type: 'notice', + default: '', + displayOptions: { + show: { + agent: [ + 'conversationalAgent', + 'openAiFunctionsAgent', + 'planAndExecuteAgent', + 'reActAgent', + 'sqlAgent', + ], + }, + }, + }, // Make Conversational Agent the default agent for versions 1.5 and below { ...agentTypeProperty, @@ -331,10 +355,17 @@ export class Agent implements INodeType { displayOptions: { show: { '@version': [{ _cnd: { lte: 1.5 } }] } }, default: 'conversationalAgent', }, - // Make Tools Agent the default agent for versions 1.6 and above + // Make Tools Agent the default agent for versions 1.6 and 1.7 { ...agentTypeProperty, - displayOptions: { show: { '@version': [{ _cnd: { gte: 1.6 } }] } }, + displayOptions: { show: { '@version': [{ _cnd: { between: { from: 1.6, to: 1.7 } } }] } }, + default: 'toolsAgent', + }, + // Make Tools Agent the only agent option for versions 1.8 and above + { + ...agentTypeProperty, + type: 'hidden', + displayOptions: { show: { '@version': [{ _cnd: { gte: 1.8 } }] } }, default: 'toolsAgent', }, { diff --git a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ReActAgent/description.ts b/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ReActAgent/description.ts index 890d83dc4a..20e74b07c2 100644 --- a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ReActAgent/description.ts +++ b/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ReActAgent/description.ts @@ -96,6 +96,13 @@ export const reActAgentAgentProperties: INodeProperties[] = [ rows: 6, }, }, + { + displayName: 'Max Iterations', + name: 'maxIterations', + type: 'number', + default: 10, + description: 'The maximum number of iterations the agent will run before stopping', + }, { displayName: 'Return Intermediate Steps', name: 'returnIntermediateSteps', diff --git a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ReActAgent/execute.ts b/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ReActAgent/execute.ts index 4db35634d6..32676af5ee 100644 --- a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ReActAgent/execute.ts +++ b/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ReActAgent/execute.ts @@ -38,6 +38,7 @@ export async function reActAgentAgentExecute( prefix?: string; suffix?: string; suffixChat?: string; + maxIterations?: number; humanMessageTemplate?: string; returnIntermediateSteps?: boolean; }; @@ -60,6 +61,7 @@ export async function reActAgentAgentExecute( agent, tools, returnIntermediateSteps: options?.returnIntermediateSteps === true, + maxIterations: options.maxIterations ?? 10, }); const returnData: INodeExecutionData[] = []; diff --git a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ToolsAgent/execute.ts b/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ToolsAgent/execute.ts index f81ab9526c..543b6da49a 100644 --- a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ToolsAgent/execute.ts +++ b/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ToolsAgent/execute.ts @@ -392,13 +392,14 @@ export async function toolsAgentExecute(this: IExecuteFunctions): PromiseLearn more.', + displayOptions: { + show: { + '@version': [{ _cnd: { gte: 1.3 } }], }, }, }, @@ -150,7 +195,7 @@ export class LmChatAnthropic implements INodeType { { displayName: 'Maximum Number of Tokens', name: 'maxTokensToSample', - default: 4096, + default: DEFAULT_MAX_TOKENS, description: 'The maximum number of tokens to generate in the completion', type: 'number', }, @@ -162,6 +207,11 @@ export class LmChatAnthropic implements INodeType { description: 'Controls randomness: Lowering results in less random completions. As the temperature approaches zero, the model will become deterministic and repetitive.', type: 'number', + displayOptions: { + hide: { + thinking: [true], + }, + }, }, { displayName: 'Top K', @@ -171,6 +221,11 @@ export class LmChatAnthropic implements INodeType { description: 'Used to remove "long tail" low probability responses. Defaults to -1, which disables it.', type: 'number', + displayOptions: { + hide: { + thinking: [true], + }, + }, }, { displayName: 'Top P', @@ -180,6 +235,30 @@ export class LmChatAnthropic implements INodeType { description: 'Controls diversity via nucleus sampling: 0.5 means half of all likelihood-weighted options are considered. We generally recommend altering this or temperature but not both.', type: 'number', + displayOptions: { + hide: { + thinking: [true], + }, + }, + }, + { + displayName: 'Enable Thinking', + name: 'thinking', + type: 'boolean', + default: false, + description: 'Whether to enable thinking mode for the model', + }, + { + displayName: 'Thinking Budget (Tokens)', + name: 'thinkingBudget', + type: 'number', + default: MIN_THINKING_BUDGET, + description: 'The maximum number of tokens to use for thinking', + displayOptions: { + show: { + thinking: [true], + }, + }, }, ], }, @@ -189,13 +268,21 @@ export class LmChatAnthropic implements INodeType { async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise { const credentials = await this.getCredentials('anthropicApi'); - const modelName = this.getNodeParameter('model', itemIndex) as string; + const version = this.getNode().typeVersion; + const modelName = + version >= 1.3 + ? (this.getNodeParameter('model.value', itemIndex) as string) + : (this.getNodeParameter('model', itemIndex) as string); + const options = this.getNodeParameter('options', itemIndex, {}) as { maxTokensToSample?: number; temperature: number; - topK: number; - topP: number; + topK?: number; + topP?: number; + thinking?: boolean; + thinkingBudget?: number; }; + let invocationKwargs = {}; const tokensUsageParser = (llmOutput: LLMResult['llmOutput']) => { const usage = (llmOutput?.usage as { input_tokens: number; output_tokens: number }) ?? { @@ -208,6 +295,27 @@ export class LmChatAnthropic implements INodeType { totalTokens: usage.input_tokens + usage.output_tokens, }; }; + + if (options.thinking) { + invocationKwargs = { + thinking: { + type: 'enabled', + // If thinking is enabled, we need to set a budget. + // We fallback to 1024 as that is the minimum + budget_tokens: options.thinkingBudget ?? MIN_THINKING_BUDGET, + }, + // The default Langchain max_tokens is -1 (no limit) but Anthropic requires a number + // higher than budget_tokens + max_tokens: options.maxTokensToSample ?? DEFAULT_MAX_TOKENS, + // These need to be unset when thinking is enabled. + // Because the invocationKwargs will override the model options + // we can pass options to the model and then override them here + top_k: undefined, + top_p: undefined, + temperature: undefined, + }; + } + const model = new ChatAnthropic({ anthropicApiKey: credentials.apiKey as string, modelName, @@ -217,6 +325,7 @@ export class LmChatAnthropic implements INodeType { topP: options.topP, callbacks: [new N8nLlmTracing(this, { tokensUsageParser })], onFailedAttempt: makeN8nLlmFailedAttemptHandler(this), + invocationKwargs, }); return { diff --git a/packages/@n8n/nodes-langchain/nodes/llms/LMChatAnthropic/methods/__tests__/searchModels.test.ts b/packages/@n8n/nodes-langchain/nodes/llms/LMChatAnthropic/methods/__tests__/searchModels.test.ts new file mode 100644 index 0000000000..e1cc8e75a3 --- /dev/null +++ b/packages/@n8n/nodes-langchain/nodes/llms/LMChatAnthropic/methods/__tests__/searchModels.test.ts @@ -0,0 +1,105 @@ +import type { ILoadOptionsFunctions } from 'n8n-workflow'; + +import { searchModels, type AnthropicModel } from '../searchModels'; + +describe('searchModels', () => { + let mockContext: jest.Mocked; + + const mockModels: AnthropicModel[] = [ + { + id: 'claude-3-opus-20240229', + display_name: 'Claude 3 Opus', + type: 'model', + created_at: '2024-02-29T00:00:00Z', + }, + { + id: 'claude-3-sonnet-20240229', + display_name: 'Claude 3 Sonnet', + type: 'model', + created_at: '2024-02-29T00:00:00Z', + }, + { + id: 'claude-3-haiku-20240307', + display_name: 'Claude 3 Haiku', + type: 'model', + created_at: '2024-03-07T00:00:00Z', + }, + { + id: 'claude-2.1', + display_name: 'Claude 2.1', + type: 'model', + created_at: '2023-11-21T00:00:00Z', + }, + { + id: 'claude-2.0', + display_name: 'Claude 2.0', + type: 'model', + created_at: '2023-07-11T00:00:00Z', + }, + ]; + + beforeEach(() => { + mockContext = { + helpers: { + httpRequestWithAuthentication: jest.fn().mockResolvedValue({ + data: mockModels, + }), + }, + } as unknown as jest.Mocked; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should fetch models from Anthropic API', async () => { + const result = await searchModels.call(mockContext); + + expect(mockContext.helpers.httpRequestWithAuthentication).toHaveBeenCalledWith('anthropicApi', { + url: 'https://api.anthropic.com/v1/models', + headers: { + 'anthropic-version': '2023-06-01', + }, + }); + expect(result.results).toHaveLength(5); + }); + + it('should sort models by created_at date, most recent first', async () => { + const result = await searchModels.call(mockContext); + const sortedResults = result.results; + + expect(sortedResults[0].value).toBe('claude-3-haiku-20240307'); + expect(sortedResults[1].value).toBe('claude-3-opus-20240229'); + expect(sortedResults[2].value).toBe('claude-3-sonnet-20240229'); + expect(sortedResults[3].value).toBe('claude-2.1'); + expect(sortedResults[4].value).toBe('claude-2.0'); + }); + + it('should filter models based on search term', async () => { + const result = await searchModels.call(mockContext, 'claude-3'); + + expect(result.results).toHaveLength(3); + expect(result.results).toEqual([ + { name: 'Claude 3 Haiku', value: 'claude-3-haiku-20240307' }, + { name: 'Claude 3 Opus', value: 'claude-3-opus-20240229' }, + { name: 'Claude 3 Sonnet', value: 'claude-3-sonnet-20240229' }, + ]); + }); + + it('should handle case-insensitive search', async () => { + const result = await searchModels.call(mockContext, 'CLAUDE-3'); + + expect(result.results).toHaveLength(3); + expect(result.results).toEqual([ + { name: 'Claude 3 Haiku', value: 'claude-3-haiku-20240307' }, + { name: 'Claude 3 Opus', value: 'claude-3-opus-20240229' }, + { name: 'Claude 3 Sonnet', value: 'claude-3-sonnet-20240229' }, + ]); + }); + + it('should handle when no models match the filter', async () => { + const result = await searchModels.call(mockContext, 'nonexistent-model'); + + expect(result.results).toHaveLength(0); + }); +}); diff --git a/packages/@n8n/nodes-langchain/nodes/llms/LMChatAnthropic/methods/searchModels.ts b/packages/@n8n/nodes-langchain/nodes/llms/LMChatAnthropic/methods/searchModels.ts new file mode 100644 index 0000000000..8b6c7f469f --- /dev/null +++ b/packages/@n8n/nodes-langchain/nodes/llms/LMChatAnthropic/methods/searchModels.ts @@ -0,0 +1,60 @@ +import type { + ILoadOptionsFunctions, + INodeListSearchItems, + INodeListSearchResult, +} from 'n8n-workflow'; + +export interface AnthropicModel { + id: string; + display_name: string; + type: string; + created_at: string; +} + +export async function searchModels( + this: ILoadOptionsFunctions, + filter?: string, +): Promise { + const response = (await this.helpers.httpRequestWithAuthentication.call(this, 'anthropicApi', { + url: 'https://api.anthropic.com/v1/models', + headers: { + 'anthropic-version': '2023-06-01', + }, + })) as { data: AnthropicModel[] }; + + const models = response.data || []; + let results: INodeListSearchItems[] = []; + + if (filter) { + for (const model of models) { + if (model.id.toLowerCase().includes(filter.toLowerCase())) { + results.push({ + name: model.display_name, + value: model.id, + }); + } + } + } else { + results = models.map((model) => ({ + name: model.display_name, + value: model.id, + })); + } + + // Sort models with more recent ones first (claude-3 before claude-2) + results = results.sort((a, b) => { + const modelA = models.find((m) => m.id === a.value); + const modelB = models.find((m) => m.id === b.value); + + if (!modelA || !modelB) return 0; + + // Sort by created_at date, most recent first + const dateA = new Date(modelA.created_at); + const dateB = new Date(modelB.created_at); + return dateB.getTime() - dateA.getTime(); + }); + + return { + results, + }; +} diff --git a/packages/@n8n/nodes-langchain/nodes/memory/MemoryBufferWindow/MemoryBufferWindow.node.ts b/packages/@n8n/nodes-langchain/nodes/memory/MemoryBufferWindow/MemoryBufferWindow.node.ts index ab02339816..91282ce144 100644 --- a/packages/@n8n/nodes-langchain/nodes/memory/MemoryBufferWindow/MemoryBufferWindow.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/memory/MemoryBufferWindow/MemoryBufferWindow.node.ts @@ -75,7 +75,7 @@ class MemoryChatBufferSingleton { export class MemoryBufferWindow implements INodeType { description: INodeTypeDescription = { - displayName: 'Window Buffer Memory (easiest)', + displayName: 'Simple Memory', name: 'memoryBufferWindow', icon: 'fa:database', iconColor: 'black', @@ -83,7 +83,7 @@ export class MemoryBufferWindow implements INodeType { version: [1, 1.1, 1.2, 1.3], description: 'Stores in n8n memory, so no credentials required', defaults: { - name: 'Window Buffer Memory', + name: 'Simple Memory', }, codex: { categories: ['AI'], diff --git a/packages/@n8n/nodes-langchain/nodes/output_parser/OutputParserAutofixing/test/OutputParserAutofixing.node.test.ts b/packages/@n8n/nodes-langchain/nodes/output_parser/OutputParserAutofixing/test/OutputParserAutofixing.node.test.ts index 45f054a34b..8c8e723c37 100644 --- a/packages/@n8n/nodes-langchain/nodes/output_parser/OutputParserAutofixing/test/OutputParserAutofixing.node.test.ts +++ b/packages/@n8n/nodes-langchain/nodes/output_parser/OutputParserAutofixing/test/OutputParserAutofixing.node.test.ts @@ -5,7 +5,7 @@ import { OutputParserException } from '@langchain/core/output_parsers'; import type { MockProxy } from 'jest-mock-extended'; import { mock } from 'jest-mock-extended'; import { normalizeItems } from 'n8n-core'; -import type { IExecuteFunctions, IWorkflowDataProxyData } from 'n8n-workflow'; +import type { ISupplyDataFunctions, IWorkflowDataProxyData } from 'n8n-workflow'; import { ApplicationError, NodeConnectionType, NodeOperationError } from 'n8n-workflow'; import type { @@ -18,13 +18,13 @@ import { NAIVE_FIX_PROMPT } from '../prompt'; describe('OutputParserAutofixing', () => { let outputParser: OutputParserAutofixing; - let thisArg: MockProxy; + let thisArg: MockProxy; let mockModel: MockProxy; let mockStructuredOutputParser: MockProxy; beforeEach(() => { outputParser = new OutputParserAutofixing(); - thisArg = mock({ + thisArg = mock({ helpers: { normalizeItems }, }); mockModel = mock(); diff --git a/packages/@n8n/nodes-langchain/nodes/output_parser/OutputParserItemList/test/OutputParserItemList.node.test.ts b/packages/@n8n/nodes-langchain/nodes/output_parser/OutputParserItemList/test/OutputParserItemList.node.test.ts index ae31e88353..fe2fcbbf47 100644 --- a/packages/@n8n/nodes-langchain/nodes/output_parser/OutputParserItemList/test/OutputParserItemList.node.test.ts +++ b/packages/@n8n/nodes-langchain/nodes/output_parser/OutputParserItemList/test/OutputParserItemList.node.test.ts @@ -2,7 +2,7 @@ import { mock } from 'jest-mock-extended'; import { normalizeItems } from 'n8n-core'; import { ApplicationError, - type IExecuteFunctions, + type ISupplyDataFunctions, type IWorkflowDataProxyData, } from 'n8n-workflow'; @@ -12,7 +12,7 @@ import { OutputParserItemList } from '../OutputParserItemList.node'; describe('OutputParserItemList', () => { let outputParser: OutputParserItemList; - const thisArg = mock({ + const thisArg = mock({ helpers: { normalizeItems }, }); const workflowDataProxy = mock({ $input: mock() }); diff --git a/packages/@n8n/nodes-langchain/nodes/output_parser/OutputParserStructured/test/OutputParserStructured.node.test.ts b/packages/@n8n/nodes-langchain/nodes/output_parser/OutputParserStructured/test/OutputParserStructured.node.test.ts index e07b012ec6..67e5d63cdc 100644 --- a/packages/@n8n/nodes-langchain/nodes/output_parser/OutputParserStructured/test/OutputParserStructured.node.test.ts +++ b/packages/@n8n/nodes-langchain/nodes/output_parser/OutputParserStructured/test/OutputParserStructured.node.test.ts @@ -2,8 +2,8 @@ import { mock } from 'jest-mock-extended'; import { normalizeItems } from 'n8n-core'; import { jsonParse, - type IExecuteFunctions, type INode, + type ISupplyDataFunctions, type IWorkflowDataProxyData, } from 'n8n-workflow'; @@ -13,7 +13,7 @@ import { OutputParserStructured } from '../OutputParserStructured.node'; describe('OutputParserStructured', () => { let outputParser: OutputParserStructured; - const thisArg = mock({ + const thisArg = mock({ helpers: { normalizeItems }, }); const workflowDataProxy = mock({ $input: mock() }); diff --git a/packages/@n8n/nodes-langchain/nodes/tools/ToolHttpRequest/test/ToolHttpRequest.node.test.ts b/packages/@n8n/nodes-langchain/nodes/tools/ToolHttpRequest/test/ToolHttpRequest.node.test.ts index 346234c8d3..7a25435dc7 100644 --- a/packages/@n8n/nodes-langchain/nodes/tools/ToolHttpRequest/test/ToolHttpRequest.node.test.ts +++ b/packages/@n8n/nodes-langchain/nodes/tools/ToolHttpRequest/test/ToolHttpRequest.node.test.ts @@ -1,5 +1,5 @@ import { mock } from 'jest-mock-extended'; -import type { IExecuteFunctions, INode } from 'n8n-workflow'; +import type { INode, ISupplyDataFunctions } from 'n8n-workflow'; import { jsonParse } from 'n8n-workflow'; import type { N8nTool } from '@utils/N8nTool'; @@ -8,8 +8,8 @@ import { ToolHttpRequest } from '../ToolHttpRequest.node'; describe('ToolHttpRequest', () => { const httpTool = new ToolHttpRequest(); - const helpers = mock(); - const executeFunctions = mock({ helpers }); + const helpers = mock(); + const executeFunctions = mock({ helpers }); beforeEach(() => { jest.resetAllMocks(); diff --git a/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/ToolWorkflow.node.ts b/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/ToolWorkflow.node.ts index de7abf6a8b..109a090259 100644 --- a/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/ToolWorkflow.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/ToolWorkflow.node.ts @@ -10,6 +10,7 @@ export class ToolWorkflow extends VersionedNodeType { displayName: 'Call n8n Sub-Workflow Tool', name: 'toolWorkflow', icon: 'fa:network-wired', + iconColor: 'black', group: ['transform'], description: 'Uses another n8n workflow as a tool. Allows packaging any n8n node(s) as a tool.', diff --git a/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v1/versionDescription.ts b/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v1/versionDescription.ts index da7a0e9815..f46ef05c0d 100644 --- a/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v1/versionDescription.ts +++ b/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v1/versionDescription.ts @@ -13,8 +13,6 @@ import { getConnectionHintNoticeField } from '../../../../utils/sharedFields'; export const versionDescription: INodeTypeDescription = { displayName: 'Call n8n Workflow Tool', name: 'toolWorkflow', - icon: 'fa:network-wired', - iconColor: 'black', group: ['transform'], version: [1, 1.1, 1.2, 1.3], description: 'Uses another n8n workflow as a tool. Allows packaging any n8n node(s) as a tool.', diff --git a/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/ToolWorkflowV2.test.ts b/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/ToolWorkflowV2.test.ts index a5aa4e41bc..688000b1ec 100644 --- a/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/ToolWorkflowV2.test.ts +++ b/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/ToolWorkflowV2.test.ts @@ -11,12 +11,8 @@ import type { import { WorkflowToolService } from './utils/WorkflowToolService'; -type ISupplyDataFunctionsWithRunIndex = ISupplyDataFunctions & { runIndex: number }; - // Mock ISupplyDataFunctions interface -function createMockContext( - overrides?: Partial, -): ISupplyDataFunctionsWithRunIndex { +function createMockContext(overrides?: Partial): ISupplyDataFunctions { return { runIndex: 0, getNodeParameter: jest.fn(), @@ -33,6 +29,7 @@ function createMockContext( getTimezone: jest.fn(), getWorkflow: jest.fn(), getWorkflowStaticData: jest.fn(), + cloneWith: jest.fn(), logger: { debug: jest.fn(), error: jest.fn(), @@ -40,11 +37,11 @@ function createMockContext( warn: jest.fn(), }, ...overrides, - } as ISupplyDataFunctionsWithRunIndex; + } as ISupplyDataFunctions; } describe('WorkflowTool::WorkflowToolService', () => { - let context: ISupplyDataFunctionsWithRunIndex; + let context: ISupplyDataFunctions; let service: WorkflowToolService; beforeEach(() => { @@ -92,13 +89,25 @@ describe('WorkflowTool::WorkflowToolService', () => { $execution: { id: 'exec-id' }, $workflow: { id: 'workflow-id' }, } as unknown as IWorkflowDataProxyData); + jest.spyOn(context, 'cloneWith').mockReturnValue(context); const tool = await service.createTool(toolParams); const result = await tool.func('test query'); expect(result).toBe(JSON.stringify(TEST_RESPONSE, null, 2)); expect(context.addOutputData).toHaveBeenCalled(); - expect(context.runIndex).toBe(1); + + // Here we validate that the runIndex is correctly updated + expect(context.cloneWith).toHaveBeenCalledWith({ + runIndex: 0, + inputData: [[{ json: { query: 'test query' } }]], + }); + + await tool.func('another query'); + expect(context.cloneWith).toHaveBeenCalledWith({ + runIndex: 1, + inputData: [[{ json: { query: 'another query' } }]], + }); }); it('should handle errors during tool execution', async () => { @@ -113,6 +122,7 @@ describe('WorkflowTool::WorkflowToolService', () => { .mockRejectedValueOnce(new Error('Workflow execution failed')); jest.spyOn(context, 'addInputData').mockReturnValue({ index: 0 }); jest.spyOn(context, 'getNodeParameter').mockReturnValue('database'); + jest.spyOn(context, 'cloneWith').mockReturnValue(context); const tool = await service.createTool(toolParams); const result = await tool.func('test query'); @@ -166,7 +176,12 @@ describe('WorkflowTool::WorkflowToolService', () => { jest.spyOn(context, 'executeWorkflow').mockResolvedValueOnce(mockResponse); - const result = await service['executeSubWorkflow'](workflowInfo, items, workflowProxyMock); + const result = await service['executeSubWorkflow']( + context, + workflowInfo, + items, + workflowProxyMock, + ); expect(result.response).toBe(TEST_RESPONSE); expect(result.subExecutionId).toBe('test-execution'); @@ -175,7 +190,7 @@ describe('WorkflowTool::WorkflowToolService', () => { it('should throw error when workflow execution fails', async () => { jest.spyOn(context, 'executeWorkflow').mockRejectedValueOnce(new Error('Execution failed')); - await expect(service['executeSubWorkflow']({}, [], {} as never)).rejects.toThrow( + await expect(service['executeSubWorkflow'](context, {}, [], {} as never)).rejects.toThrow( NodeOperationError, ); }); @@ -188,7 +203,7 @@ describe('WorkflowTool::WorkflowToolService', () => { jest.spyOn(context, 'executeWorkflow').mockResolvedValueOnce(mockResponse); - await expect(service['executeSubWorkflow']({}, [], {} as never)).rejects.toThrow(); + await expect(service['executeSubWorkflow'](context, {}, [], {} as never)).rejects.toThrow(); }); }); @@ -202,7 +217,12 @@ describe('WorkflowTool::WorkflowToolService', () => { jest.spyOn(context, 'getNodeParameter').mockReturnValueOnce({ value: 'workflow-id' }); - const result = await service['getSubWorkflowInfo'](source, itemIndex, workflowProxyMock); + const result = await service['getSubWorkflowInfo']( + context, + source, + itemIndex, + workflowProxyMock, + ); expect(result.workflowInfo).toHaveProperty('id', 'workflow-id'); expect(result.subWorkflowId).toBe('workflow-id'); @@ -218,7 +238,12 @@ describe('WorkflowTool::WorkflowToolService', () => { jest.spyOn(context, 'getNodeParameter').mockReturnValueOnce(JSON.stringify(mockWorkflow)); - const result = await service['getSubWorkflowInfo'](source, itemIndex, workflowProxyMock); + const result = await service['getSubWorkflowInfo']( + context, + source, + itemIndex, + workflowProxyMock, + ); expect(result.workflowInfo.code).toEqual(mockWorkflow); expect(result.subWorkflowId).toBe('proxy-id'); @@ -234,7 +259,7 @@ describe('WorkflowTool::WorkflowToolService', () => { jest.spyOn(context, 'getNodeParameter').mockReturnValueOnce('invalid json'); await expect( - service['getSubWorkflowInfo'](source, itemIndex, workflowProxyMock), + service['getSubWorkflowInfo'](context, source, itemIndex, workflowProxyMock), ).rejects.toThrow(NodeOperationError); }); }); diff --git a/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/utils/WorkflowToolService.ts b/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/utils/WorkflowToolService.ts index f3089239fe..8fc366084e 100644 --- a/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/utils/WorkflowToolService.ts +++ b/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/utils/WorkflowToolService.ts @@ -43,8 +43,8 @@ export class WorkflowToolService { // Sub-workflow execution id, will be set after the sub-workflow is executed private subExecutionId: string | undefined; - constructor(private context: ISupplyDataFunctions) { - const subWorkflowInputs = this.context.getNode().parameters + constructor(private baseContext: ISupplyDataFunctions) { + const subWorkflowInputs = this.baseContext.getNode().parameters .workflowInputs as ResourceMapperValue; this.useSchema = (subWorkflowInputs?.schema ?? []).length > 0; } @@ -59,18 +59,23 @@ export class WorkflowToolService { description: string; itemIndex: number; }): Promise { + let runIndex = 0; // Handler for the tool execution, will be called when the tool is executed // This function will execute the sub-workflow and return the response const toolHandler = async ( query: string | IDataObject, runManager?: CallbackManagerForToolRun, ): Promise => { - const { index } = this.context.addInputData(NodeConnectionType.AiTool, [ - [{ json: { query } }], - ]); - + const localRunIndex = runIndex++; + // We need to clone the context here to handle runIndex correctly + // Otherwise the runIndex will be shared between different executions + // Causing incorrect data to be passed to the sub-workflow and via $fromAI + const context = this.baseContext.cloneWith({ + runIndex: localRunIndex, + inputData: [[{ json: { query } }]], + }); try { - const response = await this.runFunction(query, itemIndex, runManager); + const response = await this.runFunction(context, query, itemIndex, runManager); const processedResponse = this.handleToolResponse(response); // Once the sub-workflow is executed, add the output data to the context @@ -87,7 +92,12 @@ export class WorkflowToolService { const json = jsonParse(processedResponse, { fallbackValue: { response: processedResponse }, }); - void this.context.addOutputData(NodeConnectionType.AiTool, index, [[{ json }]], metadata); + void context.addOutputData( + NodeConnectionType.AiTool, + localRunIndex, + [[{ json }]], + metadata, + ); return processedResponse; } catch (error) { @@ -95,11 +105,13 @@ export class WorkflowToolService { const errorResponse = `There was an error: "${executionError.message}"`; const metadata = parseErrorMetadata(error); - void this.context.addOutputData(NodeConnectionType.AiTool, index, executionError, metadata); + void context.addOutputData( + NodeConnectionType.AiTool, + localRunIndex, + executionError, + metadata, + ); return errorResponse; - } finally { - // @ts-expect-error this accesses a private member on the actual implementation to fix https://linear.app/n8n/issue/ADO-3186/bug-workflowtool-v2-always-uses-first-row-of-input-data - this.context.runIndex++; } }; @@ -119,7 +131,7 @@ export class WorkflowToolService { } if (typeof response !== 'string') { - throw new NodeOperationError(this.context.getNode(), 'Wrong output type returned', { + throw new NodeOperationError(this.baseContext.getNode(), 'Wrong output type returned', { description: `The response property should be a string, but it is an ${typeof response}`, }); } @@ -131,6 +143,7 @@ export class WorkflowToolService { * Executes specified sub-workflow with provided inputs */ private async executeSubWorkflow( + context: ISupplyDataFunctions, workflowInfo: IExecuteWorkflowInfo, items: INodeExecutionData[], workflowProxy: IWorkflowDataProxyData, @@ -138,27 +151,22 @@ export class WorkflowToolService { ): Promise<{ response: string; subExecutionId: string }> { let receivedData: ExecuteWorkflowData; try { - receivedData = await this.context.executeWorkflow( - workflowInfo, - items, - runManager?.getChild(), - { - parentExecution: { - executionId: workflowProxy.$execution.id, - workflowId: workflowProxy.$workflow.id, - }, + receivedData = await context.executeWorkflow(workflowInfo, items, runManager?.getChild(), { + parentExecution: { + executionId: workflowProxy.$execution.id, + workflowId: workflowProxy.$workflow.id, }, - ); + }); // Set sub-workflow execution id so it can be used in other places this.subExecutionId = receivedData.executionId; } catch (error) { - throw new NodeOperationError(this.context.getNode(), error as Error); + throw new NodeOperationError(context.getNode(), error as Error); } const response: string | undefined = get(receivedData, 'data[0][0].json') as string | undefined; if (response === undefined) { throw new NodeOperationError( - this.context.getNode(), + context.getNode(), 'There was an error: "The workflow did not return a response"', ); } @@ -171,20 +179,27 @@ export class WorkflowToolService { * This function will be called as part of the tool execution (from the toolHandler) */ private async runFunction( + context: ISupplyDataFunctions, query: string | IDataObject, itemIndex: number, runManager?: CallbackManagerForToolRun, ): Promise { - const source = this.context.getNodeParameter('source', itemIndex) as string; - const workflowProxy = this.context.getWorkflowDataProxy(0); + const source = context.getNodeParameter('source', itemIndex) as string; + const workflowProxy = context.getWorkflowDataProxy(0); - const { workflowInfo } = await this.getSubWorkflowInfo(source, itemIndex, workflowProxy); - const rawData = this.prepareRawData(query, itemIndex); - const items = await this.prepareWorkflowItems(query, itemIndex, rawData); + const { workflowInfo } = await this.getSubWorkflowInfo( + context, + source, + itemIndex, + workflowProxy, + ); + const rawData = this.prepareRawData(context, query, itemIndex); + const items = await this.prepareWorkflowItems(context, query, itemIndex, rawData); this.subWorkflowId = workflowInfo.id; const { response } = await this.executeSubWorkflow( + context, workflowInfo, items, workflowProxy, @@ -197,6 +212,7 @@ export class WorkflowToolService { * Gets the sub-workflow info based on the source (database or parameter) */ private async getSubWorkflowInfo( + context: ISupplyDataFunctions, source: string, itemIndex: number, workflowProxy: IWorkflowDataProxyData, @@ -208,7 +224,7 @@ export class WorkflowToolService { let subWorkflowId: string; if (source === 'database') { - const { value } = this.context.getNodeParameter( + const { value } = context.getNodeParameter( 'workflowId', itemIndex, {}, @@ -216,14 +232,14 @@ export class WorkflowToolService { workflowInfo.id = value as string; subWorkflowId = workflowInfo.id; } else if (source === 'parameter') { - const workflowJson = this.context.getNodeParameter('workflowJson', itemIndex) as string; + const workflowJson = context.getNodeParameter('workflowJson', itemIndex) as string; try { workflowInfo.code = JSON.parse(workflowJson) as IWorkflowBase; // subworkflow is same as parent workflow subWorkflowId = workflowProxy.$workflow.id; } catch (error) { throw new NodeOperationError( - this.context.getNode(), + context.getNode(), `The provided workflow is not valid JSON: "${(error as Error).message}"`, { itemIndex }, ); @@ -233,9 +249,13 @@ export class WorkflowToolService { return { workflowInfo, subWorkflowId: subWorkflowId! }; } - private prepareRawData(query: string | IDataObject, itemIndex: number): IDataObject { + private prepareRawData( + context: ISupplyDataFunctions, + query: string | IDataObject, + itemIndex: number, + ): IDataObject { const rawData: IDataObject = { query }; - const workflowFieldsJson = this.context.getNodeParameter('fields.values', itemIndex, [], { + const workflowFieldsJson = context.getNodeParameter('fields.values', itemIndex, [], { rawExpressions: true, }) as SetField[]; @@ -253,6 +273,7 @@ export class WorkflowToolService { * Prepares the sub-workflow items for execution */ private async prepareWorkflowItems( + context: ISupplyDataFunctions, query: string | IDataObject, itemIndex: number, rawData: IDataObject, @@ -261,17 +282,17 @@ export class WorkflowToolService { let jsonData = typeof query === 'object' ? query : { query }; if (this.useSchema) { - const currentWorkflowInputs = getCurrentWorkflowInputData.call(this.context); + const currentWorkflowInputs = getCurrentWorkflowInputData.call(context); jsonData = currentWorkflowInputs[itemIndex].json; } const newItem = await manual.execute.call( - this.context, + context, { json: jsonData }, itemIndex, options, rawData, - this.context.getNode(), + context.getNode(), ); return [newItem] as INodeExecutionData[]; @@ -299,7 +320,7 @@ export class WorkflowToolService { private async extractFromAIParameters(): Promise { const collectedArguments: FromAIArgument[] = []; - traverseNodeParameters(this.context.getNode().parameters, collectedArguments); + traverseNodeParameters(this.baseContext.getNode().parameters, collectedArguments); const uniqueArgsMap = new Map(); for (const arg of collectedArguments) { diff --git a/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/versionDescription.ts b/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/versionDescription.ts index 6d4275b449..46fb3d1677 100644 --- a/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/versionDescription.ts +++ b/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/versionDescription.ts @@ -7,7 +7,6 @@ import { getConnectionHintNoticeField } from '../../../../utils/sharedFields'; export const versionDescription: INodeTypeDescription = { displayName: 'Call n8n Workflow Tool', name: 'toolWorkflow', - icon: 'fa:network-wired', group: ['transform'], description: 'Uses another n8n workflow as a tool. Allows packaging any n8n node(s) as a tool.', defaults: { diff --git a/packages/@n8n/nodes-langchain/nodes/trigger/ChatTrigger/ChatTrigger.node.ts b/packages/@n8n/nodes-langchain/nodes/trigger/ChatTrigger/ChatTrigger.node.ts index 27fb1bcd35..fab05be8ec 100644 --- a/packages/@n8n/nodes-langchain/nodes/trigger/ChatTrigger/ChatTrigger.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/trigger/ChatTrigger/ChatTrigger.node.ts @@ -12,6 +12,7 @@ import type { INodeProperties, } from 'n8n-workflow'; +import { cssVariables } from './constants'; import { validateAuth } from './GenericFunctions'; import { createPage } from './templates'; import type { LoadPreviousSessionChatOption } from './types'; @@ -378,6 +379,29 @@ export class ChatTrigger extends Node { placeholder: 'e.g. Welcome', description: 'Shown at the top of the chat', }, + { + displayName: 'Custom Chat Styling', + name: 'customCss', + type: 'string', + typeOptions: { + rows: 10, + editor: 'cssEditor', + }, + displayOptions: { + show: { + '/mode': ['hostedChat'], + }, + }, + default: ` +${cssVariables} + +/* You can override any class styles, too. Right-click inspect in Chat UI to find class to override. */ +.chat-message { + max-width: 50%; +} +`.trim(), + description: 'Override default styling of the public chat interface with CSS', + }, ], }, ], @@ -466,6 +490,7 @@ export class ChatTrigger extends Node { title?: string; allowFileUploads?: boolean; allowedFilesMimeTypes?: string; + customCss?: string; }; const req = ctx.getRequestObject(); @@ -517,6 +542,7 @@ export class ChatTrigger extends Node { authentication, allowFileUploads: options.allowFileUploads, allowedFilesMimeTypes: options.allowedFilesMimeTypes, + customCss: options.customCss, }); res.status(200).send(page).end(); diff --git a/packages/@n8n/nodes-langchain/nodes/trigger/ChatTrigger/constants.ts b/packages/@n8n/nodes-langchain/nodes/trigger/ChatTrigger/constants.ts new file mode 100644 index 0000000000..379629a896 --- /dev/null +++ b/packages/@n8n/nodes-langchain/nodes/trigger/ChatTrigger/constants.ts @@ -0,0 +1,122 @@ +// CSS Variables are defined in `@n8n/chat/src/css/_tokens.scss` +export const cssVariables = ` +:root { + /* Colors */ + --chat--color-primary: #e74266; + --chat--color-primary-shade-50: #db4061; + --chat--color-primary-shade-100: #cf3c5c; + --chat--color-secondary: #20b69e; + --chat--color-secondary-shade-50: #1ca08a; + --chat--color-white: #ffffff; + --chat--color-light: #f2f4f8; + --chat--color-light-shade-50: #e6e9f1; + --chat--color-light-shade-100: #c2c5cc; + --chat--color-medium: #d2d4d9; + --chat--color-dark: #101330; + --chat--color-disabled: #777980; + --chat--color-typing: #404040; + + /* Base Layout */ + --chat--spacing: 1rem; + --chat--border-radius: 0.25rem; + --chat--transition-duration: 0.15s; + --chat--font-family: ( + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + Roboto, + Oxygen-Sans, + Ubuntu, + Cantarell, + 'Helvetica Neue', + sans-serif + ); + + /* Window Dimensions */ + --chat--window--width: 400px; + --chat--window--height: 600px; + --chat--window--bottom: var(--chat--spacing); + --chat--window--right: var(--chat--spacing); + --chat--window--z-index: 9999; + --chat--window--border: 1px solid var(--chat--color-light-shade-50); + --chat--window--border-radius: var(--chat--border-radius); + --chat--window--margin-bottom: var(--chat--spacing); + + /* Header Styles */ + --chat--header-height: auto; + --chat--header--padding: var(--chat--spacing); + --chat--header--background: var(--chat--color-dark); + --chat--header--color: var(--chat--color-light); + --chat--header--border-top: none; + --chat--header--border-bottom: none; + --chat--header--border-left: none; + --chat--header--border-right: none; + --chat--heading--font-size: 2em; + --chat--subtitle--font-size: inherit; + --chat--subtitle--line-height: 1.8; + + /* Message Styles */ + --chat--message--font-size: 1rem; + --chat--message--padding: var(--chat--spacing); + --chat--message--border-radius: var(--chat--border-radius); + --chat--message-line-height: 1.5; + --chat--message--margin-bottom: calc(var(--chat--spacing) * 1); + --chat--message--bot--background: var(--chat--color-white); + --chat--message--bot--color: var(--chat--color-dark); + --chat--message--bot--border: none; + --chat--message--user--background: var(--chat--color-secondary); + --chat--message--user--color: var(--chat--color-white); + --chat--message--user--border: none; + --chat--message--pre--background: rgba(0, 0, 0, 0.05); + --chat--messages-list--padding: var(--chat--spacing); + + /* Toggle Button */ + --chat--toggle--size: 64px; + --chat--toggle--width: var(--chat--toggle--size); + --chat--toggle--height: var(--chat--toggle--size); + --chat--toggle--border-radius: 50%; + --chat--toggle--background: var(--chat--color-primary); + --chat--toggle--hover--background: var(--chat--color-primary-shade-50); + --chat--toggle--active--background: var(--chat--color-primary-shade-100); + --chat--toggle--color: var(--chat--color-white); + + /* Input Area */ + --chat--textarea--height: 50px; + --chat--textarea--max-height: 30rem; + --chat--input--font-size: inherit; + --chat--input--border: 0; + --chat--input--border-radius: 0; + --chat--input--padding: 0.8rem; + --chat--input--background: var(--chat--color-white); + --chat--input--text-color: initial; + --chat--input--line-height: 1.5; + --chat--input--placeholder--font-size: var(--chat--input--font-size); + --chat--input--border-active: 0; + --chat--input--left--panel--width: 2rem; + + /* Button Styles */ + --chat--button--color: var(--chat--color-light); + --chat--button--background: var(--chat--color-primary); + --chat--button--padding: calc(var(--chat--spacing) * 1 / 2) var(--chat--spacing); + --chat--button--border-radius: var(--chat--border-radius); + --chat--button--hover--color: var(--chat--color-light); + --chat--button--hover--background: var(--chat--color-primary-shade-50); + --chat--close--button--color-hover: var(--chat--color-primary); + + /* Send and File Buttons */ + --chat--input--send--button--background: var(--chat--color-white); + --chat--input--send--button--color: var(--chat--color-light); + --chat--input--send--button--background-hover: var(--chat--color-primary-shade-50); + --chat--input--send--button--color-hover: var(--chat--color-secondary-shade-50); + --chat--input--file--button--background: var(--chat--color-white); + --chat--input--file--button--color: var(--chat--color-secondary); + --chat--input--file--button--background-hover: var(--chat--input--file--button--background); + --chat--input--file--button--color-hover: var(--chat--color-secondary-shade-50); + --chat--files-spacing: 0.25rem; + + /* Body and Footer */ + --chat--body--background: var(--chat--color-light); + --chat--footer--background: var(--chat--color-light); + --chat--footer--color: var(--chat--color-dark); +} +`; diff --git a/packages/@n8n/nodes-langchain/nodes/trigger/ChatTrigger/templates.ts b/packages/@n8n/nodes-langchain/nodes/trigger/ChatTrigger/templates.ts index 0435df59e8..d224a89361 100644 --- a/packages/@n8n/nodes-langchain/nodes/trigger/ChatTrigger/templates.ts +++ b/packages/@n8n/nodes-langchain/nodes/trigger/ChatTrigger/templates.ts @@ -1,5 +1,6 @@ -import type { AuthenticationChatOption, LoadPreviousSessionChatOption } from './types'; +import sanitizeHtml from 'sanitize-html'; +import type { AuthenticationChatOption, LoadPreviousSessionChatOption } from './types'; export function createPage({ instanceId, webhookUrl, @@ -10,6 +11,7 @@ export function createPage({ authentication, allowFileUploads, allowedFilesMimeTypes, + customCss, }: { instanceId: string; webhookUrl?: string; @@ -23,6 +25,7 @@ export function createPage({ authentication: AuthenticationChatOption; allowFileUploads?: boolean; allowedFilesMimeTypes?: string; + customCss?: string; }) { const validAuthenticationOptions: AuthenticationChatOption[] = [ 'none', @@ -41,6 +44,11 @@ export function createPage({ const sanitizedShowWelcomeScreen = !!showWelcomeScreen; const sanitizedAllowFileUploads = !!allowFileUploads; const sanitizedAllowedFilesMimeTypes = allowedFilesMimeTypes?.toString() ?? ''; + const sanitizedCustomCss = sanitizeHtml(``, { + allowedTags: ['style'], + allowedAttributes: false, + }); + const sanitizedLoadPreviousSession = validLoadPreviousSessionOptions.includes( loadPreviousSession as LoadPreviousSessionChatOption, ) @@ -63,6 +71,7 @@ export function createPage({ height: 100%; } + ${sanitizedCustomCss} - diff --git a/packages/editor-ui/src/components/TestDefinition/EditDefinition/BlockArrow.vue b/packages/editor-ui/src/components/TestDefinition/EditDefinition/BlockArrow.vue deleted file mode 100644 index 48267b3df4..0000000000 --- a/packages/editor-ui/src/components/TestDefinition/EditDefinition/BlockArrow.vue +++ /dev/null @@ -1,40 +0,0 @@ - - - diff --git a/packages/editor-ui/src/components/TestDefinition/EditDefinition/EvaluationStep.vue b/packages/editor-ui/src/components/TestDefinition/EditDefinition/EvaluationStep.vue deleted file mode 100644 index 01ab2188d9..0000000000 --- a/packages/editor-ui/src/components/TestDefinition/EditDefinition/EvaluationStep.vue +++ /dev/null @@ -1,164 +0,0 @@ - - - - - diff --git a/packages/editor-ui/src/components/TestDefinition/EditDefinition/MetricsInput.vue b/packages/editor-ui/src/components/TestDefinition/EditDefinition/MetricsInput.vue deleted file mode 100644 index 0597ac9c58..0000000000 --- a/packages/editor-ui/src/components/TestDefinition/EditDefinition/MetricsInput.vue +++ /dev/null @@ -1,89 +0,0 @@ - - - - - diff --git a/packages/editor-ui/src/components/TestDefinition/EditDefinition/WorkflowSelector.vue b/packages/editor-ui/src/components/TestDefinition/EditDefinition/WorkflowSelector.vue deleted file mode 100644 index 45b1080fa2..0000000000 --- a/packages/editor-ui/src/components/TestDefinition/EditDefinition/WorkflowSelector.vue +++ /dev/null @@ -1,69 +0,0 @@ - - diff --git a/packages/editor-ui/src/components/TestDefinition/EditDefinition/sections/ConfigSection.vue b/packages/editor-ui/src/components/TestDefinition/EditDefinition/sections/ConfigSection.vue deleted file mode 100644 index 36c9fefe3c..0000000000 --- a/packages/editor-ui/src/components/TestDefinition/EditDefinition/sections/ConfigSection.vue +++ /dev/null @@ -1,240 +0,0 @@ - - - - - diff --git a/packages/editor-ui/src/components/TestDefinition/EditDefinition/sections/HeaderSection.vue b/packages/editor-ui/src/components/TestDefinition/EditDefinition/sections/HeaderSection.vue deleted file mode 100644 index 01b46fc216..0000000000 --- a/packages/editor-ui/src/components/TestDefinition/EditDefinition/sections/HeaderSection.vue +++ /dev/null @@ -1,134 +0,0 @@ - - - - - diff --git a/packages/editor-ui/src/components/TestDefinition/EditDefinition/sections/RunsSection.vue b/packages/editor-ui/src/components/TestDefinition/EditDefinition/sections/RunsSection.vue deleted file mode 100644 index 1e1b62330a..0000000000 --- a/packages/editor-ui/src/components/TestDefinition/EditDefinition/sections/RunsSection.vue +++ /dev/null @@ -1,52 +0,0 @@ - - - - - diff --git a/packages/editor-ui/src/components/TestDefinition/ListDefinition/EmptyState.vue b/packages/editor-ui/src/components/TestDefinition/ListDefinition/EmptyState.vue deleted file mode 100644 index d2f022777b..0000000000 --- a/packages/editor-ui/src/components/TestDefinition/ListDefinition/EmptyState.vue +++ /dev/null @@ -1,57 +0,0 @@ - - - - - diff --git a/packages/editor-ui/src/components/TestDefinition/ListDefinition/TestItem.vue b/packages/editor-ui/src/components/TestDefinition/ListDefinition/TestItem.vue deleted file mode 100644 index 0b702ce2f7..0000000000 --- a/packages/editor-ui/src/components/TestDefinition/ListDefinition/TestItem.vue +++ /dev/null @@ -1,139 +0,0 @@ - - - - - diff --git a/packages/editor-ui/src/components/TestDefinition/ListDefinition/TestsList.vue b/packages/editor-ui/src/components/TestDefinition/ListDefinition/TestsList.vue deleted file mode 100644 index 6b615d30f1..0000000000 --- a/packages/editor-ui/src/components/TestDefinition/ListDefinition/TestsList.vue +++ /dev/null @@ -1,43 +0,0 @@ - - - - - diff --git a/packages/editor-ui/src/components/TestDefinition/ListRuns/TestRunsTable.vue b/packages/editor-ui/src/components/TestDefinition/ListRuns/TestRunsTable.vue deleted file mode 100644 index aa8726fce1..0000000000 --- a/packages/editor-ui/src/components/TestDefinition/ListRuns/TestRunsTable.vue +++ /dev/null @@ -1,152 +0,0 @@ - - - - - diff --git a/packages/editor-ui/src/components/TestDefinition/types.ts b/packages/editor-ui/src/components/TestDefinition/types.ts deleted file mode 100644 index 4bc2bc1c3f..0000000000 --- a/packages/editor-ui/src/components/TestDefinition/types.ts +++ /dev/null @@ -1,46 +0,0 @@ -import type { TestMetricRecord, TestRunRecord } from '@/api/testDefinition.ee'; -import type { INodeParameterResourceLocator } from 'n8n-workflow'; - -export interface EditableField { - value: T; - tempValue: T; - isEditing: boolean; -} - -export interface TestItemAction { - icon: string; - id: string; - event: (testId: string) => void | Promise; - tooltip: (testId: string) => string; - disabled?: (testId: string) => boolean; - show?: (testId: string) => boolean; -} - -export interface EditableFormState { - name: EditableField; - tags: EditableField; - description: EditableField; -} - -export interface EvaluationFormState extends EditableFormState { - evaluationWorkflow: INodeParameterResourceLocator; - metrics: TestMetricRecord[]; - mockedNodes: Array<{ name: string; id: string }>; -} - -export interface TestExecution { - lastRun: string | null; - errorRate: number | null; - metrics: Record; - status: TestRunRecord['status']; - id: string | null; -} - -export interface TestListItem { - id: string; - name: string; - tagName: string; - testCases: number; - execution: TestExecution; - fieldsIssues?: Array<{ field: string; message: string }>; -} diff --git a/packages/editor-ui/src/views/TestDefinition/TestDefinitionEditView.vue b/packages/editor-ui/src/views/TestDefinition/TestDefinitionEditView.vue deleted file mode 100644 index d2699dedff..0000000000 --- a/packages/editor-ui/src/views/TestDefinition/TestDefinitionEditView.vue +++ /dev/null @@ -1,296 +0,0 @@ - - - - - diff --git a/packages/editor-ui/src/views/TestDefinition/TestDefinitionListView.vue b/packages/editor-ui/src/views/TestDefinition/TestDefinitionListView.vue deleted file mode 100644 index 5742060df4..0000000000 --- a/packages/editor-ui/src/views/TestDefinition/TestDefinitionListView.vue +++ /dev/null @@ -1,282 +0,0 @@ - - - - diff --git a/packages/editor-ui/src/views/TestDefinition/TestDefinitionRunDetailView.vue b/packages/editor-ui/src/views/TestDefinition/TestDefinitionRunDetailView.vue deleted file mode 100644 index c67297e5e0..0000000000 --- a/packages/editor-ui/src/views/TestDefinition/TestDefinitionRunDetailView.vue +++ /dev/null @@ -1,316 +0,0 @@ - - - - - diff --git a/packages/editor-ui/src/views/TestDefinition/TestDefinitionRunsListView.vue b/packages/editor-ui/src/views/TestDefinition/TestDefinitionRunsListView.vue deleted file mode 100644 index 1f6ac49d96..0000000000 --- a/packages/editor-ui/src/views/TestDefinition/TestDefinitionRunsListView.vue +++ /dev/null @@ -1,151 +0,0 @@ - - - - diff --git a/packages/editor-ui/src/views/TestDefinition/tests/TestDefinitionEditView.test.ts b/packages/editor-ui/src/views/TestDefinition/tests/TestDefinitionEditView.test.ts deleted file mode 100644 index 9824dda7d9..0000000000 --- a/packages/editor-ui/src/views/TestDefinition/tests/TestDefinitionEditView.test.ts +++ /dev/null @@ -1,271 +0,0 @@ -import type { Mock } from 'vitest'; -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { createPinia, setActivePinia } from 'pinia'; -import { createTestingPinia } from '@pinia/testing'; -import { createComponentRenderer } from '@/__tests__/render'; -import TestDefinitionEditView from '@/views/TestDefinition/TestDefinitionEditView.vue'; -import { useRoute, useRouter } from 'vue-router'; -import { useToast } from '@/composables/useToast'; -import { useTestDefinitionForm } from '@/components/TestDefinition/composables/useTestDefinitionForm'; -import { useAnnotationTagsStore } from '@/stores/tags.store'; -import { ref, nextTick } from 'vue'; -import { cleanupAppModals, createAppModals, mockedStore } from '@/__tests__/utils'; -import { VIEWS } from '@/constants'; -import { useTestDefinitionStore } from '@/stores/testDefinition.store.ee'; -import type { TestRunRecord } from '@/api/testDefinition.ee'; - -vi.mock('vue-router'); -vi.mock('@/composables/useToast'); -vi.mock('@/components/TestDefinition/composables/useTestDefinitionForm'); -vi.mock('@/stores/projects.store'); - -describe('TestDefinitionEditView', () => { - const renderComponent = createComponentRenderer(TestDefinitionEditView); - - let createTestMock: Mock; - let updateTestMock: Mock; - let loadTestDataMock: Mock; - let deleteMetricMock: Mock; - let updateMetricsMock: Mock; - let showMessageMock: Mock; - let showErrorMock: Mock; - - const renderComponentWithFeatureEnabled = ({ - testRunsById = {}, - }: { testRunsById?: Record } = {}) => { - const pinia = createTestingPinia(); - setActivePinia(pinia); - - const mockedTestDefinitionStore = mockedStore(useTestDefinitionStore); - mockedTestDefinitionStore.isFeatureEnabled = true; - mockedTestDefinitionStore.testRunsById = testRunsById; - return { ...renderComponent({ pinia }), mockedTestDefinitionStore }; - }; - - beforeEach(() => { - setActivePinia(createPinia()); - createAppModals(); - - // Default route mock: no testId - vi.mocked(useRoute).mockReturnValue({ - params: {}, - name: VIEWS.NEW_TEST_DEFINITION, - } as ReturnType); - - vi.mocked(useRouter).mockReturnValue({ - push: vi.fn(), - replace: vi.fn(), - resolve: vi.fn().mockReturnValue({ href: '/test-href' }), - currentRoute: { value: { params: {} } }, - } as unknown as ReturnType); - - createTestMock = vi.fn().mockResolvedValue({ id: 'newTestId' }); - updateTestMock = vi.fn().mockResolvedValue({}); - loadTestDataMock = vi.fn(); - deleteMetricMock = vi.fn(); - updateMetricsMock = vi.fn(); - showMessageMock = vi.fn(); - showErrorMock = vi.fn(); - // const mockedTestDefinitionStore = mockedStore(useTestDefinitionStore); - - vi.mocked(useToast).mockReturnValue({ - showMessage: showMessageMock, - showError: showErrorMock, - } as unknown as ReturnType); - vi.mocked(useTestDefinitionForm).mockReturnValue({ - state: ref({ - name: { value: '', isEditing: false, tempValue: '' }, - description: { value: '', isEditing: false, tempValue: '' }, - tags: { value: [], tempValue: [], isEditing: false }, - evaluationWorkflow: { mode: 'list', value: '', __rl: true }, - metrics: [], - }), - fieldsIssues: ref([]), - isSaving: ref(false), - loadTestData: loadTestDataMock, - createTest: createTestMock, - updateTest: updateTestMock, - startEditing: vi.fn(), - saveChanges: vi.fn(), - cancelEditing: vi.fn(), - handleKeydown: vi.fn(), - deleteMetric: deleteMetricMock, - updateMetrics: updateMetricsMock, - } as unknown as ReturnType); - - vi.mock('@/stores/projects.store', () => ({ - useProjectsStore: vi.fn().mockReturnValue({ - isTeamProjectFeatureEnabled: false, - currentProject: null, - currentProjectId: null, - }), - })); - }); - - afterEach(() => { - vi.clearAllMocks(); - cleanupAppModals(); - }); - - it('should load test data when testId is provided', async () => { - vi.mocked(useRoute).mockReturnValue({ - params: { testId: '1' }, - name: VIEWS.TEST_DEFINITION_EDIT, - } as unknown as ReturnType); - renderComponentWithFeatureEnabled(); - - mockedStore(useAnnotationTagsStore).fetchAll.mockResolvedValue([]); - - expect(loadTestDataMock).toHaveBeenCalledWith('1'); - }); - - it('should not load test data when testId is not provided', async () => { - // Here route returns no testId - vi.mocked(useRoute).mockReturnValue({ - params: {}, - name: VIEWS.NEW_TEST_DEFINITION, - } as unknown as ReturnType); - renderComponentWithFeatureEnabled(); - - expect(loadTestDataMock).not.toHaveBeenCalled(); - }); - - it('should create a new test and show success message on save if no testId is present', async () => { - vi.mocked(useRoute).mockReturnValue({ - params: {}, - name: VIEWS.NEW_TEST_DEFINITION, - } as ReturnType); - const { getByTestId } = renderComponentWithFeatureEnabled(); - - mockedStore(useAnnotationTagsStore).fetchAll.mockResolvedValue([]); - - await nextTick(); - const saveButton = getByTestId('run-test-button'); - saveButton.click(); - await nextTick(); - - expect(createTestMock).toHaveBeenCalled(); - }); - - it('should show error message on failed test creation', async () => { - createTestMock.mockRejectedValue(new Error('Save failed')); - - vi.mocked(useRoute).mockReturnValue({ - params: {}, - name: VIEWS.NEW_TEST_DEFINITION, - } as unknown as ReturnType); - - const { getByTestId } = renderComponentWithFeatureEnabled(); - - const saveButton = getByTestId('run-test-button'); - saveButton.click(); - await nextTick(); - - expect(createTestMock).toHaveBeenCalled(); - expect(showErrorMock).toHaveBeenCalledWith(expect.any(Error), expect.any(String)); - }); - - it('should display disabled "run test" button when editing test without tags', async () => { - vi.mocked(useRoute).mockReturnValue({ - params: { testId: '1' }, - name: VIEWS.TEST_DEFINITION_EDIT, - } as unknown as ReturnType); - - const { getByTestId, mockedTestDefinitionStore } = renderComponentWithFeatureEnabled(); - - mockedTestDefinitionStore.getFieldIssues = vi - .fn() - .mockReturnValue([{ field: 'tags', message: 'Tag is required' }]); - - await nextTick(); - - const updateButton = getByTestId('run-test-button'); - expect(updateButton.textContent?.toLowerCase()).toContain('run test'); - expect(updateButton).toHaveClass('disabled'); - - mockedTestDefinitionStore.getFieldIssues = vi.fn().mockReturnValue([]); - await nextTick(); - expect(updateButton).not.toHaveClass('disabled'); - }); - - it('should apply "has-issues" class to inputs with issues', async () => { - const { container, mockedTestDefinitionStore } = renderComponentWithFeatureEnabled(); - mockedTestDefinitionStore.getFieldIssues = vi - .fn() - .mockReturnValue([{ field: 'tags', message: 'Tag is required' }]); - await nextTick(); - const issueElements = container.querySelectorAll('.has-issues'); - expect(issueElements.length).toBeGreaterThan(0); - }); - - describe('Test Runs functionality', () => { - it('should display test runs table when runs exist', async () => { - vi.mocked(useRoute).mockReturnValue({ - params: { testId: '1' }, - name: VIEWS.TEST_DEFINITION_EDIT, - } as unknown as ReturnType); - - const { getByTestId } = renderComponentWithFeatureEnabled({ - testRunsById: { - run1: { - id: 'run1', - testDefinitionId: '1', - status: 'completed', - runAt: '2023-01-01', - createdAt: '2023-01-01', - updatedAt: '2023-01-01', - completedAt: '2023-01-01', - }, - run2: { - id: 'run2', - testDefinitionId: '1', - status: 'running', - runAt: '2023-01-02', - createdAt: '2023-01-02', - updatedAt: '2023-01-02', - completedAt: '', - }, - }, - }); - - const runsTable = getByTestId('past-runs-table'); - expect(runsTable).toBeTruthy(); - }); - - it('should not display test runs table when no runs exist', async () => { - const { container } = renderComponentWithFeatureEnabled(); - - const runsTable = container.querySelector('[data-test-id="past-runs-table"]'); - expect(runsTable).toBeFalsy(); - }); - - it('should start a test run when run test button is clicked', async () => { - vi.mocked(useTestDefinitionForm).mockReturnValue({ - ...vi.mocked(useTestDefinitionForm)(), - state: ref({ - name: { value: 'Test', isEditing: false, tempValue: '' }, - description: { value: '', isEditing: false, tempValue: '' }, - tags: { value: ['tag1'], tempValue: [], isEditing: false }, - evaluationWorkflow: { mode: 'list', value: 'workflow1', __rl: true }, - metrics: [], - mockedNodes: [], - }), - } as unknown as ReturnType); - - vi.mocked(useRoute).mockReturnValue({ - params: { testId: '1' }, - name: VIEWS.TEST_DEFINITION_EDIT, - } as unknown as ReturnType); - - const { getByTestId, mockedTestDefinitionStore } = renderComponentWithFeatureEnabled(); - await nextTick(); - - const runButton = getByTestId('run-test-button'); - runButton.click(); - await nextTick(); - - expect(mockedTestDefinitionStore.startTestRun).toHaveBeenCalledWith('1'); - expect(mockedTestDefinitionStore.fetchTestRuns).toHaveBeenCalledWith('1'); - }); - }); -}); diff --git a/packages/editor-ui/src/views/TestDefinition/tests/TestDefinitionListView.test.ts b/packages/editor-ui/src/views/TestDefinition/tests/TestDefinitionListView.test.ts deleted file mode 100644 index c4b427f4bd..0000000000 --- a/packages/editor-ui/src/views/TestDefinition/tests/TestDefinitionListView.test.ts +++ /dev/null @@ -1,177 +0,0 @@ -import type { Mock } from 'vitest'; -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { createPinia, setActivePinia } from 'pinia'; -import { createTestingPinia } from '@pinia/testing'; -import { createComponentRenderer } from '@/__tests__/render'; -import TestDefinitionListView from '@/views/TestDefinition/TestDefinitionListView.vue'; -import { useRoute, useRouter } from 'vue-router'; -import { useToast } from '@/composables/useToast'; -import { useMessage } from '@/composables/useMessage'; -import { useTestDefinitionStore } from '@/stores/testDefinition.store.ee'; -import { nextTick, ref } from 'vue'; -import { mockedStore, waitAllPromises } from '@/__tests__/utils'; -import { MODAL_CONFIRM, VIEWS } from '@/constants'; -import type { TestDefinitionRecord } from '@/api/testDefinition.ee'; - -vi.mock('vue-router'); -vi.mock('@/composables/useToast'); -vi.mock('@/composables/useMessage'); -describe('TestDefinitionListView', () => { - const renderComponent = createComponentRenderer(TestDefinitionListView); - - let showMessageMock: Mock; - let showErrorMock: Mock; - let confirmMock: Mock; - let startTestRunMock: Mock; - let fetchTestRunsMock: Mock; - let deleteByIdMock: Mock; - let fetchAllMock: Mock; - - const mockTestDefinitions: TestDefinitionRecord[] = [ - { - id: '1', - name: 'Test 1', - workflowId: 'workflow1', - updatedAt: '2023-01-01T00:00:00.000Z', - annotationTagId: 'tag1', - }, - { - id: '2', - name: 'Test 2', - workflowId: 'workflow1', - updatedAt: '2023-01-02T00:00:00.000Z', - }, - { - id: '3', - name: 'Test 3', - workflowId: 'workflow1', - updatedAt: '2023-01-03T00:00:00.000Z', - }, - ]; - - beforeEach(() => { - setActivePinia(createPinia()); - - vi.mocked(useRoute).mockReturnValue( - ref({ - params: { name: 'workflow1' }, - name: VIEWS.TEST_DEFINITION, - }) as unknown as ReturnType, - ); - - vi.mocked(useRouter).mockReturnValue({ - push: vi.fn(), - currentRoute: { value: { params: { name: 'workflow1' } } }, - } as unknown as ReturnType); - - showMessageMock = vi.fn(); - showErrorMock = vi.fn(); - confirmMock = vi.fn().mockResolvedValue(MODAL_CONFIRM); - startTestRunMock = vi.fn().mockResolvedValue({ success: true }); - fetchTestRunsMock = vi.fn(); - deleteByIdMock = vi.fn(); - fetchAllMock = vi.fn().mockResolvedValue({ testDefinitions: mockTestDefinitions }); - - vi.mocked(useToast).mockReturnValue({ - showMessage: showMessageMock, - showError: showErrorMock, - } as unknown as ReturnType); - - vi.mocked(useMessage).mockReturnValue({ - confirm: confirmMock, - } as unknown as ReturnType); - }); - - afterEach(() => { - vi.clearAllMocks(); - }); - - const renderComponentWithFeatureEnabled = async ( - { testDefinitions }: { testDefinitions: TestDefinitionRecord[] } = { - testDefinitions: mockTestDefinitions, - }, - ) => { - const pinia = createTestingPinia(); - setActivePinia(pinia); - - const testDefinitionStore = mockedStore(useTestDefinitionStore); - testDefinitionStore.isFeatureEnabled = true; - testDefinitionStore.fetchAll = fetchAllMock; - testDefinitionStore.startTestRun = startTestRunMock; - testDefinitionStore.fetchTestRuns = fetchTestRunsMock; - testDefinitionStore.deleteById = deleteByIdMock; - testDefinitionStore.allTestDefinitionsByWorkflowId = { workflow1: testDefinitions }; - - const component = renderComponent({ pinia }); - await waitAllPromises(); - return { ...component, testDefinitionStore }; - }; - - it('should render empty state when no tests exist', async () => { - const { getByTestId } = await renderComponentWithFeatureEnabled({ testDefinitions: [] }); - - expect(getByTestId('test-definition-empty-state')).toBeTruthy(); - }); - - it('should render tests list when tests exist', async () => { - const { getByTestId } = await renderComponentWithFeatureEnabled(); - - expect(getByTestId('test-definition-list')).toBeTruthy(); - }); - - it('should load initial data on mount', async () => { - const { testDefinitionStore } = await renderComponentWithFeatureEnabled(); - - expect(testDefinitionStore.fetchAll).toHaveBeenCalledWith({ - workflowId: 'workflow1', - }); - }); - - it('should start test run and show success message', async () => { - const { getByTestId } = await renderComponentWithFeatureEnabled(); - - const runButton = getByTestId('run-test-button-1'); - runButton.click(); - await nextTick(); - - expect(startTestRunMock).toHaveBeenCalledWith('1'); - expect(fetchTestRunsMock).toHaveBeenCalledWith('1'); - expect(showMessageMock).toHaveBeenCalledWith({ - title: expect.any(String), - type: 'success', - }); - }); - - it('should show error message on failed test run', async () => { - const { getByTestId, testDefinitionStore } = await renderComponentWithFeatureEnabled(); - testDefinitionStore.startTestRun = vi.fn().mockRejectedValue(new Error('Run failed')); - - const runButton = getByTestId('run-test-button-1'); - runButton.click(); - await nextTick(); - - expect(showErrorMock).toHaveBeenCalledWith(expect.any(Error), expect.any(String)); - }); - - it('should delete test and show success message', async () => { - const { getByTestId } = await renderComponentWithFeatureEnabled(); - const deleteButton = getByTestId('delete-test-button-1'); - deleteButton.click(); - await waitAllPromises(); - - expect(deleteByIdMock).toHaveBeenCalledWith('1'); - expect(showMessageMock).toHaveBeenCalledWith({ - title: expect.any(String), - type: 'success', - }); - }); - - it('should sort tests by updated date in descending order', async () => { - const { container } = await renderComponentWithFeatureEnabled(); - - const testItems = container.querySelectorAll('[data-test-id^="test-item-"]'); - expect(testItems[0].getAttribute('data-test-id')).toBe('test-item-3'); - expect(testItems[1].getAttribute('data-test-id')).toBe('test-item-2'); - expect(testItems[2].getAttribute('data-test-id')).toBe('test-item-1'); - }); -}); diff --git a/packages/editor-ui/src/views/TestDefinition/tests/TestDefinitionRunDetailView.test.ts b/packages/editor-ui/src/views/TestDefinition/tests/TestDefinitionRunDetailView.test.ts deleted file mode 100644 index 63fabd6f5d..0000000000 --- a/packages/editor-ui/src/views/TestDefinition/tests/TestDefinitionRunDetailView.test.ts +++ /dev/null @@ -1,245 +0,0 @@ -import type { Mock } from 'vitest'; -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { createPinia, setActivePinia } from 'pinia'; -import { createTestingPinia } from '@pinia/testing'; -import { createComponentRenderer } from '@/__tests__/render'; -import TestDefinitionRunDetailView from '@/views/TestDefinition/TestDefinitionRunDetailView.vue'; -import { useRoute, useRouter } from 'vue-router'; -import { useToast } from '@/composables/useToast'; -import { useTestDefinitionStore } from '@/stores/testDefinition.store.ee'; -import { useWorkflowsStore } from '@/stores/workflows.store'; -import { nextTick, ref } from 'vue'; -import { mockedStore, waitAllPromises } from '@/__tests__/utils'; -import { VIEWS } from '@/constants'; -import type { TestRunRecord } from '@/api/testDefinition.ee'; -import type { IWorkflowDb } from '@/Interface'; - -vi.mock('vue-router'); -vi.mock('@/composables/useToast'); - -describe('TestDefinitionRunDetailView', () => { - const renderComponent = createComponentRenderer(TestDefinitionRunDetailView); - - let showErrorMock: Mock; - let getTestRunMock: Mock; - let fetchTestCaseExecutionsMock: Mock; - - const mockTestRun: TestRunRecord = { - id: 'run1', - status: 'completed', - runAt: '2023-01-01T00:00:00.000Z', - metrics: { - accuracy: 0.95, - precision: 0.88, - }, - testDefinitionId: 'test1', - createdAt: '2023-01-01T00:00:00.000Z', - updatedAt: '2023-01-01T00:00:00.000Z', - completedAt: '2023-01-01T00:00:00.000Z', - }; - - const mockTestDefinition = { - id: 'test1', - name: 'Test Definition 1', - evaluationWorkflowId: 'workflow1', - workflowId: 'workflow1', - }; - - const mockWorkflow = { - id: 'workflow1', - name: 'Evaluation Workflow', - }; - - const mockTestCaseExecutions = [ - { id: 'exec1', status: 'success' }, - { id: 'exec2', status: 'error' }, - ]; - - beforeEach(() => { - setActivePinia(createPinia()); - - // Mock route with testId and runId - vi.mocked(useRoute).mockReturnValue( - ref({ - params: { testId: 'test1', runId: 'run1' }, - name: VIEWS.TEST_DEFINITION_RUNS, - }) as unknown as ReturnType, - ); - - vi.mocked(useRouter).mockReturnValue({ - back: vi.fn(), - currentRoute: { value: { params: { testId: 'test1', runId: 'run1' } } }, - resolve: vi.fn().mockResolvedValue({ href: 'test-definition-run-detail' }), - } as unknown as ReturnType); - - showErrorMock = vi.fn(); - getTestRunMock = vi.fn().mockResolvedValue(mockTestRun); - fetchTestCaseExecutionsMock = vi.fn().mockResolvedValue(mockTestCaseExecutions); - - vi.mocked(useToast).mockReturnValue({ - showError: showErrorMock, - } as unknown as ReturnType); - }); - - afterEach(() => { - vi.clearAllMocks(); - }); - - it('should load run details on mount', async () => { - const pinia = createTestingPinia(); - setActivePinia(pinia); - - const testDefinitionStore = mockedStore(useTestDefinitionStore); - testDefinitionStore.testRunsById = { run1: mockTestRun }; - testDefinitionStore.testDefinitionsById = { test1: mockTestDefinition }; - testDefinitionStore.getTestRun = getTestRunMock; - - const workflowsStore = mockedStore(useWorkflowsStore); - workflowsStore.workflowsById = { workflow1: mockWorkflow as IWorkflowDb }; - - const { getByTestId } = renderComponent({ pinia }); - await nextTick(); - - expect(getTestRunMock).toHaveBeenCalledWith({ - testDefinitionId: 'test1', - runId: 'run1', - }); - // expect(fetchExecutionsMock).toHaveBeenCalled(); - expect(getByTestId('test-definition-run-detail')).toBeTruthy(); - }); - - it('should display test run metrics', async () => { - const pinia = createTestingPinia(); - setActivePinia(pinia); - - const testDefinitionStore = mockedStore(useTestDefinitionStore); - testDefinitionStore.testRunsById = { run1: mockTestRun }; - testDefinitionStore.testDefinitionsById = { test1: mockTestDefinition }; - testDefinitionStore.getTestRun = getTestRunMock; - - const { container } = renderComponent({ pinia }); - await nextTick(); - - const metricsCards = container.querySelectorAll('.summaryCard'); - expect(metricsCards.length).toBeGreaterThan(0); - expect(container.textContent).toContain('0.95'); // Check for accuracy metric - }); - - it('should handle errors when loading run details', async () => { - const pinia = createTestingPinia(); - setActivePinia(pinia); - - const testDefinitionStore = mockedStore(useTestDefinitionStore); - testDefinitionStore.getTestRun = vi.fn().mockRejectedValue(new Error('Failed to load')); - - renderComponent({ pinia }); - await waitAllPromises(); - - expect(showErrorMock).toHaveBeenCalledWith(expect.any(Error), 'Failed to load run details'); - }); - - it('should navigate back when back button is clicked', async () => { - const pinia = createTestingPinia(); - setActivePinia(pinia); - - const router = useRouter(); - const { getByTestId } = renderComponent({ pinia }); - await nextTick(); - - const backButton = getByTestId('test-definition-run-detail').querySelector('.backButton'); - backButton?.dispatchEvent(new Event('click')); - - expect(router.back).toHaveBeenCalled(); - }); - - // Test loading states - it('should show loading state while fetching data', async () => { - const pinia = createTestingPinia(); - setActivePinia(pinia); - - const testDefinitionStore = mockedStore(useTestDefinitionStore); - testDefinitionStore.getTestRun = vi - .fn() - .mockImplementation(async () => await new Promise(() => {})); // Never resolves - - const { container } = renderComponent({ pinia }); - await nextTick(); - - expect(container.querySelector('.loading')).toBeTruthy(); - }); - - // Test metrics display - it('should correctly format and display all metrics', async () => { - const pinia = createTestingPinia(); - setActivePinia(pinia); - - const testRunWithMultipleMetrics = { - ...mockTestRun, - metrics: { - accuracy: 0.956789, - precision: 0.887654, - recall: 0.923456, - f1_score: 0.901234, - }, - }; - - const testDefinitionStore = mockedStore(useTestDefinitionStore); - testDefinitionStore.testRunsById = { run1: testRunWithMultipleMetrics }; - testDefinitionStore.testDefinitionsById = { test1: mockTestDefinition }; - - const { container } = renderComponent({ pinia }); - await nextTick(); - - // Check if the metrics are displayed correctly with 2 decimal places - expect(container.textContent).toContain('0.96'); - expect(container.textContent).toContain('0.89'); - expect(container.textContent).toContain('0.92'); - expect(container.textContent).toContain('0.90'); - }); - - // Test status display - it('should display correct status with appropriate styling', async () => { - const pinia = createTestingPinia(); - setActivePinia(pinia); - - const testRunWithStatus: TestRunRecord = { - ...mockTestRun, - status: 'error', - }; - - const testDefinitionStore = mockedStore(useTestDefinitionStore); - testDefinitionStore.testRunsById = { run1: testRunWithStatus }; - testDefinitionStore.testDefinitionsById = { test1: mockTestDefinition }; - - const { container } = renderComponent({ pinia }); - await nextTick(); - - const statusElement = container.querySelector('.error'); - expect(statusElement).toBeTruthy(); - expect(statusElement?.textContent?.trim()).toBe('error'); - }); - - // Test table data - it('should correctly populate the test cases table', async () => { - const pinia = createTestingPinia(); - setActivePinia(pinia); - - const testDefinitionStore = mockedStore(useTestDefinitionStore); - - // Mock all required store methods - testDefinitionStore.testRunsById = { run1: mockTestRun }; - testDefinitionStore.testDefinitionsById = { test1: mockTestDefinition }; - testDefinitionStore.getTestRun = getTestRunMock; - // Add this mock for fetchTestDefinition - testDefinitionStore.fetchTestDefinition = vi.fn().mockResolvedValue(mockTestDefinition); - testDefinitionStore.fetchTestCaseExecutions = fetchTestCaseExecutionsMock; - - const { container } = renderComponent({ pinia }); - - // Wait for all promises to resolve - await waitAllPromises(); - - const tableRows = container.querySelectorAll('.el-table__row'); - expect(tableRows.length).toBe(mockTestCaseExecutions.length); - }); -}); diff --git a/packages/editor-ui/src/views/WorkflowsView.vue b/packages/editor-ui/src/views/WorkflowsView.vue deleted file mode 100644 index e67099f78a..0000000000 --- a/packages/editor-ui/src/views/WorkflowsView.vue +++ /dev/null @@ -1,626 +0,0 @@ - - - - - diff --git a/packages/editor-ui/tsconfig.json b/packages/editor-ui/tsconfig.json deleted file mode 100644 index f0b5642a9d..0000000000 --- a/packages/editor-ui/tsconfig.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "extends": "@n8n/frontend-typescript-config", - "compilerOptions": { - "baseUrl": ".", - "rootDirs": [ - ".", - "../../frontend/@n8n/composables/src", - "../design-system/src", - "../@n8n/chat/src" - ], - "outDir": "dist", - "types": [ - "vitest/globals", - "unplugin-icons/types/vue", - "../design-system/src/shims-modules.d.ts" - ], - "paths": { - "@/*": ["./src/*"], - "n8n-design-system*": ["../design-system/src*"], - "@n8n/composables*": ["../frontend/@n8n/composables/src*"], - "@n8n/chat/*": ["../@n8n/chat/src/*"], - "@n8n/api-types*": ["../@n8n/api-types/src*"] - }, - // TODO: remove all options below this line - "useUnknownInCatchVariables": false - }, - "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.vue"], - "exclude": ["src/plugins/codemirror/typescript/worker/**/*.d.ts"] -} diff --git a/packages/@n8n/chat/.eslintignore b/packages/frontend/@n8n/chat/.eslintignore similarity index 100% rename from packages/@n8n/chat/.eslintignore rename to packages/frontend/@n8n/chat/.eslintignore diff --git a/packages/@n8n/chat/.eslintrc.cjs b/packages/frontend/@n8n/chat/.eslintrc.cjs similarity index 53% rename from packages/@n8n/chat/.eslintrc.cjs rename to packages/frontend/@n8n/chat/.eslintrc.cjs index a1ad467ea3..3f9a316c08 100644 --- a/packages/@n8n/chat/.eslintrc.cjs +++ b/packages/frontend/@n8n/chat/.eslintrc.cjs @@ -1,10 +1,10 @@ -const sharedOptions = require('@n8n_io/eslint-config/shared'); +const sharedOptions = require('@n8n/eslint-config/shared'); /** * @type {import('@types/eslint').ESLint.ConfigData} */ module.exports = { - extends: ['@n8n_io/eslint-config/frontend'], + extends: ['@n8n/eslint-config/frontend'], ...sharedOptions(__dirname, 'frontend'), }; diff --git a/packages/@n8n/chat/.gitignore b/packages/frontend/@n8n/chat/.gitignore similarity index 100% rename from packages/@n8n/chat/.gitignore rename to packages/frontend/@n8n/chat/.gitignore diff --git a/packages/@n8n/chat/.np-config.json b/packages/frontend/@n8n/chat/.np-config.json similarity index 100% rename from packages/@n8n/chat/.np-config.json rename to packages/frontend/@n8n/chat/.np-config.json diff --git a/packages/@n8n/chat/.storybook/main.ts b/packages/frontend/@n8n/chat/.storybook/main.ts similarity index 100% rename from packages/@n8n/chat/.storybook/main.ts rename to packages/frontend/@n8n/chat/.storybook/main.ts diff --git a/packages/frontend/@n8n/chat/.storybook/preview.scss b/packages/frontend/@n8n/chat/.storybook/preview.scss new file mode 100644 index 0000000000..318275ade0 --- /dev/null +++ b/packages/frontend/@n8n/chat/.storybook/preview.scss @@ -0,0 +1,7 @@ +html, +body, +#storybook-root, +#n8n-chat { + width: 100%; + height: 100%; +} diff --git a/packages/@n8n/chat/.storybook/preview.ts b/packages/frontend/@n8n/chat/.storybook/preview.ts similarity index 100% rename from packages/@n8n/chat/.storybook/preview.ts rename to packages/frontend/@n8n/chat/.storybook/preview.ts diff --git a/packages/frontend/@n8n/chat/.vscode/extensions.json b/packages/frontend/@n8n/chat/.vscode/extensions.json new file mode 100644 index 0000000000..91f12b27dc --- /dev/null +++ b/packages/frontend/@n8n/chat/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"] +} diff --git a/packages/@n8n/chat/README.md b/packages/frontend/@n8n/chat/README.md similarity index 100% rename from packages/@n8n/chat/README.md rename to packages/frontend/@n8n/chat/README.md diff --git a/packages/@n8n/chat/index.html b/packages/frontend/@n8n/chat/index.html similarity index 100% rename from packages/@n8n/chat/index.html rename to packages/frontend/@n8n/chat/index.html diff --git a/packages/@n8n/chat/package.json b/packages/frontend/@n8n/chat/package.json similarity index 93% rename from packages/@n8n/chat/package.json rename to packages/frontend/@n8n/chat/package.json index f80bf671ce..ed93c63430 100644 --- a/packages/@n8n/chat/package.json +++ b/packages/frontend/@n8n/chat/package.json @@ -46,6 +46,9 @@ "devDependencies": { "@iconify-json/mdi": "^1.1.54", "@n8n/storybook": "workspace:*", + "@n8n/eslint-config": "workspace:*", + "@n8n/typescript-config": "workspace:*", + "@n8n/vitest-config": "workspace:*", "@vitejs/plugin-vue": "catalog:frontend", "@vitest/coverage-v8": "catalog:frontend", "unplugin-icons": "^0.19.0", diff --git a/packages/@n8n/chat/public/favicon.ico b/packages/frontend/@n8n/chat/public/favicon.ico similarity index 100% rename from packages/@n8n/chat/public/favicon.ico rename to packages/frontend/@n8n/chat/public/favicon.ico diff --git a/packages/@n8n/chat/resources/images/fullscreen.png b/packages/frontend/@n8n/chat/resources/images/fullscreen.png similarity index 100% rename from packages/@n8n/chat/resources/images/fullscreen.png rename to packages/frontend/@n8n/chat/resources/images/fullscreen.png diff --git a/packages/@n8n/chat/resources/images/windowed.png b/packages/frontend/@n8n/chat/resources/images/windowed.png similarity index 100% rename from packages/@n8n/chat/resources/images/windowed.png rename to packages/frontend/@n8n/chat/resources/images/windowed.png diff --git a/packages/frontend/@n8n/chat/resources/workflow-manual.json b/packages/frontend/@n8n/chat/resources/workflow-manual.json new file mode 100644 index 0000000000..7aa88a6831 --- /dev/null +++ b/packages/frontend/@n8n/chat/resources/workflow-manual.json @@ -0,0 +1,214 @@ +{ + "name": "Hosted n8n AI Chat Manual", + "nodes": [ + { + "parameters": { + "options": {} + }, + "id": "e6043748-44fc-4019-9301-5690fe26c614", + "name": "OpenAI Chat Model", + "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi", + "typeVersion": 1, + "position": [860, 540], + "credentials": { + "openAiApi": { + "id": "cIIkOhl7tUX1KsL6", + "name": "OpenAi account" + } + } + }, + { + "parameters": { + "sessionKey": "={{ $json.sessionId }}" + }, + "id": "0a68a59a-8ab6-4fa5-a1ea-b7f99a93109b", + "name": "Simple Memory", + "type": "@n8n/n8n-nodes-langchain.memoryBufferWindow", + "typeVersion": 1, + "position": [640, 540] + }, + { + "parameters": { + "text": "={{ $json.chatInput }}", + "options": {} + }, + "id": "3d4e0fbf-d761-4569-b02e-f5c1eeb830c8", + "name": "AI Agent", + "type": "@n8n/n8n-nodes-langchain.agent", + "typeVersion": 1.1, + "position": [840, 300] + }, + { + "parameters": { + "dataType": "string", + "value1": "={{ $json.action }}", + "rules": { + "rules": [ + { + "value2": "loadPreviousSession", + "outputKey": "loadPreviousSession" + }, + { + "value2": "sendMessage", + "outputKey": "sendMessage" + } + ] + } + }, + "id": "84213c7b-abc7-4f40-9567-cd3484a4ae6b", + "name": "Switch", + "type": "n8n-nodes-base.switch", + "typeVersion": 2, + "position": [300, 280] + }, + { + "parameters": { + "simplifyOutput": false + }, + "id": "3be7f076-98ed-472a-80b6-bf8d9538ac87", + "name": "Chat Messages Retriever", + "type": "@n8n/n8n-nodes-langchain.memoryChatRetriever", + "typeVersion": 1, + "position": [620, 140] + }, + { + "parameters": { + "options": {} + }, + "id": "3417c644-8a91-4524-974a-45b4a46d0e2e", + "name": "Respond to Webhook", + "type": "n8n-nodes-base.respondToWebhook", + "typeVersion": 1, + "position": [1240, 140] + }, + { + "parameters": { + "public": true, + "authentication": "n8nUserAuth", + "options": { + "loadPreviousSession": "manually", + "responseMode": "responseNode" + } + }, + "id": "1b30c239-a819-45b4-b0ae-bdd5b92a5424", + "name": "Chat Trigger", + "type": "@n8n/n8n-nodes-langchain.chatTrigger", + "typeVersion": 1, + "position": [80, 280], + "webhookId": "ed3dea26-7d68-42b3-9032-98fe967d441d" + }, + { + "parameters": { + "aggregate": "aggregateAllItemData", + "options": {} + }, + "id": "79672cf0-686b-41eb-90ae-fd31b6da837d", + "name": "Aggregate", + "type": "n8n-nodes-base.aggregate", + "typeVersion": 1, + "position": [1000, 140] + } + ], + "pinData": {}, + "connections": { + "OpenAI Chat Model": { + "ai_languageModel": [ + [ + { + "node": "AI Agent", + "type": "ai_languageModel", + "index": 0 + } + ] + ] + }, + "Simple Memory": { + "ai_memory": [ + [ + { + "node": "AI Agent", + "type": "ai_memory", + "index": 0 + }, + { + "node": "Chat Messages Retriever", + "type": "ai_memory", + "index": 0 + } + ] + ] + }, + "Switch": { + "main": [ + [ + { + "node": "Chat Messages Retriever", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "AI Agent", + "type": "main", + "index": 0 + } + ] + ] + }, + "Chat Messages Retriever": { + "main": [ + [ + { + "node": "Aggregate", + "type": "main", + "index": 0 + } + ] + ] + }, + "AI Agent": { + "main": [ + [ + { + "node": "Respond to Webhook", + "type": "main", + "index": 0 + } + ] + ] + }, + "Chat Trigger": { + "main": [ + [ + { + "node": "Switch", + "type": "main", + "index": 0 + } + ] + ] + }, + "Aggregate": { + "main": [ + [ + { + "node": "Respond to Webhook", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": true, + "settings": { + "executionOrder": "v1" + }, + "versionId": "425c0efe-3aa0-4e0e-8c06-abe12234b1fd", + "id": "1569HF92Y02EUtsU", + "meta": { + "instanceId": "374b43d8b8d6299cc777811a4ad220fc688ee2d54a308cfb0de4450a5233ca9e" + }, + "tags": [] +} diff --git a/packages/@n8n/chat/resources/workflow.json b/packages/frontend/@n8n/chat/resources/workflow.json similarity index 89% rename from packages/@n8n/chat/resources/workflow.json rename to packages/frontend/@n8n/chat/resources/workflow.json index 1bf4be681b..83e1e81285 100644 --- a/packages/@n8n/chat/resources/workflow.json +++ b/packages/frontend/@n8n/chat/resources/workflow.json @@ -9,10 +9,7 @@ "name": "OpenAI Chat Model", "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi", "typeVersion": 1, - "position": [ - 640, - 540 - ], + "position": [640, 540], "credentials": { "openAiApi": { "id": "cIIkOhl7tUX1KsL6", @@ -25,13 +22,10 @@ "sessionKey": "={{ $json.sessionId }}" }, "id": "b416df7b-4802-462f-8f74-f0a71dc4c0be", - "name": "Window Buffer Memory", + "name": "Simple Memory", "type": "@n8n/n8n-nodes-langchain.memoryBufferWindow", "typeVersion": 1, - "position": [ - 340, - 540 - ] + "position": [340, 540] }, { "parameters": { @@ -42,10 +36,7 @@ "name": "AI Agent", "type": "@n8n/n8n-nodes-langchain.agent", "typeVersion": 1.1, - "position": [ - 620, - 300 - ] + "position": [620, 300] }, { "parameters": { @@ -58,10 +49,7 @@ "name": "Chat Trigger", "type": "@n8n/n8n-nodes-langchain.chatTrigger", "typeVersion": 1, - "position": [ - 340, - 300 - ], + "position": [340, 300], "webhookId": "f406671e-c954-4691-b39a-66c90aa2f103" } ], @@ -78,7 +66,7 @@ ] ] }, - "Window Buffer Memory": { + "Simple Memory": { "ai_memory": [ [ { diff --git a/packages/@n8n/chat/src/App.vue b/packages/frontend/@n8n/chat/src/App.vue similarity index 100% rename from packages/@n8n/chat/src/App.vue rename to packages/frontend/@n8n/chat/src/App.vue diff --git a/packages/@n8n/chat/src/__stories__/App.stories.ts b/packages/frontend/@n8n/chat/src/__stories__/App.stories.ts similarity index 100% rename from packages/@n8n/chat/src/__stories__/App.stories.ts rename to packages/frontend/@n8n/chat/src/__stories__/App.stories.ts diff --git a/packages/@n8n/chat/src/__tests__/index.spec.ts b/packages/frontend/@n8n/chat/src/__tests__/index.spec.ts similarity index 100% rename from packages/@n8n/chat/src/__tests__/index.spec.ts rename to packages/frontend/@n8n/chat/src/__tests__/index.spec.ts diff --git a/packages/@n8n/chat/src/__tests__/setup.ts b/packages/frontend/@n8n/chat/src/__tests__/setup.ts similarity index 100% rename from packages/@n8n/chat/src/__tests__/setup.ts rename to packages/frontend/@n8n/chat/src/__tests__/setup.ts diff --git a/packages/@n8n/chat/src/__tests__/utils/create.ts b/packages/frontend/@n8n/chat/src/__tests__/utils/create.ts similarity index 100% rename from packages/@n8n/chat/src/__tests__/utils/create.ts rename to packages/frontend/@n8n/chat/src/__tests__/utils/create.ts diff --git a/packages/@n8n/chat/src/__tests__/utils/fetch.ts b/packages/frontend/@n8n/chat/src/__tests__/utils/fetch.ts similarity index 100% rename from packages/@n8n/chat/src/__tests__/utils/fetch.ts rename to packages/frontend/@n8n/chat/src/__tests__/utils/fetch.ts diff --git a/packages/@n8n/chat/src/__tests__/utils/index.ts b/packages/frontend/@n8n/chat/src/__tests__/utils/index.ts similarity index 100% rename from packages/@n8n/chat/src/__tests__/utils/index.ts rename to packages/frontend/@n8n/chat/src/__tests__/utils/index.ts diff --git a/packages/@n8n/chat/src/__tests__/utils/selectors.ts b/packages/frontend/@n8n/chat/src/__tests__/utils/selectors.ts similarity index 100% rename from packages/@n8n/chat/src/__tests__/utils/selectors.ts rename to packages/frontend/@n8n/chat/src/__tests__/utils/selectors.ts diff --git a/packages/@n8n/chat/src/api/generic.ts b/packages/frontend/@n8n/chat/src/api/generic.ts similarity index 100% rename from packages/@n8n/chat/src/api/generic.ts rename to packages/frontend/@n8n/chat/src/api/generic.ts diff --git a/packages/@n8n/chat/src/api/index.ts b/packages/frontend/@n8n/chat/src/api/index.ts similarity index 100% rename from packages/@n8n/chat/src/api/index.ts rename to packages/frontend/@n8n/chat/src/api/index.ts diff --git a/packages/@n8n/chat/src/api/message.ts b/packages/frontend/@n8n/chat/src/api/message.ts similarity index 100% rename from packages/@n8n/chat/src/api/message.ts rename to packages/frontend/@n8n/chat/src/api/message.ts diff --git a/packages/@n8n/chat/src/components/Button.vue b/packages/frontend/@n8n/chat/src/components/Button.vue similarity index 59% rename from packages/@n8n/chat/src/components/Button.vue rename to packages/frontend/@n8n/chat/src/components/Button.vue index ca35153fc6..fe590facad 100644 --- a/packages/@n8n/chat/src/components/Button.vue +++ b/packages/frontend/@n8n/chat/src/components/Button.vue @@ -9,13 +9,13 @@ text-align: center; vertical-align: middle; user-select: none; - color: var(--chat--button--color, var(--chat--color-light)); - background-color: var(--chat--button--background, var(--chat--color-primary)); + color: var(--chat--button--color); + background-color: var(--chat--button--background); border: 1px solid transparent; - padding: var(--chat--button--padding, calc(var(--chat--spacing) * 1 / 2) var(--chat--spacing)); + padding: var(--chat--button--padding); font-size: 1rem; line-height: 1.5; - border-radius: var(--chat--button--border-radius, var(--chat--border-radius)); + border-radius: var(--chat--button--border-radius); transition: color var(--chat--transition-duration) ease-in-out, background-color var(--chat--transition-duration) ease-in-out, @@ -24,8 +24,8 @@ cursor: pointer; &:hover { - color: var(--chat--button--hover--color, var(--chat--color-light)); - background-color: var(--chat--button--hover--background, var(--chat--color-primary-shade-50)); + color: var(--chat--button--hover--color); + background-color: var(--chat--button--hover--background); text-decoration: none; } diff --git a/packages/@n8n/chat/src/components/Chat.vue b/packages/frontend/@n8n/chat/src/components/Chat.vue similarity index 100% rename from packages/@n8n/chat/src/components/Chat.vue rename to packages/frontend/@n8n/chat/src/components/Chat.vue diff --git a/packages/@n8n/chat/src/components/ChatFile.vue b/packages/frontend/@n8n/chat/src/components/ChatFile.vue similarity index 100% rename from packages/@n8n/chat/src/components/ChatFile.vue rename to packages/frontend/@n8n/chat/src/components/ChatFile.vue diff --git a/packages/@n8n/chat/src/components/ChatWindow.vue b/packages/frontend/@n8n/chat/src/components/ChatWindow.vue similarity index 90% rename from packages/@n8n/chat/src/components/ChatWindow.vue rename to packages/frontend/@n8n/chat/src/components/ChatWindow.vue index bd5ddbbb5b..39a3a092d4 100644 --- a/packages/@n8n/chat/src/components/ChatWindow.vue +++ b/packages/frontend/@n8n/chat/src/components/ChatWindow.vue @@ -40,9 +40,9 @@ function toggle() { position: fixed; display: flex; flex-direction: column; - bottom: var(--chat--window--bottom, var(--chat--spacing)); - right: var(--chat--window--right, var(--chat--spacing)); - z-index: var(--chat--window--z-index, 9999); + bottom: var(--chat--window--bottom); + right: var(--chat--window--right); + z-index: var(--chat--window--z-index); max-width: calc(100% - var(--chat--window--right, var(--chat--spacing)) * 2); max-height: calc(100% - var(--chat--window--bottom, var(--chat--spacing)) * 2); @@ -71,8 +71,8 @@ function toggle() { background: var(--chat--toggle--background); color: var(--chat--toggle--color); cursor: pointer; - width: var(--chat--toggle--width, var(--chat--toggle--size)); - height: var(--chat--toggle--height, var(--chat--toggle--size)); + width: var(--chat--toggle--width); + height: var(--chat--toggle--height); border-radius: var(--chat--toggle--border-radius, 50%); display: inline-flex; align-items: center; diff --git a/packages/@n8n/chat/src/components/GetStarted.vue b/packages/frontend/@n8n/chat/src/components/GetStarted.vue similarity index 100% rename from packages/@n8n/chat/src/components/GetStarted.vue rename to packages/frontend/@n8n/chat/src/components/GetStarted.vue diff --git a/packages/@n8n/chat/src/components/GetStartedFooter.vue b/packages/frontend/@n8n/chat/src/components/GetStartedFooter.vue similarity index 100% rename from packages/@n8n/chat/src/components/GetStartedFooter.vue rename to packages/frontend/@n8n/chat/src/components/GetStartedFooter.vue diff --git a/packages/@n8n/chat/src/components/Input.vue b/packages/frontend/@n8n/chat/src/components/Input.vue similarity index 91% rename from packages/@n8n/chat/src/components/Input.vue rename to packages/frontend/@n8n/chat/src/components/Input.vue index 1b9e0b9608..428787950c 100644 --- a/packages/@n8n/chat/src/components/Input.vue +++ b/packages/frontend/@n8n/chat/src/components/Input.vue @@ -258,13 +258,13 @@ function adjustHeight(event: Event) { textarea { font-family: inherit; - font-size: var(--chat--input--font-size, inherit); + font-size: var(--chat--input--font-size); width: 100%; border: var(--chat--input--border, 0); - border-radius: var(--chat--input--border-radius, 0); - padding: var(--chat--input--padding, 0.8rem); + border-radius: var(--chat--input--border-radius); + padding: var(--chat--input--padding); min-height: var(--chat--textarea--height, 2.5rem); // Set a smaller initial height - max-height: var(--chat--textarea--max-height, 30rem); + max-height: var(--chat--textarea--max-height); height: var(--chat--textarea--height, 2.5rem); // Set initial height same as min-height resize: none; overflow-y: auto; @@ -274,7 +274,7 @@ function adjustHeight(event: Event) { line-height: var(--chat--input--line-height, 1.5); &::placeholder { - font-size: var(--chat--input--placeholder--font-size, var(--chat--input--font-size, inherit)); + font-size: var(--chat--input--placeholder--font-size, var(--chat--input--font-size)); } &:focus, &:hover { @@ -315,20 +315,17 @@ function adjustHeight(event: Event) { --chat--input--send--button--background-hover, var(--chat--input--send--button--background) ); - color: var(--chat--input--send--button--color-hover, var(--chat--color-secondary-shade-50)); + color: var(--chat--input--send--button--color-hover); } } } .chat-input-file-button { background: var(--chat--input--file--button--background, white); - color: var(--chat--input--file--button--color, var(--chat--color-secondary)); + color: var(--chat--input--file--button--color); &:hover { - background: var( - --chat--input--file--button--background-hover, - var(--chat--input--file--button--background) - ); - color: var(--chat--input--file--button--color-hover, var(--chat--color-secondary-shade-50)); + background: var(--chat--input--file--button--background-hover); + color: var(--chat--input--file--button--color-hover); } } @@ -340,11 +337,11 @@ function adjustHeight(event: Event) { flex-direction: row; flex-wrap: wrap; gap: 0.5rem; - padding: var(--chat--files-spacing, 0.25rem); + padding: var(--chat--files-spacing); } .chat-input-left-panel { - width: var(--chat--input--left--panel--width, 2rem); + width: var(--chat--input--left--panel--width); margin-left: 0.4rem; } diff --git a/packages/@n8n/chat/src/components/Layout.vue b/packages/frontend/@n8n/chat/src/components/Layout.vue similarity index 57% rename from packages/@n8n/chat/src/components/Layout.vue rename to packages/frontend/@n8n/chat/src/components/Layout.vue index 3c1546e8d4..d1226146c2 100644 --- a/packages/@n8n/chat/src/components/Layout.vue +++ b/packages/frontend/@n8n/chat/src/components/Layout.vue @@ -43,46 +43,33 @@ onBeforeUnmount(() => { display: flex; overflow-y: auto; flex-direction: column; - font-family: var( - --chat--font-family, - ( - -apple-system, - BlinkMacSystemFont, - 'Segoe UI', - Roboto, - Oxygen-Sans, - Ubuntu, - Cantarell, - 'Helvetica Neue', - sans-serif - ) - ); + font-family: var(--chat--font-family); .chat-header { display: flex; flex-direction: column; justify-content: center; gap: 1em; - height: var(--chat--header-height, auto); - padding: var(--chat--header--padding, var(--chat--spacing)); - background: var(--chat--header--background, var(--chat--color-dark)); - color: var(--chat--header--color, var(--chat--color-light)); - border-top: var(--chat--header--border-top, none); - border-bottom: var(--chat--header--border-bottom, none); - border-left: var(--chat--header--border-left, none); - border-right: var(--chat--header--border-right, none); + height: var(--chat--header-height); + padding: var(--chat--header--padding); + background: var(--chat--header--background); + color: var(--chat--header--color); + border-top: var(--chat--header--border-top); + border-bottom: var(--chat--header--border-bottom); + border-left: var(--chat--header--border-left); + border-right: var(--chat--header--border-right); h1 { font-size: var(--chat--heading--font-size); - color: var(--chat--header--color, var(--chat--color-light)); + color: var(--chat--header--color); } p { - font-size: var(--chat--subtitle--font-size, inherit); - line-height: var(--chat--subtitle--line-height, 1.8); + font-size: var(--chat--subtitle--font-size); + line-height: var(--chat--subtitle--line-height); } } .chat-body { - background: var(--chat--body--background, var(--chat--color-light)); + background: var(--chat--body--background); flex: 1; display: flex; flex-direction: column; @@ -93,8 +80,8 @@ onBeforeUnmount(() => { .chat-footer { border-top: 1px solid var(--chat--color-light-shade-100); - background: var(--chat--footer--background, var(--chat--color-light)); - color: var(--chat--footer--color, var(--chat--color-dark)); + background: var(--chat--footer--background); + color: var(--chat--footer--color); } } diff --git a/packages/@n8n/chat/src/components/Message.vue b/packages/frontend/@n8n/chat/src/components/Message.vue similarity index 92% rename from packages/@n8n/chat/src/components/Message.vue rename to packages/frontend/@n8n/chat/src/components/Message.vue index 4ee4514632..0cecd78cfc 100644 --- a/packages/@n8n/chat/src/components/Message.vue +++ b/packages/frontend/@n8n/chat/src/components/Message.vue @@ -133,9 +133,9 @@ onMounted(async () => { display: block; position: relative; max-width: fit-content; - font-size: var(--chat--message--font-size, 1rem); - padding: var(--chat--message--padding, var(--chat--spacing)); - border-radius: var(--chat--message--border-radius, var(--chat--border-radius)); + font-size: var(--chat--message--font-size); + padding: var(--chat--message--padding); + border-radius: var(--chat--message--border-radius); scroll-margin: 3rem; .chat-message-actions { @@ -160,13 +160,13 @@ onMounted(async () => { } p { - line-height: var(--chat--message-line-height, 1.5); + line-height: var(--chat--message-line-height); word-wrap: break-word; } // Default message gap is half of the spacing + .chat-message { - margin-top: var(--chat--message--margin-bottom, calc(var(--chat--spacing) * 1)); + margin-top: var(--chat--message--margin-bottom); } // Spacing between messages from different senders is double the individual message gap @@ -178,7 +178,7 @@ onMounted(async () => { &.chat-message-from-bot { &:not(.chat-message-transparent) { background-color: var(--chat--message--bot--background); - border: var(--chat--message--bot--border, none); + border: var(--chat--message--bot--border); } color: var(--chat--message--bot--color); border-bottom-left-radius: 0; @@ -187,7 +187,7 @@ onMounted(async () => { &.chat-message-from-user { &:not(.chat-message-transparent) { background-color: var(--chat--message--user--background); - border: var(--chat--message--user--border, none); + border: var(--chat--message--user--border); } color: var(--chat--message--user--color); margin-left: auto; diff --git a/packages/@n8n/chat/src/components/MessageTyping.vue b/packages/frontend/@n8n/chat/src/components/MessageTyping.vue similarity index 100% rename from packages/@n8n/chat/src/components/MessageTyping.vue rename to packages/frontend/@n8n/chat/src/components/MessageTyping.vue diff --git a/packages/@n8n/chat/src/components/MessagesList.vue b/packages/frontend/@n8n/chat/src/components/MessagesList.vue similarity index 95% rename from packages/@n8n/chat/src/components/MessagesList.vue rename to packages/frontend/@n8n/chat/src/components/MessagesList.vue index 1a0cc31874..215e1baffe 100644 --- a/packages/@n8n/chat/src/components/MessagesList.vue +++ b/packages/frontend/@n8n/chat/src/components/MessagesList.vue @@ -51,6 +51,6 @@ watch( .chat-messages-list { margin-top: auto; display: block; - padding: var(--chat--messages-list--padding, var(--chat--spacing)); + padding: var(--chat--messages-list--padding); } diff --git a/packages/@n8n/chat/src/components/PoweredBy.vue b/packages/frontend/@n8n/chat/src/components/PoweredBy.vue similarity index 100% rename from packages/@n8n/chat/src/components/PoweredBy.vue rename to packages/frontend/@n8n/chat/src/components/PoweredBy.vue diff --git a/packages/@n8n/chat/src/components/index.ts b/packages/frontend/@n8n/chat/src/components/index.ts similarity index 100% rename from packages/@n8n/chat/src/components/index.ts rename to packages/frontend/@n8n/chat/src/components/index.ts diff --git a/packages/@n8n/chat/src/composables/index.ts b/packages/frontend/@n8n/chat/src/composables/index.ts similarity index 100% rename from packages/@n8n/chat/src/composables/index.ts rename to packages/frontend/@n8n/chat/src/composables/index.ts diff --git a/packages/@n8n/chat/src/composables/useChat.ts b/packages/frontend/@n8n/chat/src/composables/useChat.ts similarity index 100% rename from packages/@n8n/chat/src/composables/useChat.ts rename to packages/frontend/@n8n/chat/src/composables/useChat.ts diff --git a/packages/@n8n/chat/src/composables/useI18n.ts b/packages/frontend/@n8n/chat/src/composables/useI18n.ts similarity index 100% rename from packages/@n8n/chat/src/composables/useI18n.ts rename to packages/frontend/@n8n/chat/src/composables/useI18n.ts diff --git a/packages/@n8n/chat/src/composables/useOptions.ts b/packages/frontend/@n8n/chat/src/composables/useOptions.ts similarity index 100% rename from packages/@n8n/chat/src/composables/useOptions.ts rename to packages/frontend/@n8n/chat/src/composables/useOptions.ts diff --git a/packages/@n8n/chat/src/constants/defaults.ts b/packages/frontend/@n8n/chat/src/constants/defaults.ts similarity index 100% rename from packages/@n8n/chat/src/constants/defaults.ts rename to packages/frontend/@n8n/chat/src/constants/defaults.ts diff --git a/packages/@n8n/chat/src/constants/index.ts b/packages/frontend/@n8n/chat/src/constants/index.ts similarity index 100% rename from packages/@n8n/chat/src/constants/index.ts rename to packages/frontend/@n8n/chat/src/constants/index.ts diff --git a/packages/@n8n/chat/src/constants/localStorage.ts b/packages/frontend/@n8n/chat/src/constants/localStorage.ts similarity index 100% rename from packages/@n8n/chat/src/constants/localStorage.ts rename to packages/frontend/@n8n/chat/src/constants/localStorage.ts diff --git a/packages/@n8n/chat/src/constants/symbols.ts b/packages/frontend/@n8n/chat/src/constants/symbols.ts similarity index 100% rename from packages/@n8n/chat/src/constants/symbols.ts rename to packages/frontend/@n8n/chat/src/constants/symbols.ts diff --git a/packages/frontend/@n8n/chat/src/css/_tokens.scss b/packages/frontend/@n8n/chat/src/css/_tokens.scss new file mode 100644 index 0000000000..d5357ee863 --- /dev/null +++ b/packages/frontend/@n8n/chat/src/css/_tokens.scss @@ -0,0 +1,119 @@ +:root { + /* Colors */ + --chat--color-primary: #e74266; + --chat--color-primary-shade-50: #db4061; + --chat--color-primary-shade-100: #cf3c5c; + --chat--color-secondary: #20b69e; + --chat--color-secondary-shade-50: #1ca08a; + --chat--color-white: #ffffff; + --chat--color-light: #f2f4f8; + --chat--color-light-shade-50: #e6e9f1; + --chat--color-light-shade-100: #c2c5cc; + --chat--color-medium: #d2d4d9; + --chat--color-dark: #101330; + --chat--color-disabled: #777980; + --chat--color-typing: #404040; + + /* Base Layout */ + --chat--spacing: 1rem; + --chat--border-radius: 0.25rem; + --chat--transition-duration: 0.15s; + --chat--font-family: ( + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + Roboto, + Oxygen-Sans, + Ubuntu, + Cantarell, + 'Helvetica Neue', + sans-serif + ); + + /* Window Dimensions */ + --chat--window--width: 400px; + --chat--window--height: 600px; + --chat--window--bottom: var(--chat--spacing); + --chat--window--right: var(--chat--spacing); + --chat--window--z-index: 9999; + --chat--window--border: 1px solid var(--chat--color-light-shade-50); + --chat--window--border-radius: var(--chat--border-radius); + --chat--window--margin-bottom: var(--chat--spacing); + + /* Header Styles */ + --chat--header-height: auto; + --chat--header--padding: var(--chat--spacing); + --chat--header--background: var(--chat--color-dark); + --chat--header--color: var(--chat--color-light); + --chat--header--border-top: none; + --chat--header--border-bottom: none; + --chat--header--border-left: none; + --chat--header--border-right: none; + --chat--heading--font-size: 2em; + --chat--subtitle--font-size: inherit; + --chat--subtitle--line-height: 1.8; + + /* Message Styles */ + --chat--message--font-size: 1rem; + --chat--message--padding: var(--chat--spacing); + --chat--message--border-radius: var(--chat--border-radius); + --chat--message-line-height: 1.5; + --chat--message--margin-bottom: calc(var(--chat--spacing) * 1); + --chat--message--bot--background: var(--chat--color-white); + --chat--message--bot--color: var(--chat--color-dark); + --chat--message--bot--border: none; + --chat--message--user--background: var(--chat--color-secondary); + --chat--message--user--color: var(--chat--color-white); + --chat--message--user--border: none; + --chat--message--pre--background: rgba(0, 0, 0, 0.05); + --chat--messages-list--padding: var(--chat--spacing); + + /* Toggle Button */ + --chat--toggle--size: 64px; + --chat--toggle--width: var(--chat--toggle--size); + --chat--toggle--height: var(--chat--toggle--size); + --chat--toggle--border-radius: 50%; + --chat--toggle--background: var(--chat--color-primary); + --chat--toggle--hover--background: var(--chat--color-primary-shade-50); + --chat--toggle--active--background: var(--chat--color-primary-shade-100); + --chat--toggle--color: var(--chat--color-white); + + /* Input Area */ + --chat--textarea--height: 50px; + --chat--textarea--max-height: 30rem; + --chat--input--font-size: inherit; + --chat--input--border: 0; + --chat--input--border-radius: 0; + --chat--input--padding: 0.8rem; + --chat--input--background: var(--chat--color-white); + --chat--input--text-color: initial; + --chat--input--line-height: 1.5; + --chat--input--placeholder--font-size: var(--chat--input--font-size); + --chat--input--border-active: 0; + --chat--input--left--panel--width: 2rem; + + /* Button Styles */ + --chat--button--color: var(--chat--color-light); + --chat--button--background: var(--chat--color-primary); + --chat--button--padding: calc(var(--chat--spacing) * 1 / 2) var(--chat--spacing); + --chat--button--border-radius: var(--chat--border-radius); + --chat--button--hover--color: var(--chat--color-light); + --chat--button--hover--background: var(--chat--color-primary-shade-50); + --chat--close--button--color-hover: var(--chat--color-primary); + + /* Send and File Buttons */ + --chat--input--send--button--background: var(--chat--color-white); + --chat--input--send--button--color: var(--chat--color-light); + --chat--input--send--button--background-hover: var(--chat--color-primary-shade-50); + --chat--input--send--button--color-hover: var(--chat--color-secondary-shade-50); + --chat--input--file--button--background: var(--chat--color-white); + --chat--input--file--button--color: var(--chat--color-secondary); + --chat--input--file--button--background-hover: var(--chat--input--file--button--background); + --chat--input--file--button--color-hover: var(--chat--color-secondary-shade-50); + --chat--files-spacing: 0.25rem; + + /* Body and Footer */ + --chat--body--background: var(--chat--color-light); + --chat--footer--background: var(--chat--color-light); + --chat--footer--color: var(--chat--color-dark); +} diff --git a/packages/@n8n/chat/src/css/index.scss b/packages/frontend/@n8n/chat/src/css/index.scss similarity index 100% rename from packages/@n8n/chat/src/css/index.scss rename to packages/frontend/@n8n/chat/src/css/index.scss diff --git a/packages/@n8n/chat/src/css/markdown.scss b/packages/frontend/@n8n/chat/src/css/markdown.scss similarity index 100% rename from packages/@n8n/chat/src/css/markdown.scss rename to packages/frontend/@n8n/chat/src/css/markdown.scss diff --git a/packages/frontend/@n8n/chat/src/env.d.ts b/packages/frontend/@n8n/chat/src/env.d.ts new file mode 100644 index 0000000000..11f02fe2a0 --- /dev/null +++ b/packages/frontend/@n8n/chat/src/env.d.ts @@ -0,0 +1 @@ +/// diff --git a/packages/@n8n/chat/src/event-buses/chatEventBus.ts b/packages/frontend/@n8n/chat/src/event-buses/chatEventBus.ts similarity index 100% rename from packages/@n8n/chat/src/event-buses/chatEventBus.ts rename to packages/frontend/@n8n/chat/src/event-buses/chatEventBus.ts diff --git a/packages/@n8n/chat/src/event-buses/index.ts b/packages/frontend/@n8n/chat/src/event-buses/index.ts similarity index 100% rename from packages/@n8n/chat/src/event-buses/index.ts rename to packages/frontend/@n8n/chat/src/event-buses/index.ts diff --git a/packages/@n8n/chat/src/index.ts b/packages/frontend/@n8n/chat/src/index.ts similarity index 100% rename from packages/@n8n/chat/src/index.ts rename to packages/frontend/@n8n/chat/src/index.ts diff --git a/packages/@n8n/chat/src/main.scss b/packages/frontend/@n8n/chat/src/main.scss similarity index 100% rename from packages/@n8n/chat/src/main.scss rename to packages/frontend/@n8n/chat/src/main.scss diff --git a/packages/@n8n/chat/src/plugins/chat.ts b/packages/frontend/@n8n/chat/src/plugins/chat.ts similarity index 100% rename from packages/@n8n/chat/src/plugins/chat.ts rename to packages/frontend/@n8n/chat/src/plugins/chat.ts diff --git a/packages/@n8n/chat/src/plugins/index.ts b/packages/frontend/@n8n/chat/src/plugins/index.ts similarity index 100% rename from packages/@n8n/chat/src/plugins/index.ts rename to packages/frontend/@n8n/chat/src/plugins/index.ts diff --git a/packages/@n8n/chat/src/shims.d.ts b/packages/frontend/@n8n/chat/src/shims.d.ts similarity index 100% rename from packages/@n8n/chat/src/shims.d.ts rename to packages/frontend/@n8n/chat/src/shims.d.ts diff --git a/packages/@n8n/chat/src/style.scss b/packages/frontend/@n8n/chat/src/style.scss similarity index 100% rename from packages/@n8n/chat/src/style.scss rename to packages/frontend/@n8n/chat/src/style.scss diff --git a/packages/@n8n/chat/src/types/chat.ts b/packages/frontend/@n8n/chat/src/types/chat.ts similarity index 100% rename from packages/@n8n/chat/src/types/chat.ts rename to packages/frontend/@n8n/chat/src/types/chat.ts diff --git a/packages/@n8n/chat/src/types/icons.d.ts b/packages/frontend/@n8n/chat/src/types/icons.d.ts similarity index 100% rename from packages/@n8n/chat/src/types/icons.d.ts rename to packages/frontend/@n8n/chat/src/types/icons.d.ts diff --git a/packages/@n8n/chat/src/types/index.ts b/packages/frontend/@n8n/chat/src/types/index.ts similarity index 100% rename from packages/@n8n/chat/src/types/index.ts rename to packages/frontend/@n8n/chat/src/types/index.ts diff --git a/packages/@n8n/chat/src/types/messages.ts b/packages/frontend/@n8n/chat/src/types/messages.ts similarity index 100% rename from packages/@n8n/chat/src/types/messages.ts rename to packages/frontend/@n8n/chat/src/types/messages.ts diff --git a/packages/@n8n/chat/src/types/options.ts b/packages/frontend/@n8n/chat/src/types/options.ts similarity index 100% rename from packages/@n8n/chat/src/types/options.ts rename to packages/frontend/@n8n/chat/src/types/options.ts diff --git a/packages/@n8n/chat/src/types/webhook.ts b/packages/frontend/@n8n/chat/src/types/webhook.ts similarity index 100% rename from packages/@n8n/chat/src/types/webhook.ts rename to packages/frontend/@n8n/chat/src/types/webhook.ts diff --git a/packages/@n8n/chat/src/utils/event-bus.ts b/packages/frontend/@n8n/chat/src/utils/event-bus.ts similarity index 100% rename from packages/@n8n/chat/src/utils/event-bus.ts rename to packages/frontend/@n8n/chat/src/utils/event-bus.ts diff --git a/packages/@n8n/chat/src/utils/index.ts b/packages/frontend/@n8n/chat/src/utils/index.ts similarity index 100% rename from packages/@n8n/chat/src/utils/index.ts rename to packages/frontend/@n8n/chat/src/utils/index.ts diff --git a/packages/@n8n/chat/src/utils/mount.ts b/packages/frontend/@n8n/chat/src/utils/mount.ts similarity index 100% rename from packages/@n8n/chat/src/utils/mount.ts rename to packages/frontend/@n8n/chat/src/utils/mount.ts diff --git a/packages/@n8n/chat/tsconfig.json b/packages/frontend/@n8n/chat/tsconfig.json similarity index 89% rename from packages/@n8n/chat/tsconfig.json rename to packages/frontend/@n8n/chat/tsconfig.json index 341c8b186d..34bb759969 100644 --- a/packages/@n8n/chat/tsconfig.json +++ b/packages/frontend/@n8n/chat/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.json", + "extends": "@n8n/typescript-config/tsconfig.common.json", "compilerOptions": { "rootDir": "src", "outDir": "dist", diff --git a/packages/frontend/@n8n/chat/vite.config.mts b/packages/frontend/@n8n/chat/vite.config.mts new file mode 100644 index 0000000000..38722cbea7 --- /dev/null +++ b/packages/frontend/@n8n/chat/vite.config.mts @@ -0,0 +1,57 @@ +import { defineConfig, mergeConfig } from 'vite'; +import { resolve } from 'path'; +import vue from '@vitejs/plugin-vue'; +import icons from 'unplugin-icons/vite'; +import dts from 'vite-plugin-dts'; +import { vitestConfig } from '@n8n/vitest-config/frontend'; + +const includeVue = process.env.INCLUDE_VUE === 'true'; +const srcPath = resolve(__dirname, 'src'); + +// https://vitejs.dev/config/ +export default mergeConfig( + defineConfig({ + plugins: [ + vue(), + icons({ + compiler: 'vue3', + autoInstall: true, + }), + dts(), + ], + resolve: { + alias: { + '@': srcPath, + '@n8n/chat': srcPath, + lodash: 'lodash-es', + }, + }, + define: { + 'process.env.NODE_ENV': process.env.NODE_ENV ? `"${process.env.NODE_ENV}"` : '"development"', + }, + build: { + emptyOutDir: !includeVue, + lib: { + entry: resolve(__dirname, 'src', 'index.ts'), + name: 'N8nChat', + fileName: (format) => (includeVue ? `chat.bundle.${format}.js` : `chat.${format}.js`), + }, + rollupOptions: { + // make sure to externalize deps that shouldn't be bundled + // into your library + external: includeVue ? [] : ['vue'], + output: { + exports: 'named', + // Provide global variables to use in the UMD build + // for externalized deps + globals: includeVue + ? {} + : { + vue: 'Vue', + }, + }, + }, + }, + }), + vitestConfig, +); diff --git a/packages/frontend/@n8n/composables/.eslintrc.cjs b/packages/frontend/@n8n/composables/.eslintrc.cjs index 94152c01f7..3f9a316c08 100644 --- a/packages/frontend/@n8n/composables/.eslintrc.cjs +++ b/packages/frontend/@n8n/composables/.eslintrc.cjs @@ -1,3 +1,10 @@ -const { createFrontendEslintConfig } = require('@n8n/frontend-eslint-config'); +const sharedOptions = require('@n8n/eslint-config/shared'); -module.exports = createFrontendEslintConfig(__dirname); +/** + * @type {import('@types/eslint').ESLint.ConfigData} + */ +module.exports = { + extends: ['@n8n/eslint-config/frontend'], + + ...sharedOptions(__dirname, 'frontend'), +}; diff --git a/packages/frontend/@n8n/composables/package.json b/packages/frontend/@n8n/composables/package.json index 92c125461c..55c7a0a8cb 100644 --- a/packages/frontend/@n8n/composables/package.json +++ b/packages/frontend/@n8n/composables/package.json @@ -1,7 +1,7 @@ { "name": "@n8n/composables", "type": "module", - "version": "1.1.0", + "version": "1.2.0", "files": [ "dist" ], @@ -25,9 +25,9 @@ "format:check": "biome ci . && prettier --check . --ignore-path ../../../../.prettierignore" }, "devDependencies": { - "@n8n/frontend-eslint-config": "workspace:*", - "@n8n/frontend-typescript-config": "workspace:*", - "@n8n/frontend-vitest-config": "workspace:*", + "@n8n/eslint-config": "workspace:*", + "@n8n/typescript-config": "workspace:*", + "@n8n/vitest-config": "workspace:*", "@testing-library/jest-dom": "catalog:frontend", "@testing-library/user-event": "catalog:frontend", "@testing-library/vue": "catalog:frontend", diff --git a/packages/frontend/@n8n/composables/tsconfig.json b/packages/frontend/@n8n/composables/tsconfig.json index 3884b84bf2..10f9ee1b73 100644 --- a/packages/frontend/@n8n/composables/tsconfig.json +++ b/packages/frontend/@n8n/composables/tsconfig.json @@ -1,10 +1,11 @@ { - "extends": "@n8n/frontend-typescript-config", + "extends": "@n8n/typescript-config/tsconfig.frontend.json", "compilerOptions": { "baseUrl": ".", "rootDir": ".", "outDir": "dist", - "types": ["vite/client", "vitest/globals"] + "types": ["vite/client", "vitest/globals"], + "isolatedModules": true }, "include": ["src/**/*.ts", "src/**/*.vue", "vite.config.ts", "tsup.config.ts"] } diff --git a/packages/frontend/@n8n/composables/tsup.config.ts b/packages/frontend/@n8n/composables/tsup.config.ts index a9dc991b43..0d555b1ab0 100644 --- a/packages/frontend/@n8n/composables/tsup.config.ts +++ b/packages/frontend/@n8n/composables/tsup.config.ts @@ -1,7 +1,7 @@ import { defineConfig } from 'tsup'; export default defineConfig({ - entry: ['src/useDeviceSupport.ts'], + entry: ['src/**/*.ts', '!src/**/*.test.ts', '!src/**/*.d.ts', '!src/__tests__**/*'], format: ['cjs', 'esm'], clean: true, dts: true, diff --git a/packages/frontend/@n8n/composables/vite.config.ts b/packages/frontend/@n8n/composables/vite.config.ts index 68833e7059..784f3fb497 100644 --- a/packages/frontend/@n8n/composables/vite.config.ts +++ b/packages/frontend/@n8n/composables/vite.config.ts @@ -1,4 +1,4 @@ import { defineConfig, mergeConfig } from 'vite'; -import { vitestConfig } from '@n8n/frontend-vitest-config'; +import { vitestConfig } from '@n8n/vitest-config/frontend'; export default mergeConfig(defineConfig({}), vitestConfig); diff --git a/packages/design-system/.browserslistrc b/packages/frontend/@n8n/design-system/.browserslistrc similarity index 100% rename from packages/design-system/.browserslistrc rename to packages/frontend/@n8n/design-system/.browserslistrc diff --git a/packages/design-system/.eslintrc.js b/packages/frontend/@n8n/design-system/.eslintrc.js similarity index 84% rename from packages/design-system/.eslintrc.js rename to packages/frontend/@n8n/design-system/.eslintrc.js index 99d5ff4c6e..ad1f1884d3 100644 --- a/packages/design-system/.eslintrc.js +++ b/packages/frontend/@n8n/design-system/.eslintrc.js @@ -1,9 +1,13 @@ -const { createFrontendEslintConfig } = require('@n8n/frontend-eslint-config'); +const sharedOptions = require('@n8n/eslint-config/shared'); /** * @type {import('@types/eslint').ESLint.ConfigData} */ -module.exports = createFrontendEslintConfig(__dirname, { +module.exports = { + extends: ['@n8n/eslint-config/frontend'], + + ...sharedOptions(__dirname, 'frontend'), + rules: { // TODO: Remove these 'import/no-default-export': 'warn', @@ -40,4 +44,4 @@ module.exports = createFrontendEslintConfig(__dirname, { }, }, ], -}); +}; diff --git a/packages/design-system/.gitignore b/packages/frontend/@n8n/design-system/.gitignore similarity index 100% rename from packages/design-system/.gitignore rename to packages/frontend/@n8n/design-system/.gitignore diff --git a/packages/design-system/.npmignore b/packages/frontend/@n8n/design-system/.npmignore similarity index 100% rename from packages/design-system/.npmignore rename to packages/frontend/@n8n/design-system/.npmignore diff --git a/packages/design-system/.storybook/fonts.scss b/packages/frontend/@n8n/design-system/.storybook/fonts.scss similarity index 100% rename from packages/design-system/.storybook/fonts.scss rename to packages/frontend/@n8n/design-system/.storybook/fonts.scss diff --git a/packages/design-system/.storybook/main.ts b/packages/frontend/@n8n/design-system/.storybook/main.ts similarity index 100% rename from packages/design-system/.storybook/main.ts rename to packages/frontend/@n8n/design-system/.storybook/main.ts diff --git a/packages/design-system/.storybook/preview.js b/packages/frontend/@n8n/design-system/.storybook/preview.js similarity index 100% rename from packages/design-system/.storybook/preview.js rename to packages/frontend/@n8n/design-system/.storybook/preview.js diff --git a/packages/design-system/.storybook/storybook.scss b/packages/frontend/@n8n/design-system/.storybook/storybook.scss similarity index 100% rename from packages/design-system/.storybook/storybook.scss rename to packages/frontend/@n8n/design-system/.storybook/storybook.scss diff --git a/packages/design-system/README.md b/packages/frontend/@n8n/design-system/README.md similarity index 97% rename from packages/design-system/README.md rename to packages/frontend/@n8n/design-system/README.md index c43317d077..03b49fd4db 100644 --- a/packages/design-system/README.md +++ b/packages/frontend/@n8n/design-system/README.md @@ -1,6 +1,6 @@ ![n8n.io - Workflow Automation](https://user-images.githubusercontent.com/65276001/173571060-9f2f6d7b-bac0-43b6-bdb2-001da9694058.png) -# n8n-design-system +# @n8n/design-system A component system for [n8n](https://n8n.io) using Storybook to preview. diff --git a/packages/frontend/@n8n/design-system/biome.jsonc b/packages/frontend/@n8n/design-system/biome.jsonc new file mode 100644 index 0000000000..10254c3c13 --- /dev/null +++ b/packages/frontend/@n8n/design-system/biome.jsonc @@ -0,0 +1,7 @@ +{ + "$schema": "../../../../node_modules/@biomejs/biome/configuration_schema.json", + "extends": ["../../../../biome.jsonc"], + "formatter": { + "ignore": ["theme/**"] + } +} diff --git a/packages/design-system/chromatic.config.json b/packages/frontend/@n8n/design-system/chromatic.config.json similarity index 100% rename from packages/design-system/chromatic.config.json rename to packages/frontend/@n8n/design-system/chromatic.config.json diff --git a/packages/design-system/package.json b/packages/frontend/@n8n/design-system/package.json similarity index 87% rename from packages/design-system/package.json rename to packages/frontend/@n8n/design-system/package.json index 3ddc30ebbe..5189f53216 100644 --- a/packages/design-system/package.json +++ b/packages/frontend/@n8n/design-system/package.json @@ -1,6 +1,6 @@ { - "name": "n8n-design-system", - "version": "1.68.0", + "name": "@n8n/design-system", + "version": "1.69.0", "main": "src/index.ts", "import": "src/index.ts", "scripts": { @@ -13,15 +13,15 @@ "build:storybook": "storybook build", "storybook": "storybook dev -p 6006 --no-open", "chromatic": "chromatic", - "format": "biome format --write . && prettier --write . --ignore-path ../../.prettierignore", - "format:check": "biome ci . && prettier --check . --ignore-path ../../.prettierignore", + "format": "biome format --write . && prettier --write . --ignore-path ../../../../.prettierignore", + "format:check": "biome ci . && prettier --check . --ignore-path ../../../../.prettierignore", "lint": "eslint src --ext .js,.ts,.vue --quiet", "lintfix": "eslint src --ext .js,.ts,.vue --fix" }, "devDependencies": { - "@n8n/frontend-eslint-config": "workspace:*", - "@n8n/frontend-typescript-config": "workspace:*", - "@n8n/frontend-vitest-config": "workspace:*", + "@n8n/eslint-config": "workspace:*", + "@n8n/typescript-config": "workspace:*", + "@n8n/vitest-config": "workspace:*", "@n8n/storybook": "workspace:*", "@testing-library/jest-dom": "^6.6.3", "@testing-library/user-event": "^14.6.0", @@ -45,6 +45,7 @@ }, "dependencies": { "@n8n/composables": "workspace:*", + "@n8n/utils": "workspace:*", "@fortawesome/fontawesome-svg-core": "^1.2.36", "@fortawesome/free-solid-svg-icons": "^5.15.4", "@fortawesome/vue-fontawesome": "^3.0.3", diff --git a/packages/design-system/postcss.config.js b/packages/frontend/@n8n/design-system/postcss.config.js similarity index 100% rename from packages/design-system/postcss.config.js rename to packages/frontend/@n8n/design-system/postcss.config.js diff --git a/packages/design-system/public/.nojekyll b/packages/frontend/@n8n/design-system/public/.nojekyll similarity index 100% rename from packages/design-system/public/.nojekyll rename to packages/frontend/@n8n/design-system/public/.nojekyll diff --git a/packages/design-system/public/assets/images/storybook-logo-dark.png b/packages/frontend/@n8n/design-system/public/assets/images/storybook-logo-dark.png similarity index 100% rename from packages/design-system/public/assets/images/storybook-logo-dark.png rename to packages/frontend/@n8n/design-system/public/assets/images/storybook-logo-dark.png diff --git a/packages/design-system/public/assets/images/storybook-logo-light.png b/packages/frontend/@n8n/design-system/public/assets/images/storybook-logo-light.png similarity index 100% rename from packages/design-system/public/assets/images/storybook-logo-light.png rename to packages/frontend/@n8n/design-system/public/assets/images/storybook-logo-light.png diff --git a/packages/design-system/src/__tests__/render.ts b/packages/frontend/@n8n/design-system/src/__tests__/render.ts similarity index 90% rename from packages/design-system/src/__tests__/render.ts rename to packages/frontend/@n8n/design-system/src/__tests__/render.ts index afe27e6855..1dc03ed2ef 100644 --- a/packages/design-system/src/__tests__/render.ts +++ b/packages/frontend/@n8n/design-system/src/__tests__/render.ts @@ -1,6 +1,6 @@ import { render } from '@testing-library/vue'; -import { N8nPlugin } from 'n8n-design-system/plugin'; +import { N8nPlugin } from '@n8n/design-system/plugin'; type Component = Parameters[0]; type RenderOptions = Parameters[1]; diff --git a/packages/design-system/src/__tests__/setup.ts b/packages/frontend/@n8n/design-system/src/__tests__/setup.ts similarity index 89% rename from packages/design-system/src/__tests__/setup.ts rename to packages/frontend/@n8n/design-system/src/__tests__/setup.ts index 5c091e2925..20c4db3cb1 100644 --- a/packages/design-system/src/__tests__/setup.ts +++ b/packages/frontend/@n8n/design-system/src/__tests__/setup.ts @@ -2,7 +2,7 @@ import '@testing-library/jest-dom'; import { configure } from '@testing-library/vue'; import { config } from '@vue/test-utils'; -import { N8nPlugin } from 'n8n-design-system/plugin'; +import { N8nPlugin } from '@n8n/design-system/plugin'; configure({ testIdAttribute: 'data-test-id' }); diff --git a/packages/design-system/src/components/AskAssistantAvatar/AskAssistantAvatar.test.ts b/packages/frontend/@n8n/design-system/src/components/AskAssistantAvatar/AskAssistantAvatar.test.ts similarity index 100% rename from packages/design-system/src/components/AskAssistantAvatar/AskAssistantAvatar.test.ts rename to packages/frontend/@n8n/design-system/src/components/AskAssistantAvatar/AskAssistantAvatar.test.ts diff --git a/packages/design-system/src/components/AskAssistantAvatar/AssistantAvatar.stories.ts b/packages/frontend/@n8n/design-system/src/components/AskAssistantAvatar/AssistantAvatar.stories.ts similarity index 100% rename from packages/design-system/src/components/AskAssistantAvatar/AssistantAvatar.stories.ts rename to packages/frontend/@n8n/design-system/src/components/AskAssistantAvatar/AssistantAvatar.stories.ts diff --git a/packages/design-system/src/components/AskAssistantAvatar/AssistantAvatar.vue b/packages/frontend/@n8n/design-system/src/components/AskAssistantAvatar/AssistantAvatar.vue similarity index 100% rename from packages/design-system/src/components/AskAssistantAvatar/AssistantAvatar.vue rename to packages/frontend/@n8n/design-system/src/components/AskAssistantAvatar/AssistantAvatar.vue diff --git a/packages/design-system/src/components/AskAssistantAvatar/__snapshots__/AskAssistantAvatar.test.ts.snap b/packages/frontend/@n8n/design-system/src/components/AskAssistantAvatar/__snapshots__/AskAssistantAvatar.test.ts.snap similarity index 100% rename from packages/design-system/src/components/AskAssistantAvatar/__snapshots__/AskAssistantAvatar.test.ts.snap rename to packages/frontend/@n8n/design-system/src/components/AskAssistantAvatar/__snapshots__/AskAssistantAvatar.test.ts.snap diff --git a/packages/design-system/src/components/AskAssistantButton/AskAssistantButton.stories.ts b/packages/frontend/@n8n/design-system/src/components/AskAssistantButton/AskAssistantButton.stories.ts similarity index 100% rename from packages/design-system/src/components/AskAssistantButton/AskAssistantButton.stories.ts rename to packages/frontend/@n8n/design-system/src/components/AskAssistantButton/AskAssistantButton.stories.ts diff --git a/packages/design-system/src/components/AskAssistantButton/AskAssistantButton.test.ts b/packages/frontend/@n8n/design-system/src/components/AskAssistantButton/AskAssistantButton.test.ts similarity index 100% rename from packages/design-system/src/components/AskAssistantButton/AskAssistantButton.test.ts rename to packages/frontend/@n8n/design-system/src/components/AskAssistantButton/AskAssistantButton.test.ts diff --git a/packages/design-system/src/components/AskAssistantButton/AskAssistantButton.vue b/packages/frontend/@n8n/design-system/src/components/AskAssistantButton/AskAssistantButton.vue similarity index 100% rename from packages/design-system/src/components/AskAssistantButton/AskAssistantButton.vue rename to packages/frontend/@n8n/design-system/src/components/AskAssistantButton/AskAssistantButton.vue diff --git a/packages/design-system/src/components/AskAssistantButton/__snapshots__/AskAssistantButton.test.ts.snap b/packages/frontend/@n8n/design-system/src/components/AskAssistantButton/__snapshots__/AskAssistantButton.test.ts.snap similarity index 100% rename from packages/design-system/src/components/AskAssistantButton/__snapshots__/AskAssistantButton.test.ts.snap rename to packages/frontend/@n8n/design-system/src/components/AskAssistantButton/__snapshots__/AskAssistantButton.test.ts.snap diff --git a/packages/design-system/src/components/AskAssistantChat/AskAssistantChat.stories.ts b/packages/frontend/@n8n/design-system/src/components/AskAssistantChat/AskAssistantChat.stories.ts similarity index 100% rename from packages/design-system/src/components/AskAssistantChat/AskAssistantChat.stories.ts rename to packages/frontend/@n8n/design-system/src/components/AskAssistantChat/AskAssistantChat.stories.ts diff --git a/packages/design-system/src/components/AskAssistantChat/AskAssistantChat.test.ts b/packages/frontend/@n8n/design-system/src/components/AskAssistantChat/AskAssistantChat.test.ts similarity index 99% rename from packages/design-system/src/components/AskAssistantChat/AskAssistantChat.test.ts rename to packages/frontend/@n8n/design-system/src/components/AskAssistantChat/AskAssistantChat.test.ts index 9874965e82..77e76646b7 100644 --- a/packages/design-system/src/components/AskAssistantChat/AskAssistantChat.test.ts +++ b/packages/frontend/@n8n/design-system/src/components/AskAssistantChat/AskAssistantChat.test.ts @@ -1,6 +1,6 @@ import { render } from '@testing-library/vue'; -import { n8nHtml } from 'n8n-design-system/directives'; +import { n8nHtml } from '@n8n/design-system/directives'; import AskAssistantChat from './AskAssistantChat.vue'; diff --git a/packages/design-system/src/components/AskAssistantChat/AskAssistantChat.vue b/packages/frontend/@n8n/design-system/src/components/AskAssistantChat/AskAssistantChat.vue similarity index 100% rename from packages/design-system/src/components/AskAssistantChat/AskAssistantChat.vue rename to packages/frontend/@n8n/design-system/src/components/AskAssistantChat/AskAssistantChat.vue diff --git a/packages/design-system/src/components/AskAssistantChat/__snapshots__/AskAssistantChat.test.ts.snap b/packages/frontend/@n8n/design-system/src/components/AskAssistantChat/__snapshots__/AskAssistantChat.test.ts.snap similarity index 100% rename from packages/design-system/src/components/AskAssistantChat/__snapshots__/AskAssistantChat.test.ts.snap rename to packages/frontend/@n8n/design-system/src/components/AskAssistantChat/__snapshots__/AskAssistantChat.test.ts.snap diff --git a/packages/design-system/src/components/AskAssistantIcon/AssistantIcon.stories.ts b/packages/frontend/@n8n/design-system/src/components/AskAssistantIcon/AssistantIcon.stories.ts similarity index 100% rename from packages/design-system/src/components/AskAssistantIcon/AssistantIcon.stories.ts rename to packages/frontend/@n8n/design-system/src/components/AskAssistantIcon/AssistantIcon.stories.ts diff --git a/packages/design-system/src/components/AskAssistantIcon/AssistantIcon.test.ts b/packages/frontend/@n8n/design-system/src/components/AskAssistantIcon/AssistantIcon.test.ts similarity index 100% rename from packages/design-system/src/components/AskAssistantIcon/AssistantIcon.test.ts rename to packages/frontend/@n8n/design-system/src/components/AskAssistantIcon/AssistantIcon.test.ts diff --git a/packages/design-system/src/components/AskAssistantIcon/AssistantIcon.vue b/packages/frontend/@n8n/design-system/src/components/AskAssistantIcon/AssistantIcon.vue similarity index 100% rename from packages/design-system/src/components/AskAssistantIcon/AssistantIcon.vue rename to packages/frontend/@n8n/design-system/src/components/AskAssistantIcon/AssistantIcon.vue diff --git a/packages/design-system/src/components/AskAssistantIcon/__snapshots__/AssistantIcon.test.ts.snap b/packages/frontend/@n8n/design-system/src/components/AskAssistantIcon/__snapshots__/AssistantIcon.test.ts.snap similarity index 100% rename from packages/design-system/src/components/AskAssistantIcon/__snapshots__/AssistantIcon.test.ts.snap rename to packages/frontend/@n8n/design-system/src/components/AskAssistantIcon/__snapshots__/AssistantIcon.test.ts.snap diff --git a/packages/design-system/src/components/AskAssistantLoadingMessage/AskAssistantLoadingMessage.test.ts b/packages/frontend/@n8n/design-system/src/components/AskAssistantLoadingMessage/AskAssistantLoadingMessage.test.ts similarity index 100% rename from packages/design-system/src/components/AskAssistantLoadingMessage/AskAssistantLoadingMessage.test.ts rename to packages/frontend/@n8n/design-system/src/components/AskAssistantLoadingMessage/AskAssistantLoadingMessage.test.ts diff --git a/packages/design-system/src/components/AskAssistantLoadingMessage/AssistantLoadingMessage.stories.ts b/packages/frontend/@n8n/design-system/src/components/AskAssistantLoadingMessage/AssistantLoadingMessage.stories.ts similarity index 100% rename from packages/design-system/src/components/AskAssistantLoadingMessage/AssistantLoadingMessage.stories.ts rename to packages/frontend/@n8n/design-system/src/components/AskAssistantLoadingMessage/AssistantLoadingMessage.stories.ts diff --git a/packages/design-system/src/components/AskAssistantLoadingMessage/AssistantLoadingMessage.vue b/packages/frontend/@n8n/design-system/src/components/AskAssistantLoadingMessage/AssistantLoadingMessage.vue similarity index 100% rename from packages/design-system/src/components/AskAssistantLoadingMessage/AssistantLoadingMessage.vue rename to packages/frontend/@n8n/design-system/src/components/AskAssistantLoadingMessage/AssistantLoadingMessage.vue diff --git a/packages/design-system/src/components/AskAssistantLoadingMessage/DemoComponent.stories.ts b/packages/frontend/@n8n/design-system/src/components/AskAssistantLoadingMessage/DemoComponent.stories.ts similarity index 100% rename from packages/design-system/src/components/AskAssistantLoadingMessage/DemoComponent.stories.ts rename to packages/frontend/@n8n/design-system/src/components/AskAssistantLoadingMessage/DemoComponent.stories.ts diff --git a/packages/design-system/src/components/AskAssistantLoadingMessage/DemoComponent.vue b/packages/frontend/@n8n/design-system/src/components/AskAssistantLoadingMessage/DemoComponent.vue similarity index 100% rename from packages/design-system/src/components/AskAssistantLoadingMessage/DemoComponent.vue rename to packages/frontend/@n8n/design-system/src/components/AskAssistantLoadingMessage/DemoComponent.vue diff --git a/packages/design-system/src/components/AskAssistantLoadingMessage/__snapshots__/AskAssistantLoadingMessage.test.ts.snap b/packages/frontend/@n8n/design-system/src/components/AskAssistantLoadingMessage/__snapshots__/AskAssistantLoadingMessage.test.ts.snap similarity index 100% rename from packages/design-system/src/components/AskAssistantLoadingMessage/__snapshots__/AskAssistantLoadingMessage.test.ts.snap rename to packages/frontend/@n8n/design-system/src/components/AskAssistantLoadingMessage/__snapshots__/AskAssistantLoadingMessage.test.ts.snap diff --git a/packages/design-system/src/components/AskAssistantText/AssistantText.stories.ts b/packages/frontend/@n8n/design-system/src/components/AskAssistantText/AssistantText.stories.ts similarity index 100% rename from packages/design-system/src/components/AskAssistantText/AssistantText.stories.ts rename to packages/frontend/@n8n/design-system/src/components/AskAssistantText/AssistantText.stories.ts diff --git a/packages/design-system/src/components/AskAssistantText/AssistantText.test.ts b/packages/frontend/@n8n/design-system/src/components/AskAssistantText/AssistantText.test.ts similarity index 100% rename from packages/design-system/src/components/AskAssistantText/AssistantText.test.ts rename to packages/frontend/@n8n/design-system/src/components/AskAssistantText/AssistantText.test.ts diff --git a/packages/design-system/src/components/AskAssistantText/AssistantText.vue b/packages/frontend/@n8n/design-system/src/components/AskAssistantText/AssistantText.vue similarity index 100% rename from packages/design-system/src/components/AskAssistantText/AssistantText.vue rename to packages/frontend/@n8n/design-system/src/components/AskAssistantText/AssistantText.vue diff --git a/packages/design-system/src/components/AskAssistantText/__snapshots__/AssistantText.test.ts.snap b/packages/frontend/@n8n/design-system/src/components/AskAssistantText/__snapshots__/AssistantText.test.ts.snap similarity index 100% rename from packages/design-system/src/components/AskAssistantText/__snapshots__/AssistantText.test.ts.snap rename to packages/frontend/@n8n/design-system/src/components/AskAssistantText/__snapshots__/AssistantText.test.ts.snap diff --git a/packages/design-system/src/components/BetaTag/BetaTag.stories.ts b/packages/frontend/@n8n/design-system/src/components/BetaTag/BetaTag.stories.ts similarity index 100% rename from packages/design-system/src/components/BetaTag/BetaTag.stories.ts rename to packages/frontend/@n8n/design-system/src/components/BetaTag/BetaTag.stories.ts diff --git a/packages/design-system/src/components/BetaTag/BetaTag.test.ts b/packages/frontend/@n8n/design-system/src/components/BetaTag/BetaTag.test.ts similarity index 100% rename from packages/design-system/src/components/BetaTag/BetaTag.test.ts rename to packages/frontend/@n8n/design-system/src/components/BetaTag/BetaTag.test.ts diff --git a/packages/design-system/src/components/BetaTag/BetaTag.vue b/packages/frontend/@n8n/design-system/src/components/BetaTag/BetaTag.vue similarity index 100% rename from packages/design-system/src/components/BetaTag/BetaTag.vue rename to packages/frontend/@n8n/design-system/src/components/BetaTag/BetaTag.vue diff --git a/packages/design-system/src/components/BetaTag/__snapshots__/BetaTag.test.ts.snap b/packages/frontend/@n8n/design-system/src/components/BetaTag/__snapshots__/BetaTag.test.ts.snap similarity index 100% rename from packages/design-system/src/components/BetaTag/__snapshots__/BetaTag.test.ts.snap rename to packages/frontend/@n8n/design-system/src/components/BetaTag/__snapshots__/BetaTag.test.ts.snap diff --git a/packages/design-system/src/components/BlinkingCursor/BlinkingCursor.stories.ts b/packages/frontend/@n8n/design-system/src/components/BlinkingCursor/BlinkingCursor.stories.ts similarity index 100% rename from packages/design-system/src/components/BlinkingCursor/BlinkingCursor.stories.ts rename to packages/frontend/@n8n/design-system/src/components/BlinkingCursor/BlinkingCursor.stories.ts diff --git a/packages/design-system/src/components/BlinkingCursor/BlinkingCursor.test.ts b/packages/frontend/@n8n/design-system/src/components/BlinkingCursor/BlinkingCursor.test.ts similarity index 100% rename from packages/design-system/src/components/BlinkingCursor/BlinkingCursor.test.ts rename to packages/frontend/@n8n/design-system/src/components/BlinkingCursor/BlinkingCursor.test.ts diff --git a/packages/design-system/src/components/BlinkingCursor/BlinkingCursor.vue b/packages/frontend/@n8n/design-system/src/components/BlinkingCursor/BlinkingCursor.vue similarity index 100% rename from packages/design-system/src/components/BlinkingCursor/BlinkingCursor.vue rename to packages/frontend/@n8n/design-system/src/components/BlinkingCursor/BlinkingCursor.vue diff --git a/packages/design-system/src/components/BlinkingCursor/__snapshots__/BlinkingCursor.test.ts.snap b/packages/frontend/@n8n/design-system/src/components/BlinkingCursor/__snapshots__/BlinkingCursor.test.ts.snap similarity index 100% rename from packages/design-system/src/components/BlinkingCursor/__snapshots__/BlinkingCursor.test.ts.snap rename to packages/frontend/@n8n/design-system/src/components/BlinkingCursor/__snapshots__/BlinkingCursor.test.ts.snap diff --git a/packages/design-system/src/components/CodeDiff/CodeDiff.stories.ts b/packages/frontend/@n8n/design-system/src/components/CodeDiff/CodeDiff.stories.ts similarity index 100% rename from packages/design-system/src/components/CodeDiff/CodeDiff.stories.ts rename to packages/frontend/@n8n/design-system/src/components/CodeDiff/CodeDiff.stories.ts diff --git a/packages/design-system/src/components/CodeDiff/CodeDiff.test.ts b/packages/frontend/@n8n/design-system/src/components/CodeDiff/CodeDiff.test.ts similarity index 100% rename from packages/design-system/src/components/CodeDiff/CodeDiff.test.ts rename to packages/frontend/@n8n/design-system/src/components/CodeDiff/CodeDiff.test.ts diff --git a/packages/design-system/src/components/CodeDiff/CodeDiff.vue b/packages/frontend/@n8n/design-system/src/components/CodeDiff/CodeDiff.vue similarity index 98% rename from packages/design-system/src/components/CodeDiff/CodeDiff.vue rename to packages/frontend/@n8n/design-system/src/components/CodeDiff/CodeDiff.vue index 7030438571..36e6129d9a 100644 --- a/packages/design-system/src/components/CodeDiff/CodeDiff.vue +++ b/packages/frontend/@n8n/design-system/src/components/CodeDiff/CodeDiff.vue @@ -2,7 +2,7 @@ import parseDiff from 'parse-diff'; import { computed } from 'vue'; -import { useI18n } from 'n8n-design-system/composables/useI18n'; +import { useI18n } from '@n8n/design-system/composables/useI18n'; const MIN_LINES = 4; diff --git a/packages/design-system/src/components/CodeDiff/__snapshots__/CodeDiff.test.ts.snap b/packages/frontend/@n8n/design-system/src/components/CodeDiff/__snapshots__/CodeDiff.test.ts.snap similarity index 100% rename from packages/design-system/src/components/CodeDiff/__snapshots__/CodeDiff.test.ts.snap rename to packages/frontend/@n8n/design-system/src/components/CodeDiff/__snapshots__/CodeDiff.test.ts.snap diff --git a/packages/design-system/src/components/ConditionalRouterLink/ConditionalRouterLink.test.ts b/packages/frontend/@n8n/design-system/src/components/ConditionalRouterLink/ConditionalRouterLink.test.ts similarity index 100% rename from packages/design-system/src/components/ConditionalRouterLink/ConditionalRouterLink.test.ts rename to packages/frontend/@n8n/design-system/src/components/ConditionalRouterLink/ConditionalRouterLink.test.ts diff --git a/packages/design-system/src/components/ConditionalRouterLink/CondtionalRouterLink.vue b/packages/frontend/@n8n/design-system/src/components/ConditionalRouterLink/CondtionalRouterLink.vue similarity index 100% rename from packages/design-system/src/components/ConditionalRouterLink/CondtionalRouterLink.vue rename to packages/frontend/@n8n/design-system/src/components/ConditionalRouterLink/CondtionalRouterLink.vue diff --git a/packages/design-system/src/components/ConditionalRouterLink/__snapshots__/ConditionalRouterLink.test.ts.snap b/packages/frontend/@n8n/design-system/src/components/ConditionalRouterLink/__snapshots__/ConditionalRouterLink.test.ts.snap similarity index 100% rename from packages/design-system/src/components/ConditionalRouterLink/__snapshots__/ConditionalRouterLink.test.ts.snap rename to packages/frontend/@n8n/design-system/src/components/ConditionalRouterLink/__snapshots__/ConditionalRouterLink.test.ts.snap diff --git a/packages/design-system/src/components/ConditionalRouterLink/index.ts b/packages/frontend/@n8n/design-system/src/components/ConditionalRouterLink/index.ts similarity index 100% rename from packages/design-system/src/components/ConditionalRouterLink/index.ts rename to packages/frontend/@n8n/design-system/src/components/ConditionalRouterLink/index.ts diff --git a/packages/design-system/src/components/InlineAskAssistantButton/InlineAskAssistantButton.stories.ts b/packages/frontend/@n8n/design-system/src/components/InlineAskAssistantButton/InlineAskAssistantButton.stories.ts similarity index 100% rename from packages/design-system/src/components/InlineAskAssistantButton/InlineAskAssistantButton.stories.ts rename to packages/frontend/@n8n/design-system/src/components/InlineAskAssistantButton/InlineAskAssistantButton.stories.ts diff --git a/packages/design-system/src/components/InlineAskAssistantButton/InlineAskAssistantButton.vue b/packages/frontend/@n8n/design-system/src/components/InlineAskAssistantButton/InlineAskAssistantButton.vue similarity index 100% rename from packages/design-system/src/components/InlineAskAssistantButton/InlineAskAssistantButton.vue rename to packages/frontend/@n8n/design-system/src/components/InlineAskAssistantButton/InlineAskAssistantButton.vue diff --git a/packages/design-system/src/components/N8nActionBox/ActionBox.stories.ts b/packages/frontend/@n8n/design-system/src/components/N8nActionBox/ActionBox.stories.ts similarity index 100% rename from packages/design-system/src/components/N8nActionBox/ActionBox.stories.ts rename to packages/frontend/@n8n/design-system/src/components/N8nActionBox/ActionBox.stories.ts diff --git a/packages/design-system/src/components/N8nActionBox/ActionBox.test.ts b/packages/frontend/@n8n/design-system/src/components/N8nActionBox/ActionBox.test.ts similarity index 100% rename from packages/design-system/src/components/N8nActionBox/ActionBox.test.ts rename to packages/frontend/@n8n/design-system/src/components/N8nActionBox/ActionBox.test.ts diff --git a/packages/design-system/src/components/N8nActionBox/ActionBox.vue b/packages/frontend/@n8n/design-system/src/components/N8nActionBox/ActionBox.vue similarity index 94% rename from packages/design-system/src/components/N8nActionBox/ActionBox.vue rename to packages/frontend/@n8n/design-system/src/components/N8nActionBox/ActionBox.vue index b04ccb1049..a658e513fb 100644 --- a/packages/design-system/src/components/N8nActionBox/ActionBox.vue +++ b/packages/frontend/@n8n/design-system/src/components/N8nActionBox/ActionBox.vue @@ -1,6 +1,6 @@