feat(editor): Ask AI in Code node (#6672)

* feat(editor): Ask AI tab and CLi connection

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* Remove old getSchema util method

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* Increase CSS specificity of the CodeNodeEditor global overrides

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* feat(editor): Magic Connect

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* Improve AI controller, load conditionally, UX modal imporvements

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* Extract-out AI curl

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* Move loading phrases to locale, add support for ask ai experiment

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* fix build

* adjust communication

* fix: Remove duplicate source control preferences fetching (no-changelog) (#6675)

fix: remove duplicate source control preferences fetching (no-changelog)

* fix(Slack Node): Add UTM params to n8n reference in Slack message (no-changelog) (#6668)

fix(Slack Node): Add UTM params to n8n reference in Slack message

* fix(FileMaker Node): Improve returned error responses (#6585)

* fix(Microsoft Outlook Node): Fix issue with category not correctly applying (#6583)

* feat(Airtable Node): Overhaul (#6200)

* fix(core): Deleting manual executions should defer deleting binary data (#6680)

deleting manual executions should defer deleting binary data

* fix(editor): Add paywall state to non owner users for Variables (#6679)

* fix(editor): Add paywall state to non owner users for Variables

* fix(editor): Add variables view tests

* fix(editor): remove link from paywall state for non owner

* fix(editor): fix displaying logic

* refactor(core): Refactor WorkflowStatistics code (no-changelog) (#6617)

refactor(core): Refactor WorkflowStatistics code

* fix(editor): Hide Execute Node button for unknown nodes (#6684)

* feat: Allow hiding credential params on cloud (#6687)

* fix: Stop n8n from complaining about credentials when saving a new workflow form a template (#6671)

* fix(core): Upgrade semver to address CVE-2022-25883 (#6689)

* fix(core): Upgrade semver to address CVE-2022-25883

[GH Advisory](https://github.com/advisories/GHSA-c2qf-rxjj-qqgw)

* enforce the patched version of semver everywhere in the dev setup

* ci: Fix test checker glob (no changelog) (#6682)

ci: Fix test checker glob

* fix(API): Do not add starting node on workflow creation (#6686)

* fix(API): Do not add starting node on workflow creation

* chore: Remove comment

* fix(core): Filter out workflows that failed to activate on startup (#6676)

* fix(core): Deactivate on init workflow that should not be retried

* fix(core): Filter out workflows with activation errors

* fix(core): Load SAML libraries dynamically (#6690)

load SAML dynamically

* fix(crowd.dev Node): Fix documentation urls for crowd.dev credentials and nodes (#6696)

* feat(Read PDF Node): Replace pdf-parse with pdfjs, and add support for streaming and encrypted PDFs (#6640)

* feat: Allow `eslint-config` to be externally consumable (#6694)

* feat: Allow `eslint-config` to be externally consumable

* refactor: Adjust import styles

* fix(Contentful Node): Fix typo in credential name (no-changelog) (#6692)

* fix(editor): Ensure default credential values are not detected as dirty state (#6677)

* fix(editor): Ensure default credential values are not detected as dirty state

* chore: Remove logging

* refactor: Improve comment

* feat(Google Cloud Storage Node): Use streaming for file uploads (#6462)

fix(Google Cloud Storage Node): Use streaming for file uploads

* fix(editor): Prevent RMC from loading schema if it's already cached (#6695)

* fix(editor): Prevent RMC from loading schema if it's already cached
*  Adding new tests for RMC
* 👕 Fixing lint errors
* 👌 Updating inline loader styling

* fix(API): Fix issue with workflow setting not supporting newer nanoids (#6699)

* ci: Fix test workflows (no-changelog) (#6698)

* ci: Fix test workflows (no-changelog)

We removed `pdf-parse` in #6640, so we need to get these test PDF files from the `test-workflows` repo instead ([which has been updated to include these files](0f6ef1c804))

* remove `\n` from ids and skipList text files

* fix(core): Banner dismissal should also work for users migrating to v1 (no-changelog) (#6700)

* fix(Postgres Node): For select queries, empty result should be be replaced with `{"success":true}` (#6703)

* fix(Postgres Node): For select queries, empty result should be be replaced with `{"success":true}`

*  less checks

---------

Co-authored-by: Michael Kret <michael.k@radency.com>

* feat(editor): Removing `ph-no-capture` class from some elements (#6674)

* feat(editor): Remove `.ph-no-capture` class from some of the fields
* ✔️ Updating test snapshots
*  Redacting expressions preview in credentials form
* 🔧 Disable posthog input masking
* 🚨 Testing PostHog iFrame settings
* Reverting iframe test
*  Hiding API key in PostHog recordings
*  Added tests for redacted values
* ✔️ Updating checkbox snapshots after label component update
* ✔️ Updating test snapshots in editor-ui
* 👕 Fix lint errors

* fix(editor): Remove global link styling in v1 banner (#6705)

* fix: Add missing indices on sqlite (#6673)

* fix: enforce tag name uniqueness on sqlite

* rename migration and add other missing indices

* add tags tests

* test: Move test timeout to `/cli` (no-changelog) (#6712)

* fix(core): Redirect user to previous url after SSO signin (#6710)

redirect user to previous url after SSO signin

* fix(FTP Node): List recursive ignore . and .. to prevent infinite loops (#6707)

ignore . and .. to prevent infinite loop

Co-authored-by: Michael Kret <michael.k@radency.com>

* ci: Fix running e2e tests in dev mode (no-changelog) (#6717)

* fix(Google BigQuery Node): Error description improvement (#6715)

* fix(GitLab Trigger Node): Fix trigger activation 404 error  (#6711)

* fix webhook checkExists not deleting static data

* improve webhook checkExists not deleting static data

* fix(core): Support redis cluster in queue mode (#6708)

* support redis cluster

* cleanup, fix config schema

* set default prefix to bull

* fix(editor): Skip error line highlighting if out of range (#6721)

* fix(AwsS3 Node): Fix issue if bucket name contains a '.' (#6542)

* test(editor): Add canvas actions E2E tests (#6723)

* test(editor): Add canvas actions E2E tests

* test(editor): Open category items in node creator when category dropped on canvas

* test(editor): Have new position counted only once in drag

* test(editor): rename test

* feat(Rundeck Node): Add support for node filters  (#5633)

* fix(Gmail Trigger Node): Early returns in case of no data (#6727)

* fix(core): Use JWT as reset password token (#6714)

* use jwt to reset password

* increase expiration time to 1d

* drop user id query string

* refactor

* use service instead of package in tests

* sqlite migration

* postgres migration

* mysql migration

* remove unused properties

* remove userId from FE

* fix test for users.api

* move migration to the common folder

* move type assertion to the jwt.service

* Add jwt secret as a readonly property

* use signData instead of sign in user.controller

* remove base class

* remove base class

* add tests

* ci: Fix tests on postgres (no-changelog)

* refactor(core): Prevent community packages queries if feature is disabled (#6728)

* feat(core): Add cache service (#6729)

* add cache service

* PR adjustments

* switch to maxSize for memory cache

* Revert "test(editor): Add canvas actions E2E tests" (#6736)

Revert "test(editor): Add canvas actions E2E tests (#6723)"

This reverts commit 052d82b220.

* fix(Postgres Node): Arrays in query replacement fix (#6718)

* fix(Telegram Trigger Node): Add guard to 'include' call on null or undefined (#6730)

* fix(core): Use `exec` in docker images to forward signals correctly (#6732)

* refactor(core): Move webhook DB access to repository (no-changelog) (#6706)

* refactor(core): Move webhook DB access to repository (no-changelog)

* make sure `DataSource` is initialized before it's dependencies

at some point I hope to replace `DataSource` with a custom `DatabaseConnection` service class that can then disconnect and reconnect from DB without having to update all repositories.

---------

Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in>

* feat: Environments release using source control (#6653)

* initial telemetry setup and adjusted pull return

* quicksave before merge

* feat: add conflicting workflow list to pull modal

* feat: update source control pull modal

* fix: fix linting issue

* feat: add Enter keydown event for submitting source control push modal (no-changelog)

feat: add Enter keydown event for submitting source control push modal

* quicksave

* user workflow table for export

* improve telemetry data

* pull api telemetry

* fix lint

* Copy tweaks.

* remove authorName and authorEmail and pick from user

* rename owners.json to workflow_owners.json

* ignore credential conflicts on pull

* feat: several push/pull flow changes and design update

* pull and push return same data format

* fix: add One last step toast for successful pull

* feat: add up to date pull toast

* fix: add proper Learn more link for push and pull modals

* do not await tracking being sent

* fix import

* fix await

* add more sourcecontrolfile status

* Minor copy tweak for "More info".

* Minor copy tweak for "More info".

* ignore variable_stub conflicts on pull

* ignore whitespace differences

* do not show remote workflows that are not yet created

* fix telemetry

* fix toast when pulling deleted wf

* lint fix

* refactor and make some imports dynamic

* fix variable edit validation

* fix telemetry response

* improve telemetry

* fix unintenional delete commit

* fix status unknown issue

* fix up to date toast

* do not export active state and reapply versionid

* use update instead of upsert

* fix: show all workflows when clicking push to git

* feat: update Up to date pull translation

* fix: update read only env checks

* do not update versionid of only active flag changes

* feat: prevent access to new workflow and templates import when read only env

* feat: send only active state and version if workflow state is not dirty

* fix: Detect when only active state has changed and prevent generation a new version ID

* feat: improve readonly env messages

* make getPreferences public

* fix telemetry issue

* fix: add partial workflow update based on dirty state when changing active state

* update unit tests

* fix: remove unsaved changes check in readOnlyEnv

* fix: disable push to git button when read onyl env

* fix: update readonly toast duration

* fix: fix pinning and title input in protected mode

* initial commit (NOT working)

* working push

* cleanup and implement pull

* fix getstatus

* update import to new method

* var and tag diffs are no conflicts

* only show pull conflict for workflows

* refactor and ignore faulty credentials

* add sanitycheck for missing git folder

* prefer fetch over pull and limit depth to 1

* back to pull...

* fix setting branch on initial connect

* fix test

* remove clean workfolder

* refactor: Remove some unnecessary code

* Fixed links to docs.

* fix getstatus query params

* lint fix

* dialog to show local and remote name on conflict

* only show remote name on conflict

* fix credential expression export

* fix: Broken test

* dont show toast on pull with empty var/tags and refactor

* apply frontend changes from old branch

* fix tag with same name import

* fix buttons shown for non instance owners

* prepare local storage key for removal

* refactor: Change wording on pushing and pulling

* refactor: Change menu item

* test: Fix broken test

* Update packages/cli/src/environments/sourceControl/types/sourceControlPushWorkFolder.ts

Co-authored-by: Iván Ovejero <ivov.src@gmail.com>

---------

Co-authored-by: Alex Grozav <alex@grozav.com>
Co-authored-by: Giulio Andreini <g.andreini@gmail.com>
Co-authored-by: Omar Ajoue <krynble@gmail.com>
Co-authored-by: Iván Ovejero <ivov.src@gmail.com>

* fix(core): Fix RemoveResetPasswordColumns migration for sqlite (no-changelog) (#6739)

* ci: Update changelog generation to work with node 18

* refactor: Remove webhook from `IDatabaseCollections` (no-changelog) (#6745)

* refactor: Remove webhook from `IDatabaseCollections`

* refactor: Remove also from `collections`

* 🚀 Release 1.1.0 (#6746)

Co-authored-by: netroy <netroy@users.noreply.github.com>

* fix(Lemlist Node): Fix pagination issues with campaigns and activities (#6734)

* ci: Fix linting issues (no-changelog) (#6747)

* fix(core): Allow ignoring SSL issues on generic oauth2 credentials (#6702)

* refactor: Remove all references to the resetPasswordToken field (no-changelog) (#6751)

refactor: remove all references to the resetPasswordToken field (no-changelog)

* refactor(core): Use mixins to delete redundant code between Entity classes (no-changelog) (#6616)

* db entities don't need an ID before they are inserted

* don't define constructors on entity classes, use repository.create instead

* use mixins to reduce duplicate code in db entity classes

* fix: Display source control buttons properly (#6756)

* feat(editor): Migrate Design System and Editor UI to Vue 3 (#6476)

* feat: remove vue-fragment (no-changelog)

* feat: partial design-system migration

* feat: migrate info-accordion and info-tip components

* feat: migrate several components to vue 3

* feat: migrated several components

* feat: migrate several components

* feat: migrate several components

* feat: migrate several components

* feat: re-exported all design system components

* fix: fix design for popper components

* fix: editor kind of working, lots of issues to fix

* fix: fix several vue 3 migration issues

* fix: replace @change with @update:modelValue in several places

* fix: fix translation linking

* fix: fix inline-edit input

* fix: fix ndv and dialog design

* fix: update parameter input event bindings

* fix: rename deprecated lifecycle methods

* fix: fix json view mapping

* build: update lock file

* fix(editor): revisit last conflict with master and fix issues

* fix(editor): revisit last conflict with master and fix issues

* fix: fix expression editor bug causing code mirror to no longer be reactive

* fix: fix resource locator bug

* fix: fix vue-agile integration

* fix: remove global import for vue-agile

* fix: replace element-plus buttons with n8n-buttons everywhere

* fix(editor): Fix various element-plus styles (#6571)

* fix(editor): Fix various element-plus styles

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* Remove debugging code

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* Address PR comments

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

---------

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* fix(editor): Fix loading in production mode [Vue 3] (#6578)

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* fix(editor): First round of e2e tests fixes with Vue 3 (#6579)

* fix(editor): Fix broken smoke and workflow list e2e tests
* ✔️ Fix failing canvas action tests. Updating some selectors used in credentials and workflow tests

* feat: add vue 3 eslint rules and fix issues

* fix: fix tags-dropdown

* fix: fix white-space issues caused by i18n-t

* fix: rename non-generic click events

* fix: fix search in resources list layout

* fix: fix datatable paginator

* fix: fix popper select caret and dropdown size

* fix: add width to action-dropdown

* fix: fix workflow settings icon not being hidden

* fix: refactor newly added code

* fix: fix merge issue

* fix: fix ndv credentials watcher

* fix: fix workflow saving and grabber notch

* fix: fix nodes list panel transition

* fix: fix node title visibility

* fix: fix data unpinning

* fix: fix value access

* fix: show  input panel only if trigger panel enabled or not trigger node

* fix: fix tags dropdown and executions status spcing

* fix(editor): Prevent execution list to load back when leaving the route (#6697)

fix(editor): prevent execution list to load back when leaving the route

* fix: fix drawer visibility

* fix: fix expression toggle padding

* fix: fix expressions editor styling

* chore: prepare for testing

* fix: fix styling for el-button without patching

* test: fix unit tests in design-system

* test: fix most unit tests

* fix: remove import cycle.

* fix: fix personalization modal tests

* fix further resource mapper test adjustments

* fix: fix multiple tests and n8n-route attr duplication

* fix: fix source control tets

* fix: fixed remaining unit tests

* fix: fix workflows and credentials e2e tests

* fix: fix localizeNodeNames

* fix: update ndv e2e tests

* fix: fix popper left placement arrow

* fix: fix 5-ndv e2e tests

* fix: fix 6-code-node e2e tests

* fix(editor): Drop click outside directive from NodeCreator (#6716)

* fix(editor): Drop click outside directive from NodeCreator

* fix(editor): make sure mouseup outside is unbound at least before the component is unmounted

* fix: fix 10-settings-log-streaming e2e tests

* fix: fix node redrawing

* fix: fix tooltip buttons styling

* fix: fix varous e2e suites

* fix: fix 15-scheduler-node e2e suite

* fix: fix route watcher

* fix: fixed param name update and credential edit

* feat: update event names

* refactor: Remove deprecated `$data` (#6576)

Co-authored-by: Alex Grozav <alex@grozav.com>

* fix: fix 17-sharing e2e suite

* fix: fix tags dropdown

* fix: fix tags manager

* fix(editor): move :deep selectors to a separate scoped style block

* fix: fix sticky component and inline text edit

* fix: update e2e tests

* fix: remove button override references

* fix(editor): Adjust spacing in templates for Vue 3 (#6744)

* fix(editor): Adjust spacing in templates

* fix: Undo unneeded change

* fix: Undo unneeded change

* fix(editor): Adjust NDV height for Vue 3 (#6742)

fix(editor): Adjust NDV height

* fix(editor): Restore collapsed sidebar items for Vue 3 (#6743)

fix(editor): Restore collapsed sidebar items

* fix: fix linting issues

* fix: fix design-system deps

* fix: post-merge fixes

* fix: update tests

* fix: increase timeout for executionslist tets

* chore: fix linting issue

* fix: fix 14-mapping e2e tests in ci

* fix: re-enable tests

* fix: fix workflow duplication e2e tests after tags update

* fix(editor): Change component prop to be typed

* fix: fix tags dropdown in duplicate wf modal

* fix: fix focus behaviour in tags selector

* fix: fix tag creation

* fix: fix log streaming e2e race condition

* fix(editor): Fix Vue 3 linting issues (#6748)

* fix(editor): Fix Vue 3 linting issues

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* fix MainSidebar linter issues

* revert pnpm lock

* update pnpm lock file

---------

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>
Co-authored-by: Alex Grozav <alex@grozav.com>

* fix(editor): Some css fixes for vue3 branch (#6749)

*  Fixing filter button height

*  Update input modal button position

*  Updating tags styling

*  Fix event logging settings spacing

* 👕 Fixing lint errors

* fix: fix linting issues

* Revert to `// eslint-disable-next-line @typescript-eslint/no-misused-promises` disabling of mixins init

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* fix: fix css issue

* fix(editor): Lint fix

* fix(editor): Fix settings initialisation (#6750)

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* fix: fix initial settings loading

* fix: replace realClick with click force

* fix: fix randomly failing mapping e2e tests

* fix(editor): Fix menu item event handling

* fix: fix resource filters dropdown events (#6752)

* fix: fix resource filters dropdown events

* fix: remove teleported:false

* fix: fix event selection event naming (#6753)

* fix: removed console.log (#6754)

* fix: rever await nextTick changes

* fix: redo linting changes

* fix(editor): Redraw node connections if adding more than one node to canvas (#6755)

* fix(editor): Redraw node connections if adding more than one node to canvas

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* Update position before connection two nodes

* Lint fix

---------

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>
Co-authored-by: Alex Grozav <alex@grozav.com>

* fix(editor): Fix `ResourceMapper` unit tests (#6758)

* ✔️ Fix matching columns test

* ✔️ Fix multiple matching columns test

* ✔️ Removing `skip` from the last test

* fix: Allow pasting a big workflow (#6760)

* fix: pasting a big workflow

* chore: update comment

* refactor: move try/catch to function

* refactor: move try/catch to function

* fix(editor): Fix modal layer width

* fix: fix position changes

* fix: undo it.only

* fix: make undo/redo multiple steps more verbose

* fix: Fix value survey styles (#6764)

* fix: fix value survey styles

* fix: lint

* Revert "fix: lint"

72869c431f

* fix: lint

* fix(editor): Fix collapsed sub menu

* fix: Fix drawer animation (#6767)

fix: drawer animation

* fix(editor): Fix source control buttons (#6769)

* fix(editor): Fix App loading & auth  (#6768)

* fix(editor): Fix App loading & auth

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* Await promises

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* Fix eslint error

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

---------

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

---------

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>
Co-authored-by: Csaba Tuncsik <csaba@n8n.io>
Co-authored-by: OlegIvaniv <me@olegivaniv.com>
Co-authored-by: Milorad FIlipović <milorad@n8n.io>
Co-authored-by: Iván Ovejero <ivov.src@gmail.com>
Co-authored-by: Mutasem Aldmour <4711238+mutdmour@users.noreply.github.com>

* perf(editor): Memoize locale translate calls during actions generation (#6773)

performance(editor): Memoize locale translate calls during actions generation

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* fix(editor): Close tags dropdown when modal is opened (#6766)

* feat: remove vue-fragment (no-changelog)

* feat: partial design-system migration

* feat: migrate info-accordion and info-tip components

* feat: migrate several components to vue 3

* feat: migrated several components

* feat: migrate several components

* feat: migrate several components

* feat: migrate several components

* feat: re-exported all design system components

* fix: fix design for popper components

* fix: editor kind of working, lots of issues to fix

* fix: fix several vue 3 migration issues

* fix: replace @change with @update:modelValue in several places

* fix: fix translation linking

* fix: fix inline-edit input

* fix: fix ndv and dialog design

* fix: update parameter input event bindings

* fix: rename deprecated lifecycle methods

* fix: fix json view mapping

* build: update lock file

* fix(editor): revisit last conflict with master and fix issues

* fix(editor): revisit last conflict with master and fix issues

* fix: fix expression editor bug causing code mirror to no longer be reactive

* fix: fix resource locator bug

* fix: fix vue-agile integration

* fix: remove global import for vue-agile

* fix: replace element-plus buttons with n8n-buttons everywhere

* fix(editor): Fix various element-plus styles (#6571)

* fix(editor): Fix various element-plus styles

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* Remove debugging code

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* Address PR comments

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

---------

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* fix(editor): Fix loading in production mode [Vue 3] (#6578)

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* fix(editor): First round of e2e tests fixes with Vue 3 (#6579)

* fix(editor): Fix broken smoke and workflow list e2e tests
* ✔️ Fix failing canvas action tests. Updating some selectors used in credentials and workflow tests

* feat: add vue 3 eslint rules and fix issues

* fix: fix tags-dropdown

* fix: fix white-space issues caused by i18n-t

* fix: rename non-generic click events

* fix: fix search in resources list layout

* fix: fix datatable paginator

* fix: fix popper select caret and dropdown size

* fix: add width to action-dropdown

* fix: fix workflow settings icon not being hidden

* fix: refactor newly added code

* fix: fix merge issue

* fix: fix ndv credentials watcher

* fix: fix workflow saving and grabber notch

* fix: fix nodes list panel transition

* fix: fix node title visibility

* fix: fix data unpinning

* fix: fix value access

* fix: show  input panel only if trigger panel enabled or not trigger node

* fix: fix tags dropdown and executions status spcing

* fix(editor): Prevent execution list to load back when leaving the route (#6697)

fix(editor): prevent execution list to load back when leaving the route

* fix: fix drawer visibility

* fix: fix expression toggle padding

* fix: fix expressions editor styling

* chore: prepare for testing

* fix: fix styling for el-button without patching

* test: fix unit tests in design-system

* test: fix most unit tests

* fix: remove import cycle.

* fix: fix personalization modal tests

* fix further resource mapper test adjustments

* fix: fix multiple tests and n8n-route attr duplication

* fix: fix source control tets

* fix: fixed remaining unit tests

* fix: fix workflows and credentials e2e tests

* fix: fix localizeNodeNames

* fix: update ndv e2e tests

* fix: fix popper left placement arrow

* fix: fix 5-ndv e2e tests

* fix: fix 6-code-node e2e tests

* fix(editor): Drop click outside directive from NodeCreator (#6716)

* fix(editor): Drop click outside directive from NodeCreator

* fix(editor): make sure mouseup outside is unbound at least before the component is unmounted

* fix: fix 10-settings-log-streaming e2e tests

* fix: fix node redrawing

* fix: fix tooltip buttons styling

* fix: fix varous e2e suites

* fix: fix 15-scheduler-node e2e suite

* fix: fix route watcher

* fix: fixed param name update and credential edit

* feat: update event names

* refactor: Remove deprecated `$data` (#6576)

Co-authored-by: Alex Grozav <alex@grozav.com>

* fix: fix 17-sharing e2e suite

* fix: fix tags dropdown

* fix: fix tags manager

* fix(editor): move :deep selectors to a separate scoped style block

* fix: fix sticky component and inline text edit

* fix: update e2e tests

* fix: remove button override references

* fix(editor): Adjust spacing in templates for Vue 3 (#6744)

* fix(editor): Adjust spacing in templates

* fix: Undo unneeded change

* fix: Undo unneeded change

* fix(editor): Adjust NDV height for Vue 3 (#6742)

fix(editor): Adjust NDV height

* fix(editor): Restore collapsed sidebar items for Vue 3 (#6743)

fix(editor): Restore collapsed sidebar items

* fix: fix linting issues

* fix: fix design-system deps

* fix: post-merge fixes

* fix: update tests

* fix: increase timeout for executionslist tets

* chore: fix linting issue

* fix: fix 14-mapping e2e tests in ci

* fix: re-enable tests

* fix: fix workflow duplication e2e tests after tags update

* fix(editor): Change component prop to be typed

* fix: fix tags dropdown in duplicate wf modal

* fix: fix focus behaviour in tags selector

* fix: fix tag creation

* fix: fix log streaming e2e race condition

* fix(editor): Fix Vue 3 linting issues (#6748)

* fix(editor): Fix Vue 3 linting issues

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* fix MainSidebar linter issues

* revert pnpm lock

* update pnpm lock file

---------

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>
Co-authored-by: Alex Grozav <alex@grozav.com>

* fix(editor): Some css fixes for vue3 branch (#6749)

*  Fixing filter button height

*  Update input modal button position

*  Updating tags styling

*  Fix event logging settings spacing

* 👕 Fixing lint errors

* fix: fix linting issues

* Revert to `// eslint-disable-next-line @typescript-eslint/no-misused-promises` disabling of mixins init

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* fix: fix css issue

* fix(editor): Lint fix

* fix(editor): Fix settings initialisation (#6750)

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* fix: fix initial settings loading

* fix: replace realClick with click force

* fix: fix randomly failing mapping e2e tests

* fix(editor): Fix menu item event handling

* fix: fix resource filters dropdown events (#6752)

* fix: fix resource filters dropdown events

* fix: remove teleported:false

* fix: fix event selection event naming (#6753)

* fix: removed console.log (#6754)

* fix: rever await nextTick changes

* fix: redo linting changes

* fix(editor): Redraw node connections if adding more than one node to canvas (#6755)

* fix(editor): Redraw node connections if adding more than one node to canvas

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* Update position before connection two nodes

* Lint fix

---------

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>
Co-authored-by: Alex Grozav <alex@grozav.com>

* fix(editor): Fix `ResourceMapper` unit tests (#6758)

* ✔️ Fix matching columns test

* ✔️ Fix multiple matching columns test

* ✔️ Removing `skip` from the last test

* fix: Allow pasting a big workflow (#6760)

* fix: pasting a big workflow

* chore: update comment

* refactor: move try/catch to function

* refactor: move try/catch to function

* fix(editor): Fix modal layer width

* fix: fix position changes

* fix: undo it.only

* fix: make undo/redo multiple steps more verbose

* fix: Fix value survey styles (#6764)

* fix: fix value survey styles

* fix: lint

* Revert "fix: lint"

72869c431f

* fix: lint

* fix(editor): Close tags dropdown when modal is opened

* ✔️ Updating tag selectors in e2e tests

* ✔️ Using tab to blur dropdown after adding tags

* ✔️ Clicking on the New Tab button instead of the tags dropdown to open it

* Reverting merge changes added by mistake

---------

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>
Co-authored-by: Alex Grozav <alex@grozav.com>
Co-authored-by: Csaba Tuncsik <csaba@n8n.io>
Co-authored-by: OlegIvaniv <me@olegivaniv.com>
Co-authored-by: Iván Ovejero <ivov.src@gmail.com>
Co-authored-by: Mutasem Aldmour <4711238+mutdmour@users.noreply.github.com>

* fix: Show NodeIcon tooltips by removing pointer-events: none (#6777)

fix: show NodeIcon tooltips by removing pointer-events: none

* fix: Respect set modal widths (#6771)

* feat: remove vue-fragment (no-changelog)

* feat: partial design-system migration

* feat: migrate info-accordion and info-tip components

* feat: migrate several components to vue 3

* feat: migrated several components

* feat: migrate several components

* feat: migrate several components

* feat: migrate several components

* feat: re-exported all design system components

* fix: fix design for popper components

* fix: editor kind of working, lots of issues to fix

* fix: fix several vue 3 migration issues

* fix: replace @change with @update:modelValue in several places

* fix: fix translation linking

* fix: fix inline-edit input

* fix: fix ndv and dialog design

* fix: update parameter input event bindings

* fix: rename deprecated lifecycle methods

* fix: fix json view mapping

* build: update lock file

* fix(editor): revisit last conflict with master and fix issues

* fix(editor): revisit last conflict with master and fix issues

* fix: fix expression editor bug causing code mirror to no longer be reactive

* fix: fix resource locator bug

* fix: fix vue-agile integration

* fix: remove global import for vue-agile

* fix: replace element-plus buttons with n8n-buttons everywhere

* fix(editor): Fix various element-plus styles (#6571)

* fix(editor): Fix various element-plus styles

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* Remove debugging code

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* Address PR comments

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

---------

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* fix(editor): Fix loading in production mode [Vue 3] (#6578)

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* fix(editor): First round of e2e tests fixes with Vue 3 (#6579)

* fix(editor): Fix broken smoke and workflow list e2e tests
* ✔️ Fix failing canvas action tests. Updating some selectors used in credentials and workflow tests

* feat: add vue 3 eslint rules and fix issues

* fix: fix tags-dropdown

* fix: fix white-space issues caused by i18n-t

* fix: rename non-generic click events

* fix: fix search in resources list layout

* fix: fix datatable paginator

* fix: fix popper select caret and dropdown size

* fix: add width to action-dropdown

* fix: fix workflow settings icon not being hidden

* fix: refactor newly added code

* fix: fix merge issue

* fix: fix ndv credentials watcher

* fix: fix workflow saving and grabber notch

* fix: fix nodes list panel transition

* fix: fix node title visibility

* fix: fix data unpinning

* fix: fix value access

* fix: show  input panel only if trigger panel enabled or not trigger node

* fix: fix tags dropdown and executions status spcing

* fix(editor): Prevent execution list to load back when leaving the route (#6697)

fix(editor): prevent execution list to load back when leaving the route

* fix: fix drawer visibility

* fix: fix expression toggle padding

* fix: fix expressions editor styling

* chore: prepare for testing

* fix: fix styling for el-button without patching

* test: fix unit tests in design-system

* test: fix most unit tests

* fix: remove import cycle.

* fix: fix personalization modal tests

* fix further resource mapper test adjustments

* fix: fix multiple tests and n8n-route attr duplication

* fix: fix source control tets

* fix: fixed remaining unit tests

* fix: fix workflows and credentials e2e tests

* fix: fix localizeNodeNames

* fix: update ndv e2e tests

* fix: fix popper left placement arrow

* fix: fix 5-ndv e2e tests

* fix: fix 6-code-node e2e tests

* fix(editor): Drop click outside directive from NodeCreator (#6716)

* fix(editor): Drop click outside directive from NodeCreator

* fix(editor): make sure mouseup outside is unbound at least before the component is unmounted

* fix: fix 10-settings-log-streaming e2e tests

* fix: fix node redrawing

* fix: fix tooltip buttons styling

* fix: fix varous e2e suites

* fix: fix 15-scheduler-node e2e suite

* fix: fix route watcher

* fix: fixed param name update and credential edit

* feat: update event names

* refactor: Remove deprecated `$data` (#6576)

Co-authored-by: Alex Grozav <alex@grozav.com>

* fix: fix 17-sharing e2e suite

* fix: fix tags dropdown

* fix: fix tags manager

* fix(editor): move :deep selectors to a separate scoped style block

* fix: fix sticky component and inline text edit

* fix: update e2e tests

* fix: remove button override references

* fix(editor): Adjust spacing in templates for Vue 3 (#6744)

* fix(editor): Adjust spacing in templates

* fix: Undo unneeded change

* fix: Undo unneeded change

* fix(editor): Adjust NDV height for Vue 3 (#6742)

fix(editor): Adjust NDV height

* fix(editor): Restore collapsed sidebar items for Vue 3 (#6743)

fix(editor): Restore collapsed sidebar items

* fix: fix linting issues

* fix: fix design-system deps

* fix: post-merge fixes

* fix: update tests

* fix: increase timeout for executionslist tets

* chore: fix linting issue

* fix: fix 14-mapping e2e tests in ci

* fix: re-enable tests

* fix: fix workflow duplication e2e tests after tags update

* fix(editor): Change component prop to be typed

* fix: fix tags dropdown in duplicate wf modal

* fix: fix focus behaviour in tags selector

* fix: fix tag creation

* fix: fix log streaming e2e race condition

* fix(editor): Fix Vue 3 linting issues (#6748)

* fix(editor): Fix Vue 3 linting issues

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* fix MainSidebar linter issues

* revert pnpm lock

* update pnpm lock file

---------

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>
Co-authored-by: Alex Grozav <alex@grozav.com>

* fix(editor): Some css fixes for vue3 branch (#6749)

*  Fixing filter button height

*  Update input modal button position

*  Updating tags styling

*  Fix event logging settings spacing

* 👕 Fixing lint errors

* fix: fix linting issues

* Revert to `// eslint-disable-next-line @typescript-eslint/no-misused-promises` disabling of mixins init

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* fix: fix css issue

* fix(editor): Lint fix

* fix(editor): Fix settings initialisation (#6750)

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* fix: fix initial settings loading

* fix: replace realClick with click force

* fix: fix randomly failing mapping e2e tests

* fix(editor): Fix menu item event handling

* fix: fix resource filters dropdown events (#6752)

* fix: fix resource filters dropdown events

* fix: remove teleported:false

* fix: fix event selection event naming (#6753)

* fix: removed console.log (#6754)

* fix: rever await nextTick changes

* fix: redo linting changes

* fix(editor): Redraw node connections if adding more than one node to canvas (#6755)

* fix(editor): Redraw node connections if adding more than one node to canvas

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* Update position before connection two nodes

* Lint fix

---------

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>
Co-authored-by: Alex Grozav <alex@grozav.com>

* fix(editor): Fix `ResourceMapper` unit tests (#6758)

* ✔️ Fix matching columns test

* ✔️ Fix multiple matching columns test

* ✔️ Removing `skip` from the last test

* fix: Allow pasting a big workflow (#6760)

* fix: pasting a big workflow

* chore: update comment

* refactor: move try/catch to function

* refactor: move try/catch to function

* fix(editor): Fix modal layer width

* fix: fix position changes

* fix: undo it.only

* fix: make undo/redo multiple steps more verbose

* fix: Fix value survey styles (#6764)

* fix: fix value survey styles

* fix: lint

* Revert "fix: lint"

72869c431f

* fix: lint

* fix(editor): Fix collapsed sub menu

* fix: Fix drawer animation (#6767)

fix: drawer animation

* fix(editor): Fix source control buttons (#6769)

* fix: Respect modal width

---------

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>
Co-authored-by: Alex Grozav <alex@grozav.com>
Co-authored-by: Csaba Tuncsik <csaba@n8n.io>
Co-authored-by: OlegIvaniv <me@olegivaniv.com>
Co-authored-by: Milorad FIlipović <milorad@n8n.io>
Co-authored-by: Iván Ovejero <ivov.src@gmail.com>

* fix(editor): Fix tooltip opening delay prop name (#6776)

fix(editor): fix tooltip opening delay prop name

* fix(editor): Fix collapsed sub menu elements (#6778)

* fix: Remove number input arrows (no-changelog) (#6782)

fix: remove number input arrows

* ci: Update most of the dev tooling (no-changelog) (#6780)

* fix(TheHive Node): Treat  `ApiKey` as a secret (#6786)

* test(editor): Prevent node view unload by default in e2e run (#6787)

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* fix(editor): Resolve vue 3 related console-warnings (#6779)

* fix(editor): Resolve vue 3 related console-warnings

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* Use span as component wrapper instead of div

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* Wrap popover component in span

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

---------

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* fix(editor): Vue3 - Fix modal positioning and multi-select tag sizing (#6783)

*  Updating modals positioning within the overlay
* 💄 Implemented multi-select variant with small tabs
* ✔️ Removing password link clicks while modal is open in e2e tests
* Set generous timeout for $paramter resolve
Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>
---------
Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>
Co-authored-by: Oleg Ivaniv <me@olegivaniv.com>

* ci: Fix linting issues (no-changelog) (#6788)

* ci: Fix linting (no-changelog)

* lintfix for nodes-base as well

* fix(editor): Fix code node highlight error (#6791)

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* feat(core): Credentials for popular SecOps services, Part 1 (#6775)

* refactor: Clear unused ESLint directives from BE packages (no-changelog) (#6798)

* refactor(core): Cache workflow ownership (#6738)

* refactor: Set up ownership service

* refactor: Specify cache keys and values

* refactor: Replace util with service calls

* test: Mock service in tests

* refactor: Use dependency injection

* test: Write tests

* refactor: Apply feedback from Omar and Micha

* test: Fix tests

* test: Fix missing spot

* refactor: Return user entity from cache

* refactor: More dependency injection!

* fix(editor): Prevent text edit dialog from re-opening in same tick (#6781)

* fix: prevent reopenning textedit dialog in same tick

* fix: add same logic for code edit dialog

* fix: remove stop modifier

* fix: blur input field when closing modal, removing default element-plus behaviour

* test(editor): Do not chain invoke calls after assertions in 24-ndv-paired-item e2e spec (no-changelog) (#6800)

* test(editor): Do not chaing invoke calls after assertions in 24-ndv-paired-item e2e spec

* Do not chaing realHover after assertion

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* Remove .only

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

---------

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* fix(Todoist Node): Fix issue with section id being ignored (#6799)

* test(editor): Add canvas actions E2E tests (#6723) (#6790)

* test(editor): Add canvas actions E2E tests (#6723)

* test(editor): Add canvas actions E2E tests

* test(editor): Open category items in node creator when category dropped on canvas

* test(editor): Have new position counted only once in drag

* test(editor): rename test

(cherry picked from commit 052d82b220)

* test: fix drag positioning

* fix(core): Add missing primary key on the `execution_data` table on postgres (#6797)

* fix: Review fixes

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* fix: Fin locales

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* Fix merging errors

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* Map erros based on statusCode

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* Fix code replacing

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* Fix code formatting

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* Address review points

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* Optionally access total_tokens

* Clean-up Ask AI modal

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* Store prompt in sessionStorage

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* Improve schema generation, only get parent nodes

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* Send error messages to telemetry, aske before switching tabs

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* Add locale

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* Post-merge cleanup

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* Move Ask AI into separate folder

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* Lint fix

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* Constants lint fix

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* Add Ask AI e2e tests and fix linting issues

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* Move CircleLoader to design-lib

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* Replace circle-lodaer and move el-tabs styles to n8n theme

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* Fix placeholder & e2e tests

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* Remove old CircleLoader

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

---------

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>
Co-authored-by: ricardo <ricardoespinoza105@gmail.com>
Co-authored-by: Alex Grozav <alex@grozav.com>
Co-authored-by: Romain Dunand <romain@1-more-thing.com>
Co-authored-by: Jon <jonathan.bennetts@gmail.com>
Co-authored-by: Michael Kret <88898367+michael-radency@users.noreply.github.com>
Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <netroy@users.noreply.github.com>
Co-authored-by: Csaba Tuncsik <csaba@n8n.io>
Co-authored-by: Iván Ovejero <ivov.src@gmail.com>
Co-authored-by: Omar Ajoue <krynble@gmail.com>
Co-authored-by: Michael Auerswald <michael.auerswald@gmail.com>
Co-authored-by: Milorad FIlipović <milorad@n8n.io>
Co-authored-by: Michael Kret <michael.k@radency.com>
Co-authored-by: Val <68596159+valya@users.noreply.github.com>
Co-authored-by: Marcus <56945030+maspio@users.noreply.github.com>
Co-authored-by: Jordan Hall <Jordan@libertyware.co.uk>
Co-authored-by: qg-horie <36725144+qg-horie@users.noreply.github.com>
Co-authored-by: Ricardo Espinoza <ricardo@n8n.io>
Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in>
Co-authored-by: Ali Afsharzadeh <afsharzadeh8@gmail.com>
Co-authored-by: Giulio Andreini <g.andreini@gmail.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Mutasem Aldmour <4711238+mutdmour@users.noreply.github.com>
This commit is contained in:
OlegIvaniv 2023-08-16 13:08:10 +02:00 committed by GitHub
parent 44afcff959
commit fde6ad1e7f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 1440 additions and 559 deletions

View file

@ -5,24 +5,30 @@ const WorkflowPage = new WorkflowPageClass();
const ndv = new NDV(); const ndv = new NDV();
describe('Code node', () => { describe('Code node', () => {
describe('Code editor', () => {
beforeEach(() => { beforeEach(() => {
WorkflowPage.actions.visit(); WorkflowPage.actions.visit();
WorkflowPage.actions.addInitialNodeToCanvas('Manual');
WorkflowPage.actions.addNodeToCanvas('Code', true, true);
}); });
it('should execute the placeholder in all-items mode successfully', () => { it('should show correct placeholders switching modes', () => {
WorkflowPage.actions.addInitialNodeToCanvas('Manual'); cy.contains("// Loop over input items and add a new field").should('be.visible');
WorkflowPage.actions.addNodeToCanvas('Code');
WorkflowPage.actions.openNode('Code');
ndv.getters.parameterInput('mode').click();
ndv.actions.selectOptionInParameterDropdown('mode', 'Run Once for Each Item');
cy.contains("// Add a new field called 'myNewField'").should('be.visible');
ndv.getters.parameterInput('mode').click();
ndv.actions.selectOptionInParameterDropdown('mode', 'Run Once for All Items');
cy.contains("// Loop over input items and add a new field").should('be.visible');
})
it('should execute the placeholder successfully in both modes', () => {
ndv.actions.execute(); ndv.actions.execute();
WorkflowPage.getters.successToast().contains('Node executed successfully'); WorkflowPage.getters.successToast().contains('Node executed successfully');
});
it('should execute the placeholder in each-item mode successfully', () => {
WorkflowPage.actions.addInitialNodeToCanvas('Manual');
WorkflowPage.actions.addNodeToCanvas('Code');
WorkflowPage.actions.openNode('Code');
ndv.getters.parameterInput('mode').click(); ndv.getters.parameterInput('mode').click();
ndv.actions.selectOptionInParameterDropdown('mode', 'Run Once for Each Item'); ndv.actions.selectOptionInParameterDropdown('mode', 'Run Once for Each Item');
@ -30,4 +36,127 @@ describe('Code node', () => {
WorkflowPage.getters.successToast().contains('Node executed successfully'); WorkflowPage.getters.successToast().contains('Node executed successfully');
}); });
})
describe('Ask AI', () => {
it('tab should display based on experiment', () => {
WorkflowPage.actions.visit();
cy.window().then((win) => {
win.featureFlags.override('011_ask_AI', 'control');
WorkflowPage.actions.addInitialNodeToCanvas('Manual');
WorkflowPage.actions.addNodeToCanvas('Code');
WorkflowPage.actions.openNode('Code');
cy.getByTestId('code-node-tab-ai').should('not.exist');
ndv.actions.close();
win.featureFlags.override('011_ask_AI', undefined);
WorkflowPage.actions.openNode('Code');
cy.getByTestId('code-node-tab-ai').should('not.exist');
})
})
describe('Enabled', () => {
beforeEach(() => {
WorkflowPage.actions.visit();
cy.window().then((win) => {
win.featureFlags.override('011_ask_AI', 'gpt3');
WorkflowPage.actions.addInitialNodeToCanvas('Manual');
WorkflowPage.actions.addNodeToCanvas('Code', true, true);
})
})
it('tab should exist if experiment selected and be selectable', () => {
cy.getByTestId('code-node-tab-ai').should('exist');
cy.get('#tab-ask-ai').click();
cy.contains('Hey AI, generate JavaScript').should('exist');
})
it('generate code button should have correct state & tooltips', () => {
cy.getByTestId('code-node-tab-ai').should('exist');
cy.get('#tab-ask-ai').click();
cy.getByTestId('ask-ai-cta').should('be.disabled');
cy.getByTestId('ask-ai-cta').realHover();
cy.getByTestId('ask-ai-cta-tooltip-no-input-data').should('exist');
ndv.actions.executePrevious();
cy.getByTestId('ask-ai-cta').realHover();
cy.getByTestId('ask-ai-cta-tooltip-no-prompt').should('exist');
cy.getByTestId('ask-ai-prompt-input')
// Type random 14 character string
.type([...Array(14)].map(() => (Math.random() * 36 | 0).toString(36)).join(''))
cy.getByTestId('ask-ai-cta').realHover();
cy.getByTestId('ask-ai-cta-tooltip-prompt-too-short').should('exist');
cy.getByTestId('ask-ai-prompt-input')
.clear()
// Type random 15 character string
.type([...Array(15)].map(() => (Math.random() * 36 | 0).toString(36)).join(''))
cy.getByTestId('ask-ai-cta').should('be.enabled');
cy.getByTestId('ask-ai-prompt-counter').should('contain.text', '15 / 600');
})
it('should send correct schema and replace code', () => {
const prompt = [...Array(20)].map(() => (Math.random() * 36 | 0).toString(36)).join('');
cy.get('#tab-ask-ai').click();
ndv.actions.executePrevious();
cy.getByTestId('ask-ai-prompt-input').type(prompt)
cy.intercept('POST', '/rest/ask-ai', {
statusCode: 200,
body: {
data: {
code: 'console.log("Hello World")',
usage: {
prompt_tokens: 15,
completion_tokens: 15,
total_tokens: 30
}
},
}
}).as('ask-ai');
cy.getByTestId('ask-ai-cta').click();
cy.wait('@ask-ai')
.its('request.body')
.should('deep.include', {
question: prompt,
model: "gpt-3.5-turbo-16k",
context: { schema: [] }
});
cy.contains('Code generation completed').should('be.visible')
cy.getByTestId('code-node-tab-code').should('contain.text', 'console.log("Hello World")');
cy.get('#tab-code').should('have.class', 'is-active');
})
it('should show error based on status code', () => {
const prompt = [...Array(20)].map(() => (Math.random() * 36 | 0).toString(36)).join('');
cy.get('#tab-ask-ai').click();
ndv.actions.executePrevious();
cy.getByTestId('ask-ai-prompt-input').type(prompt)
const handledCodes = [
{ code: 400, message: 'Code generation failed due to an unknown reason' },
{ code: 413, message: 'Your workflow data is too large for AI to process' },
{ code: 429, message: 'We\'ve hit our rate limit with our AI partner' },
{ code: 500, message: 'Code generation failed due to an unknown reason' },
]
handledCodes.forEach(({ code, message }) => {
cy.intercept('POST', '/rest/ask-ai', {
statusCode: code,
status: code,
}).as('ask-ai');
cy.getByTestId('ask-ai-cta').click();
cy.contains(message).should('be.visible')
})
})
})
});
}); });

View file

@ -114,7 +114,7 @@ export class NDV extends BasePage {
cy.get('body').type('{enter}'); cy.get('body').type('{enter}');
}, },
executePrevious: () => { executePrevious: () => {
this.getters.executePrevious().click(); this.getters.executePrevious().click({ force: true });
}, },
mapDataFromHeader: (col: number, parameterName: string) => { mapDataFromHeader: (col: number, parameterName: string) => {
const draggable = `[data-test-id="ndv-input-panel"] [data-test-id="ndv-data-container"] table th:nth-child(${col})`; const draggable = `[data-test-id="ndv-input-panel"] [data-test-id="ndv-data-container"] table th:nth-child(${col})`;

View file

@ -325,6 +325,9 @@ export class Server extends AbstractServer {
banners: { banners: {
dismissed: [], dismissed: [],
}, },
ai: {
enabled: config.getEnv('ai.enabled'),
},
}; };
} }

View file

@ -18,6 +18,7 @@ if (inE2ETests) {
N8N_PUBLIC_API_DISABLED: 'true', N8N_PUBLIC_API_DISABLED: 'true',
EXTERNAL_FRONTEND_HOOKS_URLS: '', EXTERNAL_FRONTEND_HOOKS_URLS: '',
N8N_PERSONALIZATION_ENABLED: 'false', N8N_PERSONALIZATION_ENABLED: 'false',
N8N_AI_ENABLED: 'true',
}; };
} else if (inTest) { } else if (inTest) {
const testsDir = join(tmpdir(), 'n8n-tests/'); const testsDir = join(tmpdir(), 'n8n-tests/');

View file

@ -1174,4 +1174,13 @@ export const schema = {
}, },
}, },
}, },
ai: {
enabled: {
doc: 'Whether AI features are enabled',
format: Boolean,
default: false,
env: 'N8N_AI_ENABLED',
},
},
}; };

View file

@ -0,0 +1,50 @@
import N8nCircleLoader from './CircleLoader.vue';
import type { StoryFn } from '@storybook/vue3';
export default {
title: 'Atoms/CircleLoader',
component: N8nCircleLoader,
argTypes: {
radius: {
control: {
type: 'number',
},
},
progress: {
control: {
type: 'number',
},
},
strokeWidth: {
control: {
type: 'number',
},
},
},
};
interface Args {
radius: number;
progress: number;
strokeWidth: number;
}
const template: StoryFn<Args> = (args, { argTypes }) => ({
setup: () => ({ args }),
props: Object.keys(argTypes),
components: {
N8nCircleLoader,
},
template: `
<div>
<n8n-circle-loader v-bind="args" />
</div>
`,
});
export const defaultCircleLoader = template.bind({});
defaultCircleLoader.args = {
radius: 20,
progress: 42,
strokeWidth: 10,
};

View file

@ -0,0 +1,63 @@
<template>
<div class="progress-circle">
<svg class="progress-ring" :width="diameter" :height="diameter">
<circle
:class="$style.progressRingCircle"
:stroke-width="strokeWidth"
stroke="#DCDFE6"
fill="transparent"
:r="radius"
v-bind="{ cx, cy }"
/>
<circle
:class="$style.progressRingCircle"
stroke="#5C4EC2"
:stroke-width="strokeWidth"
fill="transparent"
:r="radius"
v-bind="{ cx, cy, style }"
/>
</svg>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
const props = defineProps({
radius: {
type: Number,
required: true,
},
progress: {
type: Number,
required: true,
},
strokeWidth: {
type: Number,
default: 4,
},
});
// for SVG viewbox and stroke array
const diameter = computed(() => 2 * (props.radius + props.strokeWidth));
const circumference = computed(() => 2 * Math.PI * props.radius);
const strokeDashoffset = computed(
() => circumference.value - (props.progress / 100) * circumference.value,
);
const cx = computed(() => props.radius + props.strokeWidth);
const cy = computed(() => props.radius + props.strokeWidth);
const style = computed(() => ({
strokeDasharray: `${circumference.value}`,
strokeDashoffset: `${strokeDashoffset.value}`,
}));
</script>
<style module>
.progressRingCircle {
transition: stroke-dashoffset 0.35s linear;
transform: rotate(-90deg);
transform-origin: 50% 50%;
}
</style>

View file

@ -0,0 +1,15 @@
import { render } from '@testing-library/vue';
import N8NCircleLoader from '../CircleLoader.vue';
describe('N8NCircleLoader', () => {
it('should render correctly', () => {
const wrapper = render(N8NCircleLoader, {
props: {
radius: 20,
progress: 42,
strokeWidth: 10,
},
});
expect(wrapper.html()).toMatchSnapshot();
});
});

View file

@ -0,0 +1,8 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`N8NCircleLoader > should render correctly 1`] = `
"<div class=\\"progress-circle\\"><svg class=\\"progress-ring\\" width=\\"60\\" height=\\"60\\">
<circle class=\\"progressRingCircle\\" stroke-width=\\"10\\" stroke=\\"#DCDFE6\\" fill=\\"transparent\\" r=\\"20\\" cx=\\"30\\" cy=\\"30\\"></circle>
<circle class=\\"progressRingCircle\\" stroke=\\"#5C4EC2\\" stroke-width=\\"10\\" fill=\\"transparent\\" r=\\"20\\" cx=\\"30\\" cy=\\"30\\" style=\\"stroke-dasharray: 125.66370614359172; stroke-dashoffset: 72.8849495632832;\\"></circle>
</svg></div>"
`;

View file

@ -0,0 +1,3 @@
import N8nCircleLoader from './CircleLoader.vue';
export default N8nCircleLoader;

View file

@ -9,6 +9,7 @@ export { default as N8nButton } from './N8nButton';
export { default as N8nCallout } from './N8nCallout'; export { default as N8nCallout } from './N8nCallout';
export { default as N8nCard } from './N8nCard'; export { default as N8nCard } from './N8nCard';
export { default as N8nCheckbox } from './N8nCheckbox'; export { default as N8nCheckbox } from './N8nCheckbox';
export { default as N8nCircleLoader } from './N8nCircleLoader';
export { default as N8nColorPicker } from './N8nColorPicker'; export { default as N8nColorPicker } from './N8nColorPicker';
export { default as N8nDatatable } from './N8nDatatable'; export { default as N8nDatatable } from './N8nDatatable';
export { default as N8nFormBox } from './N8nFormBox'; export { default as N8nFormBox } from './N8nFormBox';

View file

@ -12,6 +12,7 @@ import {
N8nCallout, N8nCallout,
N8nCard, N8nCard,
N8nCheckbox, N8nCheckbox,
N8nCircleLoader,
N8nColorPicker, N8nColorPicker,
N8nDatatable, N8nDatatable,
N8nFormBox, N8nFormBox,
@ -66,6 +67,7 @@ export const N8nPlugin: Plugin<{}> = {
app.component('n8n-callout', N8nCallout); app.component('n8n-callout', N8nCallout);
app.component('n8n-card', N8nCard); app.component('n8n-card', N8nCard);
app.component('n8n-checkbox', N8nCheckbox); app.component('n8n-checkbox', N8nCheckbox);
app.component('n8n-circle-loader', N8nCircleLoader);
app.component('n8n-color-picker', N8nColorPicker); app.component('n8n-color-picker', N8nColorPicker);
app.component('n8n-datatable', N8nDatatable); app.component('n8n-datatable', N8nDatatable);
app.component('n8n-form-box', N8nFormBox); app.component('n8n-form-box', N8nFormBox);

View file

@ -0,0 +1,34 @@
import type { IRestApiContext, Schema } from '@/Interface';
import { makeRestApiRequest } from '@/utils/apiUtils';
import type { IDataObject } from 'n8n-workflow';
type Usage = {
prompt_tokens: number;
completion_tokens: number;
total_tokens: number;
};
export async function generateCodeForPrompt(
ctx: IRestApiContext,
{
question,
context,
model,
n8nVersion,
}: {
question: string;
context: {
schema: Array<{ nodeName: string; schema: Schema }>;
inputSchema: { nodeName: string; schema: Schema };
};
model: string;
n8nVersion: string;
},
): Promise<{ code: string; usage: Usage }> {
return makeRestApiRequest(ctx, 'POST', '/ask-ai', {
question,
context,
model,
n8nVersion,
} as IDataObject);
}

View file

@ -1,52 +0,0 @@
<template>
<Modal
width="460px"
:center="true"
:eventBus="modalBus"
:name="ASK_AI_MODAL_KEY"
:title="$locale.baseText('askAi.dialog.title')"
>
<template #content>
<n8n-text v-html="$locale.baseText('askAi.dialog.body')"></n8n-text>
</template>
<template #footer>
<n8n-link :to="ASK_AI_WAITLIST_URL">
<n8n-button
float="right"
:label="$locale.baseText('askAi.dialog.signup')"
@click="onAskAiWaitlistClick"
/>
</n8n-link>
</template>
</Modal>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import Modal from './Modal.vue';
import { ASK_AI_MODAL_KEY, ASK_AI_WAITLIST_URL } from '../constants';
import { createEventBus } from 'n8n-design-system/utils';
export default defineComponent({
name: 'AskAI',
components: {
Modal,
},
data() {
return {
ASK_AI_WAITLIST_URL,
ASK_AI_MODAL_KEY,
modalBus: createEventBus(),
};
},
methods: {
onAskAiWaitlistClick() {
this.$telemetry.track('User clicked join waitlist', { source: 'ask-ai-code' });
this.modalBus.emit('close');
},
},
});
</script>
<style module lang="scss"></style>

View file

@ -0,0 +1,392 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
import { snakeCase } from 'lodash-es';
import { useSessionStorage } from '@vueuse/core';
import { N8nButton, N8nInput, N8nTooltip } from 'n8n-design-system/components';
import type { CodeExecutionMode, INodeExecutionData } from 'n8n-workflow';
import type { BaseTextKey } from '@/plugins/i18n';
import type { INodeUi, Schema } from '@/Interface';
import { generateCodeForPrompt } from '@/api/ai';
import { useDataSchema, useI18n, useMessage, useToast, useTelemetry } from '@/composables';
import { useNDVStore, usePostHog, useRootStore, useWorkflowsStore } from '@/stores';
import { executionDataToJson } from '@/utils';
import {
ASK_AI_EXPERIMENT,
ASK_AI_MAX_PROMPT_LENGTH,
ASK_AI_MIN_PROMPT_LENGTH,
ASK_AI_LOADING_DURATION_MS,
} from '@/constants';
const emit = defineEmits<{
(e: 'submit', code: string): void;
(e: 'replaceCode', code: string): void;
(e: 'startedLoading'): void;
(e: 'finishedLoading'): void;
}>();
const props = defineProps<{
hasChanges: boolean;
}>();
const { getSchemaForExecutionData, getInputDataWithPinned } = useDataSchema();
const i18n = useI18n();
const loadingPhraseIndex = ref(0);
const loaderProgress = ref(0);
const isLoading = ref(false);
const prompt = ref('');
const parentNodes = ref<INodeUi[]>([]);
const isSubmitEnabled = computed(() => {
return (
!isEachItemMode.value &&
prompt.value.length >= ASK_AI_MIN_PROMPT_LENGTH &&
hasExecutionData.value
);
});
const hasExecutionData = computed(() => (useNDVStore().ndvInputData || []).length > 0);
const loadingString = computed(() =>
i18n.baseText(`codeNodeEditor.askAi.loadingPhrase${loadingPhraseIndex.value}` as BaseTextKey),
);
const isEachItemMode = computed(() => {
const mode = useNDVStore().activeNode?.parameters.mode as CodeExecutionMode;
return mode === 'runOnceForEachItem';
});
function getErrorMessageByStatusCode(statusCode: number) {
const errorMessages: Record<number, string> = {
400: i18n.baseText('codeNodeEditor.askAi.generationFailedUnknown'),
413: i18n.baseText('codeNodeEditor.askAi.generationFailedTooLarge'),
429: i18n.baseText('codeNodeEditor.askAi.generationFailedRate'),
500: i18n.baseText('codeNodeEditor.askAi.generationFailedUnknown'),
};
return errorMessages[statusCode] || i18n.baseText('codeNodeEditor.askAi.generationFailedUnknown');
}
function getParentNodes() {
const activeNode = useNDVStore().activeNode;
const { getCurrentWorkflow, getNodeByName } = useWorkflowsStore();
const workflow = getCurrentWorkflow();
if (!activeNode || !workflow) return [];
return workflow
.getParentNodesByDepth(activeNode?.name)
.filter(({ name }, i, nodes) => {
return name !== activeNode.name && nodes.findIndex((node) => node.name === name) === i;
})
.map((n) => getNodeByName(n.name))
.filter((n) => n !== null) as INodeUi[];
}
function getSchemas() {
const parentNodesNames = parentNodes.value.map((node) => node?.name);
const parentNodesSchemas: Array<{ nodeName: string; schema: Schema }> = parentNodes.value
.map((node) => {
const inputData: INodeExecutionData[] = getInputDataWithPinned(node);
return {
nodeName: node?.name || '',
schema: getSchemaForExecutionData(executionDataToJson(inputData), true),
};
})
.filter((node) => node.schema?.value.length > 0);
const inputSchema = parentNodesSchemas.shift();
return {
parentNodesNames,
inputSchema,
parentNodesSchemas,
};
}
function startLoading() {
emit('startedLoading');
loaderProgress.value = 0;
isLoading.value = true;
triggerLoadingChange();
}
function stopLoading() {
loaderProgress.value = 100;
emit('finishedLoading');
setTimeout(() => {
isLoading.value = false;
}, 200);
}
async function onSubmit() {
const { getRestApiContext } = useRootStore();
const { activeNode } = useNDVStore();
const { showMessage } = useToast();
const { alert } = useMessage();
if (!activeNode) return;
const schemas = getSchemas();
useTelemetry().trackAskAI('ask.generationClicked', {
prompt: prompt.value,
});
if (props.hasChanges) {
const confirmModal = await alert(i18n.baseText('codeNodeEditor.askAi.areYouSureToReplace'), {
title: i18n.baseText('codeNodeEditor.askAi.replaceCurrentCode'),
confirmButtonText: i18n.baseText('codeNodeEditor.askAi.generateCodeAndReplace'),
showClose: true,
showCancelButton: true,
});
if (confirmModal === 'cancel') {
return;
}
}
startLoading();
try {
const version = useRootStore().versionCli;
const model =
usePostHog().getVariant(ASK_AI_EXPERIMENT.name) === ASK_AI_EXPERIMENT.gpt4
? 'gpt-4'
: 'gpt-3.5-turbo-16k';
const { code, usage } = await generateCodeForPrompt(getRestApiContext, {
question: prompt.value,
context: { schema: schemas.parentNodesSchemas, inputSchema: schemas.inputSchema! },
model,
n8nVersion: version,
});
stopLoading();
emit('replaceCode', code);
showMessage({
type: 'success',
title: i18n.baseText('codeNodeEditor.askAi.generationCompleted'),
});
useTelemetry().trackAskAI('askAi.generationFinished', {
prompt: prompt.value,
code,
tokensCount: usage?.total_tokens,
hasErrors: false,
error: '',
});
} catch (error) {
showMessage({
type: 'error',
title: i18n.baseText('codeNodeEditor.askAi.generationFailed'),
message: getErrorMessageByStatusCode(error.httpStatusCode || error?.response.status),
});
useTelemetry().trackAskAI('askAi.generationFinished', {
prompt: prompt.value,
code: '',
tokensCount: 0,
hasErrors: true,
error: getErrorMessageByStatusCode(error.httpStatusCode || error?.response.status),
});
stopLoading();
}
}
function triggerLoadingChange() {
const loadingPhraseUpdateMs = 2000;
const loadingPhrasesCount = 8;
let start: number | null = null;
let lastPhraseChange = 0;
const step = (timestamp: number) => {
if (!start) start = timestamp;
// Loading phrase change
if (!lastPhraseChange || timestamp - lastPhraseChange >= loadingPhraseUpdateMs) {
loadingPhraseIndex.value = Math.floor(Math.random() * loadingPhrasesCount);
lastPhraseChange = timestamp;
}
// Loader progress change
const elapsed = timestamp - start;
loaderProgress.value = Math.min((elapsed / ASK_AI_LOADING_DURATION_MS) * 100, 100);
if (!isLoading.value) return;
if (loaderProgress.value < 100 || lastPhraseChange + loadingPhraseUpdateMs > timestamp) {
window.requestAnimationFrame(step);
}
};
window.requestAnimationFrame(step);
}
function getSessionStoragePrompt() {
const codeNodeName = (useNDVStore().activeNode?.name as string) ?? '';
const hashedCode = snakeCase(codeNodeName);
return useSessionStorage(`ask_ai_prompt__${hashedCode}`, '');
}
function onPromptInput(inputValue: string) {
getSessionStoragePrompt().value = inputValue;
}
onMounted(() => {
// Restore prompt from session storage(with empty string fallback)
prompt.value = getSessionStoragePrompt().value;
parentNodes.value = getParentNodes();
});
</script>
<template>
<div>
<p :class="$style.intro" v-text="i18n.baseText('codeNodeEditor.askAi.intro')" />
<div :class="$style.inputContainer">
<div :class="$style.meta">
<span
v-show="prompt.length > 1"
:class="$style.counter"
v-text="`${prompt.length} / ${ASK_AI_MAX_PROMPT_LENGTH}`"
data-test-id="ask-ai-prompt-counter"
/>
<a href="https://docs.n8n.io/code-examples/ai-code" target="_blank" :class="$style.help">
<n8n-icon icon="question-circle" color="text-light" size="large" />{{
i18n.baseText('codeNodeEditor.askAi.help')
}}
</a>
</div>
<N8nInput
v-model="prompt"
@input="onPromptInput"
:class="$style.input"
type="textarea"
:rows="6"
:maxlength="ASK_AI_MAX_PROMPT_LENGTH"
:placeholder="i18n.baseText('codeNodeEditor.askAi.placeholder')"
data-test-id="ask-ai-prompt-input"
/>
</div>
<div :class="$style.controls">
<div :class="$style.loader" v-if="isLoading">
<transition name="text-fade-in-out" mode="out-in">
<div v-text="loadingString" :key="loadingPhraseIndex" />
</transition>
<n8n-circle-loader :radius="8" :progress="loaderProgress" :stroke-width="3" />
</div>
<n8n-tooltip :disabled="isSubmitEnabled" v-else>
<div>
<N8nButton
:disabled="!isSubmitEnabled"
@click="onSubmit"
size="small"
data-test-id="ask-ai-cta"
>
{{ i18n.baseText('codeNodeEditor.askAi.generateCode') }}
</N8nButton>
</div>
<template #content>
<span
v-if="!hasExecutionData"
v-text="i18n.baseText('codeNodeEditor.askAi.noInputData')"
data-test-id="ask-ai-cta-tooltip-no-input-data"
/>
<span
v-else-if="prompt.length === 0"
v-text="i18n.baseText('codeNodeEditor.askAi.noPrompt')"
data-test-id="ask-ai-cta-tooltip-no-prompt"
/>
<span
v-else-if="isEachItemMode"
v-text="i18n.baseText('codeNodeEditor.askAi.onlyAllItemsMode')"
data-test-id="ask-ai-cta-tooltip-only-all-items-mode"
/>
<span
v-else-if="prompt.length < ASK_AI_MIN_PROMPT_LENGTH"
data-test-id="ask-ai-cta-tooltip-prompt-too-short"
v-text="
i18n.baseText('codeNodeEditor.askAi.promptTooShort', {
interpolate: { minLength: ASK_AI_MIN_PROMPT_LENGTH.toString() },
})
"
/>
</template>
</n8n-tooltip>
</div>
</div>
</template>
<style scoped>
.text-fade-in-out-enter-active,
.text-fade-in-out-leave-active {
transition:
opacity 0.5s ease-in-out,
transform 0.5s ease-in-out;
}
.text-fade-in-out-enter,
.text-fade-in-out-leave-to {
opacity: 0;
transform: translateX(10px);
}
.text-fade-in-out-enter-to,
.text-fade-in-out-leave {
opacity: 1;
}
</style>
<style module lang="scss">
.input * {
border: 0 !important;
}
.input textarea {
font-size: var(--font-size-2xs);
padding-bottom: var(--spacing-2xl);
font-family: var(--font-family);
resize: none;
}
.intro {
font-weight: var(--font-weight-bold);
font-size: var(--font-size-2xs);
color: var(--color-text-dark);
padding: var(--spacing-2xs) var(--spacing-xs) 0;
}
.loader {
font-size: var(--font-size-2xs);
color: var(--color-text-dark);
display: flex;
align-items: center;
gap: var(--spacing-2xs);
}
.inputContainer {
position: relative;
}
.help {
text-decoration: underline;
margin-left: auto;
color: #909399;
}
.meta {
display: flex;
justify-content: space-between;
position: absolute;
bottom: var(--spacing-2xs);
left: var(--spacing-xs);
right: var(--spacing-xs);
z-index: 1;
* {
font-size: var(--font-size-2xs);
line-height: 1;
}
}
.counter {
color: var(--color-text-light);
}
.controls {
padding: var(--spacing-2xs) var(--spacing-xs);
display: flex;
justify-content: flex-end;
border-top: 1px solid var(--border-color-base);
}
</style>

View file

@ -5,24 +5,46 @@
@mouseout="onMouseOut" @mouseout="onMouseOut"
ref="codeNodeEditorContainer" ref="codeNodeEditorContainer"
> >
<div ref="codeNodeEditor" class="code-node-editor-input"></div> <el-tabs
<n8n-button type="card"
v-if="aiButtonEnabled && (isEditorHovered || isEditorFocused)" ref="tabs"
size="small" v-model="activeTab"
type="tertiary" v-if="aiEnabled"
:class="$style['ask-ai-button']" :before-leave="onBeforeTabLeave"
@mousedown="onAskAiButtonClick"
> >
{{ $locale.baseText('codeNodeEditor.askAi') }} <el-tab-pane
</n8n-button> :label="$locale.baseText('codeNodeEditor.tabs.code')"
name="code"
data-test-id="code-node-tab-code"
>
<div ref="codeNodeEditor" class="code-node-editor-input ph-no-capture code-editor-tabs" />
</el-tab-pane>
<el-tab-pane
:label="$locale.baseText('codeNodeEditor.tabs.askAi')"
name="ask-ai"
data-test-id="code-node-tab-ai"
>
<!-- Key the AskAI tab to make sure it re-mounts when changing tabs -->
<AskAI
@replaceCode="onReplaceCode"
:has-changes="hasChanges"
:key="activeTab"
@started-loading="isLoadingAIResponse = true"
@finished-loading="isLoadingAIResponse = false"
/>
</el-tab-pane>
</el-tabs>
<!-- If AskAi not enabled, there's no point in rendering tabs -->
<div v-else ref="codeNodeEditor" class="code-node-editor-input ph-no-capture" />
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import type { PropType } from 'vue'; import type { PropType } from 'vue';
import jsParser from 'prettier/parser-babel';
import prettier from 'prettier/standalone';
import { mapStores } from 'pinia'; import { mapStores } from 'pinia';
import type { LanguageSupport } from '@codemirror/language'; import type { LanguageSupport } from '@codemirror/language';
import type { Extension, Line } from '@codemirror/state'; import type { Extension, Line } from '@codemirror/state';
import { Compartment, EditorState } from '@codemirror/state'; import { Compartment, EditorState } from '@codemirror/state';
@ -35,19 +57,24 @@ import type { CodeExecutionMode, CodeNodeEditorLanguage } from 'n8n-workflow';
import { CODE_EXECUTION_MODES, CODE_LANGUAGES } from 'n8n-workflow'; import { CODE_EXECUTION_MODES, CODE_LANGUAGES } from 'n8n-workflow';
import { workflowHelpers } from '@/mixins/workflowHelpers'; // for json field completions import { workflowHelpers } from '@/mixins/workflowHelpers'; // for json field completions
import { ASK_AI_MODAL_KEY, CODE_NODE_TYPE } from '@/constants'; import { ASK_AI_EXPERIMENT, CODE_NODE_TYPE } from '@/constants';
import { codeNodeEditorEventBus } from '@/event-bus'; import { codeNodeEditorEventBus } from '@/event-bus';
import { useRootStore } from '@/stores/n8nRoot.store'; import { useRootStore, usePostHog } from '@/stores';
import { readOnlyEditorExtensions, writableEditorExtensions } from './baseExtensions'; import { readOnlyEditorExtensions, writableEditorExtensions } from './baseExtensions';
import { CODE_PLACEHOLDERS } from './constants'; import { CODE_PLACEHOLDERS } from './constants';
import { linterExtension } from './linter'; import { linterExtension } from './linter';
import { completerExtension } from './completer'; import { completerExtension } from './completer';
import { codeNodeEditorTheme } from './theme'; import { codeNodeEditorTheme } from './theme';
import AskAI from './AskAI/AskAI.vue';
import { useMessage } from '@/composables';
export default defineComponent({ export default defineComponent({
name: 'code-node-editor', name: 'code-node-editor',
mixins: [linterExtension, completerExtension, workflowHelpers], mixins: [linterExtension, completerExtension, workflowHelpers],
components: {
AskAI,
},
props: { props: {
aiButtonEnabled: { aiButtonEnabled: {
type: Boolean, type: Boolean,
@ -70,6 +97,11 @@ export default defineComponent({
type: String, type: String,
}, },
}, },
setup() {
return {
...useMessage(),
};
},
data() { data() {
return { return {
editor: null as EditorView | null, editor: null as EditorView | null,
@ -77,6 +109,10 @@ export default defineComponent({
linterCompartment: new Compartment(), linterCompartment: new Compartment(),
isEditorHovered: false, isEditorHovered: false,
isEditorFocused: false, isEditorFocused: false,
tabs: ['code', 'ask-ai'],
activeTab: 'code',
hasChanges: false,
isLoadingAIResponse: false,
}; };
}, },
watch: { watch: {
@ -101,12 +137,34 @@ export default defineComponent({
effects: this.languageCompartment.reconfigure(languageSupport), effects: this.languageCompartment.reconfigure(languageSupport),
}); });
}, },
aiEnabled: {
immediate: true,
async handler(isEnabled) {
if (isEnabled && !this.modelValue) {
this.$emit('update:modelValue', this.placeholder);
}
await this.$nextTick();
this.hasChanges = this.modelValue !== this.placeholder;
},
},
}, },
computed: { computed: {
...mapStores(useRootStore), ...mapStores(useRootStore, usePostHog),
aiEnabled(): boolean {
const isAiExperimentEnabled = [ASK_AI_EXPERIMENT.gpt3, ASK_AI_EXPERIMENT.gpt4].includes(
(this.posthogStore.getVariant(ASK_AI_EXPERIMENT.name) ?? '') as string,
);
return (
isAiExperimentEnabled &&
this.settingsStore.settings.ai.enabled &&
this.language === 'javaScript'
);
},
placeholder(): string { placeholder(): string {
return CODE_PLACEHOLDERS[this.language]?.[this.mode] ?? ''; return CODE_PLACEHOLDERS[this.language]?.[this.mode] ?? '';
}, },
// eslint-disable-next-line vue/return-in-computed-property
languageExtensions(): [LanguageSupport, ...Extension[]] { languageExtensions(): [LanguageSupport, ...Extension[]] {
switch (this.language) { switch (this.language) {
case 'json': case 'json':
@ -122,6 +180,41 @@ export default defineComponent({
getCurrentEditorContent() { getCurrentEditorContent() {
return this.editor?.state.doc.toString() ?? ''; return this.editor?.state.doc.toString() ?? '';
}, },
async onBeforeTabLeave(_activeName: string, oldActiveName: string) {
// Confirm dialog if leaving ask-ai tab during loading
if (oldActiveName === 'ask-ai' && this.isLoadingAIResponse) {
const confirmModal = await this.alert(
this.$locale.baseText('codeNodeEditor.askAi.sureLeaveTab'),
{
title: this.$locale.baseText('codeNodeEditor.askAi.areYouSure'),
confirmButtonText: this.$locale.baseText('codeNodeEditor.askAi.switchTab'),
showClose: true,
showCancelButton: true,
},
);
if (confirmModal === 'confirm') {
return true;
}
return false;
}
return true;
},
async onReplaceCode(code: string) {
const formattedCode = prettier.format(code, {
parser: 'babel',
plugins: [jsParser],
});
this.editor?.dispatch({
changes: { from: 0, to: this.getCurrentEditorContent().length, insert: formattedCode },
});
this.activeTab = 'code';
this.hasChanges = false;
},
onMouseOver(event: MouseEvent) { onMouseOver(event: MouseEvent) {
const fromElement = event.relatedTarget as HTMLElement; const fromElement = event.relatedTarget as HTMLElement;
const ref = this.$refs.codeNodeEditorContainer as HTMLDivElement | undefined; const ref = this.$refs.codeNodeEditorContainer as HTMLDivElement | undefined;
@ -134,11 +227,6 @@ export default defineComponent({
if (!ref?.contains(fromElement)) this.isEditorHovered = false; if (!ref?.contains(fromElement)) this.isEditorHovered = false;
}, },
onAskAiButtonClick() {
this.$telemetry.track('User clicked ask ai button', { source: 'code' });
this.uiStore.openModal(ASK_AI_MODAL_KEY);
},
reloadLinter() { reloadLinter() {
if (!this.editor) return; if (!this.editor) return;
@ -221,11 +309,6 @@ export default defineComponent({
mounted() { mounted() {
if (!this.isReadOnly) codeNodeEditorEventBus.on('error-line-number', this.highlightLine); if (!this.isReadOnly) codeNodeEditorEventBus.on('error-line-number', this.highlightLine);
// empty on first load, default param value
if (!this.modelValue) {
this.$emit('update:modelValue', this.placeholder);
}
const { isReadOnly, language } = this; const { isReadOnly, language } = this;
const extensions: Extension[] = [ const extensions: Extension[] = [
...readOnlyEditorExtensions, ...readOnlyEditorExtensions,
@ -250,12 +333,14 @@ export default defineComponent({
this.isEditorFocused = false; this.isEditorFocused = false;
}, },
}), }),
EditorView.updateListener.of((viewUpdate) => { EditorView.updateListener.of((viewUpdate) => {
if (!viewUpdate.docChanged) return; if (!viewUpdate.docChanged) return;
this.trackCompletion(viewUpdate); this.trackCompletion(viewUpdate);
this.$emit('update:modelValue', this.editor?.state.doc.toString()); this.$emit('update:modelValue', this.editor?.state.doc.toString());
this.hasChanges = true;
}), }),
); );
} }
@ -264,7 +349,7 @@ export default defineComponent({
extensions.push(this.languageCompartment.of(languageSupport), ...otherExtensions); extensions.push(this.languageCompartment.of(languageSupport), ...otherExtensions);
const state = EditorState.create({ const state = EditorState.create({
doc: this.modelValue || this.placeholder, doc: this.modelValue ?? this.placeholder,
extensions, extensions,
}); });
@ -272,10 +357,24 @@ export default defineComponent({
parent: this.$refs.codeNodeEditor as HTMLDivElement, parent: this.$refs.codeNodeEditor as HTMLDivElement,
state, state,
}); });
// empty on first load, default param value
if (!this.modelValue) {
this.refreshPlaceholder();
this.$emit('update:modelValue', this.placeholder);
}
}, },
}); });
</script> </script>
<style scoped lang="scss">
:deep(.el-tabs) {
.code-editor-tabs .cm-editor {
border: 0;
}
}
</style>
<style lang="scss" module> <style lang="scss" module>
.code-node-editor-container { .code-node-editor-container {
position: relative; position: relative;

View file

@ -7,8 +7,11 @@ import * as esprima from 'esprima-next';
import type { Node } from 'estree'; import type { Node } from 'estree';
import type { CodeNodeEditorLanguage } from 'n8n-workflow'; import type { CodeNodeEditorLanguage } from 'n8n-workflow';
import { DEFAULT_LINTER_DELAY_IN_MS, DEFAULT_LINTER_SEVERITY } from './constants'; import {
import { OFFSET_FOR_SCRIPT_WRAPPER } from './constants'; DEFAULT_LINTER_DELAY_IN_MS,
DEFAULT_LINTER_SEVERITY,
OFFSET_FOR_SCRIPT_WRAPPER,
} from './constants';
import { walk } from './utils'; import { walk } from './utils';
import type { RangeNode } from './types'; import type { RangeNode } from './types';
@ -148,7 +151,7 @@ export const linterExtension = defineComponent({
const isUnavailableInputItemAccess = (node: Node) => const isUnavailableInputItemAccess = (node: Node) =>
node.type === 'MemberExpression' && node.type === 'MemberExpression' &&
node.computed === false && !node.computed &&
node.object.type === 'Identifier' && node.object.type === 'Identifier' &&
node.object.name === '$input' && node.object.name === '$input' &&
node.property.type === 'Identifier' && node.property.type === 'Identifier' &&
@ -174,44 +177,6 @@ export const linterExtension = defineComponent({
}); });
} }
/**
* Lint for `item` (legacy var from Function Item node) unavailable
* in `runOnceForAllItems` mode, unless user-defined `item`.
*
* item -> $input.all()
*/
if (this.mode === 'runOnceForAllItems' && !/(let|const|var) item (=|of)/.test(script)) {
type TargetNode = RangeNode & { object: RangeNode & { name: string } };
const isUnavailableLegacyItems = (node: Node) =>
node.type === 'Identifier' && node.name === 'item';
walk<TargetNode>(ast, isUnavailableLegacyItems).forEach((node) => {
const [start, end] = this.getRange(node);
lintings.push({
from: start,
to: end,
severity: DEFAULT_LINTER_SEVERITY,
message: this.$locale.baseText('codeNodeEditor.linter.allItems.unavailableItem'),
actions: [
{
name: 'Fix',
apply(view, from, to) {
// prevent second insertion of unknown origin
if (view.state.doc.toString().slice(from, to).includes('$input.all()')) {
return;
}
view.dispatch({ changes: { from: start, to: end } });
view.dispatch({ changes: { from, insert: '$input.all()' } });
},
},
],
});
});
}
/** /**
* Lint for `items` (legacy var from Function node) unavailable * Lint for `items` (legacy var from Function node) unavailable
* in `runOnceForEachItem` mode, unless user-defined `items`. * in `runOnceForEachItem` mode, unless user-defined `items`.
@ -265,7 +230,7 @@ export const linterExtension = defineComponent({
const isUnavailableMethodinEachItem = (node: Node) => const isUnavailableMethodinEachItem = (node: Node) =>
node.type === 'MemberExpression' && node.type === 'MemberExpression' &&
node.computed === false && !node.computed &&
node.object.type === 'Identifier' && node.object.type === 'Identifier' &&
node.object.name === '$input' && node.object.name === '$input' &&
node.property.type === 'Identifier' && node.property.type === 'Identifier' &&
@ -332,7 +297,7 @@ export const linterExtension = defineComponent({
const inputFirstOrLastCalledWithArg = (node: Node) => const inputFirstOrLastCalledWithArg = (node: Node) =>
node.type === 'CallExpression' && node.type === 'CallExpression' &&
node.callee.type === 'MemberExpression' && node.callee.type === 'MemberExpression' &&
node.callee.computed === false && !node.callee.computed &&
node.callee.object.type === 'Identifier' && node.callee.object.type === 'Identifier' &&
node.callee.object.name === '$input' && node.callee.object.name === '$input' &&
node.callee.property.type === 'Identifier' && node.callee.property.type === 'Identifier' &&
@ -427,7 +392,7 @@ export const linterExtension = defineComponent({
node.left.declarations[0].id.type === 'Identifier' && node.left.declarations[0].id.type === 'Identifier' &&
node.right.type === 'CallExpression' && node.right.type === 'CallExpression' &&
node.right.callee.type === 'MemberExpression' && node.right.callee.type === 'MemberExpression' &&
node.right.callee.computed === false && !node.right.callee.computed &&
node.right.callee.object.type === 'Identifier' && node.right.callee.object.type === 'Identifier' &&
node.right.callee.object.name.startsWith('$'); // n8n var, e.g $input node.right.callee.object.name.startsWith('$'); // n8n var, e.g $input

View file

@ -15,10 +15,6 @@
<AboutModal /> <AboutModal />
</ModalRoot> </ModalRoot>
<ModalRoot :name="ASK_AI_MODAL_KEY">
<AskAiModal />
</ModalRoot>
<ModalRoot :name="CREDENTIAL_SELECT_MODAL_KEY"> <ModalRoot :name="CREDENTIAL_SELECT_MODAL_KEY">
<CredentialsSelectModal /> <CredentialsSelectModal />
</ModalRoot> </ModalRoot>
@ -145,13 +141,11 @@ import {
WORKFLOW_SHARE_MODAL_KEY, WORKFLOW_SHARE_MODAL_KEY,
IMPORT_CURL_MODAL_KEY, IMPORT_CURL_MODAL_KEY,
LOG_STREAM_MODAL_KEY, LOG_STREAM_MODAL_KEY,
ASK_AI_MODAL_KEY,
SOURCE_CONTROL_PUSH_MODAL_KEY, SOURCE_CONTROL_PUSH_MODAL_KEY,
SOURCE_CONTROL_PULL_MODAL_KEY, SOURCE_CONTROL_PULL_MODAL_KEY,
} from '@/constants'; } from '@/constants';
import AboutModal from './AboutModal.vue'; import AboutModal from './AboutModal.vue';
import AskAiModal from './AskAiModal.vue';
import CommunityPackageManageConfirmModal from './CommunityPackageManageConfirmModal.vue'; import CommunityPackageManageConfirmModal from './CommunityPackageManageConfirmModal.vue';
import CommunityPackageInstallModal from './CommunityPackageInstallModal.vue'; import CommunityPackageInstallModal from './CommunityPackageInstallModal.vue';
import ChangePasswordModal from './ChangePasswordModal.vue'; import ChangePasswordModal from './ChangePasswordModal.vue';
@ -179,7 +173,6 @@ export default defineComponent({
name: 'Modals', name: 'Modals',
components: { components: {
AboutModal, AboutModal,
AskAiModal,
ActivationModal, ActivationModal,
CommunityPackageInstallModal, CommunityPackageInstallModal,
CommunityPackageManageConfirmModal, CommunityPackageManageConfirmModal,
@ -210,7 +203,6 @@ export default defineComponent({
CREDENTIAL_EDIT_MODAL_KEY, CREDENTIAL_EDIT_MODAL_KEY,
CREDENTIAL_SELECT_MODAL_KEY, CREDENTIAL_SELECT_MODAL_KEY,
ABOUT_MODAL_KEY, ABOUT_MODAL_KEY,
ASK_AI_MODAL_KEY,
CHANGE_PASSWORD_MODAL_KEY, CHANGE_PASSWORD_MODAL_KEY,
DELETE_USER_MODAL_KEY, DELETE_USER_MODAL_KEY,
DUPLICATE_MODAL_KEY, DUPLICATE_MODAL_KEY,

View file

@ -1,17 +1,16 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import { merge } from 'lodash-es'; import type { INodeUi } from '@/Interface';
import type { INodeUi, Schema } from '@/Interface';
import RunDataSchemaItem from '@/components/RunDataSchemaItem.vue'; import RunDataSchemaItem from '@/components/RunDataSchemaItem.vue';
import Draggable from '@/components/Draggable.vue'; import Draggable from '@/components/Draggable.vue';
import { useNDVStore } from '@/stores/ndv.store'; import { useNDVStore } from '@/stores/ndv.store';
import { useWebhooksStore } from '@/stores/webhooks.store'; import { useWebhooksStore } from '@/stores/webhooks.store';
import { telemetry } from '@/plugins/telemetry'; import { telemetry } from '@/plugins/telemetry';
import type { IDataObject } from 'n8n-workflow'; import type { IDataObject } from 'n8n-workflow';
import { getSchema, isEmpty, runExternalHook } from '@/utils'; import { isEmpty, runExternalHook } from '@/utils';
import { i18n } from '@/plugins/i18n'; import { i18n } from '@/plugins/i18n';
import MappingPill from './MappingPill.vue'; import MappingPill from './MappingPill.vue';
import { useDataSchema } from '@/composables';
type Props = { type Props = {
data: IDataObject[]; data: IDataObject[];
mappingEnabled: boolean; mappingEnabled: boolean;
@ -29,15 +28,11 @@ const props = withDefaults(defineProps<Props>(), {
const draggingPath = ref<string>(''); const draggingPath = ref<string>('');
const ndvStore = useNDVStore(); const ndvStore = useNDVStore();
const webhooksStore = useWebhooksStore(); const webhooksStore = useWebhooksStore();
const { getSchemaForExecutionData } = useDataSchema();
const schema = computed<Schema>(() => { const schema = computed(() => getSchemaForExecutionData(props.data));
const [head, ...tail] = props.data;
return getSchema(merge({}, head, ...tail, head));
});
const isDataEmpty = computed(() => { const isDataEmpty = computed(() => isEmpty(props.data));
return isEmpty(props.data);
});
const onDragStart = (el: HTMLElement) => { const onDragStart = (el: HTMLElement) => {
if (el && el.dataset?.path) { if (el && el.dataset?.path) {

View file

@ -0,0 +1,355 @@
import jp from 'jsonpath';
import { useDataSchema } from '@/composables';
import type { Schema } from '@/Interface';
describe('useDataSchema', () => {
const getSchema = useDataSchema().getSchema;
describe('getSchema', () => {
test.each([
[, { type: 'undefined', value: 'undefined', path: '' }],
[undefined, { type: 'undefined', value: 'undefined', path: '' }],
[null, { type: 'null', value: '[null]', path: '' }],
['John', { type: 'string', value: 'John', path: '' }],
['123', { type: 'string', value: '123', path: '' }],
[123, { type: 'number', value: '123', path: '' }],
[true, { type: 'boolean', value: 'true', path: '' }],
[false, { type: 'boolean', value: 'false', path: '' }],
[() => {}, { type: 'function', value: '', path: '' }],
[{}, { type: 'object', value: [], path: '' }],
[[], { type: 'array', value: [], path: '' }],
[
new Date('2022-11-22T00:00:00.000Z'),
{ type: 'string', value: '2022-11-22T00:00:00.000Z', path: '' },
],
[Symbol('x'), { type: 'symbol', value: 'Symbol(x)', path: '' }],
[1n, { type: 'bigint', value: '1', path: '' }],
[
['John', 1, true],
{
type: 'array',
value: [
{ type: 'string', value: 'John', key: '0', path: '[0]' },
{ type: 'number', value: '1', key: '1', path: '[1]' },
{ type: 'boolean', value: 'true', key: '2', path: '[2]' },
],
path: '',
},
],
[
{ people: ['Joe', 'John'] },
{
type: 'object',
value: [
{
type: 'array',
key: 'people',
value: [
{ type: 'string', value: 'Joe', key: '0', path: '.people[0]' },
{ type: 'string', value: 'John', key: '1', path: '.people[1]' },
],
path: '.people',
},
],
path: '',
},
],
[
{ 'with space': [], 'with.dot': 'test' },
{
type: 'object',
value: [
{
type: 'array',
key: 'with space',
value: [],
path: "['with space']",
},
{
type: 'string',
key: 'with.dot',
value: 'test',
path: "['with.dot']",
},
],
path: '',
},
],
[
[
{ name: 'John', age: 22 },
{ name: 'Joe', age: 33 },
],
{
type: 'array',
value: [
{
type: 'object',
key: '0',
value: [
{ type: 'string', key: 'name', value: 'John', path: '[0].name' },
{ type: 'number', key: 'age', value: '22', path: '[0].age' },
],
path: '[0]',
},
{
type: 'object',
key: '1',
value: [
{ type: 'string', key: 'name', value: 'Joe', path: '[1].name' },
{ type: 'number', key: 'age', value: '33', path: '[1].age' },
],
path: '[1]',
},
],
path: '',
},
],
[
[
{ name: 'John', age: 22, hobbies: ['surfing', 'traveling'] },
{ name: 'Joe', age: 33, hobbies: ['skateboarding', 'gaming'] },
],
{
type: 'array',
value: [
{
type: 'object',
key: '0',
value: [
{ type: 'string', key: 'name', value: 'John', path: '[0].name' },
{ type: 'number', key: 'age', value: '22', path: '[0].age' },
{
type: 'array',
key: 'hobbies',
value: [
{ type: 'string', key: '0', value: 'surfing', path: '[0].hobbies[0]' },
{ type: 'string', key: '1', value: 'traveling', path: '[0].hobbies[1]' },
],
path: '[0].hobbies',
},
],
path: '[0]',
},
{
type: 'object',
key: '1',
value: [
{ type: 'string', key: 'name', value: 'Joe', path: '[1].name' },
{ type: 'number', key: 'age', value: '33', path: '[1].age' },
{
type: 'array',
key: 'hobbies',
value: [
{ type: 'string', key: '0', value: 'skateboarding', path: '[1].hobbies[0]' },
{ type: 'string', key: '1', value: 'gaming', path: '[1].hobbies[1]' },
],
path: '[1].hobbies',
},
],
path: '[1]',
},
],
path: '',
},
],
[[], { type: 'array', value: [], path: '' }],
[
[[1, 2]],
{
type: 'array',
value: [
{
type: 'array',
key: '0',
value: [
{ type: 'number', key: '0', value: '1', path: '[0][0]' },
{ type: 'number', key: '1', value: '2', path: '[0][1]' },
],
path: '[0]',
},
],
path: '',
},
],
[
[
[
{ name: 'John', age: 22 },
{ name: 'Joe', age: 33 },
],
],
{
type: 'array',
value: [
{
type: 'array',
key: '0',
value: [
{
type: 'object',
key: '0',
value: [
{ type: 'string', key: 'name', value: 'John', path: '[0][0].name' },
{ type: 'number', key: 'age', value: '22', path: '[0][0].age' },
],
path: '[0][0]',
},
{
type: 'object',
key: '1',
value: [
{ type: 'string', key: 'name', value: 'Joe', path: '[0][1].name' },
{ type: 'number', key: 'age', value: '33', path: '[0][1].age' },
],
path: '[0][1]',
},
],
path: '[0]',
},
],
path: '',
},
],
[
[
{
dates: [
[new Date('2022-11-22T00:00:00.000Z'), new Date('2022-11-23T00:00:00.000Z')],
[new Date('2022-12-22T00:00:00.000Z'), new Date('2022-12-23T00:00:00.000Z')],
],
},
],
{
type: 'array',
value: [
{
type: 'object',
key: '0',
value: [
{
type: 'array',
key: 'dates',
value: [
{
type: 'array',
key: '0',
value: [
{
type: 'string',
key: '0',
value: '2022-11-22T00:00:00.000Z',
path: '[0].dates[0][0]',
},
{
type: 'string',
key: '1',
value: '2022-11-23T00:00:00.000Z',
path: '[0].dates[0][1]',
},
],
path: '[0].dates[0]',
},
{
type: 'array',
key: '1',
value: [
{
type: 'string',
key: '0',
value: '2022-12-22T00:00:00.000Z',
path: '[0].dates[1][0]',
},
{
type: 'string',
key: '1',
value: '2022-12-23T00:00:00.000Z',
path: '[0].dates[1][1]',
},
],
path: '[0].dates[1]',
},
],
path: '[0].dates',
},
],
path: '[0]',
},
],
path: '',
},
],
])('should return the correct json schema for %s', (input, schema) => {
expect(getSchema(input)).toEqual(schema);
});
it('should return the correct data when using the generated json path on an object', () => {
const input = { people: ['Joe', 'John'] };
const schema = getSchema(input) as Schema;
const pathData = jp.query(
input,
`$${((schema.value as Schema[])[0].value as Schema[])[0].path}`,
);
expect(pathData).toEqual(['Joe']);
});
it('should return the correct data when using the generated json path on a list', () => {
const input = [
{ name: 'John', age: 22, hobbies: ['surfing', 'traveling'] },
{ name: 'Joe', age: 33, hobbies: ['skateboarding', 'gaming'] },
];
const schema = getSchema(input) as Schema;
const pathData = jp.query(
input,
`$${(((schema.value as Schema[])[0].value as Schema[])[2].value as Schema[])[1].path}`,
);
expect(pathData).toEqual(['traveling']);
});
it('should return the correct data when using the generated json path on a list of list', () => {
const input = [[1, 2]];
const schema = getSchema(input) as Schema;
const pathData = jp.query(
input,
`$${((schema.value as Schema[])[0].value as Schema[])[1].path}`,
);
expect(pathData).toEqual([2]);
});
it('should return the correct data when using the generated json path on a list of list of objects', () => {
const input = [
[
{ name: 'John', age: 22 },
{ name: 'Joe', age: 33 },
],
];
const schema = getSchema(input) as Schema;
const pathData = jp.query(
input,
`$${(((schema.value as Schema[])[0].value as Schema[])[1].value as Schema[])[1].path}`,
);
expect(pathData).toEqual([33]);
});
it('should return the correct data when using the generated json path on a list of objects with a list of date tuples', () => {
const input = [
{
dates: [
[new Date('2022-11-22T00:00:00.000Z'), new Date('2022-11-23T00:00:00.000Z')],
[new Date('2022-12-22T00:00:00.000Z'), new Date('2022-12-23T00:00:00.000Z')],
],
},
];
const schema = getSchema(input) as Schema;
const pathData = jp.query(
input,
`$${
(
(((schema.value as Schema[])[0].value as Schema[])[0].value as Schema[])[0]
.value as Schema[]
)[0].path
}`,
);
expect(pathData).toEqual([new Date('2022-11-22T00:00:00.000Z')]);
});
});
});

View file

@ -12,3 +12,4 @@ export * from './useTelemetry';
export * from './useTitleChange'; export * from './useTitleChange';
export * from './useToast'; export * from './useToast';
export * from './useNodeSpecificationValues'; export * from './useNodeSpecificationValues';
export * from './useDataSchema';

View file

@ -0,0 +1,130 @@
import type { Optional, Primitives, Schema, INodeUi, INodeExecutionData } from '@/Interface';
import type { ITaskDataConnections, type IDataObject } from 'n8n-workflow';
import { merge } from 'lodash-es';
import { generatePath } from '@/utils/mappingUtils';
import { isObj } from '@/utils/typeGuards';
import { useWorkflowsStore } from '@/stores';
export function useDataSchema() {
function getSchema(
input: Optional<Primitives | object>,
path = '',
excludeValues = false,
): Schema {
let schema: Schema = { type: 'undefined', value: 'undefined', path };
switch (typeof input) {
case 'object':
if (input === null) {
schema = { type: 'null', value: '[null]', path };
} else if (input instanceof Date) {
schema = { type: 'string', value: input.toISOString(), path };
} else if (Array.isArray(input)) {
schema = {
type: 'array',
value: input.map((item, index) => ({
key: index.toString(),
...getSchema(item, `${path}[${index}]`, excludeValues),
})),
path,
};
} else if (isObj(input)) {
schema = {
type: 'object',
value: Object.entries(input).map(([k, v]) => ({
key: k,
...getSchema(v, generatePath(path, [k]), excludeValues),
})),
path,
};
}
break;
case 'function':
schema = { type: 'function', value: '', path };
break;
default:
schema = {
type: typeof input,
value: excludeValues ? '' : String(input),
path,
};
}
return schema;
}
function getSchemaForExecutionData(data: IDataObject[], excludeValues = false) {
const [head, ...tail] = data;
return getSchema(merge({}, head, ...tail, head), undefined, excludeValues);
}
// Returns the data of the main input
function getMainInputData(
connectionsData: ITaskDataConnections,
outputIndex: number,
): INodeExecutionData[] {
if (
!connectionsData?.hasOwnProperty('main') ||
connectionsData.main === undefined ||
connectionsData.main.length < outputIndex ||
connectionsData.main[outputIndex] === null
) {
return [];
}
return connectionsData.main[outputIndex] as INodeExecutionData[];
}
function getNodeInputData(
node: INodeUi | null,
runIndex = 0,
outputIndex = 0,
): INodeExecutionData[] {
const { getWorkflowExecution } = useWorkflowsStore();
if (node === null) {
return [];
}
if (getWorkflowExecution === null) {
return [];
}
const executionData = getWorkflowExecution.data;
if (!executionData?.resultData) {
// unknown status
return [];
}
const runData = executionData.resultData.runData;
if (!runData?.[node.name]?.[runIndex].data || runData[node.name][runIndex].data === undefined) {
return [];
}
return getMainInputData(runData[node.name][runIndex].data!, outputIndex);
}
function getInputDataWithPinned(
node: INodeUi | null,
runIndex = 0,
outputIndex = 0,
): INodeExecutionData[] {
if (!node) return [];
const { pinDataByNodeName } = useWorkflowsStore();
const pinnedData = pinDataByNodeName(node.name);
let inputData = getNodeInputData(node, runIndex, outputIndex);
if (pinnedData) {
inputData = Array.isArray(pinnedData)
? pinnedData.map((json) => ({ json }))
: [{ json: pinnedData }];
}
return inputData;
}
return {
getSchema,
getSchemaForExecutionData,
getNodeInputData,
getInputDataWithPinned,
};
}

View file

@ -26,7 +26,6 @@ export const MAX_TAG_NAME_LENGTH = 24;
// modals // modals
export const ABOUT_MODAL_KEY = 'about'; export const ABOUT_MODAL_KEY = 'about';
export const ASK_AI_MODAL_KEY = 'askAi';
export const CHANGE_PASSWORD_MODAL_KEY = 'changePassword'; export const CHANGE_PASSWORD_MODAL_KEY = 'changePassword';
export const CREDENTIAL_EDIT_MODAL_KEY = 'editCredential'; export const CREDENTIAL_EDIT_MODAL_KEY = 'editCredential';
export const CREDENTIAL_SELECT_MODAL_KEY = 'selectCredential'; export const CREDENTIAL_SELECT_MODAL_KEY = 'selectCredential';
@ -179,7 +178,6 @@ export const FLOWS_CONTROL_SUBCATEGORY = 'Flow';
export const HELPERS_SUBCATEGORY = 'Helpers'; export const HELPERS_SUBCATEGORY = 'Helpers';
export const REQUEST_NODE_FORM_URL = 'https://n8n-community.typeform.com/to/K1fBVTZ3'; export const REQUEST_NODE_FORM_URL = 'https://n8n-community.typeform.com/to/K1fBVTZ3';
export const ASK_AI_WAITLIST_URL = 'https://n8n-community.typeform.com/to/odKU4oDR';
// General // General
export const INSTANCE_ID_HEADER = 'n8n-instance-id'; export const INSTANCE_ID_HEADER = 'n8n-instance-id';
@ -525,7 +523,14 @@ export const KEEP_AUTH_IN_NDV_FOR_NODES = [
export const MAIN_AUTH_FIELD_NAME = 'authentication'; export const MAIN_AUTH_FIELD_NAME = 'authentication';
export const NODE_RESOURCE_FIELD_NAME = 'resource'; export const NODE_RESOURCE_FIELD_NAME = 'resource';
export const EXPERIMENTS_TO_TRACK = []; export const ASK_AI_EXPERIMENT = {
name: '011_ask_AI',
control: 'control',
gpt3: 'gpt3',
gpt4: 'gpt4',
};
export const EXPERIMENTS_TO_TRACK = [ASK_AI_EXPERIMENT.name];
export const NODE_TYPES_EXCLUDED_FROM_OUTPUT_NAME_APPEND = [FILTER_NODE_TYPE]; export const NODE_TYPES_EXCLUDED_FROM_OUTPUT_NAME_APPEND = [FILTER_NODE_TYPE];
@ -557,3 +562,8 @@ export const CLOUD_TRIAL_CHECK_INTERVAL = 5000;
// A path that does not exist so that nothing is selected by default // A path that does not exist so that nothing is selected by default
export const nonExistingJsonPath = '_!^&*'; export const nonExistingJsonPath = '_!^&*';
// Ask AI
export const ASK_AI_MAX_PROMPT_LENGTH = 600;
export const ASK_AI_MIN_PROMPT_LENGTH = 15;
export const ASK_AI_LOADING_DURATION_MS = 12000;

View file

@ -128,6 +128,30 @@
font-weight: bold; font-weight: bold;
} }
.el-tabs__content {
border: 1px solid var(--color-foreground-base);
border-radius: 0px var(--border-radius-base) var(--border-radius-base);
}
.el-tabs__header {
border-bottom: 0 !important;
}
.el-tabs__nav {
padding: 0;
overflow: hidden;
}
.el-tabs__item {
padding: var(--spacing-5xs) var(--spacing-2xs) !important;
height: auto;
line-height: var(--font-line-height-xloose);
font-weight: var(--font-weight-regular);
font-size: var(--font-size-2xs);
&:not([aria-selected='true']) {
background-color: var(--color-background-base);
border-bottom: 1px solid var(--color-foreground-base) !important;
}
}
// Loading Indicator // Loading Indicator
.el-loading-mask { .el-loading-mask {
background-color: var(--color-foreground-xlight); background-color: var(--color-foreground-xlight);

View file

@ -284,6 +284,36 @@
"codeNodeEditor.linter.eachItem.unavailableItems": "Legacy `items` is only available in the 'Run Once for All Items' mode.", "codeNodeEditor.linter.eachItem.unavailableItems": "Legacy `items` is only available in the 'Run Once for All Items' mode.",
"codeNodeEditor.linter.eachItem.unavailableMethod": "Method `$input.{method}()` is only available in the 'Run Once for All Items' mode.", "codeNodeEditor.linter.eachItem.unavailableMethod": "Method `$input.{method}()` is only available in the 'Run Once for All Items' mode.",
"codeNodeEditor.linter.bothModes.syntaxError": "Syntax error", "codeNodeEditor.linter.bothModes.syntaxError": "Syntax error",
"codeNodeEditor.askAi.placeholder": "Tell AI what you want the code to achieve. You can reference input data fields using dot notation (e.g. user.email)",
"codeNodeEditor.askAi.intro": "Hey AI, generate JavaScript code that...",
"codeNodeEditor.askAi.help": "Help",
"codeNodeEditor.askAi.generateCode": "Generate Code",
"codeNodeEditor.askAi.noInputData": "You can generate code once this node has incoming input data (from a node earlier in your workflow)",
"codeNodeEditor.askAi.sureLeaveTab": "Are you sure you want to switch tab? The code generation will stop",
"codeNodeEditor.askAi.areYouSure": "Are you sure?",
"codeNodeEditor.askAi.switchTab": "Switch Tab",
"codeNodeEditor.askAi.noPrompt": "First enter a prompt above before generating code",
"codeNodeEditor.askAi.onlyAllItemsMode": "Ask AI generation works only in 'Run Once for All Items' mode",
"codeNodeEditor.askAi.promptTooShort": "Enter a minimum of {minLength} characters before attempting to generate code",
"codeNodeEditor.askAi.generateCodeAndReplace": "Generate and Replace Code",
"codeNodeEditor.askAi.replaceCurrentCode": "Replace current code?",
"codeNodeEditor.askAi.areYouSureToReplace": "Are you sure you want to generate new code? Your current code will be replaced.",
"codeNodeEditor.askAi.loadingPhrase0": "AI cogs whirring, almost there…",
"codeNodeEditor.askAi.loadingPhrase1": "up up down down left right b a start…",
"codeNodeEditor.askAi.loadingPhrase2": "Consulting Jan Oberhauser…",
"codeNodeEditor.askAi.loadingPhrase3": "Gathering bytes and pieces…",
"codeNodeEditor.askAi.loadingPhrase4": "Checking if another AI knows the answer…",
"codeNodeEditor.askAi.loadingPhrase5": "Checking on Stack Overflow…",
"codeNodeEditor.askAi.loadingPhrase6": "Crunching data, AI-style…",
"codeNodeEditor.askAi.loadingPhrase7": "Stand by, AI magic at work…",
"codeNodeEditor.askAi.generationCompleted": "✨ Code generation completed",
"codeNodeEditor.askAi.generationFailed": "Code generation failed",
"codeNodeEditor.askAi.generationFailedUnknown": "Code generation failed due to an unknown reason. Try again in a few minutes.",
"codeNodeEditor.askAi.generationFailedDown": "We're sorry, our AI service is currently unavailable. Please try again later. If the problem persists, contact support.",
"codeNodeEditor.askAi.generationFailedRate": "We've hit our rate limit with our AI partner (too many requests). Please wait a minute before trying again.",
"codeNodeEditor.askAi.generationFailedTooLarge": "Your workflow data is too large for AI to process. Simplify the data being sent into the Code node and retry.",
"codeNodeEditor.tabs.askAi": "✨ Ask AI",
"codeNodeEditor.tabs.code": "Code",
"collectionParameter.choose": "Choose...", "collectionParameter.choose": "Choose...",
"collectionParameter.noProperties": "No properties", "collectionParameter.noProperties": "No properties",
"credentialEdit.credentialConfig.accountConnected": "Account connected", "credentialEdit.credentialConfig.accountConnected": "Account connected",

View file

@ -114,6 +114,20 @@ export class Telemetry {
}); });
} }
trackAskAI(event: string, properties: IDataObject = {}) {
if (this.rudderStack) {
properties.session_id = useRootStore().sessionId;
switch (event) {
case 'askAi.generationFinished':
this.track('Ai code generation finished', properties);
case 'ask.generationClicked':
this.track('User clicked on generate code button', properties);
default:
break;
}
}
}
trackNodesPanel(event: string, properties: IDataObject = {}) { trackNodesPanel(event: string, properties: IDataObject = {}) {
if (this.rudderStack) { if (this.rudderStack) {
properties.nodes_panel_session_id = this.userNodesPanelSession.sessionId; properties.nodes_panel_session_id = this.userNodesPanelSession.sessionId;

View file

@ -5,7 +5,6 @@ import {
} from '@/api/workflow-webhooks'; } from '@/api/workflow-webhooks';
import { import {
ABOUT_MODAL_KEY, ABOUT_MODAL_KEY,
ASK_AI_MODAL_KEY,
CHANGE_PASSWORD_MODAL_KEY, CHANGE_PASSWORD_MODAL_KEY,
COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY, COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY,
COMMUNITY_PACKAGE_INSTALL_MODAL_KEY, COMMUNITY_PACKAGE_INSTALL_MODAL_KEY,
@ -43,6 +42,8 @@ import type {
UIState, UIState,
UTMCampaign, UTMCampaign,
XYPosition, XYPosition,
Modals,
NewCredentialsModal,
} from '@/Interface'; } from '@/Interface';
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { useRootStore } from './n8nRoot.store'; import { useRootStore } from './n8nRoot.store';
@ -52,7 +53,6 @@ import { useSettingsStore } from './settings.store';
import { useCloudPlanStore } from './cloudPlan.store'; import { useCloudPlanStore } from './cloudPlan.store';
import type { BaseTextKey } from '@/plugins/i18n'; import type { BaseTextKey } from '@/plugins/i18n';
import { i18n as locale } from '@/plugins/i18n'; import { i18n as locale } from '@/plugins/i18n';
import type { Modals, NewCredentialsModal } from '@/Interface';
import { useTelemetryStore } from '@/stores/telemetry.store'; import { useTelemetryStore } from '@/stores/telemetry.store';
import { getStyleTokenValue } from '@/utils/htmlUtils'; import { getStyleTokenValue } from '@/utils/htmlUtils';
import { dismissBannerPermanently } from '@/api/ui'; import { dismissBannerPermanently } from '@/api/ui';
@ -66,9 +66,6 @@ export const useUIStore = defineStore(STORES.UI, {
[ABOUT_MODAL_KEY]: { [ABOUT_MODAL_KEY]: {
open: false, open: false,
}, },
[ASK_AI_MODAL_KEY]: {
open: false,
},
[CHANGE_PASSWORD_MODAL_KEY]: { [CHANGE_PASSWORD_MODAL_KEY]: {
open: false, open: false,
}, },

View file

@ -1,6 +1,4 @@
import jp from 'jsonpath'; import { isEmpty, intersection, isValidDate } from '@/utils';
import { isEmpty, intersection, getSchema, isValidDate } from '@/utils';
import type { Schema } from '@/Interface';
describe('Types Utils', () => { describe('Types Utils', () => {
describe('isEmpty', () => { describe('isEmpty', () => {
@ -43,355 +41,6 @@ describe('Types Utils', () => {
}); });
}); });
describe('getSchema', () => {
test.each([
[, { type: 'undefined', value: 'undefined', path: '' }],
[undefined, { type: 'undefined', value: 'undefined', path: '' }],
[null, { type: 'null', value: '[null]', path: '' }],
['John', { type: 'string', value: 'John', path: '' }],
['123', { type: 'string', value: '123', path: '' }],
[123, { type: 'number', value: '123', path: '' }],
[true, { type: 'boolean', value: 'true', path: '' }],
[false, { type: 'boolean', value: 'false', path: '' }],
[() => {}, { type: 'function', value: '', path: '' }],
[{}, { type: 'object', value: [], path: '' }],
[[], { type: 'array', value: [], path: '' }],
[
new Date('2022-11-22T00:00:00.000Z'),
{ type: 'string', value: '2022-11-22T00:00:00.000Z', path: '' },
],
[Symbol('x'), { type: 'symbol', value: 'Symbol(x)', path: '' }],
[1n, { type: 'bigint', value: '1', path: '' }],
[
['John', 1, true],
{
type: 'array',
value: [
{ type: 'string', value: 'John', key: '0', path: '[0]' },
{ type: 'number', value: '1', key: '1', path: '[1]' },
{ type: 'boolean', value: 'true', key: '2', path: '[2]' },
],
path: '',
},
],
[
{ people: ['Joe', 'John'] },
{
type: 'object',
value: [
{
type: 'array',
key: 'people',
value: [
{ type: 'string', value: 'Joe', key: '0', path: '.people[0]' },
{ type: 'string', value: 'John', key: '1', path: '.people[1]' },
],
path: '.people',
},
],
path: '',
},
],
[
{ 'with space': [], 'with.dot': 'test' },
{
type: 'object',
value: [
{
type: 'array',
key: 'with space',
value: [],
path: "['with space']",
},
{
type: 'string',
key: 'with.dot',
value: 'test',
path: "['with.dot']",
},
],
path: '',
},
],
[
[
{ name: 'John', age: 22 },
{ name: 'Joe', age: 33 },
],
{
type: 'array',
value: [
{
type: 'object',
key: '0',
value: [
{ type: 'string', key: 'name', value: 'John', path: '[0].name' },
{ type: 'number', key: 'age', value: '22', path: '[0].age' },
],
path: '[0]',
},
{
type: 'object',
key: '1',
value: [
{ type: 'string', key: 'name', value: 'Joe', path: '[1].name' },
{ type: 'number', key: 'age', value: '33', path: '[1].age' },
],
path: '[1]',
},
],
path: '',
},
],
[
[
{ name: 'John', age: 22, hobbies: ['surfing', 'traveling'] },
{ name: 'Joe', age: 33, hobbies: ['skateboarding', 'gaming'] },
],
{
type: 'array',
value: [
{
type: 'object',
key: '0',
value: [
{ type: 'string', key: 'name', value: 'John', path: '[0].name' },
{ type: 'number', key: 'age', value: '22', path: '[0].age' },
{
type: 'array',
key: 'hobbies',
value: [
{ type: 'string', key: '0', value: 'surfing', path: '[0].hobbies[0]' },
{ type: 'string', key: '1', value: 'traveling', path: '[0].hobbies[1]' },
],
path: '[0].hobbies',
},
],
path: '[0]',
},
{
type: 'object',
key: '1',
value: [
{ type: 'string', key: 'name', value: 'Joe', path: '[1].name' },
{ type: 'number', key: 'age', value: '33', path: '[1].age' },
{
type: 'array',
key: 'hobbies',
value: [
{ type: 'string', key: '0', value: 'skateboarding', path: '[1].hobbies[0]' },
{ type: 'string', key: '1', value: 'gaming', path: '[1].hobbies[1]' },
],
path: '[1].hobbies',
},
],
path: '[1]',
},
],
path: '',
},
],
[[], { type: 'array', value: [], path: '' }],
[
[[1, 2]],
{
type: 'array',
value: [
{
type: 'array',
key: '0',
value: [
{ type: 'number', key: '0', value: '1', path: '[0][0]' },
{ type: 'number', key: '1', value: '2', path: '[0][1]' },
],
path: '[0]',
},
],
path: '',
},
],
[
[
[
{ name: 'John', age: 22 },
{ name: 'Joe', age: 33 },
],
],
{
type: 'array',
value: [
{
type: 'array',
key: '0',
value: [
{
type: 'object',
key: '0',
value: [
{ type: 'string', key: 'name', value: 'John', path: '[0][0].name' },
{ type: 'number', key: 'age', value: '22', path: '[0][0].age' },
],
path: '[0][0]',
},
{
type: 'object',
key: '1',
value: [
{ type: 'string', key: 'name', value: 'Joe', path: '[0][1].name' },
{ type: 'number', key: 'age', value: '33', path: '[0][1].age' },
],
path: '[0][1]',
},
],
path: '[0]',
},
],
path: '',
},
],
[
[
{
dates: [
[new Date('2022-11-22T00:00:00.000Z'), new Date('2022-11-23T00:00:00.000Z')],
[new Date('2022-12-22T00:00:00.000Z'), new Date('2022-12-23T00:00:00.000Z')],
],
},
],
{
type: 'array',
value: [
{
type: 'object',
key: '0',
value: [
{
type: 'array',
key: 'dates',
value: [
{
type: 'array',
key: '0',
value: [
{
type: 'string',
key: '0',
value: '2022-11-22T00:00:00.000Z',
path: '[0].dates[0][0]',
},
{
type: 'string',
key: '1',
value: '2022-11-23T00:00:00.000Z',
path: '[0].dates[0][1]',
},
],
path: '[0].dates[0]',
},
{
type: 'array',
key: '1',
value: [
{
type: 'string',
key: '0',
value: '2022-12-22T00:00:00.000Z',
path: '[0].dates[1][0]',
},
{
type: 'string',
key: '1',
value: '2022-12-23T00:00:00.000Z',
path: '[0].dates[1][1]',
},
],
path: '[0].dates[1]',
},
],
path: '[0].dates',
},
],
path: '[0]',
},
],
path: '',
},
],
])('should return the correct json schema for %s', (input, schema) => {
expect(getSchema(input)).toEqual(schema);
});
it('should return the correct data when using the generated json path on an object', () => {
const input = { people: ['Joe', 'John'] };
const schema = getSchema(input) as Schema;
const pathData = jp.query(
input,
`$${((schema.value as Schema[])[0].value as Schema[])[0].path}`,
);
expect(pathData).toEqual(['Joe']);
});
it('should return the correct data when using the generated json path on a list', () => {
const input = [
{ name: 'John', age: 22, hobbies: ['surfing', 'traveling'] },
{ name: 'Joe', age: 33, hobbies: ['skateboarding', 'gaming'] },
];
const schema = getSchema(input) as Schema;
const pathData = jp.query(
input,
`$${(((schema.value as Schema[])[0].value as Schema[])[2].value as Schema[])[1].path}`,
);
expect(pathData).toEqual(['traveling']);
});
it('should return the correct data when using the generated json path on a list of list', () => {
const input = [[1, 2]];
const schema = getSchema(input) as Schema;
const pathData = jp.query(
input,
`$${((schema.value as Schema[])[0].value as Schema[])[1].path}`,
);
expect(pathData).toEqual([2]);
});
it('should return the correct data when using the generated json path on a list of list of objects', () => {
const input = [
[
{ name: 'John', age: 22 },
{ name: 'Joe', age: 33 },
],
];
const schema = getSchema(input) as Schema;
const pathData = jp.query(
input,
`$${(((schema.value as Schema[])[0].value as Schema[])[1].value as Schema[])[1].path}`,
);
expect(pathData).toEqual([33]);
});
it('should return the correct data when using the generated json path on a list of objects with a list of date tuples', () => {
const input = [
{
dates: [
[new Date('2022-11-22T00:00:00.000Z'), new Date('2022-11-23T00:00:00.000Z')],
[new Date('2022-12-22T00:00:00.000Z'), new Date('2022-12-23T00:00:00.000Z')],
],
},
];
const schema = getSchema(input) as Schema;
const pathData = jp.query(
input,
`$${
(
(((schema.value as Schema[])[0].value as Schema[])[0].value as Schema[])[0]
.value as Schema[]
)[0].path
}`,
);
expect(pathData).toEqual([new Date('2022-11-22T00:00:00.000Z')]);
});
});
describe('dateTests', () => { describe('dateTests', () => {
test.each([ test.each([
'04-08-2021', '04-08-2021',

View file

@ -1,9 +1,6 @@
import dateformat from 'dateformat'; import dateformat from 'dateformat';
import type { IDataObject } from 'n8n-workflow'; import type { IDataObject } from 'n8n-workflow';
import { jsonParse } from 'n8n-workflow'; import { jsonParse } from 'n8n-workflow';
import type { Schema, Optional, Primitives } from '@/Interface';
import { isObj } from '@/utils/typeGuards';
import { generatePath } from '@/utils/mappingUtils';
/* /*
Constants and utility functions than can be used to manipulate different data types and objects Constants and utility functions than can be used to manipulate different data types and objects
@ -162,41 +159,3 @@ export const isValidDate = (input: string | number | Date): boolean => {
export const getObjectKeys = <T extends object, K extends keyof T>(o: T): K[] => export const getObjectKeys = <T extends object, K extends keyof T>(o: T): K[] =>
Object.keys(o) as K[]; Object.keys(o) as K[];
export const getSchema = (input: Optional<Primitives | object>, path = ''): Schema => {
let schema: Schema = { type: 'undefined', value: 'undefined', path };
switch (typeof input) {
case 'object':
if (input === null) {
schema = { type: 'null', value: '[null]', path };
} else if (input instanceof Date) {
schema = { type: 'string', value: input.toISOString(), path };
} else if (Array.isArray(input)) {
schema = {
type: 'array',
value: input.map((item, index) => ({
key: index.toString(),
...getSchema(item, `${path}[${index}]`),
})),
path,
};
} else if (isObj(input)) {
schema = {
type: 'object',
value: Object.entries(input).map(([k, v]) => ({
key: k,
...getSchema(v, generatePath(path, [k])),
})),
path,
};
}
break;
case 'function':
schema = { type: 'function', value: '', path };
break;
default:
schema = { type: typeof input, value: String(input), path };
}
return schema;
};

View file

@ -2198,6 +2198,9 @@ export interface IN8nUISettings {
banners: { banners: {
dismissed: string[]; dismissed: string[];
}; };
ai: {
enabled: boolean;
};
} }
export type Banners = 'V1' | 'TRIAL_OVER' | 'TRIAL'; export type Banners = 'V1' | 'TRIAL_OVER' | 'TRIAL';