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>
This commit is contained in:
Alex Grozav 2023-07-28 10:51:07 +03:00 committed by GitHub
parent d050b99fb2
commit dd6a4c956a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
459 changed files with 8815 additions and 9913 deletions

View file

@ -1,4 +1,6 @@
import { SettingsLogStreamingPage } from '../pages'; import { SettingsLogStreamingPage } from '../pages';
import { getVisibleModalOverlay } from '../utils/modal';
import { getVisibleDropdown } from '../utils';
const settingsLogStreamingPage = new SettingsLogStreamingPage(); const settingsLogStreamingPage = new SettingsLogStreamingPage();
@ -19,6 +21,7 @@ describe('Log Streaming Settings', () => {
}); });
it('should show the add destination modal', () => { it('should show the add destination modal', () => {
cy.enableFeature('logStreaming');
cy.visit('/settings/log-streaming'); cy.visit('/settings/log-streaming');
settingsLogStreamingPage.actions.clickAddFirstDestination(); settingsLogStreamingPage.actions.clickAddFirstDestination();
cy.wait(100); cy.wait(100);
@ -27,7 +30,7 @@ describe('Log Streaming Settings', () => {
settingsLogStreamingPage.getters.getSelectDestinationButton().should('be.visible'); settingsLogStreamingPage.getters.getSelectDestinationButton().should('be.visible');
settingsLogStreamingPage.getters.getSelectDestinationButton().should('have.attr', 'disabled'); settingsLogStreamingPage.getters.getSelectDestinationButton().should('have.attr', 'disabled');
settingsLogStreamingPage.getters settingsLogStreamingPage.getters
.getDestinationModalDialog() .getDestinationModal()
.invoke('css', 'width') .invoke('css', 'width')
.then((widthStr) => parseInt((widthStr as unknown as string).replace('px', ''))) .then((widthStr) => parseInt((widthStr as unknown as string).replace('px', '')))
.should('be.lessThan', 500); .should('be.lessThan', 500);
@ -36,65 +39,67 @@ describe('Log Streaming Settings', () => {
settingsLogStreamingPage.getters settingsLogStreamingPage.getters
.getSelectDestinationButton() .getSelectDestinationButton()
.should('not.have.attr', 'disabled'); .should('not.have.attr', 'disabled');
settingsLogStreamingPage.getters.getDestinationModal().click(1, 1); getVisibleModalOverlay().click(1, 1);
settingsLogStreamingPage.getters.getDestinationModal().should('not.exist'); settingsLogStreamingPage.getters.getDestinationModal().should('not.exist');
}); });
it('should create a destination and delete it', () => { it('should create a destination and delete it', () => {
cy.enableFeature('logStreaming');
cy.visit('/settings/log-streaming'); cy.visit('/settings/log-streaming');
cy.wait(1000); // Race condition with getDestinationDataFromBackend()
settingsLogStreamingPage.actions.clickAddFirstDestination(); settingsLogStreamingPage.actions.clickAddFirstDestination();
cy.wait(100); cy.wait(100);
settingsLogStreamingPage.getters.getDestinationModal().should('be.visible'); settingsLogStreamingPage.getters.getDestinationModal().should('be.visible');
settingsLogStreamingPage.getters.getSelectDestinationType().click(); settingsLogStreamingPage.getters.getSelectDestinationType().click();
settingsLogStreamingPage.getters.getSelectDestinationTypeItems().eq(0).click(); settingsLogStreamingPage.getters.getSelectDestinationTypeItems().eq(0).click();
settingsLogStreamingPage.getters.getSelectDestinationButton().click(); settingsLogStreamingPage.getters.getSelectDestinationButton().click();
settingsLogStreamingPage.getters.getDestinationNameInput().click() settingsLogStreamingPage.getters.getDestinationNameInput().click();
settingsLogStreamingPage.getters.getDestinationNameInput().find('input').clear().type('Destination 0'); settingsLogStreamingPage.getters
.getDestinationNameInput()
.find('input')
.clear()
.type('Destination 0');
settingsLogStreamingPage.getters.getDestinationSaveButton().click(); settingsLogStreamingPage.getters.getDestinationSaveButton().click();
cy.wait(100); cy.wait(100);
settingsLogStreamingPage.getters.getDestinationModal().click(1, 1); getVisibleModalOverlay().click(1, 1);
cy.reload(); cy.reload();
settingsLogStreamingPage.getters.getDestinationCards().eq(0).click(); settingsLogStreamingPage.getters.getDestinationCards().eq(0).click();
settingsLogStreamingPage.getters.getDestinationDeleteButton().should('be.visible').click(); settingsLogStreamingPage.getters.getDestinationDeleteButton().should('be.visible').click();
cy.get('.el-message-box').should('be.visible').find('.btn--cancel').click(); cy.get('.el-message-box').should('be.visible').find('.btn--cancel').click();
settingsLogStreamingPage.getters.getDestinationDeleteButton().click(); settingsLogStreamingPage.getters.getDestinationDeleteButton().click();
cy.get('.el-message-box').should('be.visible').find('.btn--confirm').click(); cy.get('.el-message-box').should('be.visible').find('.btn--confirm').click();
cy.reload();
}); });
it('should create a destination and delete it via card actions', () => { it('should create a destination and delete it via card actions', () => {
cy.enableFeature('logStreaming');
cy.visit('/settings/log-streaming'); cy.visit('/settings/log-streaming');
cy.wait(1000); // Race condition with getDestinationDataFromBackend()
settingsLogStreamingPage.actions.clickAddFirstDestination(); settingsLogStreamingPage.actions.clickAddFirstDestination();
cy.wait(100); cy.wait(100);
settingsLogStreamingPage.getters.getDestinationModal().should('be.visible'); settingsLogStreamingPage.getters.getDestinationModal().should('be.visible');
settingsLogStreamingPage.getters.getSelectDestinationType().click(); settingsLogStreamingPage.getters.getSelectDestinationType().click();
settingsLogStreamingPage.getters.getSelectDestinationTypeItems().eq(0).click(); settingsLogStreamingPage.getters.getSelectDestinationTypeItems().eq(0).click();
settingsLogStreamingPage.getters.getSelectDestinationButton().click(); settingsLogStreamingPage.getters.getSelectDestinationButton().click();
settingsLogStreamingPage.getters.getDestinationNameInput().click() settingsLogStreamingPage.getters.getDestinationNameInput().click();
settingsLogStreamingPage.getters.getDestinationNameInput().find('input').clear().type('Destination 1'); settingsLogStreamingPage.getters
.getDestinationNameInput()
.find('input')
.clear()
.type('Destination 1');
settingsLogStreamingPage.getters.getDestinationSaveButton().should('not.have.attr', 'disabled'); settingsLogStreamingPage.getters.getDestinationSaveButton().should('not.have.attr', 'disabled');
settingsLogStreamingPage.getters.getDestinationSaveButton().click(); settingsLogStreamingPage.getters.getDestinationSaveButton().click();
cy.wait(100); cy.wait(100);
settingsLogStreamingPage.getters.getDestinationModal().click(1, 1); getVisibleModalOverlay().click(1, 1);
cy.reload(); cy.reload();
settingsLogStreamingPage.getters settingsLogStreamingPage.getters.getDestinationCards().eq(0).find('.el-dropdown').click();
.getDestinationCards() getVisibleDropdown().find('.el-dropdown-menu__item').eq(0).click();
.eq(0)
.find('.el-dropdown-selfdefine')
.click();
cy.get('.el-dropdown-menu').find('.el-dropdown-menu__item').eq(0).click();
settingsLogStreamingPage.getters.getDestinationSaveButton().should('not.exist'); settingsLogStreamingPage.getters.getDestinationSaveButton().should('not.exist');
settingsLogStreamingPage.getters.getDestinationModal().click(1, 1); getVisibleModalOverlay().click(1, 1);
settingsLogStreamingPage.getters settingsLogStreamingPage.getters.getDestinationCards().eq(0).find('.el-dropdown').click();
.getDestinationCards() getVisibleDropdown().find('.el-dropdown-menu__item').eq(1).click();
.eq(0)
.find('.el-dropdown-selfdefine')
.click();
cy.get('.el-dropdown-menu').find('.el-dropdown-menu__item').eq(1).click();
cy.get('.el-message-box').should('be.visible').find('.btn--confirm').click(); cy.get('.el-message-box').should('be.visible').find('.btn--confirm').click();
cy.reload();
}); });
}); });

View file

@ -119,18 +119,15 @@ describe('Undo/Redo', () => {
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
cy.drag('[data-test-id="canvas-node"].jtk-drag-selected', [50, 150]); cy.drag('[data-test-id="canvas-node"].jtk-drag-selected', [50, 150]);
WorkflowPage.getters WorkflowPage.getters
.canvasNodes() .canvasNodeByName('Code')
.last()
.should('have.attr', 'style', 'left: 740px; top: 320px;'); .should('have.attr', 'style', 'left: 740px; top: 320px;');
WorkflowPage.actions.hitUndo(); WorkflowPage.actions.hitUndo();
WorkflowPage.getters WorkflowPage.getters
.canvasNodes() .canvasNodeByName('Code')
.last()
.should('have.attr', 'style', 'left: 640px; top: 220px;'); .should('have.attr', 'style', 'left: 640px; top: 220px;');
WorkflowPage.actions.hitRedo(); WorkflowPage.actions.hitRedo();
WorkflowPage.getters WorkflowPage.getters
.canvasNodes() .canvasNodeByName('Code')
.last()
.should('have.attr', 'style', 'left: 740px; top: 320px;'); .should('have.attr', 'style', 'left: 740px; top: 320px;');
}); });
@ -138,7 +135,10 @@ describe('Undo/Redo', () => {
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
WorkflowPage.getters.nodeConnections().realHover(); WorkflowPage.getters.nodeConnections().realHover();
cy.get('.connection-actions .delete').filter(':visible').should('be.visible').click(); cy.get('.connection-actions .delete')
.filter(':visible')
.should('be.visible')
.click({ force: true });
WorkflowPage.getters.nodeConnections().should('have.length', 0); WorkflowPage.getters.nodeConnections().should('have.length', 0);
WorkflowPage.actions.hitUndo(); WorkflowPage.actions.hitUndo();
WorkflowPage.getters.nodeConnections().should('have.length', 1); WorkflowPage.getters.nodeConnections().should('have.length', 1);
@ -256,6 +256,9 @@ describe('Undo/Redo', () => {
}); });
it('should undo/redo multiple steps', () => { it('should undo/redo multiple steps', () => {
const initialPosition = 'left: 420px; top: 220px;';
const movedPosition = 'left: 540px; top: 360px;';
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(SET_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(SET_NODE_NAME);
@ -266,8 +269,10 @@ describe('Undo/Redo', () => {
WorkflowPage.getters.canvasNodes().last().click(); WorkflowPage.getters.canvasNodes().last().click();
WorkflowPage.actions.hitDisableNodeShortcut(); WorkflowPage.actions.hitDisableNodeShortcut();
// Move first one // Move first one
WorkflowPage.getters.canvasNodes().first().should('have.attr', 'style', initialPosition);
WorkflowPage.getters.canvasNodes().first().click(); WorkflowPage.getters.canvasNodes().first().click();
cy.drag('[data-test-id="canvas-node"].jtk-drag-selected', [50, 150]); cy.drag('[data-test-id="canvas-node"].jtk-drag-selected', [50, 150]);
WorkflowPage.getters.canvasNodes().first().should('have.attr', 'style', movedPosition);
// Delete the set node // Delete the set node
WorkflowPage.getters.canvasNodeByName(SET_NODE_NAME).click().click(); WorkflowPage.getters.canvasNodeByName(SET_NODE_NAME).click().click();
cy.get('body').type('{backspace}'); cy.get('body').type('{backspace}');
@ -278,10 +283,7 @@ describe('Undo/Redo', () => {
WorkflowPage.getters.nodeConnections().should('have.length', 3); WorkflowPage.getters.nodeConnections().should('have.length', 3);
// Second undo: Should move first node to it's original position // Second undo: Should move first node to it's original position
WorkflowPage.actions.hitUndo(); WorkflowPage.actions.hitUndo();
WorkflowPage.getters WorkflowPage.getters.canvasNodes().first().should('have.attr', 'style', initialPosition);
.canvasNodes()
.first()
.should('have.attr', 'style', 'left: 420px; top: 220px;');
// Third undo: Should enable last node // Third undo: Should enable last node
WorkflowPage.actions.hitUndo(); WorkflowPage.actions.hitUndo();
WorkflowPage.getters.disabledNodes().should('have.length', 0); WorkflowPage.getters.disabledNodes().should('have.length', 0);
@ -291,10 +293,7 @@ describe('Undo/Redo', () => {
WorkflowPage.getters.disabledNodes().should('have.length', 1); WorkflowPage.getters.disabledNodes().should('have.length', 1);
// Second redo: Should move the first node // Second redo: Should move the first node
WorkflowPage.actions.hitRedo(); WorkflowPage.actions.hitRedo();
WorkflowPage.getters WorkflowPage.getters.canvasNodes().first().should('have.attr', 'style', movedPosition);
.canvasNodes()
.first()
.should('have.attr', 'style', 'left: 540px; top: 360px;');
// Third redo: Should delete the Set node // Third redo: Should delete the Set node
WorkflowPage.actions.hitRedo(); WorkflowPage.actions.hitRedo();
WorkflowPage.getters.canvasNodes().should('have.length', 3); WorkflowPage.getters.canvasNodes().should('have.length', 3);

View file

@ -66,7 +66,6 @@ describe('Canvas Actions', () => {
WorkflowPage.getters.nodeViewBackground().click({ force: true }); WorkflowPage.getters.nodeViewBackground().click({ force: true });
}); });
it('should add a connected node using plus endpoint', () => { it('should add a connected node using plus endpoint', () => {
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
cy.get('.plus-endpoint').should('be.visible').click(); cy.get('.plus-endpoint').should('be.visible').click();

View file

@ -107,7 +107,7 @@ describe('Canvas Node Manipulation and Navigation', () => {
WorkflowPage.actions.zoomToFit(); WorkflowPage.actions.zoomToFit();
cy.get('.plus-draggable-endpoint').filter(':visible').should('not.have.class', 'ep-success'); cy.get('.plus-draggable-endpoint').filter(':visible').should('not.have.class', 'ep-success');
cy.get('.jtk-connector.success').should('have.length', 4); cy.get('.jtk-connector.success').should('have.length', 3);
cy.get('.jtk-connector').should('have.length', 4); cy.get('.jtk-connector').should('have.length', 4);
}); });

View file

@ -7,12 +7,10 @@ describe('Data transformation expressions', () => {
beforeEach(() => { beforeEach(() => {
wf.actions.visit(); wf.actions.visit();
cy.window().then( cy.window().then((win) => {
(win) => { // @ts-ignore
// @ts-ignore win.preventNodeViewBeforeUnload = true;
win.preventNodeViewBeforeUnload = true; });
},
);
}); });
it('$json + native string methods', () => { it('$json + native string methods', () => {
@ -85,7 +83,7 @@ describe('Data transformation expressions', () => {
ndv.getters.inlineExpressionEditorInput().clear().type(input); ndv.getters.inlineExpressionEditorInput().clear().type(input);
ndv.actions.execute(); ndv.actions.execute();
ndv.getters.outputDataContainer().find('[class*=value_]').should('exist') ndv.getters.outputDataContainer().find('[class*=value_]').should('exist');
ndv.getters.outputDataContainer().find('[class*=value_]').should('contain', output); ndv.getters.outputDataContainer().find('[class*=value_]').should('contain', output);
}); });
@ -100,7 +98,7 @@ describe('Data transformation expressions', () => {
ndv.getters.inlineExpressionEditorInput().clear().type(input); ndv.getters.inlineExpressionEditorInput().clear().type(input);
ndv.actions.execute(); ndv.actions.execute();
ndv.getters.outputDataContainer().find('[class*=value_]').should('exist') ndv.getters.outputDataContainer().find('[class*=value_]').should('exist');
ndv.getters.outputDataContainer().find('[class*=value_]').should('contain', output); ndv.getters.outputDataContainer().find('[class*=value_]').should('contain', output);
}); });
}); });
@ -111,7 +109,7 @@ describe('Data transformation expressions', () => {
const addSet = () => { const addSet = () => {
wf.actions.addNodeToCanvas('Set', true, true); wf.actions.addNodeToCanvas('Set', true, true);
ndv.getters.parameterInput('keepOnlySet').find('div[role=switch]').click(); // shorten output ndv.getters.parameterInput('keepOnlySet').find('.el-switch').click(); // shorten output
cy.get('input[placeholder="Add Value"]').click(); cy.get('input[placeholder="Add Value"]').click();
cy.get('span').contains('String').click(); cy.get('span').contains('String').click();
ndv.getters.nthParam(3).contains('Expression').invoke('show').click(); // Values to Set > String > Value ndv.getters.nthParam(3).contains('Expression').invoke('show').click(); // Values to Set > String > Value

View file

@ -4,6 +4,7 @@ import {
SCHEDULE_TRIGGER_NODE_NAME, SCHEDULE_TRIGGER_NODE_NAME,
} from './../constants'; } from './../constants';
import { WorkflowPage, NDV } from '../pages'; import { WorkflowPage, NDV } from '../pages';
import { getVisibleSelect } from '../utils';
const workflowPage = new WorkflowPage(); const workflowPage = new WorkflowPage();
const ndv = new NDV(); const ndv = new NDV();
@ -28,11 +29,7 @@ describe('Data mapping', () => {
ndv.getters.inputDataContainer().get('table', { timeout: 10000 }).should('exist'); ndv.getters.inputDataContainer().get('table', { timeout: 10000 }).should('exist');
ndv.getters.nodeParameters().find('input[placeholder*="Add Value"]').click(); ndv.getters.nodeParameters().find('input[placeholder*="Add Value"]').click();
ndv.getters getVisibleSelect().find('li:nth-child(3)').should('have.text', 'String').click();
.nodeParameters()
.find('.el-select-dropdown__list li:nth-child(3)')
.should('have.text', 'String')
.click();
ndv.getters ndv.getters
.parameterInput('name') .parameterInput('name')
.should('have.length', 1) .should('have.length', 1)
@ -128,7 +125,7 @@ describe('Data mapping', () => {
.find('.json-data') .find('.json-data')
.should( .should(
'have.text', 'have.text',
'[{"input":[{"count":0,"with space":"!!","with.dot":"!!","with"quotes":"!!"}]},{"input":[{"count":1}]}]', '[{"input": [{"count": 0,"with space": "!!","with.dot": "!!","with"quotes": "!!"}]},{"input": [{"count": 1}]}]',
) )
.find('span') .find('span')
.contains('"count"') .contains('"count"')
@ -178,6 +175,7 @@ describe('Data mapping', () => {
it('maps expressions from previous nodes', () => { it('maps expressions from previous nodes', () => {
cy.createFixtureWorkflow('Test_workflow_3.json', `My test workflow`); cy.createFixtureWorkflow('Test_workflow_3.json', `My test workflow`);
workflowPage.actions.zoomToFit();
workflowPage.actions.openNode('Set1'); workflowPage.actions.openNode('Set1');
ndv.actions.selectInputNode(SCHEDULE_TRIGGER_NODE_NAME); ndv.actions.selectInputNode(SCHEDULE_TRIGGER_NODE_NAME);
@ -245,7 +243,8 @@ describe('Data mapping', () => {
workflowPage.actions.addNodeToCanvas('Item Lists'); workflowPage.actions.addNodeToCanvas('Item Lists');
workflowPage.actions.openNode('Item Lists'); workflowPage.actions.openNode('Item Lists');
ndv.getters.parameterInput('operation').click().find('li').contains('Sort').click(); ndv.getters.parameterInput('operation').click();
getVisibleSelect().find('li').contains('Sort').click();
ndv.getters.nodeParameters().find('button').contains('Add Field To Sort By').click(); ndv.getters.nodeParameters().find('button').contains('Add Field To Sort By').click();
@ -274,6 +273,8 @@ describe('Data mapping', () => {
ndv.actions.typeIntoParameterInput('value', 'fun'); ndv.actions.typeIntoParameterInput('value', 'fun');
ndv.actions.clearParameterInput('value'); // keep focus on param ndv.actions.clearParameterInput('value'); // keep focus on param
ndv.actions.dismissMappingTooltip();
cy.wait(300);
ndv.getters.inputDataContainer().should('exist').find('span').contains('count').realMouseDown(); ndv.getters.inputDataContainer().should('exist').find('span').contains('count').realMouseDown();

View file

@ -1,5 +1,6 @@
import { WorkflowPage, WorkflowsPage, NDV } from '../pages'; import { WorkflowPage, WorkflowsPage, NDV } from '../pages';
import { BACKEND_BASE_URL } from '../constants'; import { BACKEND_BASE_URL } from '../constants';
import { getVisibleSelect } from '../utils';
const workflowsPage = new WorkflowsPage(); const workflowsPage = new WorkflowsPage();
const workflowPage = new WorkflowPage(); const workflowPage = new WorkflowPage();
@ -24,11 +25,7 @@ describe('Schedule Trigger node', async () => {
workflowPage.actions.openNode('Schedule Trigger'); workflowPage.actions.openNode('Schedule Trigger');
cy.getByTestId('parameter-input-field').click(); cy.getByTestId('parameter-input-field').click();
cy.getByTestId('parameter-input-field') getVisibleSelect().find('.option-headline').contains('Seconds').click();
.find('.el-select-dropdown')
.find('.option-headline')
.contains('Seconds')
.click();
cy.getByTestId('parameter-input-secondsInterval').clear().type('1'); cy.getByTestId('parameter-input-secondsInterval').clear().type('1');
ndv.getters.backToCanvas().click(); ndv.getters.backToCanvas().click();

View file

@ -2,6 +2,7 @@ import { WorkflowPage, NDV, CredentialsModal } from '../pages';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import { cowBase64 } from '../support/binaryTestFiles'; import { cowBase64 } from '../support/binaryTestFiles';
import { BACKEND_BASE_URL } from '../constants'; import { BACKEND_BASE_URL } from '../constants';
import { getVisibleSelect } from '../utils';
const workflowPage = new WorkflowPage(); const workflowPage = new WorkflowPage();
const ndv = new NDV(); const ndv = new NDV();
@ -34,11 +35,7 @@ const simpleWebhookCall = (options: SimpleWebhookCallOptions) => {
workflowPage.actions.openNode('Webhook'); workflowPage.actions.openNode('Webhook');
cy.getByTestId('parameter-input-httpMethod').click(); cy.getByTestId('parameter-input-httpMethod').click();
cy.getByTestId('parameter-input-httpMethod') getVisibleSelect().find('.option-headline').contains(method).click();
.find('.el-select-dropdown')
.find('.option-headline')
.contains(method)
.click();
cy.getByTestId('parameter-input-path') cy.getByTestId('parameter-input-path')
.find('.parameter-input') .find('.parameter-input')
.find('input') .find('input')
@ -47,11 +44,7 @@ const simpleWebhookCall = (options: SimpleWebhookCallOptions) => {
if (authentication) { if (authentication) {
cy.getByTestId('parameter-input-authentication').click(); cy.getByTestId('parameter-input-authentication').click();
cy.getByTestId('parameter-input-authentication') getVisibleSelect().find('.option-headline').contains(authentication).click();
.find('.el-select-dropdown')
.find('.option-headline')
.contains(authentication)
.click();
} }
if (responseCode) { if (responseCode) {
@ -64,20 +57,12 @@ const simpleWebhookCall = (options: SimpleWebhookCallOptions) => {
if (respondWith) { if (respondWith) {
cy.getByTestId('parameter-input-responseMode').click(); cy.getByTestId('parameter-input-responseMode').click();
cy.getByTestId('parameter-input-responseMode') getVisibleSelect().find('.option-headline').contains(respondWith).click();
.find('.el-select-dropdown')
.find('.option-headline')
.contains(respondWith)
.click();
} }
if (responseData) { if (responseData) {
cy.getByTestId('parameter-input-responseData').click(); cy.getByTestId('parameter-input-responseData').click();
cy.getByTestId('parameter-input-responseData') getVisibleSelect().find('.option-headline').contains(responseData).click();
.find('.el-select-dropdown')
.find('.option-headline')
.contains(responseData)
.click();
} }
if (executeNow) { if (executeNow) {
@ -136,13 +121,13 @@ describe('Webhook Trigger node', async () => {
workflowPage.actions.addNodeToCanvas('Set'); workflowPage.actions.addNodeToCanvas('Set');
workflowPage.actions.openNode('Set'); workflowPage.actions.openNode('Set');
cy.get('.add-option').click(); cy.get('.add-option').click();
cy.get('.add-option').find('.el-select-dropdown__item').contains('Number').click(); getVisibleSelect().find('.el-select-dropdown__item').contains('Number').click();
cy.get('.fixed-collection-parameter') cy.get('.fixed-collection-parameter')
.getByTestId('parameter-input-name') .getByTestId('parameter-input-name')
.clear() .clear()
.type('MyValue'); .type('MyValue');
cy.get('.fixed-collection-parameter').getByTestId('parameter-input-value').clear().type('1234'); cy.get('.fixed-collection-parameter').getByTestId('parameter-input-value').clear().type('1234');
ndv.getters.backToCanvas().click(); ndv.getters.backToCanvas().click({ force: true });
workflowPage.actions.addNodeToCanvas('Respond to Webhook'); workflowPage.actions.addNodeToCanvas('Respond to Webhook');
@ -185,13 +170,18 @@ describe('Webhook Trigger node', async () => {
workflowPage.actions.addNodeToCanvas('Set'); workflowPage.actions.addNodeToCanvas('Set');
workflowPage.actions.openNode('Set'); workflowPage.actions.openNode('Set');
cy.get('.add-option').click(); cy.get('.add-option').click();
cy.get('.add-option').find('.el-select-dropdown__item').contains('Number').click(); getVisibleSelect().find('.el-select-dropdown__item').contains('Number').click();
cy.get('.fixed-collection-parameter') cy.get('.fixed-collection-parameter')
.getByTestId('parameter-input-name') .getByTestId('parameter-input-name')
.find('input')
.clear() .clear()
.type('MyValue'); .type('MyValue');
cy.get('.fixed-collection-parameter').getByTestId('parameter-input-value').clear().type('1234'); cy.get('.fixed-collection-parameter')
ndv.getters.backToCanvas().click(); .getByTestId('parameter-input-value')
.find('input')
.clear()
.type('1234');
ndv.getters.backToCanvas().click({ force: true });
workflowPage.actions.executeWorkflow(); workflowPage.actions.executeWorkflow();
cy.wait(waitForWebhook); cy.wait(waitForWebhook);
@ -216,7 +206,7 @@ describe('Webhook Trigger node', async () => {
workflowPage.actions.addNodeToCanvas('Set'); workflowPage.actions.addNodeToCanvas('Set');
workflowPage.actions.openNode('Set'); workflowPage.actions.openNode('Set');
cy.get('.add-option').click(); cy.get('.add-option').click();
cy.get('.add-option').find('.el-select-dropdown__item').contains('String').click(); getVisibleSelect().find('.el-select-dropdown__item').contains('String').click();
cy.get('.fixed-collection-parameter').getByTestId('parameter-input-name').clear().type('data'); cy.get('.fixed-collection-parameter').getByTestId('parameter-input-name').clear().type('data');
cy.get('.fixed-collection-parameter') cy.get('.fixed-collection-parameter')
.getByTestId('parameter-input-value') .getByTestId('parameter-input-value')
@ -231,11 +221,7 @@ describe('Webhook Trigger node', async () => {
workflowPage.actions.openNode('Move Binary Data'); workflowPage.actions.openNode('Move Binary Data');
cy.getByTestId('parameter-input-mode').click(); cy.getByTestId('parameter-input-mode').click();
cy.getByTestId('parameter-input-mode') getVisibleSelect().find('.option-headline').contains('JSON to Binary').click();
.find('.el-select-dropdown')
.find('.option-headline')
.contains('JSON to Binary')
.click();
ndv.getters.backToCanvas().click(); ndv.getters.backToCanvas().click();
workflowPage.actions.executeWorkflow(); workflowPage.actions.executeWorkflow();
@ -274,7 +260,7 @@ describe('Webhook Trigger node', async () => {
}); });
// add credentials // add credentials
workflowPage.getters.nodeCredentialsSelect().click(); workflowPage.getters.nodeCredentialsSelect().click();
workflowPage.getters.nodeCredentialsSelect().find('li').last().click(); getVisibleSelect().find('li').last().click();
credentialsModal.getters.credentialsEditModal().should('be.visible'); credentialsModal.getters.credentialsEditModal().should('be.visible');
credentialsModal.actions.fillCredentialsForm(); credentialsModal.actions.fillCredentialsForm();
@ -317,7 +303,7 @@ describe('Webhook Trigger node', async () => {
}); });
// add credentials // add credentials
workflowPage.getters.nodeCredentialsSelect().click(); workflowPage.getters.nodeCredentialsSelect().click();
workflowPage.getters.nodeCredentialsSelect().find('li').last().click(); getVisibleSelect().find('li').last().click();
credentialsModal.getters.credentialsEditModal().should('be.visible'); credentialsModal.getters.credentialsEditModal().should('be.visible');
credentialsModal.actions.fillCredentialsForm(); credentialsModal.actions.fillCredentialsForm();

View file

@ -48,7 +48,7 @@ describe('Sharing', { disableAutoLogin: true }, () => {
workflowPage.actions.setWorkflowName('Workflow W1'); workflowPage.actions.setWorkflowName('Workflow W1');
workflowPage.actions.addInitialNodeToCanvas('Manual Trigger'); workflowPage.actions.addInitialNodeToCanvas('Manual Trigger');
workflowPage.actions.addNodeToCanvas('Notion', true, true); workflowPage.actions.addNodeToCanvas('Notion', true, true);
ndv.getters.credentialInput().should('contain', 'Credential C1'); ndv.getters.credentialInput().find('input').should('have.value', 'Credential C1');
ndv.actions.close(); ndv.actions.close();
workflowPage.actions.openShareModal(); workflowPage.actions.openShareModal();
@ -87,16 +87,12 @@ describe('Sharing', { disableAutoLogin: true }, () => {
workflowsPage.getters.workflowCards().should('have.length', 1); workflowsPage.getters.workflowCards().should('have.length', 1);
workflowsPage.getters.workflowCard('Workflow W1').click(); workflowsPage.getters.workflowCard('Workflow W1').click();
workflowPage.actions.addNodeToCanvas('Airtable', true, true); workflowPage.actions.addNodeToCanvas('Airtable', true, true);
ndv.getters.credentialInput().should('contain', 'Credential C2'); ndv.getters.credentialInput().find('input').should('have.value', 'Credential C2');
ndv.actions.close(); ndv.actions.close();
workflowPage.actions.saveWorkflowOnButtonClick(); workflowPage.actions.saveWorkflowOnButtonClick();
workflowPage.actions.openNode('Notion'); workflowPage.actions.openNode('Notion');
ndv.getters ndv.getters.credentialInput().should('have.value', 'Credential C1').should('be.disabled');
.credentialInput()
.find('input')
.should('have.value', 'Credential C1')
.should('be.disabled');
ndv.actions.close(); ndv.actions.close();
}); });
@ -116,11 +112,7 @@ describe('Sharing', { disableAutoLogin: true }, () => {
workflowsPage.getters.workflowCards().should('have.length', 2); workflowsPage.getters.workflowCards().should('have.length', 2);
workflowsPage.getters.workflowCard('Workflow W1').click(); workflowsPage.getters.workflowCard('Workflow W1').click();
workflowPage.actions.openNode('Notion'); workflowPage.actions.openNode('Notion');
ndv.getters ndv.getters.credentialInput().should('have.value', 'Credential C1').should('be.disabled');
.credentialInput()
.find('input')
.should('have.value', 'Credential C1')
.should('be.disabled');
ndv.actions.close(); ndv.actions.close();
cy.waitForLoad(); cy.waitForLoad();

View file

@ -30,7 +30,7 @@ describe('Workflow tags', () => {
} }
cy.contains('Done').click(); cy.contains('Done').click();
wf.getters.createTagButton().click(); wf.getters.tagsDropdown().click();
wf.getters.tagsInDropdown().should('have.length', 5); wf.getters.tagsInDropdown().should('have.length', 5);
wf.getters.tagPills().should('have.length', 0); // none attached wf.getters.tagPills().should('have.length', 0); // none attached
}); });
@ -45,7 +45,7 @@ describe('Workflow tags', () => {
}); });
cy.contains('Done').click(); cy.contains('Done').click();
wf.getters.createTagButton().click(); wf.getters.tagsDropdown().click();
wf.getters.tagsInDropdown().should('have.length', 0); // none stored wf.getters.tagsInDropdown().should('have.length', 0); // none stored
wf.getters.tagPills().should('have.length', 0); // none attached wf.getters.tagPills().should('have.length', 0); // none attached
}); });
@ -57,7 +57,8 @@ describe('Workflow tags', () => {
cy.contains('Create a tag').click(); cy.contains('Create a tag').click();
cy.getByTestId('tags-table').find('input').type(first).type('{enter}'); cy.getByTestId('tags-table').find('input').type(first).type('{enter}');
cy.getByTestId('edit-tag-button').click({ force: true }); cy.getByTestId('tags-table').should('contain.text', first);
cy.getByTestId('edit-tag-button').eq(-1).click({ force: true });
cy.wait(300); cy.wait(300);
cy.getByTestId('tags-table') cy.getByTestId('tags-table')
.find('.el-input--large') .find('.el-input--large')
@ -65,7 +66,7 @@ describe('Workflow tags', () => {
.type(' Updated') .type(' Updated')
.type('{enter}'); .type('{enter}');
cy.contains('Done').click(); cy.contains('Done').click();
wf.getters.createTagButton().click(); wf.getters.tagsDropdown().click();
wf.getters.tagsInDropdown().should('have.length', 1); // one stored wf.getters.tagsInDropdown().should('have.length', 1); // one stored
wf.getters.tagsInDropdown().contains('Updated').should('exist'); wf.getters.tagsInDropdown().contains('Updated').should('exist');
wf.getters.tagPills().should('have.length', 0); // none attached wf.getters.tagPills().should('have.length', 0); // none attached
@ -76,7 +77,7 @@ describe('Workflow tags', () => {
wf.actions.addTags(TEST_TAGS); wf.actions.addTags(TEST_TAGS);
wf.getters.nthTagPill(1).click(); wf.getters.nthTagPill(1).click();
wf.getters.tagsDropdown().find('.el-tag__close').first().click(); wf.getters.tagsDropdown().find('.el-tag__close').first().click();
cy.get('body').type('{enter}'); cy.get('body').click(0, 0);
wf.getters.tagPills().should('have.length', TEST_TAGS.length - 1); wf.getters.tagPills().should('have.length', TEST_TAGS.length - 1);
}); });
@ -84,8 +85,8 @@ describe('Workflow tags', () => {
wf.getters.createTagButton().click(); wf.getters.createTagButton().click();
wf.actions.addTags(TEST_TAGS); wf.actions.addTags(TEST_TAGS);
wf.getters.nthTagPill(1).click(); wf.getters.nthTagPill(1).click();
wf.getters.tagsDropdown().find('li.selected').first().click(); wf.getters.tagsInDropdown().filter('.selected').first().click();
cy.get('body').type('{enter}'); cy.get('body').click(0, 0);
wf.getters.tagPills().should('have.length', TEST_TAGS.length - 1); wf.getters.tagPills().should('have.length', TEST_TAGS.length - 1);
}); });
}); });

View file

@ -58,19 +58,19 @@ describe('User Management', { disableAutoLogin: true }, () => {
it('should delete user and their data', () => { it('should delete user and their data', () => {
usersSettingsPage.actions.loginAndVisit(INSTANCE_OWNER.email, INSTANCE_OWNER.password, true); usersSettingsPage.actions.loginAndVisit(INSTANCE_OWNER.email, INSTANCE_OWNER.password, true);
usersSettingsPage.actions.opedDeleteDialog(INSTANCE_MEMBERS[0].email); usersSettingsPage.actions.opedDeleteDialog(INSTANCE_MEMBERS[0].email);
usersSettingsPage.getters.deleteDataRadioButton().realClick(); usersSettingsPage.getters.deleteDataRadioButton().click();
usersSettingsPage.getters.deleteDataInput().type('delete all data'); usersSettingsPage.getters.deleteDataInput().type('delete all data');
usersSettingsPage.getters.deleteUserButton().realClick(); usersSettingsPage.getters.deleteUserButton().click();
workflowPage.getters.successToast().should('contain', 'User deleted'); workflowPage.getters.successToast().should('contain', 'User deleted');
}); });
it('should delete user and transfer their data', () => { it('should delete user and transfer their data', () => {
usersSettingsPage.actions.loginAndVisit(INSTANCE_OWNER.email, INSTANCE_OWNER.password, true); usersSettingsPage.actions.loginAndVisit(INSTANCE_OWNER.email, INSTANCE_OWNER.password, true);
usersSettingsPage.actions.opedDeleteDialog(INSTANCE_MEMBERS[1].email); usersSettingsPage.actions.opedDeleteDialog(INSTANCE_MEMBERS[1].email);
usersSettingsPage.getters.transferDataRadioButton().realClick(); usersSettingsPage.getters.transferDataRadioButton().click();
usersSettingsPage.getters.userSelectDropDown().realClick(); usersSettingsPage.getters.userSelectDropDown().click();
usersSettingsPage.getters.userSelectOptions().first().realClick(); usersSettingsPage.getters.userSelectOptions().first().click();
usersSettingsPage.getters.deleteUserButton().realClick(); usersSettingsPage.getters.deleteUserButton().click();
workflowPage.getters.successToast().should('contain', 'User deleted'); workflowPage.getters.successToast().should('contain', 'User deleted');
}); });

View file

@ -4,8 +4,6 @@ import {
PIPEDRIVE_NODE_NAME, PIPEDRIVE_NODE_NAME,
HTTP_REQUEST_NODE_NAME, HTTP_REQUEST_NODE_NAME,
NEW_QUERY_AUTH_ACCOUNT_NAME, NEW_QUERY_AUTH_ACCOUNT_NAME,
} from './../constants';
import {
GMAIL_NODE_NAME, GMAIL_NODE_NAME,
NEW_GOOGLE_ACCOUNT_NAME, NEW_GOOGLE_ACCOUNT_NAME,
NEW_TRELLO_ACCOUNT_NAME, NEW_TRELLO_ACCOUNT_NAME,
@ -13,6 +11,7 @@ import {
TRELLO_NODE_NAME, TRELLO_NODE_NAME,
} from '../constants'; } from '../constants';
import { CredentialsPage, CredentialsModal, WorkflowPage, NDV } from '../pages'; import { CredentialsPage, CredentialsModal, WorkflowPage, NDV } from '../pages';
import { getVisibleSelect } from '../utils';
const credentialsPage = new CredentialsPage(); const credentialsPage = new CredentialsPage();
const credentialsModal = new CredentialsModal(); const credentialsModal = new CredentialsModal();
@ -90,13 +89,16 @@ describe('Credentials', () => {
workflowPage.getters.canvasNodes().last().click(); workflowPage.getters.canvasNodes().last().click();
cy.get('body').type('{enter}'); cy.get('body').type('{enter}');
workflowPage.getters.nodeCredentialsSelect().click(); workflowPage.getters.nodeCredentialsSelect().click();
workflowPage.getters.nodeCredentialsSelect().find('li').last().click(); getVisibleSelect().find('li').last().click();
credentialsModal.getters.credentialsEditModal().should('be.visible'); credentialsModal.getters.credentialsEditModal().should('be.visible');
credentialsModal.getters.credentialAuthTypeRadioButtons().should('have.length', 2); credentialsModal.getters.credentialAuthTypeRadioButtons().should('have.length', 2);
credentialsModal.getters.credentialAuthTypeRadioButtons().first().click(); credentialsModal.getters.credentialAuthTypeRadioButtons().first().click();
credentialsModal.actions.fillCredentialsForm(); credentialsModal.actions.fillCredentialsForm();
cy.get('.el-message-box').find('button').contains('Close').click(); cy.get('.el-message-box').find('button').contains('Close').click();
workflowPage.getters.nodeCredentialsSelect().should('contain', NEW_GOOGLE_ACCOUNT_NAME); workflowPage.getters
.nodeCredentialsSelect()
.find('input')
.should('have.value', NEW_GOOGLE_ACCOUNT_NAME);
}); });
it('should show multiple credential types in the same dropdown', () => { it('should show multiple credential types in the same dropdown', () => {
@ -107,7 +109,7 @@ describe('Credentials', () => {
cy.get('body').type('{enter}'); cy.get('body').type('{enter}');
workflowPage.getters.nodeCredentialsSelect().click(); workflowPage.getters.nodeCredentialsSelect().click();
// Add oAuth credentials // Add oAuth credentials
workflowPage.getters.nodeCredentialsSelect().find('li').last().click(); getVisibleSelect().find('li').last().click();
credentialsModal.getters.credentialsEditModal().should('be.visible'); credentialsModal.getters.credentialsEditModal().should('be.visible');
credentialsModal.getters.credentialAuthTypeRadioButtons().should('have.length', 2); credentialsModal.getters.credentialAuthTypeRadioButtons().should('have.length', 2);
credentialsModal.getters.credentialAuthTypeRadioButtons().first().click(); credentialsModal.getters.credentialAuthTypeRadioButtons().first().click();
@ -115,13 +117,14 @@ describe('Credentials', () => {
cy.get('.el-message-box').find('button').contains('Close').click(); cy.get('.el-message-box').find('button').contains('Close').click();
workflowPage.getters.nodeCredentialsSelect().click(); workflowPage.getters.nodeCredentialsSelect().click();
// Add Service account credentials // Add Service account credentials
workflowPage.getters.nodeCredentialsSelect().find('li').last().click(); getVisibleSelect().find('li').last().click();
credentialsModal.getters.credentialsEditModal().should('be.visible'); credentialsModal.getters.credentialsEditModal().should('be.visible');
credentialsModal.getters.credentialAuthTypeRadioButtons().should('have.length', 2); credentialsModal.getters.credentialAuthTypeRadioButtons().should('have.length', 2);
credentialsModal.getters.credentialAuthTypeRadioButtons().last().click(); credentialsModal.getters.credentialAuthTypeRadioButtons().last().click();
credentialsModal.actions.fillCredentialsForm(); credentialsModal.actions.fillCredentialsForm();
// Both (+ the 'Create new' option) should be in the dropdown // Both (+ the 'Create new' option) should be in the dropdown
workflowPage.getters.nodeCredentialsSelect().find('li').should('have.length.greaterThan', 3); workflowPage.getters.nodeCredentialsSelect().click();
getVisibleSelect().find('li').should('have.length.greaterThan', 2);
}); });
it('should correctly render required and optional credentials', () => { it('should correctly render required and optional credentials', () => {
@ -132,18 +135,18 @@ describe('Credentials', () => {
// Select incoming authentication // Select incoming authentication
nodeDetailsView.getters.parameterInput('incomingAuthentication').should('exist'); nodeDetailsView.getters.parameterInput('incomingAuthentication').should('exist');
nodeDetailsView.getters.parameterInput('incomingAuthentication').click(); nodeDetailsView.getters.parameterInput('incomingAuthentication').click();
nodeDetailsView.getters.parameterInput('incomingAuthentication').find('li').first().click(); getVisibleSelect().find('li').first().click();
// There should be two credential fields // There should be two credential fields
workflowPage.getters.nodeCredentialsSelect().should('have.length', 2); workflowPage.getters.nodeCredentialsSelect().should('have.length', 2);
workflowPage.getters.nodeCredentialsSelect().first().click(); workflowPage.getters.nodeCredentialsSelect().first().click();
workflowPage.getters.nodeCredentialsSelect().first().find('li').last().click(); getVisibleSelect().find('li').last().click();
// This one should show auth type selector // This one should show auth type selector
credentialsModal.getters.credentialAuthTypeRadioButtons().should('have.length', 2); credentialsModal.getters.credentialAuthTypeRadioButtons().should('have.length', 2);
cy.get('body').type('{esc}'); cy.get('body').type('{esc}');
workflowPage.getters.nodeCredentialsSelect().last().click(); workflowPage.getters.nodeCredentialsSelect().last().click();
workflowPage.getters.nodeCredentialsSelect().last().find('li').last().click(); getVisibleSelect().find('li').last().click();
// This one should not show auth type selector // This one should not show auth type selector
credentialsModal.getters.credentialsAuthTypeSelector().should('not.exist'); credentialsModal.getters.credentialsAuthTypeSelector().should('not.exist');
}); });
@ -155,10 +158,13 @@ describe('Credentials', () => {
workflowPage.getters.canvasNodes().last().click(); workflowPage.getters.canvasNodes().last().click();
cy.get('body').type('{enter}'); cy.get('body').type('{enter}');
workflowPage.getters.nodeCredentialsSelect().click(); workflowPage.getters.nodeCredentialsSelect().click();
workflowPage.getters.nodeCredentialsSelect().find('li').last().click(); getVisibleSelect().find('li').last().click();
credentialsModal.getters.credentialsAuthTypeSelector().should('not.exist'); credentialsModal.getters.credentialsAuthTypeSelector().should('not.exist');
credentialsModal.actions.fillCredentialsForm(); credentialsModal.actions.fillCredentialsForm();
workflowPage.getters.nodeCredentialsSelect().should('contain', NEW_TRELLO_ACCOUNT_NAME); workflowPage.getters
.nodeCredentialsSelect()
.find('input')
.should('have.value', NEW_TRELLO_ACCOUNT_NAME);
}); });
it('should delete credentials from NDV', () => { it('should delete credentials from NDV', () => {
@ -168,16 +174,22 @@ describe('Credentials', () => {
workflowPage.getters.canvasNodes().last().click(); workflowPage.getters.canvasNodes().last().click();
cy.get('body').type('{enter}'); cy.get('body').type('{enter}');
workflowPage.getters.nodeCredentialsSelect().click(); workflowPage.getters.nodeCredentialsSelect().click();
workflowPage.getters.nodeCredentialsSelect().find('li').last().click(); getVisibleSelect().find('li').last().click();
credentialsModal.actions.fillCredentialsForm(); credentialsModal.actions.fillCredentialsForm();
workflowPage.getters.nodeCredentialsSelect().should('contain', NEW_NOTION_ACCOUNT_NAME); workflowPage.getters
.nodeCredentialsSelect()
.find('input')
.should('have.value', NEW_NOTION_ACCOUNT_NAME);
workflowPage.getters.nodeCredentialsEditButton().click(); workflowPage.getters.nodeCredentialsEditButton().click();
credentialsModal.getters.credentialsEditModal().should('be.visible'); credentialsModal.getters.credentialsEditModal().should('be.visible');
credentialsModal.getters.deleteButton().click(); credentialsModal.getters.deleteButton().click();
cy.get('.el-message-box').find('button').contains('Yes').click(); cy.get('.el-message-box').find('button').contains('Yes').click();
workflowPage.getters.successToast().contains('Credential deleted'); workflowPage.getters.successToast().contains('Credential deleted');
workflowPage.getters.nodeCredentialsSelect().should('not.contain', NEW_TRELLO_ACCOUNT_NAME); workflowPage.getters
.nodeCredentialsSelect()
.find('input')
.should('not.have.value', NEW_TRELLO_ACCOUNT_NAME);
}); });
it('should rename credentials from NDV', () => { it('should rename credentials from NDV', () => {
@ -187,17 +199,18 @@ describe('Credentials', () => {
workflowPage.getters.canvasNodes().last().click(); workflowPage.getters.canvasNodes().last().click();
cy.get('body').type('{enter}'); cy.get('body').type('{enter}');
workflowPage.getters.nodeCredentialsSelect().click(); workflowPage.getters.nodeCredentialsSelect().click();
workflowPage.getters.nodeCredentialsSelect().find('li').last().click(); getVisibleSelect().find('li').last().click();
credentialsModal.actions.fillCredentialsForm(); credentialsModal.actions.fillCredentialsForm();
workflowPage.getters.nodeCredentialsSelect().should('contain', NEW_TRELLO_ACCOUNT_NAME);
workflowPage.getters.nodeCredentialsEditButton().click(); workflowPage.getters.nodeCredentialsEditButton().click();
credentialsModal.getters.credentialsEditModal().should('be.visible'); credentialsModal.getters.credentialsEditModal().should('be.visible');
credentialsModal.getters.name().click(); credentialsModal.getters.name().click();
credentialsModal.actions.renameCredential(NEW_CREDENTIAL_NAME); credentialsModal.actions.renameCredential(NEW_CREDENTIAL_NAME);
credentialsModal.getters.saveButton().click(); credentialsModal.getters.saveButton().click();
credentialsModal.getters.closeButton().click(); credentialsModal.getters.closeButton().click();
workflowPage.getters.nodeCredentialsSelect().should('contain', NEW_CREDENTIAL_NAME); workflowPage.getters
.nodeCredentialsSelect()
.find('input')
.should('have.value', NEW_CREDENTIAL_NAME);
}); });
it('should setup generic authentication for HTTP node', () => { it('should setup generic authentication for HTTP node', () => {
@ -207,20 +220,20 @@ describe('Credentials', () => {
workflowPage.getters.canvasNodes().last().click(); workflowPage.getters.canvasNodes().last().click();
cy.get('body').type('{enter}'); cy.get('body').type('{enter}');
nodeDetailsView.getters.parameterInput('authentication').click(); nodeDetailsView.getters.parameterInput('authentication').click();
nodeDetailsView.getters.parameterInput('authentication').find('li').should('have.length', 3); getVisibleSelect().find('li').should('have.length', 3);
nodeDetailsView.getters.parameterInput('authentication').find('li').last().click(); getVisibleSelect().find('li').last().click();
nodeDetailsView.getters.parameterInput('genericAuthType').should('exist'); nodeDetailsView.getters.parameterInput('genericAuthType').should('exist');
nodeDetailsView.getters.parameterInput('genericAuthType').click(); nodeDetailsView.getters.parameterInput('genericAuthType').click();
nodeDetailsView.getters getVisibleSelect().find('li').should('have.length.greaterThan', 0);
.parameterInput('genericAuthType') getVisibleSelect().find('li').last().click();
.find('li')
.should('have.length.greaterThan', 0);
nodeDetailsView.getters.parameterInput('genericAuthType').find('li').last().click();
workflowPage.getters.nodeCredentialsSelect().should('exist'); workflowPage.getters.nodeCredentialsSelect().should('exist');
workflowPage.getters.nodeCredentialsSelect().click(); workflowPage.getters.nodeCredentialsSelect().click();
workflowPage.getters.nodeCredentialsSelect().find('li').last().click(); getVisibleSelect().find('li').last().click();
credentialsModal.actions.fillCredentialsForm(); credentialsModal.actions.fillCredentialsForm();
workflowPage.getters.nodeCredentialsSelect().should('contain', NEW_QUERY_AUTH_ACCOUNT_NAME); workflowPage.getters
.nodeCredentialsSelect()
.find('input')
.should('have.value', NEW_QUERY_AUTH_ACCOUNT_NAME);
}); });
}); });

View file

@ -4,6 +4,7 @@ import { CredentialsModal, WorkflowPage } from '../pages';
import CustomNodeWithN8nCredentialFixture from '../fixtures/Custom_node_n8n_credential.json'; import CustomNodeWithN8nCredentialFixture from '../fixtures/Custom_node_n8n_credential.json';
import CustomNodeWithCustomCredentialFixture from '../fixtures/Custom_node_custom_credential.json'; import CustomNodeWithCustomCredentialFixture from '../fixtures/Custom_node_custom_credential.json';
import CustomCredential from '../fixtures/Custom_credential.json'; import CustomCredential from '../fixtures/Custom_credential.json';
import { getVisibleSelect } from '../utils';
const credentialsModal = new CredentialsModal(); const credentialsModal = new CredentialsModal();
const nodeCreatorFeature = new NodeCreator(); const nodeCreatorFeature = new NodeCreator();
@ -20,9 +21,13 @@ describe('Community Nodes', () => {
req.on('response', (res) => { req.on('response', (res) => {
const nodes = res.body || []; const nodes = res.body || [];
nodes.push(CustomNodeFixture, CustomNodeWithN8nCredentialFixture, CustomNodeWithCustomCredentialFixture); nodes.push(
CustomNodeFixture,
CustomNodeWithN8nCredentialFixture,
CustomNodeWithCustomCredentialFixture,
);
}); });
}) });
cy.intercept('/types/credentials.json', { middleware: true }, (req) => { cy.intercept('/types/credentials.json', { middleware: true }, (req) => {
req.headers['cache-control'] = 'no-cache, no-store'; req.headers['cache-control'] = 'no-cache, no-store';
@ -31,8 +36,8 @@ describe('Community Nodes', () => {
const credentials = res.body || []; const credentials = res.body || [];
credentials.push(CustomCredential); credentials.push(CustomCredential);
}) });
}) });
workflowPage.actions.visit(); workflowPage.actions.visit();
}); });
@ -45,7 +50,7 @@ describe('Community Nodes', () => {
nodeCreatorFeature.getters nodeCreatorFeature.getters
.getCreatorItem(customNode) .getCreatorItem(customNode)
.findChildByTestId('node-creator-item-tooltip') .find('.el-tooltip__trigger')
.should('exist'); .should('exist');
nodeCreatorFeature.actions.selectNode(customNode); nodeCreatorFeature.actions.selectNode(customNode);
@ -65,16 +70,9 @@ describe('Community Nodes', () => {
secondParameter().find('label').contains('Resource').should('exist'); secondParameter().find('label').contains('Resource').should('exist');
secondParameter().find('input.el-input__inner').should('have.value', 'option2'); secondParameter().find('input.el-input__inner').should('have.value', 'option2');
secondParameter().find('.el-select').click(); secondParameter().find('.el-select').click();
secondParameter().find('.el-select-dropdown__list').should('exist');
// Check if all options are rendered and select the fourth one // Check if all options are rendered and select the fourth one
secondParameter().find('.el-select-dropdown__list').children().should('have.length', 4); getVisibleSelect().find('li').should('have.length', 4);
secondParameter() getVisibleSelect().find('li').eq(3).contains('option4').should('exist').click();
.find('.el-select-dropdown__list')
.children()
.eq(3)
.contains('option4')
.should('exist')
.click();
secondParameter().find('input.el-input__inner').should('have.value', 'option4'); secondParameter().find('input.el-input__inner').should('have.value', 'option4');
}); });

View file

@ -28,59 +28,36 @@ describe('NDV', () => {
ndv.actions.switchOutputMode('Table'); ndv.actions.switchOutputMode('Table');
// input to output // input to output
ndv.getters.inputTableRow(1) ndv.getters
.inputTableRow(1)
.should('exist') .should('exist')
.invoke('attr', 'data-test-id') .invoke('attr', 'data-test-id')
.should('equal', 'hovering-item'); .should('equal', 'hovering-item');
ndv.getters.inputTableRow(1) ndv.getters.inputTableRow(1).realHover();
.realHover(); ndv.getters.outputTableRow(4).invoke('attr', 'data-test-id').should('equal', 'hovering-item');
ndv.getters.outputTableRow(4)
.invoke('attr', 'data-test-id')
.should('equal', 'hovering-item');
ndv.getters.inputTableRow(2) ndv.getters.inputTableRow(2).realHover();
.realHover(); ndv.getters.outputTableRow(2).invoke('attr', 'data-test-id').should('equal', 'hovering-item');
ndv.getters.outputTableRow(2)
.invoke('attr', 'data-test-id') ndv.getters.inputTableRow(3).realHover();
.should('equal', 'hovering-item'); ndv.getters.outputTableRow(6).invoke('attr', 'data-test-id').should('equal', 'hovering-item');
ndv.getters.inputTableRow(3)
.realHover();
ndv.getters.outputTableRow(6)
.invoke('attr', 'data-test-id')
.should('equal', 'hovering-item');
// output to input // output to input
ndv.getters.outputTableRow(1) ndv.getters.outputTableRow(1).realHover();
.realHover(); ndv.getters.inputTableRow(4).invoke('attr', 'data-test-id').should('equal', 'hovering-item');
ndv.getters.inputTableRow(4)
.invoke('attr', 'data-test-id')
.should('equal', 'hovering-item');
ndv.getters.outputTableRow(4) ndv.getters.outputTableRow(4).realHover();
.realHover(); ndv.getters.inputTableRow(1).invoke('attr', 'data-test-id').should('equal', 'hovering-item');
ndv.getters.inputTableRow(1)
.invoke('attr', 'data-test-id')
.should('equal', 'hovering-item');
ndv.getters.outputTableRow(2) ndv.getters.outputTableRow(2).realHover();
.realHover(); ndv.getters.inputTableRow(2).invoke('attr', 'data-test-id').should('equal', 'hovering-item');
ndv.getters.inputTableRow(2)
.invoke('attr', 'data-test-id')
.should('equal', 'hovering-item');
ndv.getters.outputTableRow(6)
.realHover();
ndv.getters.inputTableRow(3)
.invoke('attr', 'data-test-id')
.should('equal', 'hovering-item');
ndv.getters.outputTableRow(1) ndv.getters.outputTableRow(6).realHover();
.realHover(); ndv.getters.inputTableRow(3).invoke('attr', 'data-test-id').should('equal', 'hovering-item');
ndv.getters.inputTableRow(4)
.invoke('attr', 'data-test-id') ndv.getters.outputTableRow(1).realHover();
.should('equal', 'hovering-item'); ndv.getters.inputTableRow(4).invoke('attr', 'data-test-id').should('equal', 'hovering-item');
}); });
it('maps paired input and output items based on selected input node', () => { it('maps paired input and output items based on selected input node', () => {
@ -92,9 +69,11 @@ describe('NDV', () => {
workflowPage.actions.openNode('Set2'); workflowPage.actions.openNode('Set2');
ndv.getters.inputPanel().contains('6 items').should('exist'); ndv.getters.inputPanel().contains('6 items').should('exist');
ndv.getters.outputRunSelector() ndv.getters
.outputRunSelector()
.find('input')
.should('exist') .should('exist')
.should('include.text', '2 of 2 (6 items)'); .should('have.value', '2 of 2 (6 items)');
ndv.actions.switchInputMode('Table'); ndv.actions.switchInputMode('Table');
ndv.actions.switchOutputMode('Table'); ndv.actions.switchOutputMode('Table');
@ -106,7 +85,8 @@ describe('NDV', () => {
ndv.actions.selectInputNode('Set1'); ndv.actions.selectInputNode('Set1');
ndv.getters.backToCanvas().realHover(); // reset to default hover ndv.getters.backToCanvas().realHover(); // reset to default hover
ndv.getters.inputTableRow(1) ndv.getters
.inputTableRow(1)
.should('have.text', '1000') .should('have.text', '1000')
.invoke('attr', 'data-test-id') .invoke('attr', 'data-test-id')
.should('equal', 'hovering-item'); .should('equal', 'hovering-item');
@ -119,7 +99,8 @@ describe('NDV', () => {
ndv.actions.changeOutputRunSelector('1 of 2 (6 items)'); ndv.actions.changeOutputRunSelector('1 of 2 (6 items)');
ndv.getters.backToCanvas().realHover(); // reset to default hover ndv.getters.backToCanvas().realHover(); // reset to default hover
ndv.getters.inputTableRow(1) ndv.getters
.inputTableRow(1)
.should('have.text', '1111') .should('have.text', '1111')
.invoke('attr', 'data-test-id') .invoke('attr', 'data-test-id')
.should('equal', 'hovering-item'); .should('equal', 'hovering-item');
@ -137,11 +118,13 @@ describe('NDV', () => {
workflowPage.actions.executeWorkflow(); workflowPage.actions.executeWorkflow();
workflowPage.actions.openNode('Set3'); workflowPage.actions.openNode('Set3');
ndv.getters.inputRunSelector() ndv.getters
.inputRunSelector()
.should('exist') .should('exist')
.find('input') .find('input')
.should('include.value', '2 of 2 (6 items)'); .should('include.value', '2 of 2 (6 items)');
ndv.getters.outputRunSelector() ndv.getters
.outputRunSelector()
.should('exist') .should('exist')
.find('input') .find('input')
.should('include.value', '2 of 2 (6 items)'); .should('include.value', '2 of 2 (6 items)');
@ -150,23 +133,19 @@ describe('NDV', () => {
ndv.actions.switchOutputMode('Table'); ndv.actions.switchOutputMode('Table');
ndv.actions.changeOutputRunSelector('1 of 2 (6 items)'); ndv.actions.changeOutputRunSelector('1 of 2 (6 items)');
ndv.getters.inputRunSelector().find('input') ndv.getters.inputRunSelector().find('input').should('include.value', '1 of 2 (6 items)');
.should('include.value', '1 of 2 (6 items)'); ndv.getters.outputRunSelector().find('input').should('include.value', '1 of 2 (6 items)');
ndv.getters.outputRunSelector().find('input')
.should('include.value', '1 of 2 (6 items)');
ndv.getters.inputTableRow(1) ndv.getters
.inputTableRow(1)
.should('have.text', '1111') .should('have.text', '1111')
.invoke('attr', 'data-test-id') .invoke('attr', 'data-test-id')
.should('equal', 'hovering-item'); .should('equal', 'hovering-item');
ndv.getters.outputTableRow(1) ndv.getters.outputTableRow(1).should('have.text', '1111').realHover();
.should('have.text', '1111')
.realHover();
ndv.getters.outputTableRow(3) ndv.getters.outputTableRow(3).should('have.text', '4444').realHover();
.should('have.text', '4444') ndv.getters
.realHover(); .inputTableRow(3)
ndv.getters.inputTableRow(3)
.should('have.text', '4444') .should('have.text', '4444')
.invoke('attr', 'data-test-id') .invoke('attr', 'data-test-id')
.should('equal', 'hovering-item'); .should('equal', 'hovering-item');
@ -174,18 +153,16 @@ describe('NDV', () => {
ndv.actions.changeOutputRunSelector('2 of 2 (6 items)'); ndv.actions.changeOutputRunSelector('2 of 2 (6 items)');
cy.wait(50); cy.wait(50);
ndv.getters.inputTableRow(1) ndv.getters.inputTableRow(1).should('have.text', '1000').realHover();
.should('have.text', '1000') ndv.getters
.realHover(); .outputTableRow(1)
ndv.getters.outputTableRow(1)
.should('have.text', '1000') .should('have.text', '1000')
.invoke('attr', 'data-test-id') .invoke('attr', 'data-test-id')
.should('equal', 'hovering-item'); .should('equal', 'hovering-item');
ndv.getters.outputTableRow(3) ndv.getters.outputTableRow(3).should('have.text', '2000').realHover();
.should('have.text', '2000') ndv.getters
.realHover(); .inputTableRow(3)
ndv.getters.inputTableRow(3)
.should('have.text', '2000') .should('have.text', '2000')
.invoke('attr', 'data-test-id') .invoke('attr', 'data-test-id')
.should('equal', 'hovering-item'); .should('equal', 'hovering-item');
@ -200,15 +177,18 @@ describe('NDV', () => {
workflowPage.actions.openNode('Set2'); workflowPage.actions.openNode('Set2');
ndv.getters.inputPanel().contains('6 items').should('exist'); ndv.getters.inputPanel().contains('6 items').should('exist');
ndv.getters.outputRunSelector() ndv.getters
.outputRunSelector()
.find('input')
.should('exist') .should('exist')
.should('include.text', '2 of 2 (6 items)'); .should('have.value', '2 of 2 (6 items)');
ndv.actions.switchInputMode('Table'); ndv.actions.switchInputMode('Table');
ndv.actions.switchOutputMode('Table'); ndv.actions.switchOutputMode('Table');
ndv.getters.backToCanvas().realHover(); // reset to default hover ndv.getters.backToCanvas().realHover(); // reset to default hover
ndv.getters.inputTableRow(1) ndv.getters
.inputTableRow(1)
.should('have.text', '1111') .should('have.text', '1111')
.invoke('attr', 'data-test-id') .invoke('attr', 'data-test-id')
.should('equal', 'hovering-item'); .should('equal', 'hovering-item');
@ -218,28 +198,32 @@ describe('NDV', () => {
ndv.actions.selectInputNode('Code1'); ndv.actions.selectInputNode('Code1');
ndv.getters.inputTableRow(1).realHover(); ndv.getters.inputTableRow(1).realHover();
ndv.getters.inputTableRow(1) ndv.getters
.inputTableRow(1)
.should('have.text', '1000') .should('have.text', '1000')
.invoke('attr', 'data-test-id') .invoke('attr', 'data-test-id')
.should('equal', 'hovering-item'); .should('equal', 'hovering-item');
ndv.getters.outputTableRow(1) ndv.getters.outputTableRow(1).should('have.text', '1000');
.should('have.text', '1000');
ndv.getters.parameterExpressionPreview('value').should('include.text', '1000'); ndv.getters.parameterExpressionPreview('value').should('include.text', '1000');
ndv.actions.selectInputNode('Code'); ndv.actions.selectInputNode('Code');
ndv.getters.inputTableRow(1).realHover(); ndv.getters.inputTableRow(1).realHover();
ndv.getters.inputTableRow(1) ndv.getters
.should('have.text', '6666') .inputTableRow(1)
.invoke('attr', 'data-test-id') .should('have.text', '6666')
.should('equal', 'hovering-item'); .invoke('attr', 'data-test-id')
.should('equal', 'hovering-item');
ndv.getters.outputHoveringItem().should('not.exist'); ndv.getters.outputHoveringItem().should('not.exist');
ndv.getters.parameterExpressionPreview('value').should('include.text', '1000'); ndv.getters.parameterExpressionPreview('value').should('include.text', '1000');
ndv.actions.selectInputNode('When clicking'); ndv.actions.selectInputNode('When clicking');
ndv.getters.inputTableRow(1).realHover(); ndv.getters.inputTableRow(1).realHover();
ndv.getters.inputTableRow(1).should('have.text', "This is an item, but it's empty.").realHover(); ndv.getters
.inputTableRow(1)
.should('have.text', "This is an item, but it's empty.")
.realHover();
ndv.getters.outputHoveringItem().should('have.length', 6); ndv.getters.outputHoveringItem().should('have.length', 6);
ndv.getters.parameterExpressionPreview('value').should('include.text', '1000'); ndv.getters.parameterExpressionPreview('value').should('include.text', '1000');
}); });
@ -256,18 +240,16 @@ describe('NDV', () => {
ndv.actions.switchOutputMode('Table'); ndv.actions.switchOutputMode('Table');
ndv.actions.switchOutputBranch('False Branch (2 items)'); ndv.actions.switchOutputBranch('False Branch (2 items)');
ndv.getters.outputTableRow(1) ndv.getters.outputTableRow(1).should('have.text', '8888').realHover();
.should('have.text', '8888') ndv.getters
.realHover(); .inputTableRow(5)
ndv.getters.inputTableRow(5)
.should('have.text', '8888') .should('have.text', '8888')
.invoke('attr', 'data-test-id') .invoke('attr', 'data-test-id')
.should('equal', 'hovering-item'); .should('equal', 'hovering-item');
ndv.getters.outputTableRow(2) ndv.getters.outputTableRow(2).should('have.text', '9999').realHover();
.should('have.text', '9999') ndv.getters
.realHover(); .inputTableRow(6)
ndv.getters.inputTableRow(6)
.should('have.text', '9999') .should('have.text', '9999')
.invoke('attr', 'data-test-id') .invoke('attr', 'data-test-id')
.should('equal', 'hovering-item'); .should('equal', 'hovering-item');
@ -277,31 +259,21 @@ describe('NDV', () => {
workflowPage.actions.openNode('Set5'); workflowPage.actions.openNode('Set5');
ndv.actions.switchInputBranch('True Branch'); ndv.actions.switchInputBranch('True Branch');
ndv.actions.changeOutputRunSelector('1 of 2 (2 items)') ndv.actions.changeOutputRunSelector('1 of 2 (2 items)');
ndv.getters.outputTableRow(1) ndv.getters.outputTableRow(1).should('have.text', '8888').realHover();
.should('have.text', '8888')
.realHover();
ndv.getters.inputHoveringItem().should('not.exist'); ndv.getters.inputHoveringItem().should('not.exist');
ndv.getters.inputTableRow(1) ndv.getters.inputTableRow(1).should('have.text', '1111').realHover();
.should('have.text', '1111')
.realHover();
ndv.getters.outputHoveringItem().should('not.exist'); ndv.getters.outputHoveringItem().should('not.exist');
ndv.actions.switchInputBranch('False Branch'); ndv.actions.switchInputBranch('False Branch');
ndv.getters.inputTableRow(1) ndv.getters.inputTableRow(1).should('have.text', '8888').realHover();
.should('have.text', '8888')
.realHover();
ndv.actions.changeOutputRunSelector('2 of 2 (4 items)') ndv.actions.changeOutputRunSelector('2 of 2 (4 items)');
ndv.getters.outputTableRow(1) ndv.getters.outputTableRow(1).should('have.text', '1111').realHover();
.should('have.text', '1111')
.realHover();
ndv.actions.changeOutputRunSelector('1 of 2 (2 items)') ndv.actions.changeOutputRunSelector('1 of 2 (2 items)');
ndv.getters.inputTableRow(1) ndv.getters.inputTableRow(1).should('have.text', '8888').realHover();
.should('have.text', '8888')
.realHover();
ndv.getters.outputHoveringItem().should('have.text', '8888'); ndv.getters.outputHoveringItem().should('have.text', '8888');
// todo there's a bug here need to fix ADO-534 // todo there's a bug here need to fix ADO-534
// ndv.getters.outputHoveringItem().should('not.exist'); // ndv.getters.outputHoveringItem().should('not.exist');

View file

@ -2,7 +2,13 @@ import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
const workflowPage = new WorkflowPageClass(); const workflowPage = new WorkflowPageClass();
function checkStickiesStyle( top: number, left: number, height: number, width: number, zIndex?: number) { function checkStickiesStyle(
top: number,
left: number,
height: number,
width: number,
zIndex?: number,
) {
workflowPage.getters.stickies().should(($el) => { workflowPage.getters.stickies().should(($el) => {
expect($el).to.have.css('top', `${top}px`); expect($el).to.have.css('top', `${top}px`);
expect($el).to.have.css('left', `${left}px`); expect($el).to.have.css('left', `${left}px`);
@ -18,22 +24,23 @@ describe('Canvas Actions', () => {
beforeEach(() => { beforeEach(() => {
workflowPage.actions.visit(); workflowPage.actions.visit();
cy.window().then( cy.window().then((win) => {
(win) => { // @ts-ignore
// @ts-ignore win.preventNodeViewBeforeUnload = true;
win.preventNodeViewBeforeUnload = true; });
},
);
}); });
it('adds sticky to canvas with default text and position', () => { it('adds sticky to canvas with default text and position', () => {
workflowPage.getters.addStickyButton().should('not.be.visible'); workflowPage.getters.addStickyButton().should('not.be.visible');
addDefaultSticky() addDefaultSticky();
workflowPage.getters.stickies().eq(0) workflowPage.getters
.stickies()
.eq(0)
.should('have.text', 'Im a note\nDouble click to edit me. Guide\n') .should('have.text', 'Im a note\nDouble click to edit me. Guide\n')
.find('a').contains('Guide').should('have.attr', 'href'); .find('a')
.contains('Guide')
.should('have.attr', 'href');
}); });
it('drags sticky around to top left corner', () => { it('drags sticky around to top left corner', () => {
@ -57,18 +64,19 @@ describe('Canvas Actions', () => {
it('deletes sticky', () => { it('deletes sticky', () => {
workflowPage.actions.addSticky(); workflowPage.actions.addSticky();
workflowPage.getters.stickies().should('have.length', 1) workflowPage.getters.stickies().should('have.length', 1);
workflowPage.actions.deleteSticky(); workflowPage.actions.deleteSticky();
workflowPage.getters.stickies().should('have.length', 0) workflowPage.getters.stickies().should('have.length', 0);
}); });
it('edits sticky and updates content as markdown', () => { it('edits sticky and updates content as markdown', () => {
workflowPage.actions.addSticky(); workflowPage.actions.addSticky();
workflowPage.getters.stickies() workflowPage.getters
.should('have.text', 'Im a note\nDouble click to edit me. Guide\n') .stickies()
.should('have.text', 'Im a note\nDouble click to edit me. Guide\n');
workflowPage.getters.stickies().dblclick(); workflowPage.getters.stickies().dblclick();
workflowPage.actions.editSticky('# hello world \n ## text text'); workflowPage.actions.editSticky('# hello world \n ## text text');
@ -159,32 +167,41 @@ describe('Canvas Actions', () => {
cy.drag('[data-test-id="sticky"] [data-dir="topLeft"]', [-150, -150]); cy.drag('[data-test-id="sticky"] [data-dir="topLeft"]', [-150, -150]);
checkStickiesStyle(124, 256, 316, 384, -121); checkStickiesStyle(124, 256, 316, 384, -121);
workflowPage.getters.canvasNodes().eq(0) workflowPage.getters
.canvasNodes()
.eq(0)
.should(($el) => { .should(($el) => {
expect($el).to.have.css('z-index', 'auto'); expect($el).to.have.css('z-index', 'auto');
}); });
workflowPage.actions.addSticky(); workflowPage.actions.addSticky();
workflowPage.getters.stickies().eq(0) workflowPage.getters
.stickies()
.eq(0)
.should(($el) => { .should(($el) => {
expect($el).to.have.css('z-index', '-121'); expect($el).to.have.css('z-index', '-121');
}); });
workflowPage.getters.stickies().eq(1) workflowPage.getters
.stickies()
.eq(1)
.should(($el) => { .should(($el) => {
expect($el).to.have.css('z-index', '-38'); expect($el).to.have.css('z-index', '-38');
}); });
cy.drag('[data-test-id="sticky"] [data-dir="topLeft"]', [-200, -200], { index: 1 }); cy.drag('[data-test-id="sticky"] [data-dir="topLeft"]', [-200, -200], { index: 1 });
workflowPage.getters.stickies().eq(0) workflowPage.getters
.stickies()
.eq(0)
.should(($el) => { .should(($el) => {
expect($el).to.have.css('z-index', '-121'); expect($el).to.have.css('z-index', '-121');
}); });
workflowPage.getters.stickies().eq(1) workflowPage.getters
.stickies()
.eq(1)
.should(($el) => { .should(($el) => {
expect($el).to.have.css('z-index', '-158'); expect($el).to.have.css('z-index', '-158');
}); });
}); });
}); });
@ -198,15 +215,20 @@ type BoundingBox = {
width: number; width: number;
top: number; top: number;
left: number; left: number;
} };
function dragRightEdge(curr: BoundingBox, move: number) { function dragRightEdge(curr: BoundingBox, move: number) {
workflowPage.getters.stickies().first().then(($el) => { workflowPage.getters
const { left, top, height, width } = curr; .stickies()
cy.drag(`[data-test-id="sticky"] [data-dir="right"]`, [left + width + move, 0], { abs: true }); .first()
stickyShouldBePositionedCorrectly({ top, left }); .then(($el) => {
stickyShouldHaveCorrectSize([height, width * 1.5 + move]); const { left, top, height, width } = curr;
}); cy.drag(`[data-test-id="sticky"] [data-dir="right"]`, [left + width + move, 0], {
abs: true,
});
stickyShouldBePositionedCorrectly({ top, left });
stickyShouldHaveCorrectSize([height, width * 1.5 + move]);
});
} }
function shouldHaveOneSticky() { function shouldHaveOneSticky() {
@ -214,17 +236,20 @@ function shouldHaveOneSticky() {
} }
function shouldBeInDefaultLocation() { function shouldBeInDefaultLocation() {
workflowPage.getters.stickies().eq(0).should(($el) => { workflowPage.getters
expect($el).to.have.css('height', '160px'); .stickies()
expect($el).to.have.css('width', '240px'); .eq(0)
}) .should(($el) => {
expect($el).to.have.css('height', '160px');
expect($el).to.have.css('width', '240px');
});
} }
function shouldHaveDefaultSize() { function shouldHaveDefaultSize() {
workflowPage.getters.stickies().should(($el) => { workflowPage.getters.stickies().should(($el) => {
expect($el).to.have.css('height', '160px'); expect($el).to.have.css('height', '160px');
expect($el).to.have.css('width', '240px'); expect($el).to.have.css('width', '240px');
}) });
} }
function addDefaultSticky() { function addDefaultSticky() {
@ -237,21 +262,19 @@ function addDefaultSticky() {
function stickyShouldBePositionedCorrectly(position: Position) { function stickyShouldBePositionedCorrectly(position: Position) {
const yOffset = -100; const yOffset = -100;
const xOffset = -180; const xOffset = -180;
workflowPage.getters.stickies() workflowPage.getters.stickies().should(($el) => {
.should(($el) => { expect($el).to.have.css('top', `${yOffset + position.top}px`);
expect($el).to.have.css('top', `${yOffset + position.top}px`); expect($el).to.have.css('left', `${xOffset + position.left}px`);
expect($el).to.have.css('left', `${xOffset + position.left}px`); });
});
} }
function stickyShouldHaveCorrectSize(size: [number, number]) { function stickyShouldHaveCorrectSize(size: [number, number]) {
const yOffset = 0; const yOffset = 0;
const xOffset = 0; const xOffset = 0;
workflowPage.getters.stickies() workflowPage.getters.stickies().should(($el) => {
.should(($el) => { expect($el).to.have.css('height', `${yOffset + size[0]}px`);
expect($el).to.have.css('height', `${yOffset + size[0]}px`); expect($el).to.have.css('width', `${xOffset + size[1]}px`);
expect($el).to.have.css('width', `${xOffset + size[1]}px`); });
});
} }
function moveSticky(target: Position) { function moveSticky(target: Position) {

View file

@ -1,4 +1,5 @@
import { WorkflowPage, NDV, CredentialsModal } from '../pages'; import { WorkflowPage, NDV, CredentialsModal } from '../pages';
import { getVisibleSelect } from '../utils';
const workflowPage = new WorkflowPage(); const workflowPage = new WorkflowPage();
const ndv = new NDV(); const ndv = new NDV();
@ -32,7 +33,7 @@ describe('Resource Locator', () => {
workflowPage.actions.addNodeToCanvas('Google Sheets', true, true); workflowPage.actions.addNodeToCanvas('Google Sheets', true, true);
workflowPage.getters.nodeCredentialsSelect().click(); workflowPage.getters.nodeCredentialsSelect().click();
// Add oAuth credentials // Add oAuth credentials
workflowPage.getters.nodeCredentialsSelect().find('li').last().click(); getVisibleSelect().find('li').last().click();
credentialsModal.getters.credentialsEditModal().should('be.visible'); credentialsModal.getters.credentialsEditModal().should('be.visible');
credentialsModal.getters.credentialAuthTypeRadioButtons().should('have.length', 2); credentialsModal.getters.credentialAuthTypeRadioButtons().should('have.length', 2);
credentialsModal.getters.credentialAuthTypeRadioButtons().first().click(); credentialsModal.getters.credentialAuthTypeRadioButtons().first().click();

View file

@ -1,6 +1,7 @@
import { NodeCreator } from '../pages/features/node-creator'; import { NodeCreator } from '../pages/features/node-creator';
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow'; import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
import { NDV } from '../pages/ndv'; import { NDV } from '../pages/ndv';
import { getVisibleSelect } from '../utils';
const nodeCreatorFeature = new NodeCreator(); const nodeCreatorFeature = new NodeCreator();
const WorkflowPage = new WorkflowPageClass(); const WorkflowPage = new WorkflowPageClass();
@ -85,7 +86,7 @@ describe('Node Creator', () => {
nodeCreatorFeature.getters.getCreatorItem(editImageNode).click(); nodeCreatorFeature.getters.getCreatorItem(editImageNode).click();
nodeCreatorFeature.getters.activeSubcategory().should('have.text', editImageNode); nodeCreatorFeature.getters.activeSubcategory().should('have.text', editImageNode);
nodeCreatorFeature.getters.getCreatorItem('Crop Image').click(); nodeCreatorFeature.getters.getCreatorItem('Crop Image').click();
NDVModal.getters.parameterInput('operation').should('contain.text', 'Crop'); NDVModal.getters.parameterInput('operation').find('input').should('have.value', 'Crop');
}); });
it('should search through actions and confirm added action', () => { it('should search through actions and confirm added action', () => {
@ -95,9 +96,9 @@ describe('Node Creator', () => {
nodeCreatorFeature.getters.activeSubcategory().should('have.text', 'FTP'); nodeCreatorFeature.getters.activeSubcategory().should('have.text', 'FTP');
nodeCreatorFeature.getters.searchBar().find('input').clear().type('file'); nodeCreatorFeature.getters.searchBar().find('input').clear().type('file');
// Navigate to rename action which should be the 4th item // Navigate to rename action which should be the 4th item
nodeCreatorFeature.getters.searchBar().find('input').type('{uparrow}{uparrow}{rightarrow}'); nodeCreatorFeature.getters.searchBar().find('input').type('{uparrow}{rightarrow}');
NDVModal.getters.parameterInput('operation').should('contain.text', 'Rename'); NDVModal.getters.parameterInput('operation').find('input').should('have.value', 'Rename');
}) });
it('should not show actions for single action nodes', () => { it('should not show actions for single action nodes', () => {
const singleActionNodes = [ const singleActionNodes = [
@ -110,19 +111,22 @@ describe('Node Creator', () => {
'Spontit', 'Spontit',
'Vonage', 'Vonage',
'Send Email', 'Send Email',
'Toggl Trigger' 'Toggl Trigger',
] ];
const doubleActionNode = 'OpenWeatherMap' const doubleActionNode = 'OpenWeatherMap';
nodeCreatorFeature.actions.openNodeCreator(); nodeCreatorFeature.actions.openNodeCreator();
singleActionNodes.forEach((node) => { singleActionNodes.forEach((node) => {
nodeCreatorFeature.getters.searchBar().find('input').clear().type(node); nodeCreatorFeature.getters.searchBar().find('input').clear().type(node);
nodeCreatorFeature.getters.getCreatorItem(node).find('button[class*="panelIcon"]').should('not.exist'); nodeCreatorFeature.getters
}) .getCreatorItem(node)
.find('button[class*="panelIcon"]')
.should('not.exist');
});
nodeCreatorFeature.getters.searchBar().find('input').clear().type(doubleActionNode); nodeCreatorFeature.getters.searchBar().find('input').clear().type(doubleActionNode);
nodeCreatorFeature.getters.getCreatorItem(doubleActionNode).click(); nodeCreatorFeature.getters.getCreatorItem(doubleActionNode).click();
nodeCreatorFeature.getters.creatorItem().should('have.length', 4); nodeCreatorFeature.getters.creatorItem().should('have.length', 4);
}) });
it('should have "Actions" section collapsed when opening actions view from Trigger root view', () => { it('should have "Actions" section collapsed when opening actions view from Trigger root view', () => {
nodeCreatorFeature.actions.openNodeCreator(); nodeCreatorFeature.actions.openNodeCreator();
@ -131,10 +135,19 @@ describe('Node Creator', () => {
nodeCreatorFeature.getters.getCategoryItem('Actions').should('exist'); nodeCreatorFeature.getters.getCategoryItem('Actions').should('exist');
nodeCreatorFeature.getters.getCategoryItem('Triggers').should('exist'); nodeCreatorFeature.getters.getCategoryItem('Triggers').should('exist');
nodeCreatorFeature.getters.getCategoryItem('Triggers').parent().should('not.have.attr', 'data-category-collapsed'); nodeCreatorFeature.getters
nodeCreatorFeature.getters.getCategoryItem('Actions').parent().should('have.attr', 'data-category-collapsed', 'true'); .getCategoryItem('Triggers')
nodeCreatorFeature.getters.getCategoryItem('Actions').click() .parent()
nodeCreatorFeature.getters.getCategoryItem('Actions').parent().should('not.have.attr', 'data-category-collapsed'); .should('have.attr', 'data-category-collapsed', 'false');
nodeCreatorFeature.getters
.getCategoryItem('Actions')
.parent()
.should('have.attr', 'data-category-collapsed', 'true');
nodeCreatorFeature.getters.getCategoryItem('Actions').click();
nodeCreatorFeature.getters
.getCategoryItem('Actions')
.parent()
.should('have.attr', 'data-category-collapsed', 'false');
}); });
it('should have "Triggers" section collapsed when opening actions view from Regular root view', () => { it('should have "Triggers" section collapsed when opening actions view from Regular root view', () => {
@ -145,17 +158,33 @@ describe('Node Creator', () => {
nodeCreatorFeature.getters.searchBar().find('input').clear().type('n8n'); nodeCreatorFeature.getters.searchBar().find('input').clear().type('n8n');
nodeCreatorFeature.getters.getCreatorItem('n8n').click(); nodeCreatorFeature.getters.getCreatorItem('n8n').click();
nodeCreatorFeature.getters.getCategoryItem('Actions').parent().should('not.have.attr', 'data-category-collapsed'); nodeCreatorFeature.getters
nodeCreatorFeature.getters.getCategoryItem('Actions').click() .getCategoryItem('Actions')
nodeCreatorFeature.getters.getCategoryItem('Actions').parent().should('have.attr', 'data-category-collapsed'); .parent()
nodeCreatorFeature.getters.getCategoryItem('Triggers').parent().should('have.attr', 'data-category-collapsed'); .should('have.attr', 'data-category-collapsed', 'false');
nodeCreatorFeature.getters.getCategoryItem('Triggers').click() nodeCreatorFeature.getters.getCategoryItem('Actions').click();
nodeCreatorFeature.getters.getCategoryItem('Triggers').parent().should('not.have.attr', 'data-category-collapsed'); nodeCreatorFeature.getters
.getCategoryItem('Actions')
.parent()
.should('have.attr', 'data-category-collapsed', 'true');
nodeCreatorFeature.getters
.getCategoryItem('Triggers')
.parent()
.should('have.attr', 'data-category-collapsed', 'true');
nodeCreatorFeature.getters.getCategoryItem('Triggers').click();
nodeCreatorFeature.getters
.getCategoryItem('Triggers')
.parent()
.should('have.attr', 'data-category-collapsed', 'false');
}); });
it('should show callout and two suggested nodes if node has no trigger actions', () => { it('should show callout and two suggested nodes if node has no trigger actions', () => {
nodeCreatorFeature.actions.openNodeCreator(); nodeCreatorFeature.actions.openNodeCreator();
nodeCreatorFeature.getters.searchBar().find('input').clear().type('Customer Datastore (n8n training)'); nodeCreatorFeature.getters
.searchBar()
.find('input')
.clear()
.type('Customer Datastore (n8n training)');
nodeCreatorFeature.getters.getCreatorItem('Customer Datastore (n8n training)').click(); nodeCreatorFeature.getters.getCreatorItem('Customer Datastore (n8n training)').click();
cy.getByTestId('actions-panel-no-triggers-callout').should('be.visible'); cy.getByTestId('actions-panel-no-triggers-callout').should('be.visible');
@ -165,28 +194,32 @@ describe('Node Creator', () => {
it('should show intro callout if user has not made a production execution', () => { it('should show intro callout if user has not made a production execution', () => {
nodeCreatorFeature.actions.openNodeCreator(); nodeCreatorFeature.actions.openNodeCreator();
nodeCreatorFeature.getters.searchBar().find('input').clear().type('Customer Datastore (n8n training)'); nodeCreatorFeature.getters
.searchBar()
.find('input')
.clear()
.type('Customer Datastore (n8n training)');
nodeCreatorFeature.getters.getCreatorItem('Customer Datastore (n8n training)').click(); nodeCreatorFeature.getters.getCreatorItem('Customer Datastore (n8n training)').click();
cy.getByTestId('actions-panel-activation-callout').should('be.visible'); cy.getByTestId('actions-panel-activation-callout').should('be.visible');
nodeCreatorFeature.getters.activeSubcategory().find('button').click(); nodeCreatorFeature.getters.activeSubcategory().find('button').click();
nodeCreatorFeature.getters.searchBar().find('input').clear() nodeCreatorFeature.getters.searchBar().find('input').clear();
nodeCreatorFeature.getters.getCreatorItem('On a schedule').click(); nodeCreatorFeature.getters.getCreatorItem('On a schedule').click();
// Setup 1s interval execution // Setup 1s interval execution
cy.getByTestId('parameter-input-field').click(); cy.getByTestId('parameter-input-field').click();
cy.getByTestId('parameter-input-field') getVisibleSelect().find('.option-headline').contains('Seconds').click();
.find('.el-select-dropdown')
.find('.option-headline')
.contains('Seconds')
.click();
cy.getByTestId('parameter-input-secondsInterval').clear().type('1'); cy.getByTestId('parameter-input-secondsInterval').clear().type('1');
NDVModal.actions.close(); NDVModal.actions.close();
nodeCreatorFeature.actions.openNodeCreator(); nodeCreatorFeature.actions.openNodeCreator();
nodeCreatorFeature.getters.searchBar().find('input').clear().type('Customer Datastore (n8n training)'); nodeCreatorFeature.getters
.searchBar()
.find('input')
.clear()
.type('Customer Datastore (n8n training)');
nodeCreatorFeature.getters.getCreatorItem('Customer Datastore (n8n training)').click(); nodeCreatorFeature.getters.getCreatorItem('Customer Datastore (n8n training)').click();
nodeCreatorFeature.getters.getCreatorItem('Get All People').click(); nodeCreatorFeature.getters.getCreatorItem('Get All People').click();
NDVModal.actions.close(); NDVModal.actions.close();
@ -197,11 +230,15 @@ describe('Node Creator', () => {
// Wait for schedule 1s execution to mark user as having made a production execution // Wait for schedule 1s execution to mark user as having made a production execution
cy.wait(1500); cy.wait(1500);
cy.reload() cy.reload();
// Action callout should not be visible after user has made a production execution // Action callout should not be visible after user has made a production execution
nodeCreatorFeature.actions.openNodeCreator(); nodeCreatorFeature.actions.openNodeCreator();
nodeCreatorFeature.getters.searchBar().find('input').clear().type('Customer Datastore (n8n training)'); nodeCreatorFeature.getters
.searchBar()
.find('input')
.clear()
.type('Customer Datastore (n8n training)');
nodeCreatorFeature.getters.getCreatorItem('Customer Datastore (n8n training)').click(); nodeCreatorFeature.getters.getCreatorItem('Customer Datastore (n8n training)').click();
cy.getByTestId('actions-panel-activation-callout').should('not.exist'); cy.getByTestId('actions-panel-activation-callout').should('not.exist');
@ -210,7 +247,11 @@ describe('Node Creator', () => {
it('should show Trigger and Actions sections during search', () => { it('should show Trigger and Actions sections during search', () => {
nodeCreatorFeature.actions.openNodeCreator(); nodeCreatorFeature.actions.openNodeCreator();
nodeCreatorFeature.getters.searchBar().find('input').clear().type('Customer Datastore (n8n training)'); nodeCreatorFeature.getters
.searchBar()
.find('input')
.clear()
.type('Customer Datastore (n8n training)');
nodeCreatorFeature.getters.getCreatorItem('Customer Datastore (n8n training)').click(); nodeCreatorFeature.getters.getCreatorItem('Customer Datastore (n8n training)').click();
nodeCreatorFeature.getters.searchBar().find('input').clear().type('Non existent action name'); nodeCreatorFeature.getters.searchBar().find('input').clear().type('Non existent action name');
@ -228,7 +269,8 @@ describe('Node Creator', () => {
{ {
name: 'canvas add button', name: 'canvas add button',
handler: () => nodeCreatorFeature.getters.canvasAddButton().click(), handler: () => nodeCreatorFeature.getters.canvasAddButton().click(),
}, { },
{
name: 'plus button', name: 'plus button',
handler: () => nodeCreatorFeature.getters.plusButton().click(), handler: () => nodeCreatorFeature.getters.plusButton().click(),
}, },
@ -238,10 +280,10 @@ describe('Node Creator', () => {
// name: 'tab key', // name: 'tab key',
// handler: () => cy.realPress('Tab'), // handler: () => cy.realPress('Tab'),
// }, // },
] ];
sourcesWithAppend.forEach((source) => { sourcesWithAppend.forEach((source) => {
it(`should append manual trigger when source is ${source.name}`, () => { it(`should append manual trigger when source is ${source.name}`, () => {
source.handler() source.handler();
nodeCreatorFeature.getters.searchBar().find('input').clear().type('n8n'); nodeCreatorFeature.getters.searchBar().find('input').clear().type('n8n');
nodeCreatorFeature.getters.getCreatorItem('n8n').click(); nodeCreatorFeature.getters.getCreatorItem('n8n').click();
nodeCreatorFeature.getters.getCategoryItem('Actions').click(); nodeCreatorFeature.getters.getCategoryItem('Actions').click();
@ -251,6 +293,7 @@ describe('Node Creator', () => {
}); });
}); });
// @TODO FIX ADDING 2 NODES IN ONE GO
it('should not append manual trigger when source is canvas related', () => { it('should not append manual trigger when source is canvas related', () => {
nodeCreatorFeature.getters.canvasAddButton().click(); nodeCreatorFeature.getters.canvasAddButton().click();
nodeCreatorFeature.getters.searchBar().find('input').clear().type('n8n'); nodeCreatorFeature.getters.searchBar().find('input').clear().type('n8n');
@ -258,8 +301,8 @@ describe('Node Creator', () => {
nodeCreatorFeature.getters.getCategoryItem('Actions').click(); nodeCreatorFeature.getters.getCategoryItem('Actions').click();
nodeCreatorFeature.getters.getCreatorItem('Create a credential').click(); nodeCreatorFeature.getters.getCreatorItem('Create a credential').click();
NDVModal.actions.close(); NDVModal.actions.close();
WorkflowPage.actions.deleteNode('When clicking "Execute Workflow"') WorkflowPage.actions.deleteNode('When clicking "Execute Workflow"');
WorkflowPage.getters.canvasNodePlusEndpointByName('n8n').click() WorkflowPage.getters.canvasNodePlusEndpointByName('n8n').click();
nodeCreatorFeature.getters.searchBar().find('input').clear().type('n8n'); nodeCreatorFeature.getters.searchBar().find('input').clear().type('n8n');
nodeCreatorFeature.getters.getCreatorItem('n8n').click(); nodeCreatorFeature.getters.getCreatorItem('n8n').click();
nodeCreatorFeature.getters.getCategoryItem('Actions').click(); nodeCreatorFeature.getters.getCategoryItem('Actions').click();
@ -267,8 +310,8 @@ describe('Node Creator', () => {
NDVModal.actions.close(); NDVModal.actions.close();
WorkflowPage.getters.canvasNodes().should('have.length', 2); WorkflowPage.getters.canvasNodes().should('have.length', 2);
WorkflowPage.actions.zoomToFit(); WorkflowPage.actions.zoomToFit();
WorkflowPage.actions.addNodeBetweenNodes('n8n', 'n8n1', 'Item Lists', 'Summarize') WorkflowPage.actions.addNodeBetweenNodes('n8n', 'n8n1', 'Item Lists', 'Summarize');
WorkflowPage.getters.canvasNodes().should('have.length', 3); WorkflowPage.getters.canvasNodes().should('have.length', 3);
}) });
}); });
}); });

View file

@ -117,7 +117,7 @@ describe('NDV', () => {
setupSchemaWorkflow(); setupSchemaWorkflow();
ndv.getters.outputDisplayMode().children().should('have.length', 3); ndv.getters.outputDisplayMode().children().should('have.length', 3);
ndv.getters.outputDisplayMode().find('[class*=active]').should('contain', 'Table'); ndv.getters.outputDisplayMode().find('[class*=active]').should('contain', 'Table');
ndv.getters.outputDisplayMode().contains('Schema').click(); ndv.actions.switchOutputMode('Schema');
ndv.getters.outputDisplayMode().find('[class*=active]').should('contain', 'Schema'); ndv.getters.outputDisplayMode().find('[class*=active]').should('contain', 'Schema');
schemaKeys.forEach((key) => { schemaKeys.forEach((key) => {
@ -130,7 +130,7 @@ describe('NDV', () => {
}); });
it('should preserve schema view after execution', () => { it('should preserve schema view after execution', () => {
setupSchemaWorkflow(); setupSchemaWorkflow();
ndv.getters.outputDisplayMode().contains('Schema').click(); ndv.actions.switchOutputMode('Schema');
ndv.actions.execute(); ndv.actions.execute();
ndv.getters.outputDisplayMode().find('[class*=active]').should('contain', 'Schema'); ndv.getters.outputDisplayMode().find('[class*=active]').should('contain', 'Schema');
}); });
@ -142,7 +142,7 @@ describe('NDV', () => {
.outputPanel() .outputPanel()
.find('[data-test-id=run-data-schema-item]') .find('[data-test-id=run-data-schema-item]')
.filter(':contains("objectValue")'); .filter(':contains("objectValue")');
ndv.getters.outputDisplayMode().contains('Schema').click(); ndv.actions.switchOutputMode('Schema');
expandedObjectProps.forEach((key) => { expandedObjectProps.forEach((key) => {
ndv.getters ndv.getters
@ -173,9 +173,9 @@ describe('NDV', () => {
ndv.actions.execute(); ndv.actions.execute();
ndv.getters.outputPanel().contains('25 items').should('exist'); ndv.getters.outputPanel().contains('25 items').should('exist');
ndv.getters.outputPanel().find('[class*=_pagination]').should('exist'); ndv.getters.outputPanel().find('[class*=_pagination]').should('exist');
ndv.getters.outputDisplayMode().contains('Schema').click(); ndv.actions.switchOutputMode('Schema');
ndv.getters.outputPanel().find('[class*=_pagination]').should('not.exist'); ndv.getters.outputPanel().find('[class*=_pagination]').should('not.exist');
ndv.getters.outputDisplayMode().contains('JSON').click(); ndv.actions.switchOutputMode('JSON');
ndv.getters.outputPanel().find('[class*=_pagination]').should('exist'); ndv.getters.outputPanel().find('[class*=_pagination]').should('exist');
}); });
it('should display large schema', () => { it('should display large schema', () => {
@ -188,7 +188,7 @@ describe('NDV', () => {
ndv.getters.outputPanel().contains('20 items').should('exist'); ndv.getters.outputPanel().contains('20 items').should('exist');
ndv.getters.outputPanel().find('[class*=_pagination]').should('exist'); ndv.getters.outputPanel().find('[class*=_pagination]').should('exist');
ndv.getters.outputDisplayMode().contains('Schema').click(); ndv.actions.switchOutputMode('Schema');
ndv.getters.outputPanel().find('[class*=_pagination]').should('not.exist'); ndv.getters.outputPanel().find('[class*=_pagination]').should('not.exist');
ndv.getters ndv.getters
.outputPanel() .outputPanel()

View file

@ -6,9 +6,11 @@ import {
} from '../constants'; } from '../constants';
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow'; import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
import { WorkflowsPage as WorkflowsPageClass } from '../pages/workflows'; import { WorkflowsPage as WorkflowsPageClass } from '../pages/workflows';
import { getVisibleDropdown, getVisibleSelect } from '../utils';
const NEW_WORKFLOW_NAME = 'Something else'; const NEW_WORKFLOW_NAME = 'Something else';
const IMPORT_WORKFLOW_URL = 'https://gist.githubusercontent.com/OlegIvaniv/010bd3f45c8a94f8eb7012e663a8b671/raw/3afea1aec15573cc168d9af7e79395bd76082906/test-workflow.json'; const IMPORT_WORKFLOW_URL =
'https://gist.githubusercontent.com/OlegIvaniv/010bd3f45c8a94f8eb7012e663a8b671/raw/3afea1aec15573cc168d9af7e79395bd76082906/test-workflow.json';
const DUPLICATE_WORKFLOW_NAME = 'Duplicated workflow'; const DUPLICATE_WORKFLOW_NAME = 'Duplicated workflow';
const DUPLICATE_WORKFLOW_TAG = 'Duplicate'; const DUPLICATE_WORKFLOW_TAG = 'Duplicate';
@ -67,11 +69,11 @@ describe('Workflow Actions', () => {
it('should not save workflow if canvas is loading', () => { it('should not save workflow if canvas is loading', () => {
let interceptCalledCount = 0; let interceptCalledCount = 0;
// There's no way in Cypress to check if intercept was not called // There's no way in Cypress to check if intercept was not called
// so we'll count the number of times it was called // so we'll count the number of times it was called
cy.intercept('PATCH', '/rest/workflows/*', () => { cy.intercept('PATCH', '/rest/workflows/*', () => {
interceptCalledCount++; interceptCalledCount++;
}).as('saveWorkflow'); }).as('saveWorkflow');
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
WorkflowPage.actions.saveWorkflowOnButtonClick(); WorkflowPage.actions.saveWorkflowOnButtonClick();
@ -84,11 +86,11 @@ describe('Workflow Actions', () => {
(req) => { (req) => {
// Delay the response to give time for the save to be triggered // Delay the response to give time for the save to be triggered
req.on('response', async (res) => { req.on('response', async (res) => {
await new Promise((resolve) => setTimeout(resolve, 2000)) await new Promise((resolve) => setTimeout(resolve, 2000));
res.send(); res.send();
}) });
} },
) );
cy.reload(); cy.reload();
cy.get('.el-loading-mask').should('exist'); cy.get('.el-loading-mask').should('exist');
cy.get('body').type(META_KEY, { release: false }).type('s'); cy.get('body').type(META_KEY, { release: false }).type('s');
@ -99,7 +101,7 @@ describe('Workflow Actions', () => {
cy.get('body').type(META_KEY, { release: false }).type('s'); cy.get('body').type(META_KEY, { release: false }).type('s');
cy.wait('@saveWorkflow'); cy.wait('@saveWorkflow');
cy.wrap(null).then(() => expect(interceptCalledCount).to.eq(1)); cy.wrap(null).then(() => expect(interceptCalledCount).to.eq(1));
}) });
it('should copy nodes', () => { it('should copy nodes', () => {
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
@ -127,7 +129,7 @@ describe('Workflow Actions', () => {
cy.get('.el-message-box').should('be.visible'); cy.get('.el-message-box').should('be.visible');
cy.get('.el-message-box').find('input').type(IMPORT_WORKFLOW_URL); cy.get('.el-message-box').find('input').type(IMPORT_WORKFLOW_URL);
cy.get('body').type('{enter}'); cy.get('body').type('{enter}');
cy.waitForLoad(false) cy.waitForLoad(false);
WorkflowPage.actions.zoomToFit(); WorkflowPage.actions.zoomToFit();
WorkflowPage.getters.canvasNodes().should('have.length', 2); WorkflowPage.getters.canvasNodes().should('have.length', 2);
WorkflowPage.getters.nodeConnections().should('have.length', 1); WorkflowPage.getters.nodeConnections().should('have.length', 1);
@ -137,7 +139,7 @@ describe('Workflow Actions', () => {
WorkflowPage.getters WorkflowPage.getters
.workflowImportInput() .workflowImportInput()
.selectFile('cypress/fixtures/Test_workflow-actions_paste-data.json', { force: true }); .selectFile('cypress/fixtures/Test_workflow-actions_paste-data.json', { force: true });
cy.waitForLoad(false) cy.waitForLoad(false);
WorkflowPage.actions.zoomToFit(); WorkflowPage.actions.zoomToFit();
WorkflowPage.getters.canvasNodes().should('have.length', 2); WorkflowPage.getters.canvasNodes().should('have.length', 2);
WorkflowPage.getters.nodeConnections().should('have.length', 1); WorkflowPage.getters.nodeConnections().should('have.length', 1);
@ -157,57 +159,33 @@ describe('Workflow Actions', () => {
WorkflowPage.getters.workflowMenuItemSettings().click(); WorkflowPage.getters.workflowMenuItemSettings().click();
// Change all settings // Change all settings
// totalWorkflows + 1 (current workflow) + 1 (no workflow option) // totalWorkflows + 1 (current workflow) + 1 (no workflow option)
WorkflowPage.getters.workflowSettingsErrorWorkflowSelect().find('li').should('have.length', totalWorkflows + 2); WorkflowPage.getters.workflowSettingsErrorWorkflowSelect().click();
WorkflowPage.getters getVisibleSelect()
.workflowSettingsErrorWorkflowSelect()
.find('li') .find('li')
.last() .should('have.length', totalWorkflows + 2);
.click({ force: true }); getVisibleSelect().find('li').last().click({ force: true });
WorkflowPage.getters.workflowSettingsTimezoneSelect().find('li').should('exist'); WorkflowPage.getters.workflowSettingsTimezoneSelect().click();
WorkflowPage.getters.workflowSettingsTimezoneSelect().find('li').eq(1).click({ force: true }); getVisibleSelect().find('li').should('exist');
WorkflowPage.getters getVisibleSelect().find('li').eq(1).click({ force: true });
.workflowSettingsSaveFiledExecutionsSelect() WorkflowPage.getters.workflowSettingsSaveFiledExecutionsSelect().click();
.find('li') getVisibleSelect().find('li').should('have.length', 3);
.should('have.length', 3); getVisibleSelect().find('li').last().click({ force: true });
WorkflowPage.getters WorkflowPage.getters.workflowSettingsSaveSuccessExecutionsSelect().click();
.workflowSettingsSaveFiledExecutionsSelect() getVisibleSelect().find('li').should('have.length', 3);
.find('li') getVisibleSelect().find('li').last().click({ force: true });
.last() WorkflowPage.getters.workflowSettingsSaveManualExecutionsSelect().click();
.click({ force: true }); getVisibleSelect().find('li').should('have.length', 3);
WorkflowPage.getters getVisibleSelect().find('li').last().click({ force: true });
.workflowSettingsSaveSuccessExecutionsSelect() WorkflowPage.getters.workflowSettingsSaveExecutionProgressSelect().click();
.find('li') getVisibleSelect().find('li').should('have.length', 3);
.should('have.length', 3); getVisibleSelect().find('li').last().click({ force: true });
WorkflowPage.getters
.workflowSettingsSaveSuccessExecutionsSelect()
.find('li')
.last()
.click({ force: true });
WorkflowPage.getters
.workflowSettingsSaveManualExecutionsSelect()
.find('li')
.should('have.length', 3);
WorkflowPage.getters
.workflowSettingsSaveManualExecutionsSelect()
.find('li')
.last()
.click({ force: true });
WorkflowPage.getters
.workflowSettingsSaveExecutionProgressSelect()
.find('li')
.should('have.length', 3);
WorkflowPage.getters
.workflowSettingsSaveExecutionProgressSelect()
.find('li')
.last()
.click({ force: true });
WorkflowPage.getters.workflowSettingsTimeoutWorkflowSwitch().click(); WorkflowPage.getters.workflowSettingsTimeoutWorkflowSwitch().click();
WorkflowPage.getters.workflowSettingsTimeoutForm().find('input').first().type('1'); WorkflowPage.getters.workflowSettingsTimeoutForm().find('input').first().type('1');
// Save settings // Save settings
WorkflowPage.getters.workflowSettingsSaveButton().click(); WorkflowPage.getters.workflowSettingsSaveButton().click();
WorkflowPage.getters.workflowSettingsModal().should('not.exist'); WorkflowPage.getters.workflowSettingsModal().should('not.exist');
WorkflowPage.getters.successToast().should('exist'); WorkflowPage.getters.successToast().should('exist');
}) });
}); });
it('should not be able to delete unsaved workflow', () => { it('should not be able to delete unsaved workflow', () => {
@ -245,7 +223,7 @@ describe('Workflow Actions', () => {
.find('.el-select__tags input') .find('.el-select__tags input')
.type(DUPLICATE_WORKFLOW_TAG); .type(DUPLICATE_WORKFLOW_TAG);
WorkflowPage.getters.duplicateWorkflowModal().find('.el-select__tags input').type('{enter}'); WorkflowPage.getters.duplicateWorkflowModal().find('.el-select__tags input').type('{enter}');
WorkflowPage.getters.duplicateWorkflowModal().find('.el-select__tags input').type('{enter}'); WorkflowPage.getters.duplicateWorkflowModal().find('.el-select__tags input').type('{esc}');
WorkflowPage.getters WorkflowPage.getters
.duplicateWorkflowModal() .duplicateWorkflowModal()
.find('button') .find('button')

View file

@ -5,7 +5,7 @@ export class CredentialsPage extends BasePage {
getters = { getters = {
emptyListCreateCredentialButton: () => cy.getByTestId('empty-resources-list').find('button'), emptyListCreateCredentialButton: () => cy.getByTestId('empty-resources-list').find('button'),
createCredentialButton: () => cy.getByTestId('resources-list-add'), createCredentialButton: () => cy.getByTestId('resources-list-add'),
searchInput: () => cy.getByTestId('resources-list-search').find('input'), searchInput: () => cy.getByTestId('resources-list-search'),
emptyList: () => cy.getByTestId('resources-list-empty'), emptyList: () => cy.getByTestId('resources-list-empty'),
credentialCards: () => cy.getByTestId('resources-list-item'), credentialCards: () => cy.getByTestId('resources-list-item'),
credentialCard: (credentialName: string) => credentialCard: (credentialName: string) =>
@ -17,8 +17,8 @@ export class CredentialsPage extends BasePage {
this.getters.credentialCard(credentialName).findChildByTestId('credential-card-actions'), this.getters.credentialCard(credentialName).findChildByTestId('credential-card-actions'),
credentialDeleteButton: () => credentialDeleteButton: () =>
cy.getByTestId('action-toggle-dropdown').filter(':visible').contains('Delete'), cy.getByTestId('action-toggle-dropdown').filter(':visible').contains('Delete'),
sort: () => cy.getByTestId('resources-list-sort'), sort: () => cy.getByTestId('resources-list-sort').first(),
sortOption: (label: string) => this.getters.sort().contains(label).first(), sortOption: (label: string) => cy.getByTestId('resources-list-sort-item').contains(label).first(),
filtersTrigger: () => cy.getByTestId('resources-list-filters-trigger'), filtersTrigger: () => cy.getByTestId('resources-list-filters-trigger'),
filtersDropdown: () => cy.getByTestId('resources-list-filters-dropdown'), filtersDropdown: () => cy.getByTestId('resources-list-filters-dropdown'),
}; };

View file

@ -20,7 +20,7 @@ export class CredentialsModal extends BasePage {
credentialsEditModal: () => cy.getByTestId('credential-edit-dialog'), credentialsEditModal: () => cy.getByTestId('credential-edit-dialog'),
credentialsAuthTypeSelector: () => cy.getByTestId('node-auth-type-selector'), credentialsAuthTypeSelector: () => cy.getByTestId('node-auth-type-selector'),
credentialAuthTypeRadioButtons: () => credentialAuthTypeRadioButtons: () =>
this.getters.credentialsAuthTypeSelector().find('label[role=radio]'), this.getters.credentialsAuthTypeSelector().find('label.el-radio'),
credentialInputs: () => cy.getByTestId('credential-connection-parameter'), credentialInputs: () => cy.getByTestId('credential-connection-parameter'),
menu: () => this.getters.editCredentialModal().get('.menu-container'), menu: () => this.getters.editCredentialModal().get('.menu-container'),
menuItem: (name: string) => this.getters.menu().get('.n8n-menu-item').contains(name), menuItem: (name: string) => this.getters.menu().get('.n8n-menu-item').contains(name),
@ -42,7 +42,7 @@ export class CredentialsModal extends BasePage {
}, },
save: (test = false) => { save: (test = false) => {
cy.intercept('POST', '/rest/credentials').as('saveCredential'); cy.intercept('POST', '/rest/credentials').as('saveCredential');
this.getters.saveButton().click(); this.getters.saveButton().click({ force: true });
cy.wait('@saveCredential'); cy.wait('@saveCredential');
if (test) cy.wait('@testCredential'); if (test) cy.wait('@testCredential');

View file

@ -5,15 +5,15 @@ export class MessageBox extends BasePage {
modal: () => cy.get('.el-message-box', { withinSubject: null }), modal: () => cy.get('.el-message-box', { withinSubject: null }),
header: () => this.getters.modal().find('.el-message-box__title'), header: () => this.getters.modal().find('.el-message-box__title'),
content: () => this.getters.modal().find('.el-message-box__content'), content: () => this.getters.modal().find('.el-message-box__content'),
confirm: () => this.getters.modal().find('.btn--confirm'), confirm: () => this.getters.modal().find('.btn--confirm').first(),
cancel: () => this.getters.modal().find('.btn--cancel'), cancel: () => this.getters.modal().find('.btn--cancel').first(),
}; };
actions = { actions = {
confirm: () => { confirm: () => {
this.getters.confirm().click(); this.getters.confirm().click({ force: true});
}, },
cancel: () => { cancel: () => {
this.getters.cancel().click(); this.getters.cancel().click({ force: true});
}, },
}; };
} }

View file

@ -1,4 +1,5 @@
import { BasePage } from './base'; import { BasePage } from './base';
import { getVisibleSelect } from '../utils';
export class NDV extends BasePage { export class NDV extends BasePage {
getters = { getters = {
@ -101,10 +102,11 @@ export class NDV extends BasePage {
this.getters.parameterInput(parameterName).type(content); this.getters.parameterInput(parameterName).type(content);
}, },
selectOptionInParameterDropdown: (parameterName: string, content: string) => { selectOptionInParameterDropdown: (parameterName: string, content: string) => {
this.getters.parameterInput(parameterName).find('.option-headline').contains(content).click(); getVisibleSelect().find('.option-headline').contains(content).click();
}, },
dismissMappingTooltip: () => { dismissMappingTooltip: () => {
cy.getByTestId('dismiss-mapping-tooltip').click(); cy.getByTestId('dismiss-mapping-tooltip').click();
cy.getByTestId('dismiss-mapping-tooltip').should('not.be.visible');
}, },
rename: (newName: string) => { rename: (newName: string) => {
this.getters.nodeNameContainer().click(); this.getters.nodeNameContainer().click();
@ -139,11 +141,11 @@ export class NDV extends BasePage {
}, },
changeInputRunSelector: (runName: string) => { changeInputRunSelector: (runName: string) => {
this.getters.inputRunSelector().click(); this.getters.inputRunSelector().click();
cy.get('.el-select-dropdown:visible .el-select-dropdown__item').contains(runName).click(); getVisibleSelect().find('.el-select-dropdown__item').contains(runName).click();
}, },
changeOutputRunSelector: (runName: string) => { changeOutputRunSelector: (runName: string) => {
this.getters.outputRunSelector().click(); this.getters.outputRunSelector().click();
cy.get('.el-select-dropdown:visible .el-select-dropdown__item').contains(runName).click(); getVisibleSelect().find('.el-select-dropdown__item').contains(runName).click();
}, },
toggleOutputRunLinking: () => { toggleOutputRunLinking: () => {
this.getters.outputRunSelector().find('button').click(); this.getters.outputRunSelector().find('button').click();
@ -159,7 +161,7 @@ export class NDV extends BasePage {
}, },
setRLCValue: (paramName: string, value: string) => { setRLCValue: (paramName: string, value: string) => {
this.getters.resourceLocatorModeSelector(paramName).click(); this.getters.resourceLocatorModeSelector(paramName).click();
this.getters.resourceLocatorModeSelector(paramName).find('li').last().click(); getVisibleSelect().find('li').last().click();
this.getters.resourceLocatorInput(paramName).type(value); this.getters.resourceLocatorInput(paramName).type(value);
}, },
validateExpressionPreview: (paramName: string, value: string) => { validateExpressionPreview: (paramName: string, value: string) => {

View file

@ -1,4 +1,5 @@
import { BasePage } from './base'; import { BasePage } from './base';
import { getVisibleSelect } from '../utils';
export class SettingsLogStreamingPage extends BasePage { export class SettingsLogStreamingPage extends BasePage {
url = '/settings/log-streaming'; url = '/settings/log-streaming';
@ -6,11 +7,9 @@ export class SettingsLogStreamingPage extends BasePage {
getActionBoxUnlicensed: () => cy.getByTestId('action-box-unlicensed'), getActionBoxUnlicensed: () => cy.getByTestId('action-box-unlicensed'),
getActionBoxLicensed: () => cy.getByTestId('action-box-licensed'), getActionBoxLicensed: () => cy.getByTestId('action-box-licensed'),
getDestinationModal: () => cy.getByTestId('destination-modal'), getDestinationModal: () => cy.getByTestId('destination-modal'),
getDestinationModalDialog: () => this.getters.getDestinationModal().find('.el-dialog'),
getSelectDestinationType: () => cy.getByTestId('select-destination-type'), getSelectDestinationType: () => cy.getByTestId('select-destination-type'),
getDestinationNameInput: () => cy.getByTestId('subtitle-showing-type'), getDestinationNameInput: () => cy.getByTestId('subtitle-showing-type'),
getSelectDestinationTypeItems: () => getSelectDestinationTypeItems: () => getVisibleSelect().find('.el-select-dropdown__item'),
this.getters.getSelectDestinationType().find('.el-select-dropdown__item'),
getSelectDestinationButton: () => cy.getByTestId('select-destination-button'), getSelectDestinationButton: () => cy.getByTestId('select-destination-button'),
getContactUsButton: () => this.getters.getActionBoxUnlicensed().find('button'), getContactUsButton: () => this.getters.getActionBoxUnlicensed().find('button'),
getAddFirstDestinationButton: () => this.getters.getActionBoxLicensed().find('button'), getAddFirstDestinationButton: () => this.getters.getActionBoxLicensed().find('button'),

View file

@ -11,7 +11,7 @@ export class PersonalSettingsPage extends BasePage {
lastNameInput: () => cy.getByTestId('lastName').find('input').first(), lastNameInput: () => cy.getByTestId('lastName').find('input').first(),
emailInputContainer: () => cy.getByTestId('email'), emailInputContainer: () => cy.getByTestId('email'),
emailInput: () => cy.getByTestId('email').find('input').first(), emailInput: () => cy.getByTestId('email').find('input').first(),
changePasswordLink: () => cy.getByTestId('change-password-link').find('a').first(), changePasswordLink: () => cy.getByTestId('change-password-link').first(),
saveSettingsButton: () => cy.getByTestId('save-settings-button'), saveSettingsButton: () => cy.getByTestId('save-settings-button'),
}; };
actions = { actions = {
@ -34,7 +34,10 @@ export class PersonalSettingsPage extends BasePage {
}, },
tryToSetWeakPassword: (oldPassword: string, newPassword: string) => { tryToSetWeakPassword: (oldPassword: string, newPassword: string) => {
this.actions.updatePassword(oldPassword, newPassword); this.actions.updatePassword(oldPassword, newPassword);
changePasswordModal.getters.newPasswordInputContainer().find('div[class^="_errorInput"]').should('exist'); changePasswordModal.getters
.newPasswordInputContainer()
.find('div[class^="_errorInput"]')
.should('exist');
}, },
updateEmail: (newEmail: string) => { updateEmail: (newEmail: string) => {
this.getters.emailInput().type('{selectall}').type(newEmail).type('{enter}'); this.getters.emailInput().type('{selectall}').type(newEmail).type('{enter}');

View file

@ -4,8 +4,8 @@ import { WorkflowPage } from './workflow';
import { WorkflowsPage } from './workflows'; import { WorkflowsPage } from './workflows';
import { BasePage } from './base'; import { BasePage } from './base';
const workflowPage = new WorkflowPage(); const workflowPage = new WorkflowPage();
const workflowsPage = new WorkflowsPage(); const workflowsPage = new WorkflowsPage();
const mainSidebar = new MainSidebar(); const mainSidebar = new MainSidebar();
const settingsSidebar = new SettingsSidebar(); const settingsSidebar = new SettingsSidebar();
@ -18,11 +18,15 @@ export class SettingsUsersPage extends BasePage {
inviteUsersModalEmailsInput: () => cy.getByTestId('emails').find('input').first(), inviteUsersModalEmailsInput: () => cy.getByTestId('emails').find('input').first(),
userListItems: () => cy.get('[data-test-id^="user-list-item"]'), userListItems: () => cy.get('[data-test-id^="user-list-item"]'),
userItem: (email: string) => cy.getByTestId(`user-list-item-${email.toLowerCase()}`), userItem: (email: string) => cy.getByTestId(`user-list-item-${email.toLowerCase()}`),
userActionsToggle: (email: string) => this.getters.userItem(email).find('[data-test-id="action-toggle"]'), userActionsToggle: (email: string) =>
deleteUserAction: () => cy.getByTestId('action-toggle-dropdown').find('li:contains("Delete"):visible'), this.getters.userItem(email).find('[data-test-id="action-toggle"]'),
deleteUserAction: () =>
cy.getByTestId('action-toggle-dropdown').find('li:contains("Delete"):visible'),
confirmDeleteModal: () => cy.getByTestId('deleteUser-modal').last(), confirmDeleteModal: () => cy.getByTestId('deleteUser-modal').last(),
transferDataRadioButton: () => this.getters.confirmDeleteModal().find('[role="radio"]').first(), transferDataRadioButton: () =>
deleteDataRadioButton: () => this.getters.confirmDeleteModal().find('[role="radio"]').last(), this.getters.confirmDeleteModal().find('.el-radio .el-radio__input').first(),
deleteDataRadioButton: () =>
this.getters.confirmDeleteModal().find('.el-radio .el-radio__input').last(),
userSelectDropDown: () => this.getters.confirmDeleteModal().find('.n8n-select'), userSelectDropDown: () => this.getters.confirmDeleteModal().find('.n8n-select'),
userSelectOptions: () => cy.get('.el-select-dropdown:visible .el-select-dropdown__item'), userSelectOptions: () => cy.get('.el-select-dropdown:visible .el-select-dropdown__item'),
deleteUserButton: () => this.getters.confirmDeleteModal().find('button:contains("Delete")'), deleteUserButton: () => this.getters.confirmDeleteModal().find('button:contains("Delete")'),

View file

@ -10,7 +10,7 @@ export class VariablesPage extends BasePage {
goToUpgrade: () => cy.getByTestId('go-to-upgrade'), goToUpgrade: () => cy.getByTestId('go-to-upgrade'),
actionBox: () => cy.getByTestId('action-box'), actionBox: () => cy.getByTestId('action-box'),
emptyResourcesListNewVariableButton: () => this.getters.emptyResourcesList().find('button'), emptyResourcesListNewVariableButton: () => this.getters.emptyResourcesList().find('button'),
searchBar: () => cy.getByTestId('resources-list-search').find('input'), searchBar: () => cy.getByTestId('resources-list-search'),
createVariableButton: () => cy.getByTestId('resources-list-add'), createVariableButton: () => cy.getByTestId('resources-list-add'),
variablesRows: () => cy.getByTestId('variables-row'), variablesRows: () => cy.getByTestId('variables-row'),
variablesEditableRows: () => variablesEditableRows: () =>

View file

@ -1,5 +1,6 @@
import { META_KEY } from '../constants'; import { META_KEY } from '../constants';
import { BasePage } from './base'; import { BasePage } from './base';
import { getVisibleSelect } from '../utils';
export class WorkflowPage extends BasePage { export class WorkflowPage extends BasePage {
url = '/workflow/new'; url = '/workflow/new';
@ -16,7 +17,7 @@ export class WorkflowPage extends BasePage {
nthTagPill: (n: number) => nthTagPill: (n: number) =>
cy.get(`[data-test-id="workflow-tags-container"] span.tags > span:nth-child(${n})`), cy.get(`[data-test-id="workflow-tags-container"] span.tags > span:nth-child(${n})`),
tagsDropdown: () => cy.getByTestId('workflow-tags-dropdown'), tagsDropdown: () => cy.getByTestId('workflow-tags-dropdown'),
tagsInDropdown: () => cy.getByTestId('workflow-tags-dropdown').find('li').filter('.tag'), tagsInDropdown: () => getVisibleSelect().find('li').filter('.tag'),
createTagButton: () => cy.getByTestId('new-tag-link'), createTagButton: () => cy.getByTestId('new-tag-link'),
saveButton: () => cy.getByTestId('workflow-save-button'), saveButton: () => cy.getByTestId('workflow-save-button'),
nodeCreatorSearchBar: () => cy.getByTestId('node-creator-search-bar'), nodeCreatorSearchBar: () => cy.getByTestId('node-creator-search-bar'),
@ -37,8 +38,8 @@ export class WorkflowPage extends BasePage {
canvasNodePlusEndpointByName: (nodeName: string, index = 0) => { canvasNodePlusEndpointByName: (nodeName: string, index = 0) => {
return cy.get(this.getters.getEndpointSelector('plus', nodeName, index)); return cy.get(this.getters.getEndpointSelector('plus', nodeName, index));
}, },
successToast: () => cy.get('.el-notification .el-icon-success').parent(), successToast: () => cy.get('.el-notification .el-notification--success').parent(),
errorToast: () => cy.get('.el-notification .el-icon-error'), errorToast: () => cy.get('.el-notification .el-notification--error'),
activatorSwitch: () => cy.getByTestId('workflow-activate-switch'), activatorSwitch: () => cy.getByTestId('workflow-activate-switch'),
workflowMenu: () => cy.getByTestId('workflow-menu'), workflowMenu: () => cy.getByTestId('workflow-menu'),
firstStepButton: () => cy.getByTestId('canvas-add-button'), firstStepButton: () => cy.getByTestId('canvas-add-button'),
@ -84,7 +85,8 @@ export class WorkflowPage extends BasePage {
duplicateWorkflowModal: () => cy.getByTestId('duplicate-modal'), duplicateWorkflowModal: () => cy.getByTestId('duplicate-modal'),
nodeViewBackground: () => cy.getByTestId('node-view-background'), nodeViewBackground: () => cy.getByTestId('node-view-background'),
nodeView: () => cy.getByTestId('node-view'), nodeView: () => cy.getByTestId('node-view'),
inlineExpressionEditorInput: () => cy.getByTestId('inline-expression-editor-input').find('[role=textbox]'), inlineExpressionEditorInput: () =>
cy.getByTestId('inline-expression-editor-input').find('[role=textbox]'),
inlineExpressionEditorOutput: () => cy.getByTestId('inline-expression-editor-output'), inlineExpressionEditorOutput: () => cy.getByTestId('inline-expression-editor-output'),
zoomInButton: () => cy.getByTestId('zoom-in-button'), zoomInButton: () => cy.getByTestId('zoom-in-button'),
zoomOutButton: () => cy.getByTestId('zoom-out-button'), zoomOutButton: () => cy.getByTestId('zoom-out-button'),
@ -92,8 +94,10 @@ export class WorkflowPage extends BasePage {
executeWorkflowButton: () => cy.getByTestId('execute-workflow-button'), executeWorkflowButton: () => cy.getByTestId('execute-workflow-button'),
clearExecutionDataButton: () => cy.getByTestId('clear-execution-data-button'), clearExecutionDataButton: () => cy.getByTestId('clear-execution-data-button'),
stopExecutionButton: () => cy.getByTestId('stop-execution-button'), stopExecutionButton: () => cy.getByTestId('stop-execution-button'),
stopExecutionWaitingForWebhookButton: () => cy.getByTestId('stop-execution-waiting-for-webhook-button'), stopExecutionWaitingForWebhookButton: () =>
cy.getByTestId('stop-execution-waiting-for-webhook-button'),
nodeCredentialsSelect: () => cy.getByTestId('node-credentials-select'), nodeCredentialsSelect: () => cy.getByTestId('node-credentials-select'),
nodeCredentialsCreateOption: () => cy.getByTestId('node-credentials-select-item-new'),
nodeCredentialsEditButton: () => cy.getByTestId('credential-edit-button'), nodeCredentialsEditButton: () => cy.getByTestId('credential-edit-button'),
nodeCreatorItems: () => cy.getByTestId('item-iterator-item'), nodeCreatorItems: () => cy.getByTestId('item-iterator-item'),
ndvParameters: () => cy.getByTestId('parameter-item'), ndvParameters: () => cy.getByTestId('parameter-item'),
@ -134,17 +138,17 @@ export class WorkflowPage extends BasePage {
this.getters.nodeCreatorSearchBar().type(nodeDisplayName); this.getters.nodeCreatorSearchBar().type(nodeDisplayName);
this.getters.nodeCreatorSearchBar().type('{enter}'); this.getters.nodeCreatorSearchBar().type('{enter}');
cy.wait(500) cy.wait(500);
cy.get('body').then((body) => { cy.get('body').then((body) => {
if(body.find('[data-test-id=node-creator]').length > 0) { if (body.find('[data-test-id=node-creator]').length > 0) {
if(action) { if (action) {
cy.contains(action).click() cy.contains(action).click();
} else { } else {
// Select the first action // Select the first action
cy.get('[data-keyboard-nav-type="action"]').eq(0).click() cy.get('[data-keyboard-nav-type="action"]').eq(0).click();
} }
} }
}) });
if (!preventNdvClose) cy.get('body').type('{esc}'); if (!preventNdvClose) cy.get('body').type('{esc}');
}, },
@ -157,7 +161,8 @@ export class WorkflowPage extends BasePage {
}, },
openTagManagerModal: () => { openTagManagerModal: () => {
this.getters.createTagButton().click(); this.getters.createTagButton().click();
this.getters.tagsDropdown().find('li.manage-tags').first().click(); this.getters.tagsDropdown().click();
getVisibleSelect().find('li.manage-tags').first().click();
}, },
openInlineExpressionEditor: () => { openInlineExpressionEditor: () => {
cy.contains('Expression').invoke('show').click(); cy.contains('Expression').invoke('show').click();
@ -209,7 +214,7 @@ export class WorkflowPage extends BasePage {
this.getters.workflowTagsInput().type(tag); this.getters.workflowTagsInput().type(tag);
this.getters.workflowTagsInput().type('{enter}'); this.getters.workflowTagsInput().type('{enter}');
}); });
cy.get('body').type('{enter}'); cy.get('body').click(0, 0);
// For a brief moment the Element UI tag component shows the tags as(+X) string // For a brief moment the Element UI tag component shows the tags as(+X) string
// so we need to wait for it to disappear // so we need to wait for it to disappear
this.getters.workflowTagsContainer().should('not.contain', `+${tags.length}`); this.getters.workflowTagsContainer().should('not.contain', `+${tags.length}`);
@ -241,7 +246,12 @@ export class WorkflowPage extends BasePage {
executeWorkflow: () => { executeWorkflow: () => {
this.getters.executeWorkflowButton().click(); this.getters.executeWorkflowButton().click();
}, },
addNodeBetweenNodes: (sourceNodeName: string, targetNodeName: string, newNodeName: string, action?: string) => { addNodeBetweenNodes: (
sourceNodeName: string,
targetNodeName: string,
newNodeName: string,
action?: string,
) => {
this.getters.getConnectionBetweenNodes(sourceNodeName, targetNodeName).first().realHover(); this.getters.getConnectionBetweenNodes(sourceNodeName, targetNodeName).first().realHover();
this.getters this.getters
.getConnectionActionsBetweenNodes(sourceNodeName, targetNodeName) .getConnectionActionsBetweenNodes(sourceNodeName, targetNodeName)
@ -268,18 +278,10 @@ export class WorkflowPage extends BasePage {
this.getters.addStickyButton().click(); this.getters.addStickyButton().click();
}, },
deleteSticky: () => { deleteSticky: () => {
this.getters.stickies().eq(0) this.getters.stickies().eq(0).realHover().find('[data-test-id="delete-sticky"]').click();
.realHover()
.find('[data-test-id="delete-sticky"]')
.click();
}, },
editSticky: (content: string) => { editSticky: (content: string) => {
this.getters.stickies() this.getters.stickies().dblclick().find('textarea').clear().type(content).type('{esc}');
.dblclick()
.find('textarea')
.clear()
.type(content)
.type('{esc}');
}, },
}; };
} }

View file

@ -5,7 +5,7 @@ export class WorkflowsPage extends BasePage {
getters = { getters = {
newWorkflowButtonCard: () => cy.getByTestId('new-workflow-card'), newWorkflowButtonCard: () => cy.getByTestId('new-workflow-card'),
newWorkflowTemplateCard: () => cy.getByTestId('new-workflow-template-card'), newWorkflowTemplateCard: () => cy.getByTestId('new-workflow-template-card'),
searchBar: () => cy.getByTestId('resources-list-search').find('input'), searchBar: () => cy.getByTestId('resources-list-search'),
createWorkflowButton: () => cy.getByTestId('resources-list-add'), createWorkflowButton: () => cy.getByTestId('resources-list-add'),
workflowCards: () => cy.getByTestId('resources-list-item'), workflowCards: () => cy.getByTestId('resources-list-item'),
workflowCard: (workflowName: string) => workflowCard: (workflowName: string) =>

View file

@ -53,12 +53,12 @@ Cypress.Commands.add('signin', ({ email, password }) => {
}); });
Cypress.Commands.add('signout', () => { Cypress.Commands.add('signout', () => {
cy.request('POST', '/rest/logout'); cy.request('POST', `${BACKEND_BASE_URL}/rest/logout`);
cy.getCookie(N8N_AUTH_COOKIE).should('not.exist'); cy.getCookie(N8N_AUTH_COOKIE).should('not.exist');
}); });
Cypress.Commands.add('interceptREST', (method, url) => { Cypress.Commands.add('interceptREST', (method, url) => {
cy.intercept(method, `http://localhost:5678/rest${url}`); cy.intercept(method, `${BACKEND_BASE_URL}/rest${url}`);
}); });
const setFeature = (feature: string, enabled: boolean) => const setFeature = (feature: string, enabled: boolean) =>

View file

@ -6,6 +6,10 @@ before(() => {
owner: INSTANCE_OWNER, owner: INSTANCE_OWNER,
members: INSTANCE_MEMBERS, members: INSTANCE_MEMBERS,
}); });
Cypress.on('uncaught:exception', (err) => {
return !err.message.includes('ResizeObserver');
});
}); });
beforeEach(() => { beforeEach(() => {

1
cypress/utils/index.ts Normal file
View file

@ -0,0 +1 @@
export * from './popper';

3
cypress/utils/modal.ts Normal file
View file

@ -0,0 +1,3 @@
export function getVisibleModalOverlay() {
return cy.get('.el-overlay .el-overlay-dialog').filter(':visible');
}

11
cypress/utils/popper.ts Normal file
View file

@ -0,0 +1,11 @@
export function getVisiblePopper() {
return cy.get('.el-popper').filter(':visible');
}
export function getVisibleSelect() {
return getVisiblePopper().filter('.el-select__popper');
}
export function getVisibleDropdown() {
return getVisiblePopper().filter('.el-dropdown__popper');
}

View file

@ -40,7 +40,6 @@
"@ngneat/falso": "^6.1.0", "@ngneat/falso": "^6.1.0",
"@types/jest": "^29.5.0", "@types/jest": "^29.5.0",
"@types/supertest": "^2.0.12", "@types/supertest": "^2.0.12",
"@vitejs/plugin-vue2": "^2.2.0",
"@vitest/coverage-c8": "^0.28.5", "@vitest/coverage-c8": "^0.28.5",
"c8": "^7.12.0", "c8": "^7.12.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
@ -64,7 +63,6 @@
"typescript": "*", "typescript": "*",
"vite": "^4.0.4", "vite": "^4.0.4",
"vitest": "^0.28.5", "vitest": "^0.28.5",
"vue-template-compiler": "^2.7.14",
"vue-tsc": "^1.0.24" "vue-tsc": "^1.0.24"
}, },
"pnpm": { "pnpm": {
@ -92,7 +90,6 @@
"qqjs>globby": "^11.1.0" "qqjs>globby": "^11.1.0"
}, },
"patchedDependencies": { "patchedDependencies": {
"element-ui@2.15.12": "patches/element-ui@2.15.12.patch",
"typedi@0.10.0": "patches/typedi@0.10.0.patch", "typedi@0.10.0": "patches/typedi@0.10.0.patch",
"@sentry/cli@2.17.0": "patches/@sentry__cli@2.17.0.patch", "@sentry/cli@2.17.0": "patches/@sentry__cli@2.17.0.patch",
"pkce-challenge@3.0.0": "patches/pkce-challenge@3.0.0.patch", "pkce-challenge@3.0.0": "patches/pkce-challenge@3.0.0.patch",

View file

@ -4,7 +4,7 @@
module.exports = { module.exports = {
plugins: ['vue'], plugins: ['vue'],
extends: ['plugin:vue/essential', '@vue/typescript', './base'], extends: ['plugin:vue/vue3-essential', '@vue/typescript', './base'],
env: { env: {
browser: true, browser: true,
@ -37,6 +37,12 @@ module.exports = {
'vue/no-unused-components': 'error', 'vue/no-unused-components': 'error',
'vue/multi-word-component-names': 'off', 'vue/multi-word-component-names': 'off',
// TODO: fix these
'@typescript-eslint/no-unsafe-call': 'off',
'@typescript-eslint/no-unsafe-assignment': 'off',
'@typescript-eslint/restrict-template-expressions': 'off',
'@typescript-eslint/unbound-method': 'off',
// TODO: remove these // TODO: remove these
'vue/no-mutating-props': 'warn', 'vue/no-mutating-props': 'warn',
'vue/no-side-effects-in-computed-properties': 'warn', 'vue/no-side-effects-in-computed-properties': 'warn',

View file

@ -1,8 +0,0 @@
/**
* These icons are only defined for storybook build
* Editor icons are defined seperately
*/
import { library } from '@fortawesome/fontawesome-svg-core';
import { fas } from '@fortawesome/free-solid-svg-icons';
library.add(fas);

View file

@ -1,59 +1,42 @@
const path = require('path'); const { mergeConfig } = require('vite');
const { resolve } = require('path');
/**
* @type {import('@storybook/types').StorybookConfig}
*/
module.exports = { module.exports = {
framework: { stories: ['../src/**/*.stories.@(js|jsx|ts|tsx|mdx)'],
name: '@storybook/vue-webpack5',
options: {},
},
stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.{ts,js}'],
addons: [ addons: [
'@storybook/addon-styling',
'@storybook/addon-links', '@storybook/addon-links',
'@storybook/addon-essentials', '@storybook/addon-essentials',
{ // Disabled until this is actually used rather otherwise its a blank tab
name: '@storybook/addon-postcss', // '@storybook/addon-interactions',
options: { '@storybook/addon-a11y',
postcssLoaderOptions: { 'storybook-dark-mode',
implementation: require('postcss'),
},
},
},
'storybook-addon-themes',
], ],
webpackFinal: async (config) => { staticDirs: ['../public'],
config.module.rules.push({ framework: {
test: /\.scss$/, name: '@storybook/vue3-vite',
oneOf: [ options: {},
{ },
resourceQuery: /module/, disableTelemetry: true,
use: [ async viteFinal(config, { configType }) {
'vue-style-loader', // return the customized config
{ return mergeConfig(config, {
loader: 'css-loader', // customize the Vite config here
options: { resolve: {
modules: { alias: [
localIdentName: '[path][name]__[local]--[hash:base64:5]', {
}, find: /^@n8n-design-system\//,
}, replacement: `${resolve(__dirname, '..')}/src/`,
}, },
'sass-loader', {
], find: /^n8n-design-system$/,
include: path.resolve(__dirname, '../'), replacement: `${resolve(__dirname, '..')}/src/main.ts`,
}, },
{ ],
use: ['vue-style-loader', 'css-loader', 'sass-loader'], },
include: path.resolve(__dirname, '../'),
},
],
}); });
},
config.resolve.alias = { docs: {
...config.resolve.alias, autodocs: true,
'@/': path.resolve(__dirname, '../src/'),
};
return config;
}, },
}; };

View file

@ -1,23 +1,24 @@
import './font-awesome-icons'; import { setup } from '@storybook/vue3';
import './storybook.scss'; import './storybook.scss';
import ElementUI from 'element-ui'; import { library } from '@fortawesome/fontawesome-svg-core';
import lang from 'element-ui/lib/locale/lang/en'; import { fas } from '@fortawesome/free-solid-svg-icons';
import locale from 'element-ui/lib/locale';
import ElementPlus from 'element-plus';
import lang from 'element-plus/lib/locale/lang/en';
import { N8nPlugin } from '../src/plugin'; import { N8nPlugin } from '../src/plugin';
import Vue from 'vue'; setup((app) => {
library.add(fas);
Vue.use(ElementUI); app.use(ElementPlus, {
Vue.use(N8nPlugin); locale: lang,
});
locale.use(lang); app.use(N8nPlugin);
});
// https://github.com/storybookjs/storybook/issues/6153
Vue.prototype.toJSON = function () {
return this;
};
export const parameters = { export const parameters = {
actions: { actions: {

View file

@ -1,12 +1,17 @@
@use './fonts.scss'; @use './fonts.scss';
@use '~/src/css/base.scss' with ( @use '../src/css/base.scss'; // @TODO CHECK IF NEEDED with (
$font-path: '~element-ui/lib/theme-chalk/fonts' // $font-path: 'element-ui/lib/theme-chalk/fonts'
); //);
@use '~/src/css/reset.scss'; @use '../src/css/reset.scss';
@use '~/src/css/index.scss'; @use '../src/css/index.scss';
.multi-container > * { .multi-container > * {
margin-bottom: 10px; margin-bottom: 10px;
} }
#storybook-root > div:not([class]) > *,
#storybook-root > * {
margin: var(--spacing-5xs);
}

View file

@ -40,38 +40,43 @@
"devDependencies": { "devDependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.36", "@fortawesome/fontawesome-svg-core": "^1.2.36",
"@fortawesome/free-solid-svg-icons": "^5.15.4", "@fortawesome/free-solid-svg-icons": "^5.15.4",
"@fortawesome/vue-fontawesome": "^2.0.9", "@fortawesome/vue-fontawesome": "^3.0.3",
"@storybook/addon-actions": "^7.0.7", "@storybook/addon-a11y": "^7.0.21",
"@storybook/addon-docs": "^7.0.7", "@storybook/addon-actions": "^7.0.21",
"@storybook/addon-essentials": "^7.0.7", "@storybook/addon-docs": "^7.0.21",
"@storybook/addon-links": "^7.0.7", "@storybook/addon-essentials": "^7.0.21",
"@storybook/addon-postcss": "^3.0.0-alpha.1", "@storybook/addon-links": "^7.0.21",
"@storybook/vue": "^7.0.7", "@storybook/addon-postcss": "3.0.0-alpha.1",
"@storybook/vue-webpack5": "^7.0.7", "@storybook/addon-styling": "^1.3.0",
"@storybook/vue3": "^7.0.21",
"@storybook/vue3-vite": "^7.0.21",
"@testing-library/jest-dom": "^5.16.5", "@testing-library/jest-dom": "^5.16.5",
"@testing-library/user-event": "^14.4.3", "@testing-library/user-event": "^14.4.3",
"@testing-library/vue": "^5.8.3", "@testing-library/vue": "^6.6.1",
"@types/markdown-it": "^12.2.3", "@types/markdown-it": "^12.2.3",
"@types/markdown-it-emoji": "^2.0.2", "@types/markdown-it-emoji": "^2.0.2",
"@types/markdown-it-link-attributes": "^3.0.1", "@types/markdown-it-link-attributes": "^3.0.1",
"@types/sanitize-html": "^2.8.0", "@types/sanitize-html": "^2.9.0",
"autoprefixer": "^10.4.13", "@vitejs/plugin-vue": "^4.2.3",
"core-js": "^3.27.2", "@vue/test-utils": "^2.4.1",
"autoprefixer": "^10.4.14",
"core-js": "^3.31.0",
"jsdom": "21.1.0", "jsdom": "21.1.0",
"sass": "^1.58.0", "sass": "^1.63.4",
"sass-loader": "^13.2.0", "sass-loader": "^13.3.2",
"storybook": "^7.0.7", "storybook": "^7.0.21",
"storybook-addon-themes": "^6.1.0" "storybook-addon-themes": "^6.1.0",
"storybook-dark-mode": "^3.0.0"
}, },
"dependencies": { "dependencies": {
"element-ui": "~2.15.12", "element-plus": "^2.3.6",
"markdown-it": "^13.0.1", "markdown-it": "^13.0.1",
"markdown-it-emoji": "^2.0.2", "markdown-it-emoji": "^2.0.2",
"markdown-it-link-attributes": "^4.0.1", "markdown-it-link-attributes": "^4.0.1",
"markdown-it-task-lists": "^2.1.1", "markdown-it-task-lists": "^2.1.1",
"sanitize-html": "2.10.0", "sanitize-html": "2.10.0",
"vue": "^2.7.14", "vue": "^3.3.4",
"vue2-boring-avatars": "^0.3.8", "vue-boring-avatars": "^1.3.0",
"xss": "^1.0.14" "xss": "^1.0.14"
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

View file

@ -1 +1,5 @@
import '@testing-library/jest-dom'; import '@testing-library/jest-dom';
import { config } from '@vue/test-utils';
import { N8nPlugin } from '@/plugin';
config.global.plugins = [N8nPlugin];

View file

@ -1,6 +1,6 @@
import N8nActionBox from './ActionBox.vue'; import N8nActionBox from './ActionBox.vue';
import { action } from '@storybook/addon-actions'; import { action } from '@storybook/addon-actions';
import type { StoryFn } from '@storybook/vue'; import type { StoryFn } from '@storybook/vue3';
export default { export default {
title: 'Atoms/ActionBox', title: 'Atoms/ActionBox',
@ -9,8 +9,8 @@ export default {
calloutTheme: { calloutTheme: {
control: { control: {
type: 'select', type: 'select',
options: ['info', 'success', 'warning', 'danger', 'custom'],
}, },
options: ['info', 'success', 'warning', 'danger', 'custom'],
}, },
}, },
parameters: { parameters: {
@ -23,11 +23,12 @@ const methods = {
}; };
const Template: StoryFn = (args, { argTypes }) => ({ const Template: StoryFn = (args, { argTypes }) => ({
setup: () => ({ args }),
props: Object.keys(argTypes), props: Object.keys(argTypes),
components: { components: {
N8nActionBox, N8nActionBox,
}, },
template: '<n8n-action-box v-bind="$props" @click="onClick" />', template: '<n8n-action-box v-bind="args" @click="onClick" />',
methods, methods,
}); });

View file

@ -20,7 +20,7 @@
:label="buttonText" :label="buttonText"
:type="buttonType" :type="buttonType"
size="large" size="large"
@click="$emit('click', $event)" @click="$emit('click:button', $event)"
/> />
<n8n-callout <n8n-callout
v-if="calloutText" v-if="calloutText"

View file

@ -11,7 +11,9 @@ describe('N8NActionBox', () => {
'Long description that you should know something is the way it is because of how it is. ', 'Long description that you should know something is the way it is because of how it is. ',
buttonText: 'Do something', buttonText: 'Do something',
}, },
stubs: ['n8n-heading', 'n8n-text', 'n8n-button', 'n8n-callout'], global: {
stubs: ['n8n-heading', 'n8n-text', 'n8n-button', 'n8n-callout'],
},
}); });
expect(wrapper.html()).toMatchSnapshot(); expect(wrapper.html()).toMatchSnapshot();
}); });

View file

@ -1,15 +1,15 @@
// Vitest Snapshot v1 // Vitest Snapshot v1
exports[`N8NActionBox > should render correctly 1`] = ` exports[`N8NActionBox > should render correctly 1`] = `
"<div data-test-id=\\"action-box\\" class=\\"n8n-action-box container\\"> "<div class=\\"n8n-action-box container\\" data-test-id=\\"action-box\\">
<div class=\\"emoji\\"> 😿 </div> <div class=\\"emoji\\">😿</div>
<div class=\\"heading\\"> <div class=\\"heading\\">
<n8n-heading-stub tag=\\"span\\" size=\\"xlarge\\" align=\\"center\\">Headline you need to know</n8n-heading-stub> <n8n-heading-stub align=\\"center\\" tag=\\"span\\" bold=\\"false\\" size=\\"xlarge\\"></n8n-heading-stub>
</div> </div>
<div class=\\"description\\"> <div class=\\"description\\">
<n8n-text-stub size=\\"medium\\" color=\\"text-base\\" tag=\\"span\\"><span>Long description that you should know something is the way it is because of how it is. </span></n8n-text-stub> <n8n-text-stub color=\\"text-base\\" bold=\\"false\\" size=\\"medium\\" compact=\\"false\\" tag=\\"span\\"></n8n-text-stub>
</div> </div>
<n8n-button-stub label=\\"Do something\\" type=\\"primary\\" size=\\"large\\"></n8n-button-stub> <n8n-button-stub label=\\"Do something\\" type=\\"primary\\" size=\\"large\\" loading=\\"false\\" disabled=\\"false\\" outline=\\"false\\" text=\\"false\\" block=\\"false\\" active=\\"false\\" square=\\"false\\"></n8n-button-stub>
<!----> <!--v-if-->
</div>" </div>"
`; `;

View file

@ -1,5 +1,5 @@
import N8nActionDropdown from './ActionDropdown.vue'; import N8nActionDropdown from './ActionDropdown.vue';
import type { StoryFn } from '@storybook/vue'; import type { StoryFn } from '@storybook/vue3';
export default { export default {
title: 'Atoms/ActionDropdown', title: 'Atoms/ActionDropdown',
@ -8,8 +8,8 @@ export default {
placement: { placement: {
control: { control: {
type: 'select', type: 'select',
options: ['top', 'top-end', 'top-start', 'bottom', 'bottom-end', 'bottom-start'],
}, },
options: ['top', 'top-end', 'top-start', 'bottom', 'bottom-end', 'bottom-start'],
}, },
activatorIcon: { activatorIcon: {
control: { control: {
@ -19,18 +19,19 @@ export default {
trigger: { trigger: {
control: { control: {
type: 'select', type: 'select',
options: ['click', 'hover'],
}, },
options: ['click', 'hover'],
}, },
}, },
}; };
const template: StoryFn = (args, { argTypes }) => ({ const template: StoryFn = (args, { argTypes }) => ({
setup: () => ({ args }),
props: Object.keys(argTypes), props: Object.keys(argTypes),
components: { components: {
N8nActionDropdown, N8nActionDropdown,
}, },
template: '<n8n-action-dropdown v-bind="$props" />', template: '<n8n-action-dropdown v-bind="args" />',
}); });
export const defaultActionDropdown = template.bind({}); export const defaultActionDropdown = template.bind({});

View file

@ -6,7 +6,7 @@
@command="onSelect" @command="onSelect"
ref="elementDropdown" ref="elementDropdown"
> >
<div :class="$style.activator" @click.prevent @blur="onButtonBlur"> <div :class="$style.activator" @click.stop.prevent @blur="onButtonBlur">
<n8n-icon :icon="activatorIcon" /> <n8n-icon :icon="activatorIcon" />
</div> </div>
<template #dropdown> <template #dropdown>
@ -36,11 +36,7 @@
<script lang="ts"> <script lang="ts">
import type { PropType } from 'vue'; import type { PropType } from 'vue';
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { import { ElDropdown, ElDropdownMenu, ElDropdownItem } from 'element-plus';
Dropdown as ElDropdown,
DropdownMenu as ElDropdownMenu,
DropdownItem as ElDropdownItem,
} from 'element-ui';
import N8nIcon from '../N8nIcon'; import N8nIcon from '../N8nIcon';
export interface IActionDropdownItem { export interface IActionDropdownItem {
@ -108,12 +104,11 @@ export default defineComponent({
this.$emit('select', action); this.$emit('select', action);
}, },
onButtonBlur(event: FocusEvent): void { onButtonBlur(event: FocusEvent): void {
const elementDropdown = this.$refs.elementDropdown as const elementDropdown = this.$refs.elementDropdown as InstanceType<ElDropdown>;
| (Vue & { hide: () => void })
| undefined;
// Hide dropdown when clicking outside of current document // Hide dropdown when clicking outside of current document
if (elementDropdown && event.relatedTarget === null) { if (elementDropdown?.handleClose && event.relatedTarget === null) {
elementDropdown.hide(); elementDropdown.handleClose();
} }
}, },
}, },
@ -121,6 +116,10 @@ export default defineComponent({
</script> </script>
<style lang="scss" module> <style lang="scss" module>
.userActionsMenu {
min-width: 160px;
}
.activator { .activator {
cursor: pointer; cursor: pointer;
padding: var(--spacing-2xs); padding: var(--spacing-2xs);

View file

@ -6,6 +6,7 @@ describe('components', () => {
it('should render default styling correctly', () => { it('should render default styling correctly', () => {
const wrapper = render(N8nActionDropdown, { const wrapper = render(N8nActionDropdown, {
props: { props: {
teleported: false,
items: [ items: [
{ {
id: 'item1', id: 'item1',
@ -17,10 +18,13 @@ describe('components', () => {
}, },
], ],
}, },
stubs: ['n8n-icon', 'el-dropdown', 'el-dropdown-menu', 'el-dropdown-item'], global: {
stubs: ['n8n-icon', 'el-tooltip', 'el-dropdown', 'el-dropdown-menu', 'el-dropdown-item'],
},
}); });
expect(wrapper.html()).toMatchSnapshot(); expect(wrapper.html()).toMatchSnapshot();
}); });
it('should render custom styling correctly', () => { it('should render custom styling correctly', () => {
const wrapper = render(N8nActionDropdown, { const wrapper = render(N8nActionDropdown, {
props: { props: {
@ -44,7 +48,9 @@ describe('components', () => {
}, },
], ],
}, },
stubs: ['n8n-icon', 'el-dropdown', 'el-dropdown-menu', 'el-dropdown-item'], global: {
stubs: ['n8n-icon', 'el-dropdown', 'el-dropdown-menu', 'el-dropdown-item'],
},
}); });
expect(wrapper.html()).toMatchSnapshot(); expect(wrapper.html()).toMatchSnapshot();
}); });

View file

@ -2,20 +2,12 @@
exports[`components > N8nActionDropdown > should render custom styling correctly 1`] = ` exports[`components > N8nActionDropdown > should render custom styling correctly 1`] = `
"<div class=\\"action-dropdown-container actionDropdownContainer\\"> "<div class=\\"action-dropdown-container actionDropdownContainer\\">
<el-dropdown-stub trigger=\\"click\\" size=\\"\\" hideonclick=\\"true\\" placement=\\"bottom\\" visiblearrow=\\"true\\" showtimeout=\\"250\\" hidetimeout=\\"150\\" tabindex=\\"0\\"> <el-dropdown-stub trigger=\\"click\\" effect=\\"light\\" placement=\\"bottom\\" popperoptions=\\"[object Object]\\" size=\\"\\" splitbutton=\\"false\\" hideonclick=\\"true\\" loop=\\"true\\" showtimeout=\\"150\\" hidetimeout=\\"150\\" tabindex=\\"0\\" maxheight=\\"\\" popperclass=\\"\\" disabled=\\"false\\" role=\\"menu\\" teleported=\\"true\\"></el-dropdown-stub>
<div class=\\"activator\\">
<n8n-icon-stub icon=\\"ellipsis-v\\" size=\\"medium\\"></n8n-icon-stub>
</div>
</el-dropdown-stub>
</div>" </div>"
`; `;
exports[`components > N8nActionDropdown > should render default styling correctly 1`] = ` exports[`components > N8nActionDropdown > should render default styling correctly 1`] = `
"<div class=\\"action-dropdown-container actionDropdownContainer\\"> "<div class=\\"action-dropdown-container actionDropdownContainer\\" teleported=\\"false\\">
<el-dropdown-stub trigger=\\"click\\" size=\\"\\" hideonclick=\\"true\\" placement=\\"bottom\\" visiblearrow=\\"true\\" showtimeout=\\"250\\" hidetimeout=\\"150\\" tabindex=\\"0\\"> <el-dropdown-stub trigger=\\"click\\" effect=\\"light\\" placement=\\"bottom\\" popperoptions=\\"[object Object]\\" size=\\"\\" splitbutton=\\"false\\" hideonclick=\\"true\\" loop=\\"true\\" showtimeout=\\"150\\" hidetimeout=\\"150\\" tabindex=\\"0\\" maxheight=\\"\\" popperclass=\\"\\" disabled=\\"false\\" role=\\"menu\\" teleported=\\"true\\"></el-dropdown-stub>
<div class=\\"activator\\">
<n8n-icon-stub icon=\\"ellipsis-v\\" size=\\"medium\\"></n8n-icon-stub>
</div>
</el-dropdown-stub>
</div>" </div>"
`; `;

View file

@ -1,6 +1,6 @@
import N8nActionToggle from './ActionToggle.vue'; import N8nActionToggle from './ActionToggle.vue';
import { action } from '@storybook/addon-actions'; import { action } from '@storybook/addon-actions';
import type { StoryFn } from '@storybook/vue'; import type { StoryFn } from '@storybook/vue3';
export default { export default {
title: 'Atoms/ActionToggle', title: 'Atoms/ActionToggle',
@ -25,12 +25,14 @@ const methods = {
}; };
const Template: StoryFn = (args, { argTypes }) => ({ const Template: StoryFn = (args, { argTypes }) => ({
setup: () => ({ args }),
props: Object.keys(argTypes), props: Object.keys(argTypes),
components: { components: {
N8nActionToggle, N8nActionToggle,
}, },
template: template: `<div style="height:300px; width:300px; display:flex; align-items:center; justify-content:center">
'<div style="height:300px;width:300px;display:flex;align-items:center;justify-content:center"><n8n-action-toggle v-bind="$props" @action="onAction" /></div>', <n8n-action-toggle v-bind="args" @action="onAction" />
</div>`,
methods, methods,
}); });

View file

@ -1,10 +1,9 @@
<template> <template>
<span :class="$style.container" data-test-id="action-toggle"> <span @click.stop.prevent :class="$style.container" data-test-id="action-toggle">
<el-dropdown <el-dropdown
:placement="placement" :placement="placement"
:size="size" :size="size"
trigger="click" trigger="click"
@click.native.stop
@command="onCommand" @command="onCommand"
@visible-change="onVisibleChange" @visible-change="onVisibleChange"
> >
@ -42,11 +41,7 @@
<script lang="ts"> <script lang="ts">
import type { PropType } from 'vue'; import type { PropType } from 'vue';
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { import { ElDropdown, ElDropdownMenu, ElDropdownItem } from 'element-plus';
Dropdown as ElDropdown,
DropdownMenu as ElDropdownMenu,
DropdownItem as ElDropdownItem,
} from 'element-ui';
import N8nIcon from '../N8nIcon'; import N8nIcon from '../N8nIcon';
import type { UserAction } from '@/types'; import type { UserAction } from '@/types';

View file

@ -1,4 +1,4 @@
import type { StoryFn } from '@storybook/vue'; import type { StoryFn } from '@storybook/vue3';
import N8nAlert from './Alert.vue'; import N8nAlert from './Alert.vue';
import N8nIcon from '../N8nIcon'; import N8nIcon from '../N8nIcon';
@ -18,12 +18,13 @@ export default {
}; };
const Template: StoryFn = (args, { argTypes }) => ({ const Template: StoryFn = (args, { argTypes }) => ({
setup: () => ({ args }),
props: Object.keys(argTypes), props: Object.keys(argTypes),
components: { components: {
N8nAlert, N8nAlert,
}, },
template: template:
'<div style="position: relative; width: 100%; height: 300px;"><n8n-alert v-bind="$props"><template #aside>custom content slot</template></n8n-alert></div>', '<div style="position: relative; width: 100%; height: 300px;"><n8n-alert v-bind="args"><template #aside>custom content slot</template></n8n-alert></div>',
}); });
export const ContentAsProps = Template.bind({}); export const ContentAsProps = Template.bind({});
@ -38,15 +39,16 @@ ContentAsProps.args = {
}; };
const TemplateForSlots: StoryFn = (args, { argTypes }) => ({ const TemplateForSlots: StoryFn = (args, { argTypes }) => ({
setup: () => ({ args }),
props: Object.keys(argTypes), props: Object.keys(argTypes),
components: { components: {
N8nAlert, N8nAlert,
N8nIcon, N8nIcon,
}, },
template: `<div style="position: relative; width: 100%; height: 300px;"> template: `<div style="position: relative; width: 100%; height: 300px;">
<n8n-alert v-bind="$props"> <n8n-alert v-bind="args">
<template #title>Title</template> <template #title>Title</template>
<template>Description</template> Description
<template #aside><button>Button</button></template> <template #aside><button>Button</button></template>
<template #icon> <template #icon>
<n8n-icon icon="grin-stars" size="xlarge" /> <n8n-icon icon="grin-stars" size="xlarge" />

View file

@ -14,21 +14,20 @@ describe('components', () => {
}); });
it('should render slots instead of props', () => { it('should render slots instead of props', () => {
const { container } = render( const { container } = render(N8nAlert, {
N8nAlert, props: { showIcon: false },
{ slots: {
props: { showIcon: false }, title: 'Title',
slots: { default: 'Message',
title: 'Title', aside: '<button>Click me</button>',
default: 'Message', icon: '<n8n-icon icon="plus-circle" />',
aside: '<button>Click me</button>', },
icon: '<n8n-icon icon="plus-circle" />', global: {
components: {
'n8n-icon': N8nIcon,
}, },
}, },
(localVue) => { });
localVue.component('n8n-icon', N8nIcon);
},
);
expect(screen.getByRole('alert')).toBeVisible(); expect(screen.getByRole('alert')).toBeVisible();
expect(screen.getByText('Title')).toBeVisible(); expect(screen.getByText('Title')).toBeVisible();
expect(screen.getByText('Message')).toBeVisible(); expect(screen.getByText('Message')).toBeVisible();

View file

@ -1,4 +1,4 @@
import type { StoryFn } from '@storybook/vue'; import type { StoryFn } from '@storybook/vue3';
import N8nAvatar from './Avatar.vue'; import N8nAvatar from './Avatar.vue';
export default { export default {
@ -13,14 +13,16 @@ export default {
}; };
const Template: StoryFn = (args, { argTypes }) => ({ const Template: StoryFn = (args, { argTypes }) => ({
setup: () => ({ args }),
props: Object.keys(argTypes), props: Object.keys(argTypes),
components: { components: {
N8nAvatar, N8nAvatar,
}, },
template: '<n8n-avatar v-bind="$props" />', template: '<n8n-avatar v-bind="args" />',
}); });
export const Avatar = Template.bind({}); export const Avatar = Template.bind({});
Avatar.args = { Avatar.args = {
name: 'Sunny Side', firstName: 'Sunny',
lastName: 'Side',
}; };

View file

@ -1,6 +1,6 @@
<template> <template>
<span :class="['n8n-avatar', $style.container]" v-on="$listeners"> <span :class="['n8n-avatar', $style.container]" v-bind="$attrs">
<avatar <Avatar
v-if="firstName" v-if="firstName"
:size="getSize(size)" :size="getSize(size)"
:name="firstName + ' ' + lastName" :name="firstName + ' ' + lastName"
@ -13,7 +13,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import Avatar from 'vue2-boring-avatars'; import Avatar from 'vue-boring-avatars';
const sizes: { [size: string]: number } = { const sizes: { [size: string]: number } = {
small: 28, small: 28,
@ -28,9 +28,11 @@ export default defineComponent({
props: { props: {
firstName: { firstName: {
type: String, type: String,
default: '',
}, },
lastName: { lastName: {
type: String, type: String,
default: '',
}, },
size: { size: {
type: String, type: String,
@ -47,7 +49,7 @@ export default defineComponent({
}, },
}, },
components: { components: {
Avatar, // eslint-disable-line @typescript-eslint/no-unsafe-assignment Avatar,
}, },
computed: { computed: {
initials() { initials() {

View file

@ -1,4 +1,4 @@
import type { StoryFn } from '@storybook/vue'; import type { StoryFn } from '@storybook/vue3';
import N8nBadge from './Badge.vue'; import N8nBadge from './Badge.vue';
export default { export default {
@ -17,11 +17,12 @@ export default {
}; };
const Template: StoryFn = (args, { argTypes }) => ({ const Template: StoryFn = (args, { argTypes }) => ({
setup: () => ({ args }),
props: Object.keys(argTypes), props: Object.keys(argTypes),
components: { components: {
N8nBadge, N8nBadge,
}, },
template: '<n8n-badge v-bind="$props">Badge</n8n-badge>', template: '<n8n-badge v-bind="args">Badge</n8n-badge>',
}); });
export const Badge = Template.bind({}); export const Badge = Template.bind({});

View file

@ -14,7 +14,9 @@ describe('components', () => {
slots: { slots: {
default: '<n8n-text>Default badge</n8n-text>', default: '<n8n-text>Default badge</n8n-text>',
}, },
stubs: ['n8n-text'], global: {
stubs: ['n8n-text'],
},
}); });
expect(wrapper.html()).toMatchSnapshot(); expect(wrapper.html()).toMatchSnapshot();
}); });
@ -28,7 +30,9 @@ describe('components', () => {
slots: { slots: {
default: '<n8n-text>Secondary badge</n8n-text>', default: '<n8n-text>Secondary badge</n8n-text>',
}, },
stubs: ['n8n-text'], global: {
stubs: ['n8n-text'],
},
}); });
expect(wrapper.html()).toMatchSnapshot(); expect(wrapper.html()).toMatchSnapshot();
}); });
@ -37,7 +41,9 @@ describe('components', () => {
slots: { slots: {
default: '<n8n-text>A Badge</n8n-text>', default: '<n8n-text>A Badge</n8n-text>',
}, },
stubs: ['n8n-text'], global: {
stubs: ['n8n-text'],
},
}); });
expect(wrapper.html()).toMatchSnapshot(); expect(wrapper.html()).toMatchSnapshot();
}); });

View file

@ -1,7 +1,7 @@
// Vitest Snapshot v1 // Vitest Snapshot v1
exports[`components > N8nBadge > props > should render default theme correctly 1`] = `"<span class=\\"n8n-badge default\\"><n8n-text-stub bold=\\"true\\" size=\\"large\\" compact=\\"true\\" tag=\\"span\\"><n8n-text-stub size=\\"medium\\" tag=\\"span\\">Default badge</n8n-text-stub></n8n-text-stub></span>"`; exports[`components > N8nBadge > props > should render default theme correctly 1`] = `"<span class=\\"n8n-badge default\\"><n8n-text-stub bold=\\"true\\" size=\\"large\\" compact=\\"true\\" tag=\\"span\\"></n8n-text-stub></span>"`;
exports[`components > N8nBadge > props > should render secondary theme correctly 1`] = `"<span class=\\"n8n-badge secondary\\"><n8n-text-stub size=\\"medium\\" compact=\\"true\\" tag=\\"span\\"><n8n-text-stub size=\\"medium\\" tag=\\"span\\">Secondary badge</n8n-text-stub></n8n-text-stub></span>"`; exports[`components > N8nBadge > props > should render secondary theme correctly 1`] = `"<span class=\\"n8n-badge secondary\\"><n8n-text-stub bold=\\"false\\" size=\\"medium\\" compact=\\"true\\" tag=\\"span\\"></n8n-text-stub></span>"`;
exports[`components > N8nBadge > props > should render with default values correctly 1`] = `"<span class=\\"n8n-badge default\\"><n8n-text-stub size=\\"small\\" compact=\\"true\\" tag=\\"span\\"><n8n-text-stub size=\\"medium\\" tag=\\"span\\">A Badge</n8n-text-stub></n8n-text-stub></span>"`; exports[`components > N8nBadge > props > should render with default values correctly 1`] = `"<span class=\\"n8n-badge default\\"><n8n-text-stub bold=\\"false\\" size=\\"small\\" compact=\\"true\\" tag=\\"span\\"></n8n-text-stub></span>"`;

View file

@ -1,4 +1,4 @@
import type { StoryFn } from '@storybook/vue'; import type { StoryFn } from '@storybook/vue3';
import N8nBlockUi from './BlockUi.vue'; import N8nBlockUi from './BlockUi.vue';
export default { export default {
@ -7,12 +7,13 @@ export default {
}; };
const Template: StoryFn = (args, { argTypes }) => ({ const Template: StoryFn = (args, { argTypes }) => ({
setup: () => ({ args }),
props: Object.keys(argTypes), props: Object.keys(argTypes),
components: { components: {
N8nBlockUi, N8nBlockUi,
}, },
template: template:
'<div style="position: relative; width: 100%; height: 300px;"><n8n-block-ui v-bind="$props" /></div>', '<div style="position: relative; width: 100%; height: 300px;"><n8n-block-ui v-bind="args" /></div>',
}); });
export const BlockUi = Template.bind({}); export const BlockUi = Template.bind({});

View file

@ -38,7 +38,7 @@ withDefaults(defineProps<BlockUiProps>(), {
.fade-leave-active { .fade-leave-active {
transition: opacity 200ms; transition: opacity 200ms;
} }
.fade-enter, .fade-enter-from,
.fade-leave-to { .fade-leave-to {
opacity: 0; opacity: 0;
} }

View file

@ -0,0 +1,81 @@
@import '../../css/mixins/utils';
@import '../../css/common/var';
@mixin n8n-button($override: false) {
$important: if($override, !important, '');
display: inline-block;
line-height: 1;
white-space: nowrap;
cursor: pointer;
border: var(--border-width-base) $button-border-color var(--border-style-base) unquote($important);
color: $button-font-color unquote($important);
background-color: $button-background-color unquote($important);
font-weight: var(--font-weight-bold) unquote($important);
border-radius: $button-border-radius unquote($important);
padding: $button-padding-vertical $button-padding-horizontal unquote($important);
font-size: $button-font-size unquote($important);
-webkit-appearance: none;
text-align: center;
box-sizing: border-box;
outline: none;
margin: 0;
transition: 0.3s;
@include utils-user-select(none);
&:hover {
color: $button-hover-color unquote($important);
border-color: $button-hover-border-color unquote($important);
background-color: $button-hover-background-color unquote($important);
}
&:focus {
border-color: $button-focus-outline-color unquote($important);
outline: $focus-outline-width solid $button-focus-outline-color unquote($important);
}
&:active,
&.active {
color: $button-active-color unquote($important);
border-color: $button-active-border-color unquote($important);
background-color: $button-active-background-color unquote($important);
outline: none;
}
&::-moz-focus-inner {
border: 0;
}
> i {
display: none;
}
> span {
display: flex;
justify-content: center;
align-items: center;
}
span + span {
margin-left: var(--spacing-3xs);
}
}
@mixin n8n-button-secondary {
--button-color: var(--color-primary);
--button-border-color: var(--color-primary);
--button-background-color: var(--color-background-xlight);
--button-active-background-color: var(--color-primary-tint-2);
--button-active-color: var(--color-primary);
--button-active-border-color: var(--color-primary);
--button-hover-background-color: var(--color-primary-tint-3);
--button-hover-color: var(--color-primary);
--button-hover-border-color: var(--color-primary);
--button-focus-outline-color: var(--color-primary-tint-1);
}

View file

@ -1,6 +1,6 @@
import N8nButton from './Button.vue'; import N8nButton from './Button.vue';
import { action } from '@storybook/addon-actions'; import { action } from '@storybook/addon-actions';
import type { StoryFn } from '@storybook/vue'; import type { StoryFn } from '@storybook/vue3';
export default { export default {
title: 'Atoms/Button', title: 'Atoms/Button',
@ -13,8 +13,8 @@ export default {
size: { size: {
control: { control: {
type: 'select', type: 'select',
options: ['mini', 'small', 'medium', 'large', 'xlarge'],
}, },
options: ['mini', 'small', 'medium', 'large', 'xlarge'],
}, },
float: { float: {
type: 'select', type: 'select',
@ -34,11 +34,12 @@ const methods = {
}; };
const Template: StoryFn = (args, { argTypes }) => ({ const Template: StoryFn = (args, { argTypes }) => ({
setup: () => ({ args }),
props: Object.keys(argTypes), props: Object.keys(argTypes),
components: { components: {
N8nButton, N8nButton,
}, },
template: '<n8n-button v-bind="$props" @click="onClick" />', template: '<n8n-button v-bind="args" @click="onClick" />',
methods, methods,
}); });
@ -48,48 +49,50 @@ Button.args = {
}; };
const AllSizesTemplate: StoryFn = (args, { argTypes }) => ({ const AllSizesTemplate: StoryFn = (args, { argTypes }) => ({
setup: () => ({ args }),
props: Object.keys(argTypes), props: Object.keys(argTypes),
components: { components: {
N8nButton, N8nButton,
}, },
template: `<div> template: `<div>
<n8n-button v-bind="$props" size="large" @click="onClick" /> <n8n-button v-bind="args" size="large" @click="onClick" />
<n8n-button v-bind="$props" size="medium" @click="onClick" /> <n8n-button v-bind="args" size="medium" @click="onClick" />
<n8n-button v-bind="$props" size="small" @click="onClick" /> <n8n-button v-bind="args" size="small" @click="onClick" />
<n8n-button v-bind="$props" :loading="true" @click="onClick" /> <n8n-button v-bind="args" :loading="true" @click="onClick" />
<n8n-button v-bind="$props" :disabled="true" @click="onClick" /> <n8n-button v-bind="args" :disabled="true" @click="onClick" />
</div>`, </div>`,
methods, methods,
}); });
const AllColorsAndSizesTemplate: StoryFn = (args, { argTypes }) => ({ const AllColorsAndSizesTemplate: StoryFn = (args, { argTypes }) => ({
setup: () => ({ args }),
props: Object.keys(argTypes), props: Object.keys(argTypes),
components: { components: {
N8nButton, N8nButton,
}, },
template: `<div> template: `<div>
<n8n-button v-bind="$props" size="large" type="primary" @click="onClick" /> <n8n-button v-bind="args" size="large" type="primary" @click="onClick" />
<n8n-button v-bind="$props" size="large" type="secondary" @click="onClick" /> <n8n-button v-bind="args" size="large" type="secondary" @click="onClick" />
<n8n-button v-bind="$props" size="large" type="tertiary" @click="onClick" /> <n8n-button v-bind="args" size="large" type="tertiary" @click="onClick" />
<n8n-button v-bind="$props" size="large" type="success" @click="onClick" /> <n8n-button v-bind="args" size="large" type="success" @click="onClick" />
<n8n-button v-bind="$props" size="large" type="warning" @click="onClick" /> <n8n-button v-bind="args" size="large" type="warning" @click="onClick" />
<n8n-button v-bind="$props" size="large" type="danger" @click="onClick" /> <n8n-button v-bind="args" size="large" type="danger" @click="onClick" />
<br/> <br/>
<br/> <br/>
<n8n-button v-bind="$props" size="medium" type="primary" @click="onClick" /> <n8n-button v-bind="args" size="medium" type="primary" @click="onClick" />
<n8n-button v-bind="$props" size="medium" type="secondary" @click="onClick" /> <n8n-button v-bind="args" size="medium" type="secondary" @click="onClick" />
<n8n-button v-bind="$props" size="medium" type="tertiary" @click="onClick" /> <n8n-button v-bind="args" size="medium" type="tertiary" @click="onClick" />
<n8n-button v-bind="$props" size="medium" type="success" @click="onClick" /> <n8n-button v-bind="args" size="medium" type="success" @click="onClick" />
<n8n-button v-bind="$props" size="medium" type="warning" @click="onClick" /> <n8n-button v-bind="args" size="medium" type="warning" @click="onClick" />
<n8n-button v-bind="$props" size="medium" type="danger" @click="onClick" /> <n8n-button v-bind="args" size="medium" type="danger" @click="onClick" />
<br/> <br/>
<br/> <br/>
<n8n-button v-bind="$props" size="small" type="primary" @click="onClick" /> <n8n-button v-bind="args" size="small" type="primary" @click="onClick" />
<n8n-button v-bind="$props" size="small" type="secondary" @click="onClick" /> <n8n-button v-bind="args" size="small" type="secondary" @click="onClick" />
<n8n-button v-bind="$props" size="small" type="tertiary" @click="onClick" /> <n8n-button v-bind="args" size="small" type="tertiary" @click="onClick" />
<n8n-button v-bind="$props" size="small" type="success" @click="onClick" /> <n8n-button v-bind="args" size="small" type="success" @click="onClick" />
<n8n-button v-bind="$props" size="small" type="warning" @click="onClick" /> <n8n-button v-bind="args" size="small" type="warning" @click="onClick" />
<n8n-button v-bind="$props" size="small" type="danger" @click="onClick" /> <n8n-button v-bind="args" size="small" type="danger" @click="onClick" />
</div>`, </div>`,
methods, methods,
}); });

View file

@ -1,11 +1,11 @@
<template> <template>
<button <button
:class="classes" :class="classes"
:disabled="disabled || loading" :disabled="isDisabled"
:aria-disabled="ariaDisabled" :aria-disabled="ariaDisabled"
:aria-busy="ariaBusy" :aria-busy="ariaBusy"
aria-live="polite" aria-live="polite"
v-on="$listeners" v-bind="$attrs"
> >
<span :class="$style.icon" v-if="loading || icon"> <span :class="$style.icon" v-if="loading || icon">
<n8n-spinner v-if="loading" :size="size" /> <n8n-spinner v-if="loading" :size="size" />
@ -17,158 +17,112 @@
</button> </button>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from 'vue';
import N8nIcon from '../N8nIcon'; import N8nIcon from '../N8nIcon';
import N8nSpinner from '../N8nSpinner'; import N8nSpinner from '../N8nSpinner';
import { useCssModule, computed, useAttrs } from 'vue';
export default defineComponent({ const $style = useCssModule();
name: 'n8n-button', const $attrs = useAttrs();
props: {
label: { const props = defineProps({
type: String, label: {
}, type: String,
type: { default: '',
type: String,
default: 'primary',
validator: (value: string): boolean =>
['primary', 'secondary', 'tertiary', 'success', 'warning', 'danger'].includes(value),
},
size: {
type: String,
default: 'medium',
validator: (value: string): boolean =>
['xmini', 'mini', 'small', 'medium', 'large', 'xlarge'].includes(value),
},
loading: {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
outline: {
type: Boolean,
default: false,
},
text: {
type: Boolean,
default: false,
},
icon: {
type: [String, Array],
},
block: {
type: Boolean,
default: false,
},
active: {
type: Boolean,
default: false,
},
float: {
type: String,
validator: (value: string): boolean => ['left', 'right'].includes(value),
},
square: {
type: Boolean,
default: false,
},
}, },
components: { type: {
N8nSpinner, type: String,
N8nIcon, default: 'primary',
}, },
computed: { size: {
ariaBusy(): 'true' | undefined { type: String,
return this.loading ? 'true' : undefined; default: 'medium',
},
ariaDisabled(): 'true' | undefined {
return this.disabled ? 'true' : undefined;
},
classes(): string {
return (
`button ${this.$style.button} ${this.$style[this.type]}` +
`${this.size ? ` ${this.$style[this.size]}` : ''}` +
`${this.outline ? ` ${this.$style.outline}` : ''}` +
`${this.loading ? ` ${this.$style.loading}` : ''}` +
`${this.float ? ` ${this.$style[`float-${this.float}`]}` : ''}` +
`${this.text ? ` ${this.$style.text}` : ''}` +
`${this.disabled ? ` ${this.$style.disabled}` : ''}` +
`${this.block ? ` ${this.$style.block}` : ''}` +
`${this.active ? ` ${this.$style.active}` : ''}` +
`${this.icon || this.loading ? ` ${this.$style.withIcon}` : ''}` +
`${this.square ? ` ${this.$style.square}` : ''}`
);
},
}, },
loading: {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
outline: {
type: Boolean,
default: false,
},
text: {
type: Boolean,
default: false,
},
icon: {
type: [String, Array],
},
block: {
type: Boolean,
default: false,
},
active: {
type: Boolean,
default: false,
},
float: {
type: String,
},
square: {
type: Boolean,
default: false,
},
});
const ariaBusy = computed(() => (props.loading ? 'true' : undefined));
const ariaDisabled = computed(() => (props.disabled ? 'true' : undefined));
const isDisabled = computed(() => props.disabled || props.loading);
const classes = computed(() => {
return (
`button ${$style.button} ${$style[props.type]}` +
`${props.size ? ` ${$style[props.size]}` : ''}` +
`${props.outline ? ` ${$style.outline}` : ''}` +
`${props.loading ? ` ${$style.loading}` : ''}` +
`${props.float ? ` ${$style[`float-${props.float}`]}` : ''}` +
`${props.text ? ` ${$style.text}` : ''}` +
`${props.disabled ? ` ${$style.disabled}` : ''}` +
`${props.block ? ` ${$style.block}` : ''}` +
`${props.active ? ` ${$style.active}` : ''}` +
`${props.icon || props.loading ? ` ${$style.withIcon}` : ''}` +
`${props.square ? ` ${$style.square}` : ''}`
);
}); });
</script> </script>
<style lang="scss">
@import './Button';
.el-button {
@include n8n-button(true);
--button-padding-vertical: var(--spacing-2xs);
--button-padding-horizontal: var(--spacing-xs);
--button-font-size: var(--font-size-2xs);
+ .el-button {
margin-left: var(--spacing-2xs);
}
&.btn--cancel,
&.el-color-dropdown__link-btn {
@include n8n-button-secondary;
}
}
</style>
<style lang="scss" module> <style lang="scss" module>
@import './Button';
@import '../../css/mixins/utils'; @import '../../css/mixins/utils';
@import '../../css/common/var'; @import '../../css/common/var';
.button { .button {
display: inline-block; @include n8n-button;
line-height: 1;
white-space: nowrap;
cursor: pointer;
border: var(--border-width-base) $button-border-color var(--border-style-base);
color: $button-font-color;
background-color: $button-background-color;
font-weight: var(--font-weight-bold);
border-radius: $button-border-radius;
padding: $button-padding-vertical $button-padding-horizontal;
font-size: $button-font-size;
-webkit-appearance: none;
text-align: center;
box-sizing: border-box;
outline: none;
margin: 0;
transition: 0.3s;
@include utils-user-select(none);
&:hover {
color: $button-hover-color;
border-color: $button-hover-border-color;
background-color: $button-hover-background-color;
}
&:focus {
border-color: $button-focus-outline-color;
outline: $focus-outline-width solid $button-focus-outline-color;
}
&:active,
&.active {
color: $button-active-color;
border-color: $button-active-border-color;
background-color: $button-active-background-color;
outline: none;
}
&::-moz-focus-inner {
border: 0;
}
> i {
display: none;
}
> span {
display: flex;
justify-content: center;
align-items: center;
}
span + span {
margin-left: var(--spacing-3xs);
}
} }
$loading-overlay-background-color: rgba(255, 255, 255, 0); $loading-overlay-background-color: rgba(255, 255, 255, 0);
@ -178,19 +132,7 @@ $loading-overlay-background-color: rgba(255, 255, 255, 0);
*/ */
.secondary { .secondary {
--button-color: var(--color-primary); @include n8n-button-secondary;
--button-border-color: var(--color-primary);
--button-background-color: var(--color-background-xlight);
--button-active-background-color: var(--color-primary-tint-2);
--button-active-color: var(--color-primary);
--button-active-border-color: var(--color-primary);
--button-hover-background-color: var(--color-primary-tint-3);
--button-hover-color: var(--color-primary);
--button-hover-border-color: var(--color-primary);
--button-focus-outline-color: var(--color-primary-tint-1);
} }
.tertiary { .tertiary {

View file

@ -1,6 +1,5 @@
import { render } from '@testing-library/vue'; import { render } from '@testing-library/vue';
import N8nButton from '../Button.vue'; import N8nButton from '../Button.vue';
import ElButton from '../overrides/ElButton.vue';
const slots = { const slots = {
default: 'Button', default: 'Button',
@ -12,7 +11,9 @@ describe('components', () => {
it('should render correctly', () => { it('should render correctly', () => {
const wrapper = render(N8nButton, { const wrapper = render(N8nButton, {
slots, slots,
stubs, global: {
stubs,
},
}); });
expect(wrapper.html()).toMatchSnapshot(); expect(wrapper.html()).toMatchSnapshot();
}); });
@ -25,7 +26,9 @@ describe('components', () => {
loading: true, loading: true,
}, },
slots, slots,
stubs, global: {
stubs,
},
}); });
expect(wrapper.html()).toMatchSnapshot(); expect(wrapper.html()).toMatchSnapshot();
}); });
@ -38,7 +41,9 @@ describe('components', () => {
icon: 'plus-circle', icon: 'plus-circle',
}, },
slots, slots,
stubs, global: {
stubs,
},
}); });
expect(wrapper.html()).toMatchSnapshot(); expect(wrapper.html()).toMatchSnapshot();
}); });
@ -51,64 +56,13 @@ describe('components', () => {
square: true, square: true,
label: '48', label: '48',
}, },
stubs, global: {
stubs,
},
}); });
expect(wrapper.html()).toMatchSnapshot(); expect(wrapper.html()).toMatchSnapshot();
}); });
}); });
}); });
describe('overrides', () => {
it('should use default (`primary`) type when no type is given', () => {
const wrapper = render(ElButton, {
props: {
icon: 'plus-circle',
},
slots,
stubs,
});
expect(wrapper.html()).toMatchSnapshot();
});
it('should use given (`secondary`) type', () => {
const wrapper = render(ElButton, {
props: {
icon: 'plus-circle',
type: 'secondary',
},
slots,
stubs,
});
expect(wrapper.html()).toMatchSnapshot();
});
it('should render as `secondary` when `text` is given as type', () => {
const wrapper = render(ElButton, {
props: {
icon: 'plus-circle',
type: 'text',
},
slots,
stubs,
});
expect(wrapper.html()).toMatchSnapshot();
});
it('should render as `tertiary` when `info` is given as type', () => {
const wrapper = render(ElButton, {
props: {
icon: 'plus-circle',
type: 'info',
},
slots,
stubs,
});
expect(wrapper.html()).toMatchSnapshot();
});
});
}); });
}); });

View file

@ -1,23 +1,17 @@
// Vitest Snapshot v1 // Vitest Snapshot v1
exports[`components > N8nButton > overrides > should render as \`secondary\` when \`text\` is given as type 1`] = `"<button aria-live=\\"polite\\" class=\\"button button secondary medium withIcon\\" icon=\\"plus-circle\\"><span class=\\"icon\\"><n8n-icon-stub icon=\\"plus-circle\\" size=\\"medium\\"></n8n-icon-stub></span><span>Button</span></button>"`; exports[`components > N8nButton > props > icon > should render icon button 1`] = `"<button class=\\"button button primary medium withIcon\\" aria-live=\\"polite\\"><span class=\\"icon\\"><n8n-icon-stub icon=\\"plus-circle\\" size=\\"medium\\" spin=\\"false\\"></n8n-icon-stub></span><span>Button</span></button>"`;
exports[`components > N8nButton > overrides > should render as \`tertiary\` when \`info\` is given as type 1`] = `"<button aria-live=\\"polite\\" class=\\"button button tertiary medium withIcon\\" icon=\\"plus-circle\\"><span class=\\"icon\\"><n8n-icon-stub icon=\\"plus-circle\\" size=\\"medium\\"></n8n-icon-stub></span><span>Button</span></button>"`; exports[`components > N8nButton > props > loading > should render loading spinner 1`] = `"<button class=\\"button button primary medium loading withIcon\\" disabled=\\"\\" aria-busy=\\"true\\" aria-live=\\"polite\\"><span class=\\"icon\\"><n8n-spinner-stub size=\\"medium\\" type=\\"dots\\"></n8n-spinner-stub></span><span>Button</span></button>"`;
exports[`components > N8nButton > overrides > should use default (\`primary\`) type when no type is given 1`] = `"<button aria-live=\\"polite\\" class=\\"button button primary medium withIcon\\" icon=\\"plus-circle\\"><span class=\\"icon\\"><n8n-icon-stub icon=\\"plus-circle\\" size=\\"medium\\"></n8n-icon-stub></span><span>Button</span></button>"`;
exports[`components > N8nButton > overrides > should use given (\`secondary\`) type 1`] = `"<button aria-live=\\"polite\\" class=\\"button button secondary medium withIcon\\" icon=\\"plus-circle\\"><span class=\\"icon\\"><n8n-icon-stub icon=\\"plus-circle\\" size=\\"medium\\"></n8n-icon-stub></span><span>Button</span></button>"`;
exports[`components > N8nButton > props > icon > should render icon button 1`] = `"<button aria-live=\\"polite\\" class=\\"button button primary medium withIcon\\"><span class=\\"icon\\"><n8n-icon-stub icon=\\"plus-circle\\" size=\\"medium\\"></n8n-icon-stub></span><span>Button</span></button>"`;
exports[`components > N8nButton > props > loading > should render loading spinner 1`] = `"<button disabled=\\"disabled\\" aria-busy=\\"true\\" aria-live=\\"polite\\" class=\\"button button primary medium loading withIcon\\"><span class=\\"icon\\"><n8n-spinner-stub size=\\"medium\\" type=\\"dots\\"></n8n-spinner-stub></span><span>Button</span></button>"`;
exports[`components > N8nButton > props > square > should render square button 1`] = ` exports[`components > N8nButton > props > square > should render square button 1`] = `
"<button aria-live=\\"polite\\" class=\\"button button primary medium square\\"> "<button class=\\"button button primary medium square\\" aria-live=\\"polite\\">
<!----><span>48</span></button>" <!--v-if--><span>48</span>
</button>"
`; `;
exports[`components > N8nButton > should render correctly 1`] = ` exports[`components > N8nButton > should render correctly 1`] = `
"<button aria-live=\\"polite\\" class=\\"button button primary medium\\"> "<button class=\\"button button primary medium\\" aria-live=\\"polite\\">
<!----><span>Button</span></button>" <!--v-if--><span>Button</span>
</button>"
`; `;

View file

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

View file

@ -1,51 +0,0 @@
<template>
<n8n-button ref="button" v-bind="attrs" v-on="$listeners">
<slot />
</n8n-button>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import N8nButton from '../Button.vue';
const classToTypeMap = {
'btn--cancel': 'secondary',
'el-picker-panel__link-btn': 'secondary',
};
type ButtonRef = InstanceType<typeof N8nButton>;
export default defineComponent({
components: {
N8nButton,
},
computed: {
attrs() {
let type = this.$attrs.type || 'primary';
/* Element UI Button can have 'text' or 'info' type which is not supported by n8n-button
so render it as 'secondary' or 'tertiary' */
if (type === 'text') {
type = 'secondary';
}
if (type === 'info') {
type = 'tertiary';
}
Object.entries(classToTypeMap).forEach(([className, mappedType]) => {
if ((this.$refs.button as ButtonRef)?.$el.classList.contains(className)) {
type = mappedType;
}
});
delete this.$attrs.type;
return {
...this.$attrs,
type,
};
},
},
});
</script>

View file

@ -1 +0,0 @@
export { default as N8nElButton } from './ElButton.vue';

View file

@ -1,7 +1,7 @@
import N8nCallout from './Callout.vue'; import N8nCallout from './Callout.vue';
import N8nLink from '../N8nLink'; import N8nLink from '../N8nLink';
import N8nText from '../N8nText'; import N8nText from '../N8nText';
import type { StoryFn } from '@storybook/vue'; import type { StoryFn } from '@storybook/vue3';
export default { export default {
title: 'Atoms/Callout', title: 'Atoms/Callout',
@ -10,8 +10,8 @@ export default {
theme: { theme: {
control: { control: {
type: 'select', type: 'select',
options: ['info', 'secondary', 'success', 'warning', 'danger', 'custom'],
}, },
options: ['info', 'secondary', 'success', 'warning', 'danger', 'custom'],
}, },
message: { message: {
control: { control: {
@ -41,6 +41,7 @@ interface Args {
} }
const template: StoryFn<Args> = (args, { argTypes }) => ({ const template: StoryFn<Args> = (args, { argTypes }) => ({
setup: () => ({ args }),
props: Object.keys(argTypes), props: Object.keys(argTypes),
components: { components: {
N8nLink, N8nLink,
@ -48,12 +49,12 @@ const template: StoryFn<Args> = (args, { argTypes }) => ({
N8nCallout, N8nCallout,
}, },
template: ` template: `
<n8n-callout v-bind="$props"> <n8n-callout v-bind="args">
${args.default} ${args.default}
<template #actions v-if="actions"> <template #actions v-if="args.actions">
${args.actions} ${args.actions}
</template> </template>
<template #trailingContent v-if="trailingContent"> <template #trailingContent v-if="args.trailingContent">
${args.trailingContent} ${args.trailingContent}
</template> </template>
</n8n-callout> </n8n-callout>

View file

@ -8,7 +8,9 @@ describe('components', () => {
props: { props: {
theme: 'info', theme: 'info',
}, },
stubs: ['n8n-icon', 'n8n-text'], global: {
stubs: ['n8n-icon', 'n8n-text'],
},
slots: { slots: {
default: '<n8n-text size="small">This is an info callout.</n8n-text>', default: '<n8n-text size="small">This is an info callout.</n8n-text>',
}, },
@ -20,7 +22,9 @@ describe('components', () => {
props: { props: {
theme: 'success', theme: 'success',
}, },
stubs: ['n8n-icon', 'n8n-text'], global: {
stubs: ['n8n-icon', 'n8n-text'],
},
slots: { slots: {
default: '<n8n-text size="small">This is a success callout.</n8n-text>', default: '<n8n-text size="small">This is a success callout.</n8n-text>',
}, },
@ -32,7 +36,9 @@ describe('components', () => {
props: { props: {
theme: 'warning', theme: 'warning',
}, },
stubs: ['n8n-icon', 'n8n-text'], global: {
stubs: ['n8n-icon', 'n8n-text'],
},
slots: { slots: {
default: '<n8n-text size="small">This is a warning callout.</n8n-text>', default: '<n8n-text size="small">This is a warning callout.</n8n-text>',
}, },
@ -44,7 +50,9 @@ describe('components', () => {
props: { props: {
theme: 'danger', theme: 'danger',
}, },
stubs: ['n8n-icon', 'n8n-text'], global: {
stubs: ['n8n-icon', 'n8n-text'],
},
slots: { slots: {
default: '<n8n-text size="small">This is a danger callout.</n8n-text>', default: '<n8n-text size="small">This is a danger callout.</n8n-text>',
}, },
@ -56,7 +64,9 @@ describe('components', () => {
props: { props: {
theme: 'secondary', theme: 'secondary',
}, },
stubs: ['n8n-icon', 'n8n-text'], global: {
stubs: ['n8n-icon', 'n8n-text'],
},
slots: { slots: {
default: '<n8n-text size="small">This is a secondary callout.</n8n-text>', default: '<n8n-text size="small">This is a secondary callout.</n8n-text>',
}, },
@ -69,7 +79,9 @@ describe('components', () => {
theme: 'custom', theme: 'custom',
icon: 'code-branch', icon: 'code-branch',
}, },
stubs: ['n8n-icon', 'n8n-text'], global: {
stubs: ['n8n-icon', 'n8n-text'],
},
slots: { slots: {
default: '<n8n-text size="small">This is a secondary callout.</n8n-text>', default: '<n8n-text size="small">This is a secondary callout.</n8n-text>',
}, },
@ -82,7 +94,9 @@ describe('components', () => {
theme: 'custom', theme: 'custom',
icon: 'code-branch', icon: 'code-branch',
}, },
stubs: ['n8n-icon', 'n8n-text', 'n8n-link'], global: {
stubs: ['n8n-icon', 'n8n-text', 'n8n-link'],
},
slots: { slots: {
default: '<n8n-text size="small">This is a secondary callout.</n8n-text>', default: '<n8n-text size="small">This is a secondary callout.</n8n-text>',
actions: '<n8n-link size="small">Do something!</n8n-link>', actions: '<n8n-link size="small">Do something!</n8n-link>',

View file

@ -1,93 +1,79 @@
// Vitest Snapshot v1 // Vitest Snapshot v1
exports[`components > N8nCallout > should render additional slots correctly 1`] = ` exports[`components > N8nCallout > should render additional slots correctly 1`] = `
"<div role=\\"alert\\" class=\\"n8n-callout callout custom round\\"> "<div class=\\"n8n-callout callout custom round\\" role=\\"alert\\">
<div class=\\"messageSection\\"> <div class=\\"messageSection\\">
<div class=\\"icon\\"> <div class=\\"icon\\">
<n8n-icon-stub icon=\\"code-branch\\" size=\\"medium\\"></n8n-icon-stub> <n8n-icon-stub icon=\\"code-branch\\" size=\\"medium\\" spin=\\"false\\"></n8n-icon-stub>
</div> </div>
<n8n-text-stub size=\\"small\\" tag=\\"span\\"> <n8n-text-stub bold=\\"false\\" size=\\"small\\" compact=\\"false\\" tag=\\"span\\"></n8n-text-stub> &nbsp; <n8n-link-stub size=\\"small\\"></n8n-link-stub>
<n8n-text-stub size=\\"small\\" tag=\\"span\\">This is a secondary callout.</n8n-text-stub>
</n8n-text-stub> &nbsp; <n8n-link-stub size=\\"small\\">Do something!</n8n-link-stub>
</div> </div>
<n8n-link-stub theme=\\"secondary\\" size=\\"small\\" bold=\\"true\\" underline=\\"true\\" to=\\"https://n8n.io\\">Learn more</n8n-link-stub> <n8n-link-stub theme=\\"secondary\\" size=\\"small\\" bold=\\"true\\" underline=\\"true\\" to=\\"https://n8n.io\\"></n8n-link-stub>
</div>" </div>"
`; `;
exports[`components > N8nCallout > should render custom theme correctly 1`] = ` exports[`components > N8nCallout > should render custom theme correctly 1`] = `
"<div role=\\"alert\\" class=\\"n8n-callout callout custom round\\"> "<div class=\\"n8n-callout callout custom round\\" role=\\"alert\\">
<div class=\\"messageSection\\"> <div class=\\"messageSection\\">
<div class=\\"icon\\"> <div class=\\"icon\\">
<n8n-icon-stub icon=\\"code-branch\\" size=\\"medium\\"></n8n-icon-stub> <n8n-icon-stub icon=\\"code-branch\\" size=\\"medium\\" spin=\\"false\\"></n8n-icon-stub>
</div> </div>
<n8n-text-stub size=\\"small\\" tag=\\"span\\"> <n8n-text-stub bold=\\"false\\" size=\\"small\\" compact=\\"false\\" tag=\\"span\\"></n8n-text-stub> &nbsp;
<n8n-text-stub size=\\"small\\" tag=\\"span\\">This is a secondary callout.</n8n-text-stub>
</n8n-text-stub> &nbsp;
</div> </div>
</div>" </div>"
`; `;
exports[`components > N8nCallout > should render danger theme correctly 1`] = ` exports[`components > N8nCallout > should render danger theme correctly 1`] = `
"<div role=\\"alert\\" class=\\"n8n-callout callout danger round\\"> "<div class=\\"n8n-callout callout danger round\\" role=\\"alert\\">
<div class=\\"messageSection\\"> <div class=\\"messageSection\\">
<div class=\\"icon\\"> <div class=\\"icon\\">
<n8n-icon-stub icon=\\"times-circle\\" size=\\"medium\\"></n8n-icon-stub> <n8n-icon-stub icon=\\"times-circle\\" size=\\"medium\\" spin=\\"false\\"></n8n-icon-stub>
</div> </div>
<n8n-text-stub size=\\"small\\" tag=\\"span\\"> <n8n-text-stub bold=\\"false\\" size=\\"small\\" compact=\\"false\\" tag=\\"span\\"></n8n-text-stub> &nbsp;
<n8n-text-stub size=\\"small\\" tag=\\"span\\">This is a danger callout.</n8n-text-stub>
</n8n-text-stub> &nbsp;
</div> </div>
</div>" </div>"
`; `;
exports[`components > N8nCallout > should render info theme correctly 1`] = ` exports[`components > N8nCallout > should render info theme correctly 1`] = `
"<div role=\\"alert\\" class=\\"n8n-callout callout info round\\"> "<div class=\\"n8n-callout callout info round\\" role=\\"alert\\">
<div class=\\"messageSection\\"> <div class=\\"messageSection\\">
<div class=\\"icon\\"> <div class=\\"icon\\">
<n8n-icon-stub icon=\\"info-circle\\" size=\\"medium\\"></n8n-icon-stub> <n8n-icon-stub icon=\\"info-circle\\" size=\\"medium\\" spin=\\"false\\"></n8n-icon-stub>
</div> </div>
<n8n-text-stub size=\\"small\\" tag=\\"span\\"> <n8n-text-stub bold=\\"false\\" size=\\"small\\" compact=\\"false\\" tag=\\"span\\"></n8n-text-stub> &nbsp;
<n8n-text-stub size=\\"small\\" tag=\\"span\\">This is an info callout.</n8n-text-stub>
</n8n-text-stub> &nbsp;
</div> </div>
</div>" </div>"
`; `;
exports[`components > N8nCallout > should render secondary theme correctly 1`] = ` exports[`components > N8nCallout > should render secondary theme correctly 1`] = `
"<div role=\\"alert\\" class=\\"n8n-callout callout secondary round\\"> "<div class=\\"n8n-callout callout secondary round\\" role=\\"alert\\">
<div class=\\"messageSection\\"> <div class=\\"messageSection\\">
<div class=\\"icon\\"> <div class=\\"icon\\">
<n8n-icon-stub icon=\\"info-circle\\" size=\\"medium\\"></n8n-icon-stub> <n8n-icon-stub icon=\\"info-circle\\" size=\\"medium\\" spin=\\"false\\"></n8n-icon-stub>
</div> </div>
<n8n-text-stub size=\\"small\\" tag=\\"span\\"> <n8n-text-stub bold=\\"false\\" size=\\"small\\" compact=\\"false\\" tag=\\"span\\"></n8n-text-stub> &nbsp;
<n8n-text-stub size=\\"small\\" tag=\\"span\\">This is a secondary callout.</n8n-text-stub>
</n8n-text-stub> &nbsp;
</div> </div>
</div>" </div>"
`; `;
exports[`components > N8nCallout > should render success theme correctly 1`] = ` exports[`components > N8nCallout > should render success theme correctly 1`] = `
"<div role=\\"alert\\" class=\\"n8n-callout callout success round\\"> "<div class=\\"n8n-callout callout success round\\" role=\\"alert\\">
<div class=\\"messageSection\\"> <div class=\\"messageSection\\">
<div class=\\"icon\\"> <div class=\\"icon\\">
<n8n-icon-stub icon=\\"check-circle\\" size=\\"medium\\"></n8n-icon-stub> <n8n-icon-stub icon=\\"check-circle\\" size=\\"medium\\" spin=\\"false\\"></n8n-icon-stub>
</div> </div>
<n8n-text-stub size=\\"small\\" tag=\\"span\\"> <n8n-text-stub bold=\\"false\\" size=\\"small\\" compact=\\"false\\" tag=\\"span\\"></n8n-text-stub> &nbsp;
<n8n-text-stub size=\\"small\\" tag=\\"span\\">This is a success callout.</n8n-text-stub>
</n8n-text-stub> &nbsp;
</div> </div>
</div>" </div>"
`; `;
exports[`components > N8nCallout > should render warning theme correctly 1`] = ` exports[`components > N8nCallout > should render warning theme correctly 1`] = `
"<div role=\\"alert\\" class=\\"n8n-callout callout warning round\\"> "<div class=\\"n8n-callout callout warning round\\" role=\\"alert\\">
<div class=\\"messageSection\\"> <div class=\\"messageSection\\">
<div class=\\"icon\\"> <div class=\\"icon\\">
<n8n-icon-stub icon=\\"exclamation-triangle\\" size=\\"medium\\"></n8n-icon-stub> <n8n-icon-stub icon=\\"exclamation-triangle\\" size=\\"medium\\" spin=\\"false\\"></n8n-icon-stub>
</div> </div>
<n8n-text-stub size=\\"small\\" tag=\\"span\\"> <n8n-text-stub bold=\\"false\\" size=\\"small\\" compact=\\"false\\" tag=\\"span\\"></n8n-text-stub> &nbsp;
<n8n-text-stub size=\\"small\\" tag=\\"span\\">This is a warning callout.</n8n-text-stub>
</n8n-text-stub> &nbsp;
</div> </div>
</div>" </div>"
`; `;

View file

@ -1,5 +1,5 @@
import N8nCard from './Card.vue'; import N8nCard from './Card.vue';
import type { StoryFn } from '@storybook/vue'; import type { StoryFn } from '@storybook/vue3';
import N8nButton from '../N8nButton/Button.vue'; import N8nButton from '../N8nButton/Button.vue';
import N8nIcon from '../N8nIcon/Icon.vue'; import N8nIcon from '../N8nIcon/Icon.vue';
import N8nText from '../N8nText/Text.vue'; import N8nText from '../N8nText/Text.vue';
@ -10,14 +10,16 @@ export default {
}; };
export const Default: StoryFn = (args, { argTypes }) => ({ export const Default: StoryFn = (args, { argTypes }) => ({
setup: () => ({ args }),
props: Object.keys(argTypes), props: Object.keys(argTypes),
components: { components: {
N8nCard, N8nCard,
}, },
template: '<n8n-card v-bind="$props">This is a card.</n8n-card>', template: '<n8n-card v-bind="args">This is a card.</n8n-card>',
}); });
export const Hoverable: StoryFn = (args, { argTypes }) => ({ export const Hoverable: StoryFn = (args, { argTypes }) => ({
setup: () => ({ args }),
props: Object.keys(argTypes), props: Object.keys(argTypes),
components: { components: {
N8nCard, N8nCard,
@ -25,7 +27,7 @@ export const Hoverable: StoryFn = (args, { argTypes }) => ({
N8nText, N8nText,
}, },
template: `<div style="width: 140px; text-align: center;"> template: `<div style="width: 140px; text-align: center;">
<n8n-card v-bind="$props"> <n8n-card v-bind="args">
<n8n-icon icon="plus" size="xlarge" /> <n8n-icon icon="plus" size="xlarge" />
<n8n-text size="large" class="mt-2xs">Add</n8n-text> <n8n-text size="large" class="mt-2xs">Add</n8n-text>
</n8n-card> </n8n-card>
@ -37,6 +39,7 @@ Hoverable.args = {
}; };
export const WithSlots: StoryFn = (args, { argTypes }) => ({ export const WithSlots: StoryFn = (args, { argTypes }) => ({
setup: () => ({ args }),
props: Object.keys(argTypes), props: Object.keys(argTypes),
components: { components: {
N8nCard, N8nCard,
@ -44,22 +47,22 @@ export const WithSlots: StoryFn = (args, { argTypes }) => ({
N8nIcon, N8nIcon,
N8nText, N8nText,
}, },
template: `<n8n-card v-bind="$props"> template: `<n8n-card v-bind="args">
<template slot="prepend"> <template #prepend>
<n8n-icon icon="check" size="large" /> <n8n-icon icon="check" size="large" />
</template> </template>
<template slot="header"> <template #header>
<strong>Card header</strong> <strong>Card header</strong>
</template> </template>
<n8n-text color="text-light" size="medium" class="mt-2xs mb-2xs"> <n8n-text color="text-light" size="medium" class="mt-2xs mb-2xs">
This is the card body. This is the card body.
</n8n-text> </n8n-text>
<template slot="footer"> <template #footer>
<n8n-text size="medium"> <n8n-text size="medium">
Card footer Card footer
</n8n-text> </n8n-text>
</template> </template>
<template slot="append"> <template #append>
<n8n-button>Click me</n8n-button> <n8n-button>Click me</n8n-button>
</template> </template>
</n8n-card>`, </n8n-card>`,

View file

@ -1,5 +1,5 @@
<template> <template>
<div :class="classes" v-on="$listeners"> <div :class="classes" v-bind="$attrs">
<div :class="$style.icon" v-if="$slots.prepend"> <div :class="$style.icon" v-if="$slots.prepend">
<slot name="prepend" /> <slot name="prepend" />
</div> </div>
@ -14,7 +14,7 @@
<slot name="footer" /> <slot name="footer" />
</div> </div>
</div> </div>
<div :class="$style.actions" v-if="$slots.append"> <div v-if="$slots.append">
<slot name="append" /> <slot name="append" />
</div> </div>
</div> </div>

View file

@ -2,24 +2,24 @@
exports[`components > N8nCard > should render correctly 1`] = ` exports[`components > N8nCard > should render correctly 1`] = `
"<div class=\\"card\\"> "<div class=\\"card\\">
<!----> <!--v-if-->
<div class=\\"content\\"> <div class=\\"content\\">
<!----> <!--v-if-->
<div class=\\"body\\">This is a card.</div> <div class=\\"body\\">This is a card.</div>
<!----> <!--v-if-->
</div> </div>
<!----> <!--v-if-->
</div>" </div>"
`; `;
exports[`components > N8nCard > should render correctly with header and footer 1`] = ` exports[`components > N8nCard > should render correctly with header and footer 1`] = `
"<div class=\\"card\\"> "<div class=\\"card\\">
<!----> <!--v-if-->
<div class=\\"content\\"> <div class=\\"content\\">
<div class=\\"header\\">Header</div> <div class=\\"header\\">Header</div>
<div class=\\"body\\">This is a card.</div> <div class=\\"body\\">This is a card.</div>
<div class=\\"footer\\">Footer</div> <div class=\\"footer\\">Footer</div>
</div> </div>
<!----> <!--v-if-->
</div>" </div>"
`; `;

View file

@ -1,5 +1,5 @@
import N8nCheckbox from './Checkbox.vue'; import N8nCheckbox from './Checkbox.vue';
import type { StoryFn } from '@storybook/vue'; import type { StoryFn } from '@storybook/vue3';
import { action } from '@storybook/addon-actions'; import { action } from '@storybook/addon-actions';
export default { export default {
@ -8,12 +8,13 @@ export default {
}; };
const methods = { const methods = {
onInput: action('input'), onUpdateModelValue: action('update:modelValue'),
onFocus: action('focus'), onFocus: action('focus'),
onChange: action('change'), onChange: action('change'),
}; };
const DefaultTemplate: StoryFn = (args, { argTypes }) => ({ const DefaultTemplate: StoryFn = (args, { argTypes }) => ({
setup: () => ({ args }),
props: Object.keys(argTypes), props: Object.keys(argTypes),
components: { components: {
N8nCheckbox, N8nCheckbox,
@ -21,7 +22,8 @@ const DefaultTemplate: StoryFn = (args, { argTypes }) => ({
data: () => ({ data: () => ({
isChecked: false, isChecked: false,
}), }),
template: '<n8n-checkbox v-model="isChecked" v-bind="$props" @input="onInput"></n8n-checkbox>', template:
'<n8n-checkbox v-model="isChecked" v-bind="args" @update:modelValue="onUpdateModelValue"></n8n-checkbox>',
methods, methods,
}); });

View file

@ -5,8 +5,8 @@
:class="['n8n-checkbox', $style.n8nCheckbox]" :class="['n8n-checkbox', $style.n8nCheckbox]"
:disabled="disabled" :disabled="disabled"
:indeterminate="indeterminate" :indeterminate="indeterminate"
:value="value" :modelValue="modelValue"
@change="onChange" @update:modelValue="onUpdateModelValue"
> >
<slot></slot> <slot></slot>
<n8n-input-label <n8n-input-label
@ -22,7 +22,7 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { Checkbox as ElCheckbox } from 'element-ui'; import { ElCheckbox } from 'element-plus';
import N8nInputLabel from '../N8nInputLabel'; import N8nInputLabel from '../N8nInputLabel';
export default defineComponent({ export default defineComponent({
@ -46,7 +46,7 @@ export default defineComponent({
type: Boolean, type: Boolean,
default: false, default: false,
}, },
value: { modelValue: {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
@ -57,8 +57,8 @@ export default defineComponent({
}, },
}, },
methods: { methods: {
onChange(event: Event) { onUpdateModelValue(value: boolean) {
this.$emit('input', event); this.$emit('update:modelValue', value);
}, },
onLabelClick() { onLabelClick() {
const checkboxComponent = this.$refs.checkbox as ElCheckbox; const checkboxComponent = this.$refs.checkbox as ElCheckbox;

View file

@ -9,22 +9,25 @@ exports[`components > N8nCheckbox > should render with both child and label 1`]
<span <span
class="el-checkbox__input" class="el-checkbox__input"
> >
<span
class="el-checkbox__inner"
/>
<input <input
aria-hidden="false" aria-hidden="false"
class="el-checkbox__original" class="el-checkbox__original"
type="checkbox" type="checkbox"
value="Checkbox" value="Checkbox"
/> />
<span
class="el-checkbox__inner"
/>
</span> </span>
<span <span
class="el-checkbox__label" class="el-checkbox__label"
> >
<strong> <strong>
Bold text Bold text
</strong> </strong>
<div <div
class="container" class="container"
data-test-id="input-label" data-test-id="input-label"
@ -38,16 +41,21 @@ exports[`components > N8nCheckbox > should render with both child and label 1`]
<span <span
class="n8n-text size-medium regular" class="n8n-text size-medium regular"
> >
Checkbox
<!----> Checkbox
<!--v-if-->
</span> </span>
</div> </div>
<!----> <!--v-if-->
<!----> <!--v-if-->
<!----> <!--v-if-->
</label> </label>
</div> </div>
<!---->
<!--v-if-->
</span> </span>
</label> </label>
</div> </div>
@ -62,24 +70,27 @@ exports[`components > N8nCheckbox > should render with child 1`] = `
<span <span
class="el-checkbox__input" class="el-checkbox__input"
> >
<span
class="el-checkbox__inner"
/>
<input <input
aria-hidden="false" aria-hidden="false"
class="el-checkbox__original" class="el-checkbox__original"
type="checkbox" type="checkbox"
value="" />
<span
class="el-checkbox__inner"
/> />
</span> </span>
<span <span
class="el-checkbox__label" class="el-checkbox__label"
> >
<strong> <strong>
Bold text Bold text
</strong> </strong>
<!---->
<!----> <!--v-if-->
<!--v-if-->
</span> </span>
</label> </label>
</div> </div>
@ -94,19 +105,22 @@ exports[`components > N8nCheckbox > should render with label 1`] = `
<span <span
class="el-checkbox__input" class="el-checkbox__input"
> >
<span
class="el-checkbox__inner"
/>
<input <input
aria-hidden="false" aria-hidden="false"
class="el-checkbox__original" class="el-checkbox__original"
type="checkbox" type="checkbox"
value="Checkbox" value="Checkbox"
/> />
<span
class="el-checkbox__inner"
/>
</span> </span>
<span <span
class="el-checkbox__label" class="el-checkbox__label"
> >
<div <div
class="container" class="container"
data-test-id="input-label" data-test-id="input-label"
@ -120,16 +134,21 @@ exports[`components > N8nCheckbox > should render with label 1`] = `
<span <span
class="n8n-text size-medium regular" class="n8n-text size-medium regular"
> >
Checkbox
<!----> Checkbox
<!--v-if-->
</span> </span>
</div> </div>
<!----> <!--v-if-->
<!----> <!--v-if-->
<!----> <!--v-if-->
</label> </label>
</div> </div>
<!---->
<!--v-if-->
</span> </span>
</label> </label>
</div> </div>
@ -144,17 +163,22 @@ exports[`components > N8nCheckbox > should render without label and child conten
<span <span
class="el-checkbox__input" class="el-checkbox__input"
> >
<span
class="el-checkbox__inner"
/>
<input <input
aria-hidden="false" aria-hidden="false"
class="el-checkbox__original" class="el-checkbox__original"
type="checkbox" type="checkbox"
value="" />
<span
class="el-checkbox__inner"
/> />
</span> </span>
<!----> <span
class="el-checkbox__label"
>
<!--v-if-->
</span>
</label> </label>
</div> </div>
`; `;

View file

@ -1,5 +1,5 @@
import { action } from '@storybook/addon-actions'; import { action } from '@storybook/addon-actions';
import type { StoryFn } from '@storybook/vue'; import type { StoryFn } from '@storybook/vue3';
import N8nColorPicker from './ColorPicker.vue'; import N8nColorPicker from './ColorPicker.vue';
export default { export default {
@ -32,10 +32,11 @@ export default {
const methods = { const methods = {
onChange: action('change'), onChange: action('change'),
onActiveChange: action('active-change'), onActiveChange: action('active-change'),
onInput: action('input'), onInput: action('update:modelValue'),
}; };
const DefaultTemplate: StoryFn = (args, { argTypes }) => ({ const DefaultTemplate: StoryFn = (args, { argTypes }) => ({
setup: () => ({ args }),
props: Object.keys(argTypes), props: Object.keys(argTypes),
components: { components: {
N8nColorPicker, N8nColorPicker,
@ -44,7 +45,7 @@ const DefaultTemplate: StoryFn = (args, { argTypes }) => ({
color: null, color: null,
}), }),
template: template:
'<n8n-color-picker v-model="color" v-bind="$props" @input="onInput" @change="onChange" @active-change="onActiveChange" />', '<n8n-color-picker v-model="color" v-bind="args" @update:modelValue="onInput" @change="onChange" @active-change="onActiveChange" />',
methods, methods,
}); });

View file

@ -1,6 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import { ColorPicker } from 'element-ui'; import { uid } from '../../utils';
import { ElColorPicker } from 'element-plus';
import N8nInput from '../N8nInput'; import N8nInput from '../N8nInput';
export type Props = { export type Props = {
@ -10,8 +11,9 @@ export type Props = {
colorFormat?: 'hex' | 'rgb' | 'hsl' | 'hsv'; colorFormat?: 'hex' | 'rgb' | 'hsl' | 'hsv';
popperClass?: string; popperClass?: string;
predefine?: string[]; predefine?: string[];
value?: string; modelValue?: string;
showInput?: boolean; showInput?: boolean;
name?: string;
}; };
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
@ -21,10 +23,11 @@ const props = withDefaults(defineProps<Props>(), {
colorFormat: 'hex', colorFormat: 'hex',
popperClass: '', popperClass: '',
showInput: true, showInput: true,
value: null, modelValue: null,
name: uid('color-picker'),
}); });
const color = ref(props.value); const color = ref(props.modelValue);
const colorPickerProps = computed(() => { const colorPickerProps = computed(() => {
const { value, showInput, ...rest } = props; const { value, showInput, ...rest } = props;
@ -32,7 +35,7 @@ const colorPickerProps = computed(() => {
}); });
const emit = defineEmits<{ const emit = defineEmits<{
(event: 'input', value: string): void; (event: 'update:modelValue', value: string): void;
(event: 'change', value: string): void; (event: 'change', value: string): void;
(event: 'active-change', value: string): void; (event: 'active-change', value: string): void;
}>(); }>();
@ -43,7 +46,7 @@ const model = computed({
}, },
set(value: string) { set(value: string) {
color.value = value; color.value = value;
emit('input', value); emit('update:modelValue', value);
}, },
}); });
@ -61,7 +64,7 @@ const onActiveChange = (value: string) => {
</script> </script>
<template> <template>
<span :class="['n8n-color-picker', $style.component]"> <span :class="['n8n-color-picker', $style.component]">
<color-picker <el-color-picker
v-model="model" v-model="model"
v-bind="colorPickerProps" v-bind="colorPickerProps"
@change="onChange" @change="onChange"
@ -72,8 +75,9 @@ const onActiveChange = (value: string) => {
:class="$style.input" :class="$style.input"
:disabled="props.disabled" :disabled="props.disabled"
:size="props.size" :size="props.size"
:value="color" :modelValue="color"
@input="onInput" :name="name"
@update:modelValue="onInput"
type="text" type="text"
/> />
</span> </span>

View file

@ -4,12 +4,21 @@ import N8nColorPicker from '../ColorPicker.vue';
describe('components', () => { describe('components', () => {
describe('N8nColorPicker', () => { describe('N8nColorPicker', () => {
it('should render with input', () => { it('should render with input', () => {
const { container } = render(N8nColorPicker); const { container } = render(N8nColorPicker, {
props: {
name: 'color-picker',
},
});
expect(container).toMatchSnapshot(); expect(container).toMatchSnapshot();
}); });
it('should render without input', () => { it('should render without input', () => {
const { container } = render(N8nColorPicker, { props: { showInput: false } }); const { container } = render(N8nColorPicker, {
props: {
name: 'color-picker',
showInput: false,
},
});
expect(container).toMatchSnapshot(); expect(container).toMatchSnapshot();
}); });
}); });

View file

@ -6,11 +6,15 @@ exports[`components > N8nColorPicker > should render with input 1`] = `
class="n8n-color-picker component" class="n8n-color-picker component"
data-v-dab78bb8="" data-v-dab78bb8=""
> >
<div <div
class="el-color-picker el-color-picker--medium" aria-description="current color is . press enter to select a new color."
data-v-dab78bb8="" aria-label="color picker"
class="el-color-picker el-color-picker--medium el-tooltip__trigger el-tooltip__trigger"
role="button"
tabindex="0"
> >
<!----> <!--v-if-->
<div <div
class="el-color-picker__trigger" class="el-color-picker__trigger"
> >
@ -20,122 +24,75 @@ exports[`components > N8nColorPicker > should render with input 1`] = `
<span <span
class="el-color-picker__color-inner" class="el-color-picker__color-inner"
style="background-color: transparent;" style="background-color: transparent;"
/>
<span
class="el-color-picker__empty el-icon-close"
/>
</span>
<span
class="el-color-picker__icon el-icon-arrow-down"
style="display: none;"
/>
</div>
<transition-stub
class="el-color-picker__panel"
name="el-zoom-in-top"
>
<div
class="el-color-dropdown"
style="display: none;"
>
<div
class="el-color-dropdown__main-wrapper"
> >
<div <i
class="el-color-hue-slider is-vertical" class="el-icon el-color-picker__icon is-icon-arrow-down"
style="float: right;" style="display: none;"
> >
<div
class="el-color-hue-slider__bar" <svg
/> viewBox="0 0 1024 1024"
<div xmlns="http://www.w3.org/2000/svg"
class="el-color-hue-slider__thumb"
style="left: 0px; top: 0px;"
/>
</div>
<div
class="el-color-svpanel"
style="background-color: rgb(255, 0, 0);"
>
<div
class="el-color-svpanel__white"
/>
<div
class="el-color-svpanel__black"
/>
<div
class="el-color-svpanel__cursor"
style="top: 0px; left: 0px;"
> >
<div /> <path
</div> d="M831.872 340.864 512 652.672 192.128 340.864a30.592 30.592 0 0 0-42.752 0 29.12 29.12 0 0 0 0 41.6L489.664 714.24a32 32 0 0 0 44.672 0l340.288-331.712a29.12 29.12 0 0 0 0-41.728 30.592 30.592 0 0 0-42.752 0z"
</div> fill="currentColor"
</div>
<!---->
<!---->
<div
class="el-color-dropdown__btns"
>
<span
class="el-color-dropdown__value"
>
<div
class="el-input el-input--mini"
>
<!---->
<input
autocomplete="off"
class="el-input__inner"
type="text"
/> />
<!----> </svg>
<!---->
<!----> </i>
<!----> <i
</div> class="el-icon el-color-picker__empty is-icon-close"
</span>
<button
class="el-button el-color-dropdown__link-btn el-button--text el-button--mini"
type="button"
> >
<!---->
<!----> <svg
<span> viewBox="0 0 1024 1024"
xmlns="http://www.w3.org/2000/svg"
清空 >
<path
</span> d="M764.288 214.592 512 466.88 259.712 214.592a31.936 31.936 0 0 0-45.12 45.12L466.752 512 214.528 764.224a31.936 31.936 0 1 0 45.12 45.184L512 557.184l252.288 252.288a31.936 31.936 0 0 0 45.12-45.12L557.12 512.064l252.288-252.352a31.936 31.936 0 1 0-45.12-45.184z"
</button> fill="currentColor"
<button />
class="el-button el-color-dropdown__btn el-button--default el-button--mini is-plain" </svg>
type="button"
> </i>
<!----> </span>
<!----> </span>
<span> </div>
确定
</span>
</button>
</div>
</div>
</transition-stub>
</div> </div>
<!--teleport start-->
<!--teleport end-->
<div <div
class="el-input el-input--medium n8n-input input" class="el-input el-input--medium n8n-input input input"
data-v-dab78bb8="" data-v-dab78bb8=""
> >
<!----> <!-- input -->
<input
autocomplete="off" <!-- prepend slot -->
class="el-input__inner" <!--v-if-->
type="text" <div
/> class="el-input__wrapper"
<!----> >
<!----> <!-- prefix slot -->
<!----> <!--v-if-->
<!----> <input
autocomplete="off"
class="el-input__inner"
maxlength="Infinity"
name="color-picker"
placeholder=""
rows="2"
tabindex="0"
title=""
type="text"
/>
<!-- suffix slot -->
<!--v-if-->
</div>
<!-- append slot -->
<!--v-if-->
</div> </div>
</span> </span>
</div> </div>
@ -147,11 +104,15 @@ exports[`components > N8nColorPicker > should render without input 1`] = `
class="n8n-color-picker component" class="n8n-color-picker component"
data-v-dab78bb8="" data-v-dab78bb8=""
> >
<div <div
class="el-color-picker el-color-picker--medium" aria-description="current color is . press enter to select a new color."
data-v-dab78bb8="" aria-label="color picker"
class="el-color-picker el-color-picker--medium el-tooltip__trigger el-tooltip__trigger"
role="button"
tabindex="0"
> >
<!----> <!--v-if-->
<div <div
class="el-color-picker__trigger" class="el-color-picker__trigger"
> >
@ -161,109 +122,46 @@ exports[`components > N8nColorPicker > should render without input 1`] = `
<span <span
class="el-color-picker__color-inner" class="el-color-picker__color-inner"
style="background-color: transparent;" style="background-color: transparent;"
/>
<span
class="el-color-picker__empty el-icon-close"
/>
</span>
<span
class="el-color-picker__icon el-icon-arrow-down"
style="display: none;"
/>
</div>
<transition-stub
class="el-color-picker__panel"
name="el-zoom-in-top"
>
<div
class="el-color-dropdown"
style="display: none;"
>
<div
class="el-color-dropdown__main-wrapper"
> >
<div <i
class="el-color-hue-slider is-vertical" class="el-icon el-color-picker__icon is-icon-arrow-down"
style="float: right;" style="display: none;"
> >
<div
class="el-color-hue-slider__bar" <svg
/> viewBox="0 0 1024 1024"
<div xmlns="http://www.w3.org/2000/svg"
class="el-color-hue-slider__thumb"
style="left: 0px; top: 0px;"
/>
</div>
<div
class="el-color-svpanel"
style="background-color: rgb(255, 0, 0);"
>
<div
class="el-color-svpanel__white"
/>
<div
class="el-color-svpanel__black"
/>
<div
class="el-color-svpanel__cursor"
style="top: 0px; left: 0px;"
> >
<div /> <path
</div> d="M831.872 340.864 512 652.672 192.128 340.864a30.592 30.592 0 0 0-42.752 0 29.12 29.12 0 0 0 0 41.6L489.664 714.24a32 32 0 0 0 44.672 0l340.288-331.712a29.12 29.12 0 0 0 0-41.728 30.592 30.592 0 0 0-42.752 0z"
</div> fill="currentColor"
</div>
<!---->
<!---->
<div
class="el-color-dropdown__btns"
>
<span
class="el-color-dropdown__value"
>
<div
class="el-input el-input--mini"
>
<!---->
<input
autocomplete="off"
class="el-input__inner"
type="text"
/> />
<!----> </svg>
<!---->
<!----> </i>
<!----> <i
</div> class="el-icon el-color-picker__empty is-icon-close"
</span>
<button
class="el-button el-color-dropdown__link-btn el-button--text el-button--mini"
type="button"
> >
<!---->
<!----> <svg
<span> viewBox="0 0 1024 1024"
xmlns="http://www.w3.org/2000/svg"
清空 >
<path
</span> d="M764.288 214.592 512 466.88 259.712 214.592a31.936 31.936 0 0 0-45.12 45.12L466.752 512 214.528 764.224a31.936 31.936 0 1 0 45.12 45.184L512 557.184l252.288 252.288a31.936 31.936 0 0 0 45.12-45.12L557.12 512.064l252.288-252.352a31.936 31.936 0 1 0-45.12-45.184z"
</button> fill="currentColor"
<button />
class="el-button el-color-dropdown__btn el-button--default el-button--mini is-plain" </svg>
type="button"
> </i>
<!----> </span>
<!----> </span>
<span> </div>
确定
</span>
</button>
</div>
</div>
</transition-stub>
</div> </div>
<!----> <!--teleport start-->
<!--teleport end-->
<!--v-if-->
</span> </span>
</div> </div>
`; `;

View file

@ -1,5 +1,5 @@
import N8nDatatable from './Datatable.vue'; import N8nDatatable from './Datatable.vue';
import type { StoryFn } from '@storybook/vue'; import type { StoryFn } from '@storybook/vue3';
import { rows, columns } from './__tests__/data'; import { rows, columns } from './__tests__/data';
export default { export default {
@ -8,11 +8,12 @@ export default {
}; };
export const Default: StoryFn = (args, { argTypes }) => ({ export const Default: StoryFn = (args, { argTypes }) => ({
setup: () => ({ args }),
props: Object.keys(argTypes), props: Object.keys(argTypes),
components: { components: {
N8nDatatable, N8nDatatable,
}, },
template: '<n8n-datatable v-bind="$props"></n8n-datatable>', template: '<n8n-datatable v-bind="args"></n8n-datatable>',
}); });
Default.args = { Default.args = {

View file

@ -114,7 +114,7 @@ export default defineComponent({
</script> </script>
<template> <template>
<div :class="classes" v-on="$listeners"> <div :class="classes" v-bind="$attrs">
<table :class="$style.datatable"> <table :class="$style.datatable">
<thead :class="$style.datatableHeader"> <thead :class="$style.datatableHeader">
<tr> <tr>
@ -157,9 +157,9 @@ export default defineComponent({
<div :class="$style.pageSizeSelector"> <div :class="$style.pageSizeSelector">
<n8n-select <n8n-select
size="mini" size="mini"
:value="rowsPerPage" :modelValue="rowsPerPage"
@input="onRowsPerPageChange" @update:modelValue="onRowsPerPageChange"
popper-append-to-body teleported
> >
<template #prepend>{{ t('datatable.pageSize') }}</template> <template #prepend>{{ t('datatable.pageSize') }}</template>
<n8n-option <n8n-option

View file

@ -10,12 +10,14 @@ describe('components', () => {
it('should render correctly', () => { it('should render correctly', () => {
const wrapper = render(N8nDatatable, { const wrapper = render(N8nDatatable, {
propsData: { props: {
columns, columns,
rows, rows,
rowsPerPage, rowsPerPage,
}, },
stubs, global: {
stubs,
},
}); });
expect(wrapper.container.querySelectorAll('thead tr').length).toEqual(1); expect(wrapper.container.querySelectorAll('thead tr').length).toEqual(1);
@ -28,12 +30,14 @@ describe('components', () => {
it('should add column classes', () => { it('should add column classes', () => {
const wrapper = render(N8nDatatable, { const wrapper = render(N8nDatatable, {
propsData: { props: {
columns: columns.map((column) => ({ ...column, classes: ['example'] })), columns: columns.map((column) => ({ ...column, classes: ['example'] })),
rows, rows,
rowsPerPage, rowsPerPage,
}, },
stubs, global: {
stubs,
},
}); });
expect(wrapper.container.querySelectorAll('.example').length).toEqual( expect(wrapper.container.querySelectorAll('.example').length).toEqual(
@ -43,14 +47,16 @@ describe('components', () => {
it('should render row slot', () => { it('should render row slot', () => {
const wrapper = render(N8nDatatable, { const wrapper = render(N8nDatatable, {
propsData: { props: {
columns, columns,
rows, rows,
rowsPerPage, rowsPerPage,
}, },
stubs, global: {
scopedSlots: { stubs,
row: '<main><td v-for="column in props.columns" :key="column.id">Row slot</td></main>', // Wrapper is necessary for looping },
slots: {
row: '<template #row="props"><td v-for="column in props.columns" :key="column.id">Row slot</td></template>', // Wrapper is necessary for looping
}, },
}); });

View file

@ -5,95 +5,99 @@ exports[`components > N8nDatatable > should render correctly 1`] = `
<table class=\\"datatable\\"> <table class=\\"datatable\\">
<thead class=\\"datatableHeader\\"> <thead class=\\"datatableHeader\\">
<tr> <tr>
<th> ID </th> <th class=\\"\\">ID</th>
<th> Name </th> <th class=\\"\\">Name</th>
<th> Age </th> <th class=\\"\\">Age</th>
<th> Action </th> <th class=\\"\\">Action</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<td><span>1</span></td> <td class=\\"\\"><span>1</span></td>
<td><span>Richard Hendricks</span></td> <td class=\\"\\"><span>Richard Hendricks</span></td>
<td><span>29</span></td> <td class=\\"\\"><span>29</span></td>
<td><button aria-live=\\"polite\\" class=\\"button button primary medium\\" column=\\"[object Object]\\"> <td class=\\"\\"><button class=\\"button button primary medium\\" aria-live=\\"polite\\" column=\\"[object Object]\\">
<!----><span>Button 1</span></button></td> <!--v-if--><span>Button 1</span>
</button></td>
</tr> </tr>
<tr> <tr>
<td><span>2</span></td> <td class=\\"\\"><span>2</span></td>
<td><span>Bertram Gilfoyle</span></td> <td class=\\"\\"><span>Bertram Gilfoyle</span></td>
<td><span>44</span></td> <td class=\\"\\"><span>44</span></td>
<td><button aria-live=\\"polite\\" class=\\"button button primary medium\\" column=\\"[object Object]\\"> <td class=\\"\\"><button class=\\"button button primary medium\\" aria-live=\\"polite\\" column=\\"[object Object]\\">
<!----><span>Button 2</span></button></td> <!--v-if--><span>Button 2</span>
</button></td>
</tr> </tr>
<tr> <tr>
<td><span>3</span></td> <td class=\\"\\"><span>3</span></td>
<td><span>Dinesh Chugtai</span></td> <td class=\\"\\"><span>Dinesh Chugtai</span></td>
<td><span>31</span></td> <td class=\\"\\"><span>31</span></td>
<td><button aria-live=\\"polite\\" class=\\"button button primary medium\\" column=\\"[object Object]\\"> <td class=\\"\\"><button class=\\"button button primary medium\\" aria-live=\\"polite\\" column=\\"[object Object]\\">
<!----><span>Button 3</span></button></td> <!--v-if--><span>Button 3</span>
</button></td>
</tr> </tr>
<tr> <tr>
<td><span>4</span></td> <td class=\\"\\"><span>4</span></td>
<td><span>Jared Dunn </span></td> <td class=\\"\\"><span>Jared Dunn </span></td>
<td><span>38</span></td> <td class=\\"\\"><span>38</span></td>
<td><button aria-live=\\"polite\\" class=\\"button button primary medium\\" column=\\"[object Object]\\"> <td class=\\"\\"><button class=\\"button button primary medium\\" aria-live=\\"polite\\" column=\\"[object Object]\\">
<!----><span>Button 4</span></button></td> <!--v-if--><span>Button 4</span>
</button></td>
</tr> </tr>
<tr> <tr>
<td><span>5</span></td> <td class=\\"\\"><span>5</span></td>
<td><span>Richard Hendricks</span></td> <td class=\\"\\"><span>Richard Hendricks</span></td>
<td><span>29</span></td> <td class=\\"\\"><span>29</span></td>
<td><button aria-live=\\"polite\\" class=\\"button button primary medium\\" column=\\"[object Object]\\"> <td class=\\"\\"><button class=\\"button button primary medium\\" aria-live=\\"polite\\" column=\\"[object Object]\\">
<!----><span>Button 5</span></button></td> <!--v-if--><span>Button 5</span>
</button></td>
</tr> </tr>
<tr> <tr>
<td><span>6</span></td> <td class=\\"\\"><span>6</span></td>
<td><span>Bertram Gilfoyle</span></td> <td class=\\"\\"><span>Bertram Gilfoyle</span></td>
<td><span>44</span></td> <td class=\\"\\"><span>44</span></td>
<td><button aria-live=\\"polite\\" class=\\"button button primary medium\\" column=\\"[object Object]\\"> <td class=\\"\\"><button class=\\"button button primary medium\\" aria-live=\\"polite\\" column=\\"[object Object]\\">
<!----><span>Button 6</span></button></td> <!--v-if--><span>Button 6</span>
</button></td>
</tr> </tr>
<tr> <tr>
<td><span>7</span></td> <td class=\\"\\"><span>7</span></td>
<td><span>Dinesh Chugtai</span></td> <td class=\\"\\"><span>Dinesh Chugtai</span></td>
<td><span>31</span></td> <td class=\\"\\"><span>31</span></td>
<td><button aria-live=\\"polite\\" class=\\"button button primary medium\\" column=\\"[object Object]\\"> <td class=\\"\\"><button class=\\"button button primary medium\\" aria-live=\\"polite\\" column=\\"[object Object]\\">
<!----><span>Button 7</span></button></td> <!--v-if--><span>Button 7</span>
</button></td>
</tr> </tr>
<tr> <tr>
<td><span>8</span></td> <td class=\\"\\"><span>8</span></td>
<td><span>Jared Dunn </span></td> <td class=\\"\\"><span>Jared Dunn </span></td>
<td><span>38</span></td> <td class=\\"\\"><span>38</span></td>
<td><button aria-live=\\"polite\\" class=\\"button button primary medium\\" column=\\"[object Object]\\"> <td class=\\"\\"><button class=\\"button button primary medium\\" aria-live=\\"polite\\" column=\\"[object Object]\\">
<!----><span>Button 8</span></button></td> <!--v-if--><span>Button 8</span>
</button></td>
</tr> </tr>
<tr> <tr>
<td><span>9</span></td> <td class=\\"\\"><span>9</span></td>
<td><span>Richard Hendricks</span></td> <td class=\\"\\"><span>Richard Hendricks</span></td>
<td><span>29</span></td> <td class=\\"\\"><span>29</span></td>
<td><button aria-live=\\"polite\\" class=\\"button button primary medium\\" column=\\"[object Object]\\"> <td class=\\"\\"><button class=\\"button button primary medium\\" aria-live=\\"polite\\" column=\\"[object Object]\\">
<!----><span>Button 9</span></button></td> <!--v-if--><span>Button 9</span>
</button></td>
</tr> </tr>
<tr> <tr>
<td><span>10</span></td> <td class=\\"\\"><span>10</span></td>
<td><span>Bertram Gilfoyle</span></td> <td class=\\"\\"><span>Bertram Gilfoyle</span></td>
<td><span>44</span></td> <td class=\\"\\"><span>44</span></td>
<td><button aria-live=\\"polite\\" class=\\"button button primary medium\\" column=\\"[object Object]\\"> <td class=\\"\\"><button class=\\"button button primary medium\\" aria-live=\\"polite\\" column=\\"[object Object]\\">
<!----><span>Button 10</span></button></td> <!--v-if--><span>Button 10</span>
</button></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<div class=\\"pagination\\"> <div class=\\"pagination\\">
<n8n-pagination-stub pagesize=\\"10\\" total=\\"15\\" pagercount=\\"5\\" currentpage=\\"1\\" layout=\\"prev, pager, next\\" pagesizes=\\"10,20,30,40,50,100\\" background=\\"true\\"></n8n-pagination-stub> <n8n-pagination-stub pagesize=\\"10\\" total=\\"15\\" currentpage=\\"1\\" pagercount=\\"5\\" layout=\\"prev, pager, next\\" pagesizes=\\"10,20,30,40,50,100\\" popperclass=\\"\\" prevtext=\\"\\" previcon=\\"[object Object]\\" nexttext=\\"\\" nexticon=\\"[object Object]\\" small=\\"false\\" background=\\"true\\" disabled=\\"false\\" hideonsinglepage=\\"false\\"></n8n-pagination-stub>
<div class=\\"pageSizeSelector\\"> <div class=\\"pageSizeSelector\\">
<n8n-select-stub value=\\"10\\" size=\\"mini\\" popperappendtobody=\\"true\\"> <n8n-select-stub modelvalue=\\"10\\" autocomplete=\\"off\\" automaticdropdown=\\"false\\" size=\\"mini\\" effect=\\"light\\" disabled=\\"false\\" clearable=\\"false\\" filterable=\\"false\\" allowcreate=\\"false\\" loading=\\"false\\" popperoptions=\\"[object Object]\\" remote=\\"false\\" multiple=\\"false\\" multiplelimit=\\"0\\" defaultfirstoption=\\"false\\" reservekeyword=\\"true\\" valuekey=\\"value\\" collapsetags=\\"false\\" collapsetagstooltip=\\"false\\" maxcollapsetags=\\"1\\" teleported=\\"true\\" persistent=\\"true\\" clearicon=\\"[object Object]\\" fitinputwidth=\\"false\\" suffixicon=\\"[object Object]\\" tagtype=\\"info\\" validateevent=\\"true\\" remoteshowsuffix=\\"false\\" suffixtransition=\\"true\\" placement=\\"bottom-start\\" popperappendtobody=\\"false\\" limitpopperwidth=\\"false\\"></n8n-select-stub>
<n8n-option-stub value=\\"10\\" label=\\"10\\"></n8n-option-stub>
<n8n-option-stub value=\\"25\\" label=\\"25\\"></n8n-option-stub>
<n8n-option-stub value=\\"50\\" label=\\"50\\"></n8n-option-stub>
<n8n-option-stub value=\\"100\\" label=\\"100\\"></n8n-option-stub>
<n8n-option-stub value=\\"*\\" label=\\"All\\"></n8n-option-stub>
</n8n-select-stub>
</div> </div>
</div> </div>
</div>" </div>"

View file

@ -1,6 +1,6 @@
import N8nFormBox from './FormBox.vue'; import N8nFormBox from './FormBox.vue';
import { action } from '@storybook/addon-actions'; import { action } from '@storybook/addon-actions';
import type { StoryFn } from '@storybook/vue'; import type { StoryFn } from '@storybook/vue3';
export default { export default {
title: 'Modules/FormBox', title: 'Modules/FormBox',
@ -13,15 +13,16 @@ export default {
const methods = { const methods = {
onSubmit: action('submit'), onSubmit: action('submit'),
onInput: action('input'), onChange: action('update'),
}; };
const Template: StoryFn = (args, { argTypes }) => ({ const Template: StoryFn = (args, { argTypes }) => ({
setup: () => ({ args }),
props: Object.keys(argTypes), props: Object.keys(argTypes),
components: { components: {
N8nFormBox, N8nFormBox,
}, },
template: '<n8n-form-box v-bind="$props" @submit="onSubmit" @input="onInput" />', template: '<n8n-form-box v-bind="args" @submit="onSubmit" @update="onUpdate" />',
methods, methods,
}); });

View file

@ -10,7 +10,7 @@
:inputs="inputs" :inputs="inputs"
:eventBus="formBus" :eventBus="formBus"
:columnView="true" :columnView="true"
@input="onInput" @update="onUpdateModelValue"
@submit="onSubmit" @submit="onSubmit"
/> />
</div> </div>
@ -87,8 +87,8 @@ export default defineComponent({
}; };
}, },
methods: { methods: {
onInput(e: { name: string; value: string }) { onUpdateModelValue(e: { name: string; value: string }) {
this.$emit('input', e); this.$emit('update', e);
}, },
onSubmit(e: { [key: string]: string }) { onSubmit(e: { [key: string]: string }) {
this.$emit('submit', e); this.$emit('submit', e);

View file

@ -1,6 +1,6 @@
import N8nFormInput from './FormInput.vue'; import N8nFormInput from './FormInput.vue';
import { action } from '@storybook/addon-actions'; import { action } from '@storybook/addon-actions';
import type { StoryFn } from '@storybook/vue'; import type { StoryFn } from '@storybook/vue3';
export default { export default {
title: 'Modules/FormInput', title: 'Modules/FormInput',
@ -9,18 +9,19 @@ export default {
}; };
const methods = { const methods = {
onInput: action('input'), onUpdateModelValue: action('update:modelValue'),
onFocus: action('focus'), onFocus: action('focus'),
onChange: action('change'), onChange: action('change'),
}; };
const Template: StoryFn = (args, { argTypes }) => ({ const Template: StoryFn = (args, { argTypes }) => ({
setup: () => ({ args }),
props: Object.keys(argTypes), props: Object.keys(argTypes),
components: { components: {
N8nFormInput, N8nFormInput,
}, },
template: ` template: `
<n8n-form-input v-bind="$props" v-model="val" @input="onInput" @change="onChange" @focus="onFocus" /> <n8n-form-input v-bind="args" v-model="val" @update:modelValue="onUpdateModelValue" @change="onChange" @focus="onFocus" />
`, `,
methods, methods,
data() { data() {

View file

@ -2,7 +2,7 @@
<n8n-checkbox <n8n-checkbox
v-if="type === 'checkbox'" v-if="type === 'checkbox'"
v-bind="$props" v-bind="$props"
@input="onInput" @update:modelValue="onUpdateModelValue"
@focus="onFocus" @focus="onFocus"
ref="inputRef" ref="inputRef"
/> />
@ -17,10 +17,10 @@
{{ tooltipText }} {{ tooltipText }}
</template> </template>
<el-switch <el-switch
:value="value" :modelValue="modelValue"
@change="onInput"
:active-color="activeColor" :active-color="activeColor"
:inactive-color="inactiveColor" :inactive-color="inactiveColor"
@update:modelValue="onUpdateModelValue"
></el-switch> ></el-switch>
</n8n-input-label> </n8n-input-label>
<n8n-input-label <n8n-input-label
@ -34,14 +34,15 @@
<slot v-if="hasDefaultSlot" /> <slot v-if="hasDefaultSlot" />
<n8n-select <n8n-select
v-else-if="type === 'select' || type === 'multi-select'" v-else-if="type === 'select' || type === 'multi-select'"
:value="value" :modelValue="modelValue"
:placeholder="placeholder" :placeholder="placeholder"
:multiple="type === 'multi-select'" :multiple="type === 'multi-select'"
:disabled="disabled" :disabled="disabled"
@change="onInput" @update:modelValue="onUpdateModelValue"
@focus="onFocus" @focus="onFocus"
@blur="onBlur" @blur="onBlur"
:name="name" :name="name"
:teleported="teleported"
ref="inputRef" ref="inputRef"
> >
<n8n-option <n8n-option
@ -56,11 +57,11 @@
:name="name" :name="name"
:type="type" :type="type"
:placeholder="placeholder" :placeholder="placeholder"
:value="value" :modelValue="modelValue"
:maxlength="maxlength" :maxlength="maxlength"
:autocomplete="autocomplete" :autocomplete="autocomplete"
:disabled="disabled" :disabled="disabled"
@input="onInput" @update:modelValue="onUpdateModelValue"
@blur="onBlur" @blur="onBlur"
@focus="onFocus" @focus="onFocus"
ref="inputRef" ref="inputRef"
@ -92,7 +93,7 @@ import N8nSelect from '../N8nSelect';
import N8nOption from '../N8nOption'; import N8nOption from '../N8nOption';
import N8nInputLabel from '../N8nInputLabel'; import N8nInputLabel from '../N8nInputLabel';
import N8nCheckbox from '../N8nCheckbox'; import N8nCheckbox from '../N8nCheckbox';
import { Switch as ElSwitch } from 'element-ui'; import { ElSwitch } from 'element-plus';
import { getValidationError, VALIDATORS } from './validators'; import { getValidationError, VALIDATORS } from './validators';
import type { Rule, RuleGroup, IValidator, Validatable, FormState } from '../../types'; import type { Rule, RuleGroup, IValidator, Validatable, FormState } from '../../types';
@ -100,7 +101,7 @@ import type { Rule, RuleGroup, IValidator, Validatable, FormState } from '../../
import { t } from '../../locale'; import { t } from '../../locale';
export interface Props { export interface Props {
value: Validatable; modelValue: Validatable;
label: string; label: string;
infoText?: string; infoText?: string;
required?: boolean; required?: boolean;
@ -125,6 +126,7 @@ export interface Props {
activeColor?: string; activeColor?: string;
inactiveLabel?: string; inactiveLabel?: string;
inactiveColor?: string; inactiveColor?: string;
teleported?: boolean;
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
@ -133,11 +135,12 @@ const props = withDefaults(defineProps<Props>(), {
type: 'text', type: 'text',
showRequiredAsterisk: true, showRequiredAsterisk: true,
validateOnBlur: true, validateOnBlur: true,
teleported: true,
}); });
const emit = defineEmits<{ const emit = defineEmits<{
(event: 'validate', shouldValidate: boolean): void; (event: 'validate', shouldValidate: boolean): void;
(event: 'input', value: unknown): void; (event: 'update:modelValue', value: unknown): void;
(event: 'focus'): void; (event: 'focus'): void;
(event: 'blur'): void; (event: 'blur'): void;
(event: 'enter'): void; (event: 'enter'): void;
@ -160,7 +163,11 @@ function getInputValidationError(): ReturnType<IValidator['validate']> {
} as { [key: string]: IValidator | RuleGroup }; } as { [key: string]: IValidator | RuleGroup };
if (props.required) { if (props.required) {
const error = getValidationError(props.value, validators, validators.REQUIRED as IValidator); const error = getValidationError(
props.modelValue,
validators,
validators.REQUIRED as IValidator,
);
if (error) return error; if (error) return error;
} }
@ -169,7 +176,7 @@ function getInputValidationError(): ReturnType<IValidator['validate']> {
const rule = rules[i] as Rule; const rule = rules[i] as Rule;
if (validators[rule.name]) { if (validators[rule.name]) {
const error = getValidationError( const error = getValidationError(
props.value, props.modelValue,
validators, validators,
validators[rule.name] as IValidator, validators[rule.name] as IValidator,
rule.config, rule.config,
@ -180,7 +187,7 @@ function getInputValidationError(): ReturnType<IValidator['validate']> {
if (rules[i].hasOwnProperty('rules')) { if (rules[i].hasOwnProperty('rules')) {
const rule = rules[i] as RuleGroup; const rule = rules[i] as RuleGroup;
const error = getValidationError(props.value, validators, rule); const error = getValidationError(props.modelValue, validators, rule);
if (error) return error; if (error) return error;
} }
} }
@ -194,9 +201,9 @@ function onBlur() {
emit('blur'); emit('blur');
} }
function onInput(value: FormState) { function onUpdateModelValue(value: FormState) {
state.isTyping = true; state.isTyping = true;
emit('input', value); emit('update:modelValue', value);
} }
function onFocus() { function onFocus() {

View file

@ -1,6 +1,6 @@
import N8nFormInputs from './FormInputs.vue'; import N8nFormInputs from './FormInputs.vue';
import { action } from '@storybook/addon-actions'; import { action } from '@storybook/addon-actions';
import type { StoryFn } from '@storybook/vue'; import type { StoryFn } from '@storybook/vue3';
export default { export default {
title: 'Modules/FormInputs', title: 'Modules/FormInputs',
@ -12,16 +12,17 @@ export default {
}; };
const methods = { const methods = {
onInput: action('input'), onChange: action('change'),
onSubmit: action('submit'), onSubmit: action('submit'),
}; };
const Template: StoryFn = (args, { argTypes }) => ({ const Template: StoryFn = (args, { argTypes }) => ({
setup: () => ({ args }),
props: Object.keys(argTypes), props: Object.keys(argTypes),
components: { components: {
N8nFormInputs, N8nFormInputs,
}, },
template: '<n8n-form-inputs v-bind="$props" @submit="onSubmit" @input="onInput" />', template: '<n8n-form-inputs v-bind="args" @submit="onSubmit" @change="onChange" />',
methods, methods,
}); });

View file

@ -22,12 +22,12 @@
v-bind="input.properties" v-bind="input.properties"
:name="input.name" :name="input.name"
:label="input.properties.label || ''" :label="input.properties.label || ''"
:value="values[input.name]" :modelValue="values[input.name]"
:data-test-id="input.name" :data-test-id="input.name"
:showValidationWarnings="showValidationWarnings" :showValidationWarnings="showValidationWarnings"
@input="(value) => onInput(input.name, value)" :teleported="teleported"
@update:modelValue="(value) => onUpdateModelValue(input.name, value)"
@validate="(value) => onValidate(input.name, value)" @validate="(value) => onValidate(input.name, value)"
@change="(value) => onInput(input.name, value)"
@enter="onSubmit" @enter="onSubmit"
/> />
</div> </div>
@ -69,6 +69,10 @@ export default defineComponent({
default: '', default: '',
validator: (value: string): boolean => ['', 'xs', 's', 'm', 'm', 'l', 'xl'].includes(value), validator: (value: string): boolean => ['', 'xs', 's', 'm', 'm', 'l', 'xl'].includes(value),
}, },
teleported: {
type: Boolean,
default: true,
},
}, },
data() { data() {
return { return {
@ -80,7 +84,10 @@ export default defineComponent({
mounted() { mounted() {
this.inputs.forEach((input) => { this.inputs.forEach((input) => {
if (input.hasOwnProperty('initialValue')) { if (input.hasOwnProperty('initialValue')) {
this.$set(this.values, input.name, input.initialValue); this.values = {
...this.values,
[input.name]: input.initialValue,
};
} }
}); });
@ -105,15 +112,18 @@ export default defineComponent({
}, },
}, },
methods: { methods: {
onInput(name: string, value: unknown) { onUpdateModelValue(name: string, value: unknown) {
this.values = { this.values = {
...this.values, ...this.values,
[name]: value, [name]: value,
}; };
this.$emit('input', { name, value }); this.$emit('update', { name, value });
}, },
onValidate(name: string, valid: boolean) { onValidate(name: string, valid: boolean) {
this.$set(this.validity, name, valid); this.validity = {
...this.validity,
[name]: valid,
};
}, },
onSubmit() { onSubmit() {
this.showValidationWarnings = true; this.showValidationWarnings = true;

View file

@ -1,4 +1,4 @@
import type { StoryFn } from '@storybook/vue'; import type { StoryFn } from '@storybook/vue3';
import N8nHeading from './Heading.vue'; import N8nHeading from './Heading.vue';
export default { export default {
@ -8,24 +8,25 @@ export default {
size: { size: {
control: { control: {
type: 'select', type: 'select',
options: ['2xlarge', 'xlarge', 'large', 'medium', 'small'],
}, },
options: ['2xlarge', 'xlarge', 'large', 'medium', 'small'],
}, },
color: { color: {
control: { control: {
type: 'select', type: 'select',
options: ['primary', 'text-dark', 'text-base', 'text-light', 'text-xlight'],
}, },
options: ['primary', 'text-dark', 'text-base', 'text-light', 'text-xlight'],
}, },
}, },
}; };
const Template: StoryFn = (args, { argTypes }) => ({ const Template: StoryFn = (args, { argTypes }) => ({
setup: () => ({ args }),
props: Object.keys(argTypes), props: Object.keys(argTypes),
components: { components: {
N8nHeading, N8nHeading,
}, },
template: '<n8n-heading v-bind="$props">hello world</n8n-heading>', template: '<n8n-heading v-bind="args">hello world</n8n-heading>',
}); });
export const Heading = Template.bind({}); export const Heading = Template.bind({});

View file

@ -1,5 +1,5 @@
<template> <template>
<component :is="tag" :class="['n8n-heading', ...classes]" v-on="$listeners"> <component :is="tag" :class="['n8n-heading', ...classes]" v-bind="$attrs">
<slot></slot> <slot></slot>
</component> </component>
</template> </template>

View file

@ -1,4 +1,4 @@
import type { StoryFn } from '@storybook/vue'; import type { StoryFn } from '@storybook/vue3';
import N8nIcon from './Icon.vue'; import N8nIcon from './Icon.vue';
export default { export default {
@ -11,8 +11,8 @@ export default {
size: { size: {
control: { control: {
type: 'select', type: 'select',
options: ['xsmall', 'small', 'medium', 'large'],
}, },
options: ['xsmall', 'small', 'medium', 'large'],
}, },
spin: { spin: {
control: { control: {
@ -23,11 +23,12 @@ export default {
}; };
const Template: StoryFn = (args, { argTypes }) => ({ const Template: StoryFn = (args, { argTypes }) => ({
setup: () => ({ args }),
props: Object.keys(argTypes), props: Object.keys(argTypes),
components: { components: {
N8nIcon, N8nIcon,
}, },
template: '<n8n-icon v-bind="$props" />', template: '<n8n-icon v-bind="args" />',
}); });
export const Clock = Template.bind({}); export const Clock = Template.bind({});

View file

@ -1,5 +1,5 @@
<template> <template>
<n8n-text :size="size" :color="color" :compact="true" class="n8n-icon" v-on="$listeners"> <n8n-text :size="size" :color="color" :compact="true" class="n8n-icon" v-bind="$attrs">
<font-awesome-icon :icon="icon" :spin="spin" :class="$style[size]" /> <font-awesome-icon :icon="icon" :spin="spin" :class="$style[size]" />
</n8n-text> </n8n-text>
</template> </template>

View file

@ -1,6 +1,6 @@
import N8nIconButton from './IconButton.vue'; import N8nIconButton from './IconButton.vue';
import { action } from '@storybook/addon-actions'; import { action } from '@storybook/addon-actions';
import type { StoryFn } from '@storybook/vue'; import type { StoryFn } from '@storybook/vue3';
export default { export default {
title: 'Atoms/Icon Button', title: 'Atoms/Icon Button',
@ -13,8 +13,8 @@ export default {
size: { size: {
control: { control: {
type: 'select', type: 'select',
options: ['mini', 'small', 'medium', 'large', 'xlarge'],
}, },
options: ['mini', 'small', 'medium', 'large', 'xlarge'],
}, },
}, },
parameters: { parameters: {
@ -27,11 +27,12 @@ const methods = {
}; };
const Template: StoryFn = (args, { argTypes }) => ({ const Template: StoryFn = (args, { argTypes }) => ({
setup: () => ({ args }),
props: Object.keys(argTypes), props: Object.keys(argTypes),
components: { components: {
N8nIconButton, N8nIconButton,
}, },
template: '<n8n-icon-button v-bind="$props" @click="onClick" />', template: '<n8n-icon-button @click="onClick" v-bind="args" />',
methods, methods,
}); });
@ -42,12 +43,13 @@ Button.args = {
}; };
const ManyTemplate: StoryFn = (args, { argTypes }) => ({ const ManyTemplate: StoryFn = (args, { argTypes }) => ({
setup: () => ({ args }),
props: Object.keys(argTypes), props: Object.keys(argTypes),
components: { components: {
N8nIconButton, N8nIconButton,
}, },
template: template:
'<div> <n8n-icon-button v-bind="$props" size="xlarge" @click="onClick" /> <n8n-icon-button v-bind="$props" size="large" @click="onClick" /> <n8n-icon-button v-bind="$props" size="medium" @click="onClick" /> <n8n-icon-button v-bind="$props" size="small" @click="onClick" /> <n8n-icon-button v-bind="$props" :loading="true" @click="onClick" /> <n8n-icon-button v-bind="$props" :disabled="true" @click="onClick" /></div>', '<div> <n8n-icon-button v-bind="args" size="xlarge" @click="onClick" /> <n8n-icon-button v-bind="args" size="large" @click="onClick" /> <n8n-icon-button v-bind="args" size="medium" @click="onClick" /> <n8n-icon-button v-bind="args" size="small" @click="onClick" /> <n8n-icon-button v-bind="args" :loading="true" @click="onClick" /> <n8n-icon-button v-bind="args" :disabled="true" @click="onClick" /></div>',
methods, methods,
}); });

View file

@ -1,5 +1,5 @@
<template> <template>
<n8n-button square v-bind="$props" v-on="$listeners" /> <n8n-button square v-bind="{ ...$attrs, ...$props }" />
</template> </template>
<script lang="ts"> <script lang="ts">

Some files were not shown because too many files have changed in this diff Show more