1
1
mirror of https://github.com/n8n-io/n8n.git synced 2024-08-16 16:40:30 +03:00

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 { getVisibleModalOverlay } from '../utils/modal';
import { getVisibleDropdown } from '../utils';
const settingsLogStreamingPage = new SettingsLogStreamingPage();
@ -19,6 +21,7 @@ describe('Log Streaming Settings', () => {
});
it('should show the add destination modal', () => {
cy.enableFeature('logStreaming');
cy.visit('/settings/log-streaming');
settingsLogStreamingPage.actions.clickAddFirstDestination();
cy.wait(100);
@ -27,7 +30,7 @@ describe('Log Streaming Settings', () => {
settingsLogStreamingPage.getters.getSelectDestinationButton().should('be.visible');
settingsLogStreamingPage.getters.getSelectDestinationButton().should('have.attr', 'disabled');
settingsLogStreamingPage.getters
.getDestinationModalDialog()
.getDestinationModal()
.invoke('css', 'width')
.then((widthStr) => parseInt((widthStr as unknown as string).replace('px', '')))
.should('be.lessThan', 500);
@ -36,65 +39,67 @@ describe('Log Streaming Settings', () => {
settingsLogStreamingPage.getters
.getSelectDestinationButton()
.should('not.have.attr', 'disabled');
settingsLogStreamingPage.getters.getDestinationModal().click(1, 1);
getVisibleModalOverlay().click(1, 1);
settingsLogStreamingPage.getters.getDestinationModal().should('not.exist');
});
it('should create a destination and delete it', () => {
cy.enableFeature('logStreaming');
cy.visit('/settings/log-streaming');
cy.wait(1000); // Race condition with getDestinationDataFromBackend()
settingsLogStreamingPage.actions.clickAddFirstDestination();
cy.wait(100);
settingsLogStreamingPage.getters.getDestinationModal().should('be.visible');
settingsLogStreamingPage.getters.getSelectDestinationType().click();
settingsLogStreamingPage.getters.getSelectDestinationTypeItems().eq(0).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();
cy.wait(100);
settingsLogStreamingPage.getters.getDestinationModal().click(1, 1);
getVisibleModalOverlay().click(1, 1);
cy.reload();
settingsLogStreamingPage.getters.getDestinationCards().eq(0).click();
settingsLogStreamingPage.getters.getDestinationDeleteButton().should('be.visible').click();
cy.get('.el-message-box').should('be.visible').find('.btn--cancel').click();
settingsLogStreamingPage.getters.getDestinationDeleteButton().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', () => {
cy.enableFeature('logStreaming');
cy.visit('/settings/log-streaming');
cy.wait(1000); // Race condition with getDestinationDataFromBackend()
settingsLogStreamingPage.actions.clickAddFirstDestination();
cy.wait(100);
settingsLogStreamingPage.getters.getDestinationModal().should('be.visible');
settingsLogStreamingPage.getters.getSelectDestinationType().click();
settingsLogStreamingPage.getters.getSelectDestinationTypeItems().eq(0).click();
settingsLogStreamingPage.getters.getSelectDestinationButton().click();
settingsLogStreamingPage.getters.getDestinationNameInput().click()
settingsLogStreamingPage.getters.getDestinationNameInput().find('input').clear().type('Destination 1');
settingsLogStreamingPage.getters.getDestinationNameInput().click();
settingsLogStreamingPage.getters
.getDestinationNameInput()
.find('input')
.clear()
.type('Destination 1');
settingsLogStreamingPage.getters.getDestinationSaveButton().should('not.have.attr', 'disabled');
settingsLogStreamingPage.getters.getDestinationSaveButton().click();
cy.wait(100);
settingsLogStreamingPage.getters.getDestinationModal().click(1, 1);
getVisibleModalOverlay().click(1, 1);
cy.reload();
settingsLogStreamingPage.getters
.getDestinationCards()
.eq(0)
.find('.el-dropdown-selfdefine')
.click();
cy.get('.el-dropdown-menu').find('.el-dropdown-menu__item').eq(0).click();
settingsLogStreamingPage.getters.getDestinationCards().eq(0).find('.el-dropdown').click();
getVisibleDropdown().find('.el-dropdown-menu__item').eq(0).click();
settingsLogStreamingPage.getters.getDestinationSaveButton().should('not.exist');
settingsLogStreamingPage.getters.getDestinationModal().click(1, 1);
getVisibleModalOverlay().click(1, 1);
settingsLogStreamingPage.getters
.getDestinationCards()
.eq(0)
.find('.el-dropdown-selfdefine')
.click();
cy.get('.el-dropdown-menu').find('.el-dropdown-menu__item').eq(1).click();
settingsLogStreamingPage.getters.getDestinationCards().eq(0).find('.el-dropdown').click();
getVisibleDropdown().find('.el-dropdown-menu__item').eq(1).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);
cy.drag('[data-test-id="canvas-node"].jtk-drag-selected', [50, 150]);
WorkflowPage.getters
.canvasNodes()
.last()
.canvasNodeByName('Code')
.should('have.attr', 'style', 'left: 740px; top: 320px;');
WorkflowPage.actions.hitUndo();
WorkflowPage.getters
.canvasNodes()
.last()
.canvasNodeByName('Code')
.should('have.attr', 'style', 'left: 640px; top: 220px;');
WorkflowPage.actions.hitRedo();
WorkflowPage.getters
.canvasNodes()
.last()
.canvasNodeByName('Code')
.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(CODE_NODE_NAME);
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.actions.hitUndo();
WorkflowPage.getters.nodeConnections().should('have.length', 1);
@ -256,6 +256,9 @@ describe('Undo/Redo', () => {
});
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(CODE_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(SET_NODE_NAME);
@ -266,8 +269,10 @@ describe('Undo/Redo', () => {
WorkflowPage.getters.canvasNodes().last().click();
WorkflowPage.actions.hitDisableNodeShortcut();
// Move first one
WorkflowPage.getters.canvasNodes().first().should('have.attr', 'style', initialPosition);
WorkflowPage.getters.canvasNodes().first().click();
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
WorkflowPage.getters.canvasNodeByName(SET_NODE_NAME).click().click();
cy.get('body').type('{backspace}');
@ -278,10 +283,7 @@ describe('Undo/Redo', () => {
WorkflowPage.getters.nodeConnections().should('have.length', 3);
// Second undo: Should move first node to it's original position
WorkflowPage.actions.hitUndo();
WorkflowPage.getters
.canvasNodes()
.first()
.should('have.attr', 'style', 'left: 420px; top: 220px;');
WorkflowPage.getters.canvasNodes().first().should('have.attr', 'style', initialPosition);
// Third undo: Should enable last node
WorkflowPage.actions.hitUndo();
WorkflowPage.getters.disabledNodes().should('have.length', 0);
@ -291,10 +293,7 @@ describe('Undo/Redo', () => {
WorkflowPage.getters.disabledNodes().should('have.length', 1);
// Second redo: Should move the first node
WorkflowPage.actions.hitRedo();
WorkflowPage.getters
.canvasNodes()
.first()
.should('have.attr', 'style', 'left: 540px; top: 360px;');
WorkflowPage.getters.canvasNodes().first().should('have.attr', 'style', movedPosition);
// Third redo: Should delete the Set node
WorkflowPage.actions.hitRedo();
WorkflowPage.getters.canvasNodes().should('have.length', 3);

View File

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

View File

@ -107,7 +107,7 @@ describe('Canvas Node Manipulation and Navigation', () => {
WorkflowPage.actions.zoomToFit();
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);
});

View File

@ -7,12 +7,10 @@ describe('Data transformation expressions', () => {
beforeEach(() => {
wf.actions.visit();
cy.window().then(
(win) => {
// @ts-ignore
win.preventNodeViewBeforeUnload = true;
},
);
cy.window().then((win) => {
// @ts-ignore
win.preventNodeViewBeforeUnload = true;
});
});
it('$json + native string methods', () => {
@ -85,7 +83,7 @@ describe('Data transformation expressions', () => {
ndv.getters.inlineExpressionEditorInput().clear().type(input);
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);
});
@ -100,7 +98,7 @@ describe('Data transformation expressions', () => {
ndv.getters.inlineExpressionEditorInput().clear().type(input);
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);
});
});
@ -111,7 +109,7 @@ describe('Data transformation expressions', () => {
const addSet = () => {
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('span').contains('String').click();
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,
} from './../constants';
import { WorkflowPage, NDV } from '../pages';
import { getVisibleSelect } from '../utils';
const workflowPage = new WorkflowPage();
const ndv = new NDV();
@ -28,11 +29,7 @@ describe('Data mapping', () => {
ndv.getters.inputDataContainer().get('table', { timeout: 10000 }).should('exist');
ndv.getters.nodeParameters().find('input[placeholder*="Add Value"]').click();
ndv.getters
.nodeParameters()
.find('.el-select-dropdown__list li:nth-child(3)')
.should('have.text', 'String')
.click();
getVisibleSelect().find('li:nth-child(3)').should('have.text', 'String').click();
ndv.getters
.parameterInput('name')
.should('have.length', 1)
@ -128,7 +125,7 @@ describe('Data mapping', () => {
.find('.json-data')
.should(
'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')
.contains('"count"')
@ -178,6 +175,7 @@ describe('Data mapping', () => {
it('maps expressions from previous nodes', () => {
cy.createFixtureWorkflow('Test_workflow_3.json', `My test workflow`);
workflowPage.actions.zoomToFit();
workflowPage.actions.openNode('Set1');
ndv.actions.selectInputNode(SCHEDULE_TRIGGER_NODE_NAME);
@ -245,7 +243,8 @@ describe('Data mapping', () => {
workflowPage.actions.addNodeToCanvas('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();
@ -274,6 +273,8 @@ describe('Data mapping', () => {
ndv.actions.typeIntoParameterInput('value', 'fun');
ndv.actions.clearParameterInput('value'); // keep focus on param
ndv.actions.dismissMappingTooltip();
cy.wait(300);
ndv.getters.inputDataContainer().should('exist').find('span').contains('count').realMouseDown();

View File

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

View File

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

View File

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

View File

@ -30,7 +30,7 @@ describe('Workflow tags', () => {
}
cy.contains('Done').click();
wf.getters.createTagButton().click();
wf.getters.tagsDropdown().click();
wf.getters.tagsInDropdown().should('have.length', 5);
wf.getters.tagPills().should('have.length', 0); // none attached
});
@ -45,7 +45,7 @@ describe('Workflow tags', () => {
});
cy.contains('Done').click();
wf.getters.createTagButton().click();
wf.getters.tagsDropdown().click();
wf.getters.tagsInDropdown().should('have.length', 0); // none stored
wf.getters.tagPills().should('have.length', 0); // none attached
});
@ -57,7 +57,8 @@ describe('Workflow tags', () => {
cy.contains('Create a tag').click();
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.getByTestId('tags-table')
.find('.el-input--large')
@ -65,7 +66,7 @@ describe('Workflow tags', () => {
.type(' Updated')
.type('{enter}');
cy.contains('Done').click();
wf.getters.createTagButton().click();
wf.getters.tagsDropdown().click();
wf.getters.tagsInDropdown().should('have.length', 1); // one stored
wf.getters.tagsInDropdown().contains('Updated').should('exist');
wf.getters.tagPills().should('have.length', 0); // none attached
@ -76,7 +77,7 @@ describe('Workflow tags', () => {
wf.actions.addTags(TEST_TAGS);
wf.getters.nthTagPill(1).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);
});
@ -84,8 +85,8 @@ describe('Workflow tags', () => {
wf.getters.createTagButton().click();
wf.actions.addTags(TEST_TAGS);
wf.getters.nthTagPill(1).click();
wf.getters.tagsDropdown().find('li.selected').first().click();
cy.get('body').type('{enter}');
wf.getters.tagsInDropdown().filter('.selected').first().click();
cy.get('body').click(0, 0);
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', () => {
usersSettingsPage.actions.loginAndVisit(INSTANCE_OWNER.email, INSTANCE_OWNER.password, true);
usersSettingsPage.actions.opedDeleteDialog(INSTANCE_MEMBERS[0].email);
usersSettingsPage.getters.deleteDataRadioButton().realClick();
usersSettingsPage.getters.deleteDataRadioButton().click();
usersSettingsPage.getters.deleteDataInput().type('delete all data');
usersSettingsPage.getters.deleteUserButton().realClick();
usersSettingsPage.getters.deleteUserButton().click();
workflowPage.getters.successToast().should('contain', 'User deleted');
});
it('should delete user and transfer their data', () => {
usersSettingsPage.actions.loginAndVisit(INSTANCE_OWNER.email, INSTANCE_OWNER.password, true);
usersSettingsPage.actions.opedDeleteDialog(INSTANCE_MEMBERS[1].email);
usersSettingsPage.getters.transferDataRadioButton().realClick();
usersSettingsPage.getters.userSelectDropDown().realClick();
usersSettingsPage.getters.userSelectOptions().first().realClick();
usersSettingsPage.getters.deleteUserButton().realClick();
usersSettingsPage.getters.transferDataRadioButton().click();
usersSettingsPage.getters.userSelectDropDown().click();
usersSettingsPage.getters.userSelectOptions().first().click();
usersSettingsPage.getters.deleteUserButton().click();
workflowPage.getters.successToast().should('contain', 'User deleted');
});

View File

@ -4,8 +4,6 @@ import {
PIPEDRIVE_NODE_NAME,
HTTP_REQUEST_NODE_NAME,
NEW_QUERY_AUTH_ACCOUNT_NAME,
} from './../constants';
import {
GMAIL_NODE_NAME,
NEW_GOOGLE_ACCOUNT_NAME,
NEW_TRELLO_ACCOUNT_NAME,
@ -13,6 +11,7 @@ import {
TRELLO_NODE_NAME,
} from '../constants';
import { CredentialsPage, CredentialsModal, WorkflowPage, NDV } from '../pages';
import { getVisibleSelect } from '../utils';
const credentialsPage = new CredentialsPage();
const credentialsModal = new CredentialsModal();
@ -90,13 +89,16 @@ describe('Credentials', () => {
workflowPage.getters.canvasNodes().last().click();
cy.get('body').type('{enter}');
workflowPage.getters.nodeCredentialsSelect().click();
workflowPage.getters.nodeCredentialsSelect().find('li').last().click();
getVisibleSelect().find('li').last().click();
credentialsModal.getters.credentialsEditModal().should('be.visible');
credentialsModal.getters.credentialAuthTypeRadioButtons().should('have.length', 2);
credentialsModal.getters.credentialAuthTypeRadioButtons().first().click();
credentialsModal.actions.fillCredentialsForm();
cy.get('.el-message-box').find('button').contains('Close').click();
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', () => {
@ -107,7 +109,7 @@ describe('Credentials', () => {
cy.get('body').type('{enter}');
workflowPage.getters.nodeCredentialsSelect().click();
// Add oAuth credentials
workflowPage.getters.nodeCredentialsSelect().find('li').last().click();
getVisibleSelect().find('li').last().click();
credentialsModal.getters.credentialsEditModal().should('be.visible');
credentialsModal.getters.credentialAuthTypeRadioButtons().should('have.length', 2);
credentialsModal.getters.credentialAuthTypeRadioButtons().first().click();
@ -115,13 +117,14 @@ describe('Credentials', () => {
cy.get('.el-message-box').find('button').contains('Close').click();
workflowPage.getters.nodeCredentialsSelect().click();
// Add Service account credentials
workflowPage.getters.nodeCredentialsSelect().find('li').last().click();
getVisibleSelect().find('li').last().click();
credentialsModal.getters.credentialsEditModal().should('be.visible');
credentialsModal.getters.credentialAuthTypeRadioButtons().should('have.length', 2);
credentialsModal.getters.credentialAuthTypeRadioButtons().last().click();
credentialsModal.actions.fillCredentialsForm();
// Both (+ the 'Create new' option) should be in the dropdown
workflowPage.getters.nodeCredentialsSelect().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', () => {
@ -132,18 +135,18 @@ describe('Credentials', () => {
// Select incoming authentication
nodeDetailsView.getters.parameterInput('incomingAuthentication').should('exist');
nodeDetailsView.getters.parameterInput('incomingAuthentication').click();
nodeDetailsView.getters.parameterInput('incomingAuthentication').find('li').first().click();
getVisibleSelect().find('li').first().click();
// There should be two credential fields
workflowPage.getters.nodeCredentialsSelect().should('have.length', 2);
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
credentialsModal.getters.credentialAuthTypeRadioButtons().should('have.length', 2);
cy.get('body').type('{esc}');
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
credentialsModal.getters.credentialsAuthTypeSelector().should('not.exist');
});
@ -155,10 +158,13 @@ describe('Credentials', () => {
workflowPage.getters.canvasNodes().last().click();
cy.get('body').type('{enter}');
workflowPage.getters.nodeCredentialsSelect().click();
workflowPage.getters.nodeCredentialsSelect().find('li').last().click();
getVisibleSelect().find('li').last().click();
credentialsModal.getters.credentialsAuthTypeSelector().should('not.exist');
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', () => {
@ -168,16 +174,22 @@ describe('Credentials', () => {
workflowPage.getters.canvasNodes().last().click();
cy.get('body').type('{enter}');
workflowPage.getters.nodeCredentialsSelect().click();
workflowPage.getters.nodeCredentialsSelect().find('li').last().click();
getVisibleSelect().find('li').last().click();
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();
credentialsModal.getters.credentialsEditModal().should('be.visible');
credentialsModal.getters.deleteButton().click();
cy.get('.el-message-box').find('button').contains('Yes').click();
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', () => {
@ -187,17 +199,18 @@ describe('Credentials', () => {
workflowPage.getters.canvasNodes().last().click();
cy.get('body').type('{enter}');
workflowPage.getters.nodeCredentialsSelect().click();
workflowPage.getters.nodeCredentialsSelect().find('li').last().click();
getVisibleSelect().find('li').last().click();
credentialsModal.actions.fillCredentialsForm();
workflowPage.getters.nodeCredentialsSelect().should('contain', NEW_TRELLO_ACCOUNT_NAME);
workflowPage.getters.nodeCredentialsEditButton().click();
credentialsModal.getters.credentialsEditModal().should('be.visible');
credentialsModal.getters.name().click();
credentialsModal.actions.renameCredential(NEW_CREDENTIAL_NAME);
credentialsModal.getters.saveButton().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', () => {
@ -207,20 +220,20 @@ describe('Credentials', () => {
workflowPage.getters.canvasNodes().last().click();
cy.get('body').type('{enter}');
nodeDetailsView.getters.parameterInput('authentication').click();
nodeDetailsView.getters.parameterInput('authentication').find('li').should('have.length', 3);
nodeDetailsView.getters.parameterInput('authentication').find('li').last().click();
getVisibleSelect().find('li').should('have.length', 3);
getVisibleSelect().find('li').last().click();
nodeDetailsView.getters.parameterInput('genericAuthType').should('exist');
nodeDetailsView.getters.parameterInput('genericAuthType').click();
nodeDetailsView.getters
.parameterInput('genericAuthType')
.find('li')
.should('have.length.greaterThan', 0);
nodeDetailsView.getters.parameterInput('genericAuthType').find('li').last().click();
getVisibleSelect().find('li').should('have.length.greaterThan', 0);
getVisibleSelect().find('li').last().click();
workflowPage.getters.nodeCredentialsSelect().should('exist');
workflowPage.getters.nodeCredentialsSelect().click();
workflowPage.getters.nodeCredentialsSelect().find('li').last().click();
getVisibleSelect().find('li').last().click();
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 CustomNodeWithCustomCredentialFixture from '../fixtures/Custom_node_custom_credential.json';
import CustomCredential from '../fixtures/Custom_credential.json';
import { getVisibleSelect } from '../utils';
const credentialsModal = new CredentialsModal();
const nodeCreatorFeature = new NodeCreator();
@ -20,9 +21,13 @@ describe('Community Nodes', () => {
req.on('response', (res) => {
const nodes = res.body || [];
nodes.push(CustomNodeFixture, CustomNodeWithN8nCredentialFixture, CustomNodeWithCustomCredentialFixture);
nodes.push(
CustomNodeFixture,
CustomNodeWithN8nCredentialFixture,
CustomNodeWithCustomCredentialFixture,
);
});
})
});
cy.intercept('/types/credentials.json', { middleware: true }, (req) => {
req.headers['cache-control'] = 'no-cache, no-store';
@ -31,8 +36,8 @@ describe('Community Nodes', () => {
const credentials = res.body || [];
credentials.push(CustomCredential);
})
})
});
});
workflowPage.actions.visit();
});
@ -45,7 +50,7 @@ describe('Community Nodes', () => {
nodeCreatorFeature.getters
.getCreatorItem(customNode)
.findChildByTestId('node-creator-item-tooltip')
.find('.el-tooltip__trigger')
.should('exist');
nodeCreatorFeature.actions.selectNode(customNode);
@ -65,16 +70,9 @@ describe('Community Nodes', () => {
secondParameter().find('label').contains('Resource').should('exist');
secondParameter().find('input.el-input__inner').should('have.value', 'option2');
secondParameter().find('.el-select').click();
secondParameter().find('.el-select-dropdown__list').should('exist');
// Check if all options are rendered and select the fourth one
secondParameter().find('.el-select-dropdown__list').children().should('have.length', 4);
secondParameter()
.find('.el-select-dropdown__list')
.children()
.eq(3)
.contains('option4')
.should('exist')
.click();
getVisibleSelect().find('li').should('have.length', 4);
getVisibleSelect().find('li').eq(3).contains('option4').should('exist').click();
secondParameter().find('input.el-input__inner').should('have.value', 'option4');
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,9 +6,11 @@ import {
} from '../constants';
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
import { WorkflowsPage as WorkflowsPageClass } from '../pages/workflows';
import { getVisibleDropdown, getVisibleSelect } from '../utils';
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_TAG = 'Duplicate';
@ -67,11 +69,11 @@ describe('Workflow Actions', () => {
it('should not save workflow if canvas is loading', () => {
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
cy.intercept('PATCH', '/rest/workflows/*', () => {
interceptCalledCount++;
}).as('saveWorkflow');
cy.intercept('PATCH', '/rest/workflows/*', () => {
interceptCalledCount++;
}).as('saveWorkflow');
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
WorkflowPage.actions.saveWorkflowOnButtonClick();
@ -84,11 +86,11 @@ describe('Workflow Actions', () => {
(req) => {
// Delay the response to give time for the save to be triggered
req.on('response', async (res) => {
await new Promise((resolve) => setTimeout(resolve, 2000))
await new Promise((resolve) => setTimeout(resolve, 2000));
res.send();
})
}
)
});
},
);
cy.reload();
cy.get('.el-loading-mask').should('exist');
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.wait('@saveWorkflow');
cy.wrap(null).then(() => expect(interceptCalledCount).to.eq(1));
})
});
it('should copy nodes', () => {
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_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').find('input').type(IMPORT_WORKFLOW_URL);
cy.get('body').type('{enter}');
cy.waitForLoad(false)
cy.waitForLoad(false);
WorkflowPage.actions.zoomToFit();
WorkflowPage.getters.canvasNodes().should('have.length', 2);
WorkflowPage.getters.nodeConnections().should('have.length', 1);
@ -137,7 +139,7 @@ describe('Workflow Actions', () => {
WorkflowPage.getters
.workflowImportInput()
.selectFile('cypress/fixtures/Test_workflow-actions_paste-data.json', { force: true });
cy.waitForLoad(false)
cy.waitForLoad(false);
WorkflowPage.actions.zoomToFit();
WorkflowPage.getters.canvasNodes().should('have.length', 2);
WorkflowPage.getters.nodeConnections().should('have.length', 1);
@ -157,57 +159,33 @@ describe('Workflow Actions', () => {
WorkflowPage.getters.workflowMenuItemSettings().click();
// Change all settings
// totalWorkflows + 1 (current workflow) + 1 (no workflow option)
WorkflowPage.getters.workflowSettingsErrorWorkflowSelect().find('li').should('have.length', totalWorkflows + 2);
WorkflowPage.getters
.workflowSettingsErrorWorkflowSelect()
WorkflowPage.getters.workflowSettingsErrorWorkflowSelect().click();
getVisibleSelect()
.find('li')
.last()
.click({ force: true });
WorkflowPage.getters.workflowSettingsTimezoneSelect().find('li').should('exist');
WorkflowPage.getters.workflowSettingsTimezoneSelect().find('li').eq(1).click({ force: true });
WorkflowPage.getters
.workflowSettingsSaveFiledExecutionsSelect()
.find('li')
.should('have.length', 3);
WorkflowPage.getters
.workflowSettingsSaveFiledExecutionsSelect()
.find('li')
.last()
.click({ force: true });
WorkflowPage.getters
.workflowSettingsSaveSuccessExecutionsSelect()
.find('li')
.should('have.length', 3);
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 });
.should('have.length', totalWorkflows + 2);
getVisibleSelect().find('li').last().click({ force: true });
WorkflowPage.getters.workflowSettingsTimezoneSelect().click();
getVisibleSelect().find('li').should('exist');
getVisibleSelect().find('li').eq(1).click({ force: true });
WorkflowPage.getters.workflowSettingsSaveFiledExecutionsSelect().click();
getVisibleSelect().find('li').should('have.length', 3);
getVisibleSelect().find('li').last().click({ force: true });
WorkflowPage.getters.workflowSettingsSaveSuccessExecutionsSelect().click();
getVisibleSelect().find('li').should('have.length', 3);
getVisibleSelect().find('li').last().click({ force: true });
WorkflowPage.getters.workflowSettingsSaveManualExecutionsSelect().click();
getVisibleSelect().find('li').should('have.length', 3);
getVisibleSelect().find('li').last().click({ force: true });
WorkflowPage.getters.workflowSettingsSaveExecutionProgressSelect().click();
getVisibleSelect().find('li').should('have.length', 3);
getVisibleSelect().find('li').last().click({ force: true });
WorkflowPage.getters.workflowSettingsTimeoutWorkflowSwitch().click();
WorkflowPage.getters.workflowSettingsTimeoutForm().find('input').first().type('1');
// Save settings
WorkflowPage.getters.workflowSettingsSaveButton().click();
WorkflowPage.getters.workflowSettingsModal().should('not.exist');
WorkflowPage.getters.successToast().should('exist');
})
});
});
it('should not be able to delete unsaved workflow', () => {
@ -245,7 +223,7 @@ describe('Workflow Actions', () => {
.find('.el-select__tags input')
.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('{esc}');
WorkflowPage.getters
.duplicateWorkflowModal()
.find('button')

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,5 @@
import { BasePage } from './base';
import { getVisibleSelect } from '../utils';
export class SettingsLogStreamingPage extends BasePage {
url = '/settings/log-streaming';
@ -6,11 +7,9 @@ export class SettingsLogStreamingPage extends BasePage {
getActionBoxUnlicensed: () => cy.getByTestId('action-box-unlicensed'),
getActionBoxLicensed: () => cy.getByTestId('action-box-licensed'),
getDestinationModal: () => cy.getByTestId('destination-modal'),
getDestinationModalDialog: () => this.getters.getDestinationModal().find('.el-dialog'),
getSelectDestinationType: () => cy.getByTestId('select-destination-type'),
getDestinationNameInput: () => cy.getByTestId('subtitle-showing-type'),
getSelectDestinationTypeItems: () =>
this.getters.getSelectDestinationType().find('.el-select-dropdown__item'),
getSelectDestinationTypeItems: () => getVisibleSelect().find('.el-select-dropdown__item'),
getSelectDestinationButton: () => cy.getByTestId('select-destination-button'),
getContactUsButton: () => this.getters.getActionBoxUnlicensed().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(),
emailInputContainer: () => cy.getByTestId('email'),
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'),
};
actions = {
@ -34,7 +34,10 @@ export class PersonalSettingsPage extends BasePage {
},
tryToSetWeakPassword: (oldPassword: string, newPassword: string) => {
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) => {
this.getters.emailInput().type('{selectall}').type(newEmail).type('{enter}');

View File

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

View File

@ -1,5 +1,6 @@
import { META_KEY } from '../constants';
import { BasePage } from './base';
import { getVisibleSelect } from '../utils';
export class WorkflowPage extends BasePage {
url = '/workflow/new';
@ -16,7 +17,7 @@ export class WorkflowPage extends BasePage {
nthTagPill: (n: number) =>
cy.get(`[data-test-id="workflow-tags-container"] span.tags > span:nth-child(${n})`),
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'),
saveButton: () => cy.getByTestId('workflow-save-button'),
nodeCreatorSearchBar: () => cy.getByTestId('node-creator-search-bar'),
@ -37,8 +38,8 @@ export class WorkflowPage extends BasePage {
canvasNodePlusEndpointByName: (nodeName: string, index = 0) => {
return cy.get(this.getters.getEndpointSelector('plus', nodeName, index));
},
successToast: () => cy.get('.el-notification .el-icon-success').parent(),
errorToast: () => cy.get('.el-notification .el-icon-error'),
successToast: () => cy.get('.el-notification .el-notification--success').parent(),
errorToast: () => cy.get('.el-notification .el-notification--error'),
activatorSwitch: () => cy.getByTestId('workflow-activate-switch'),
workflowMenu: () => cy.getByTestId('workflow-menu'),
firstStepButton: () => cy.getByTestId('canvas-add-button'),
@ -84,7 +85,8 @@ export class WorkflowPage extends BasePage {
duplicateWorkflowModal: () => cy.getByTestId('duplicate-modal'),
nodeViewBackground: () => cy.getByTestId('node-view-background'),
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'),
zoomInButton: () => cy.getByTestId('zoom-in-button'),
zoomOutButton: () => cy.getByTestId('zoom-out-button'),
@ -92,8 +94,10 @@ export class WorkflowPage extends BasePage {
executeWorkflowButton: () => cy.getByTestId('execute-workflow-button'),
clearExecutionDataButton: () => cy.getByTestId('clear-execution-data-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'),
nodeCredentialsCreateOption: () => cy.getByTestId('node-credentials-select-item-new'),
nodeCredentialsEditButton: () => cy.getByTestId('credential-edit-button'),
nodeCreatorItems: () => cy.getByTestId('item-iterator-item'),
ndvParameters: () => cy.getByTestId('parameter-item'),
@ -134,17 +138,17 @@ export class WorkflowPage extends BasePage {
this.getters.nodeCreatorSearchBar().type(nodeDisplayName);
this.getters.nodeCreatorSearchBar().type('{enter}');
cy.wait(500)
cy.wait(500);
cy.get('body').then((body) => {
if(body.find('[data-test-id=node-creator]').length > 0) {
if(action) {
cy.contains(action).click()
if (body.find('[data-test-id=node-creator]').length > 0) {
if (action) {
cy.contains(action).click();
} else {
// 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}');
},
@ -157,7 +161,8 @@ export class WorkflowPage extends BasePage {
},
openTagManagerModal: () => {
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: () => {
cy.contains('Expression').invoke('show').click();
@ -209,7 +214,7 @@ export class WorkflowPage extends BasePage {
this.getters.workflowTagsInput().type(tag);
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
// so we need to wait for it to disappear
this.getters.workflowTagsContainer().should('not.contain', `+${tags.length}`);
@ -241,7 +246,12 @@ export class WorkflowPage extends BasePage {
executeWorkflow: () => {
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
.getConnectionActionsBetweenNodes(sourceNodeName, targetNodeName)
@ -268,18 +278,10 @@ export class WorkflowPage extends BasePage {
this.getters.addStickyButton().click();
},
deleteSticky: () => {
this.getters.stickies().eq(0)
.realHover()
.find('[data-test-id="delete-sticky"]')
.click();
this.getters.stickies().eq(0).realHover().find('[data-test-id="delete-sticky"]').click();
},
editSticky: (content: string) => {
this.getters.stickies()
.dblclick()
.find('textarea')
.clear()
.type(content)
.type('{esc}');
this.getters.stickies().dblclick().find('textarea').clear().type(content).type('{esc}');
},
};
}

View File

@ -5,7 +5,7 @@ export class WorkflowsPage extends BasePage {
getters = {
newWorkflowButtonCard: () => cy.getByTestId('new-workflow-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'),
workflowCards: () => cy.getByTestId('resources-list-item'),
workflowCard: (workflowName: string) =>

View File

@ -53,12 +53,12 @@ Cypress.Commands.add('signin', ({ email, password }) => {
});
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');
});
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) =>

View File

@ -6,6 +6,10 @@ before(() => {
owner: INSTANCE_OWNER,
members: INSTANCE_MEMBERS,
});
Cypress.on('uncaught:exception', (err) => {
return !err.message.includes('ResizeObserver');
});
});
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",
"@types/jest": "^29.5.0",
"@types/supertest": "^2.0.12",
"@vitejs/plugin-vue2": "^2.2.0",
"@vitest/coverage-c8": "^0.28.5",
"c8": "^7.12.0",
"cross-env": "^7.0.3",
@ -64,7 +63,6 @@
"typescript": "*",
"vite": "^4.0.4",
"vitest": "^0.28.5",
"vue-template-compiler": "^2.7.14",
"vue-tsc": "^1.0.24"
},
"pnpm": {
@ -92,7 +90,6 @@
"qqjs>globby": "^11.1.0"
},
"patchedDependencies": {
"element-ui@2.15.12": "patches/element-ui@2.15.12.patch",
"typedi@0.10.0": "patches/typedi@0.10.0.patch",
"@sentry/cli@2.17.0": "patches/@sentry__cli@2.17.0.patch",
"pkce-challenge@3.0.0": "patches/pkce-challenge@3.0.0.patch",

View File

@ -4,7 +4,7 @@
module.exports = {
plugins: ['vue'],
extends: ['plugin:vue/essential', '@vue/typescript', './base'],
extends: ['plugin:vue/vue3-essential', '@vue/typescript', './base'],
env: {
browser: true,
@ -37,6 +37,12 @@ module.exports = {
'vue/no-unused-components': 'error',
'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
'vue/no-mutating-props': '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 = {
framework: {
name: '@storybook/vue-webpack5',
options: {},
},
stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.{ts,js}'],
stories: ['../src/**/*.stories.@(js|jsx|ts|tsx|mdx)'],
addons: [
'@storybook/addon-styling',
'@storybook/addon-links',
'@storybook/addon-essentials',
{
name: '@storybook/addon-postcss',
options: {
postcssLoaderOptions: {
implementation: require('postcss'),
},
},
},
'storybook-addon-themes',
// Disabled until this is actually used rather otherwise its a blank tab
// '@storybook/addon-interactions',
'@storybook/addon-a11y',
'storybook-dark-mode',
],
webpackFinal: async (config) => {
config.module.rules.push({
test: /\.scss$/,
oneOf: [
{
resourceQuery: /module/,
use: [
'vue-style-loader',
{
loader: 'css-loader',
options: {
modules: {
localIdentName: '[path][name]__[local]--[hash:base64:5]',
},
},
},
'sass-loader',
],
include: path.resolve(__dirname, '../'),
},
{
use: ['vue-style-loader', 'css-loader', 'sass-loader'],
include: path.resolve(__dirname, '../'),
},
],
staticDirs: ['../public'],
framework: {
name: '@storybook/vue3-vite',
options: {},
},
disableTelemetry: true,
async viteFinal(config, { configType }) {
// return the customized config
return mergeConfig(config, {
// customize the Vite config here
resolve: {
alias: [
{
find: /^@n8n-design-system\//,
replacement: `${resolve(__dirname, '..')}/src/`,
},
{
find: /^n8n-design-system$/,
replacement: `${resolve(__dirname, '..')}/src/main.ts`,
},
],
},
});
config.resolve.alias = {
...config.resolve.alias,
'@/': path.resolve(__dirname, '../src/'),
};
return config;
},
docs: {
autodocs: true,
},
};

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

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

View File

@ -20,7 +20,7 @@
:label="buttonText"
:type="buttonType"
size="large"
@click="$emit('click', $event)"
@click="$emit('click:button', $event)"
/>
<n8n-callout
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. ',
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();
});

View File

@ -1,15 +1,15 @@
// Vitest Snapshot v1
exports[`N8NActionBox > should render correctly 1`] = `
"<div data-test-id=\\"action-box\\" class=\\"n8n-action-box container\\">
<div class=\\"emoji\\"> 😿 </div>
"<div class=\\"n8n-action-box container\\" data-test-id=\\"action-box\\">
<div class=\\"emoji\\">😿</div>
<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 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>
<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>"
`;

View File

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

View File

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

View File

@ -6,6 +6,7 @@ describe('components', () => {
it('should render default styling correctly', () => {
const wrapper = render(N8nActionDropdown, {
props: {
teleported: false,
items: [
{
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();
});
it('should render custom styling correctly', () => {
const wrapper = render(N8nActionDropdown, {
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();
});

View File

@ -2,20 +2,12 @@
exports[`components > N8nActionDropdown > should render custom styling correctly 1`] = `
"<div class=\\"action-dropdown-container actionDropdownContainer\\">
<el-dropdown-stub trigger=\\"click\\" size=\\"\\" hideonclick=\\"true\\" placement=\\"bottom\\" visiblearrow=\\"true\\" showtimeout=\\"250\\" hidetimeout=\\"150\\" tabindex=\\"0\\">
<div class=\\"activator\\">
<n8n-icon-stub icon=\\"ellipsis-v\\" size=\\"medium\\"></n8n-icon-stub>
</div>
</el-dropdown-stub>
<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>"
`;
exports[`components > N8nActionDropdown > should render default styling correctly 1`] = `
"<div class=\\"action-dropdown-container actionDropdownContainer\\">
<el-dropdown-stub trigger=\\"click\\" size=\\"\\" hideonclick=\\"true\\" placement=\\"bottom\\" visiblearrow=\\"true\\" showtimeout=\\"250\\" hidetimeout=\\"150\\" tabindex=\\"0\\">
<div class=\\"activator\\">
<n8n-icon-stub icon=\\"ellipsis-v\\" size=\\"medium\\"></n8n-icon-stub>
</div>
</el-dropdown-stub>
"<div class=\\"action-dropdown-container actionDropdownContainer\\" teleported=\\"false\\">
<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>"
`;

View File

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

View File

@ -1,10 +1,9 @@
<template>
<span :class="$style.container" data-test-id="action-toggle">
<span @click.stop.prevent :class="$style.container" data-test-id="action-toggle">
<el-dropdown
:placement="placement"
:size="size"
trigger="click"
@click.native.stop
@command="onCommand"
@visible-change="onVisibleChange"
>
@ -42,11 +41,7 @@
<script lang="ts">
import type { PropType } from 'vue';
import { defineComponent } from 'vue';
import {
Dropdown as ElDropdown,
DropdownMenu as ElDropdownMenu,
DropdownItem as ElDropdownItem,
} from 'element-ui';
import { ElDropdown, ElDropdownMenu, ElDropdownItem } from 'element-plus';
import N8nIcon from '../N8nIcon';
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 N8nIcon from '../N8nIcon';
@ -18,12 +18,13 @@ export default {
};
const Template: StoryFn = (args, { argTypes }) => ({
setup: () => ({ args }),
props: Object.keys(argTypes),
components: {
N8nAlert,
},
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({});
@ -38,15 +39,16 @@ ContentAsProps.args = {
};
const TemplateForSlots: StoryFn = (args, { argTypes }) => ({
setup: () => ({ args }),
props: Object.keys(argTypes),
components: {
N8nAlert,
N8nIcon,
},
template: `<div style="position: relative; width: 100%; height: 300px;">
<n8n-alert v-bind="$props">
<n8n-alert v-bind="args">
<template #title>Title</template>
<template>Description</template>
Description
<template #aside><button>Button</button></template>
<template #icon>
<n8n-icon icon="grin-stars" size="xlarge" />

View File

@ -14,21 +14,20 @@ describe('components', () => {
});
it('should render slots instead of props', () => {
const { container } = render(
N8nAlert,
{
props: { showIcon: false },
slots: {
title: 'Title',
default: 'Message',
aside: '<button>Click me</button>',
icon: '<n8n-icon icon="plus-circle" />',
const { container } = render(N8nAlert, {
props: { showIcon: false },
slots: {
title: 'Title',
default: 'Message',
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.getByText('Title')).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';
export default {
@ -13,14 +13,16 @@ export default {
};
const Template: StoryFn = (args, { argTypes }) => ({
setup: () => ({ args }),
props: Object.keys(argTypes),
components: {
N8nAvatar,
},
template: '<n8n-avatar v-bind="$props" />',
template: '<n8n-avatar v-bind="args" />',
});
export const Avatar = Template.bind({});
Avatar.args = {
name: 'Sunny Side',
firstName: 'Sunny',
lastName: 'Side',
};

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
// 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';
export default {
@ -7,12 +7,13 @@ export default {
};
const Template: StoryFn = (args, { argTypes }) => ({
setup: () => ({ args }),
props: Object.keys(argTypes),
components: {
N8nBlockUi,
},
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({});

View File

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

View File

@ -1,11 +1,11 @@
<template>
<button
:class="classes"
:disabled="disabled || loading"
:disabled="isDisabled"
:aria-disabled="ariaDisabled"
:aria-busy="ariaBusy"
aria-live="polite"
v-on="$listeners"
v-bind="$attrs"
>
<span :class="$style.icon" v-if="loading || icon">
<n8n-spinner v-if="loading" :size="size" />
@ -17,158 +17,112 @@
</button>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script setup lang="ts">
import N8nIcon from '../N8nIcon';
import N8nSpinner from '../N8nSpinner';
import { useCssModule, computed, useAttrs } from 'vue';
export default defineComponent({
name: 'n8n-button',
props: {
label: {
type: String,
},
type: {
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,
},
const $style = useCssModule();
const $attrs = useAttrs();
const props = defineProps({
label: {
type: String,
default: '',
},
components: {
N8nSpinner,
N8nIcon,
type: {
type: String,
default: 'primary',
},
computed: {
ariaBusy(): 'true' | undefined {
return this.loading ? 'true' : undefined;
},
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}` : ''}`
);
},
size: {
type: String,
default: 'medium',
},
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>
<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>
@import './Button';
@import '../../css/mixins/utils';
@import '../../css/common/var';
.button {
display: inline-block;
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);
}
@include n8n-button;
}
$loading-overlay-background-color: rgba(255, 255, 255, 0);
@ -178,19 +132,7 @@ $loading-overlay-background-color: rgba(255, 255, 255, 0);
*/
.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);
@include n8n-button-secondary;
}
.tertiary {

View File

@ -1,6 +1,5 @@
import { render } from '@testing-library/vue';
import N8nButton from '../Button.vue';
import ElButton from '../overrides/ElButton.vue';
const slots = {
default: 'Button',
@ -12,7 +11,9 @@ describe('components', () => {
it('should render correctly', () => {
const wrapper = render(N8nButton, {
slots,
stubs,
global: {
stubs,
},
});
expect(wrapper.html()).toMatchSnapshot();
});
@ -25,7 +26,9 @@ describe('components', () => {
loading: true,
},
slots,
stubs,
global: {
stubs,
},
});
expect(wrapper.html()).toMatchSnapshot();
});
@ -38,7 +41,9 @@ describe('components', () => {
icon: 'plus-circle',
},
slots,
stubs,
global: {
stubs,
},
});
expect(wrapper.html()).toMatchSnapshot();
});
@ -51,64 +56,13 @@ describe('components', () => {
square: true,
label: '48',
},
stubs,
global: {
stubs,
},
});
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
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 > 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 > 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 > props > square > should render square button 1`] = `
"<button aria-live=\\"polite\\" class=\\"button button primary medium square\\">
<!----><span>48</span></button>"
"<button class=\\"button button primary medium square\\" aria-live=\\"polite\\">
<!--v-if--><span>48</span>
</button>"
`;
exports[`components > N8nButton > should render correctly 1`] = `
"<button aria-live=\\"polite\\" class=\\"button button primary medium\\">
<!----><span>Button</span></button>"
"<button class=\\"button button primary medium\\" aria-live=\\"polite\\">
<!--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 N8nLink from '../N8nLink';
import N8nText from '../N8nText';
import type { StoryFn } from '@storybook/vue';
import type { StoryFn } from '@storybook/vue3';
export default {
title: 'Atoms/Callout',
@ -10,8 +10,8 @@ export default {
theme: {
control: {
type: 'select',
options: ['info', 'secondary', 'success', 'warning', 'danger', 'custom'],
},
options: ['info', 'secondary', 'success', 'warning', 'danger', 'custom'],
},
message: {
control: {
@ -41,6 +41,7 @@ interface Args {
}
const template: StoryFn<Args> = (args, { argTypes }) => ({
setup: () => ({ args }),
props: Object.keys(argTypes),
components: {
N8nLink,
@ -48,12 +49,12 @@ const template: StoryFn<Args> = (args, { argTypes }) => ({
N8nCallout,
},
template: `
<n8n-callout v-bind="$props">
<n8n-callout v-bind="args">
${args.default}
<template #actions v-if="actions">
<template #actions v-if="args.actions">
${args.actions}
</template>
<template #trailingContent v-if="trailingContent">
<template #trailingContent v-if="args.trailingContent">
${args.trailingContent}
</template>
</n8n-callout>

View File

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

View File

@ -1,93 +1,79 @@
// Vitest Snapshot v1
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=\\"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>
<n8n-text-stub size=\\"small\\" tag=\\"span\\">
<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>
<n8n-text-stub bold=\\"false\\" size=\\"small\\" compact=\\"false\\" tag=\\"span\\"></n8n-text-stub> &nbsp; <n8n-link-stub size=\\"small\\"></n8n-link-stub>
</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>"
`;
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=\\"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>
<n8n-text-stub size=\\"small\\" tag=\\"span\\">
<n8n-text-stub size=\\"small\\" tag=\\"span\\">This is a secondary callout.</n8n-text-stub>
</n8n-text-stub> &nbsp;
<n8n-text-stub bold=\\"false\\" size=\\"small\\" compact=\\"false\\" tag=\\"span\\"></n8n-text-stub> &nbsp;
</div>
</div>"
`;
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=\\"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>
<n8n-text-stub size=\\"small\\" tag=\\"span\\">
<n8n-text-stub size=\\"small\\" tag=\\"span\\">This is a danger callout.</n8n-text-stub>
</n8n-text-stub> &nbsp;
<n8n-text-stub bold=\\"false\\" size=\\"small\\" compact=\\"false\\" tag=\\"span\\"></n8n-text-stub> &nbsp;
</div>
</div>"
`;
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=\\"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>
<n8n-text-stub size=\\"small\\" tag=\\"span\\">
<n8n-text-stub size=\\"small\\" tag=\\"span\\">This is an info callout.</n8n-text-stub>
</n8n-text-stub> &nbsp;
<n8n-text-stub bold=\\"false\\" size=\\"small\\" compact=\\"false\\" tag=\\"span\\"></n8n-text-stub> &nbsp;
</div>
</div>"
`;
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=\\"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>
<n8n-text-stub size=\\"small\\" tag=\\"span\\">
<n8n-text-stub size=\\"small\\" tag=\\"span\\">This is a secondary callout.</n8n-text-stub>
</n8n-text-stub> &nbsp;
<n8n-text-stub bold=\\"false\\" size=\\"small\\" compact=\\"false\\" tag=\\"span\\"></n8n-text-stub> &nbsp;
</div>
</div>"
`;
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=\\"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>
<n8n-text-stub size=\\"small\\" tag=\\"span\\">
<n8n-text-stub size=\\"small\\" tag=\\"span\\">This is a success callout.</n8n-text-stub>
</n8n-text-stub> &nbsp;
<n8n-text-stub bold=\\"false\\" size=\\"small\\" compact=\\"false\\" tag=\\"span\\"></n8n-text-stub> &nbsp;
</div>
</div>"
`;
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=\\"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>
<n8n-text-stub size=\\"small\\" tag=\\"span\\">
<n8n-text-stub size=\\"small\\" tag=\\"span\\">This is a warning callout.</n8n-text-stub>
</n8n-text-stub> &nbsp;
<n8n-text-stub bold=\\"false\\" size=\\"small\\" compact=\\"false\\" tag=\\"span\\"></n8n-text-stub> &nbsp;
</div>
</div>"
`;

View File

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

View File

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

View File

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

View File

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

View File

@ -5,8 +5,8 @@
:class="['n8n-checkbox', $style.n8nCheckbox]"
:disabled="disabled"
:indeterminate="indeterminate"
:value="value"
@change="onChange"
:modelValue="modelValue"
@update:modelValue="onUpdateModelValue"
>
<slot></slot>
<n8n-input-label
@ -22,7 +22,7 @@
<script lang="ts">
import { defineComponent } from 'vue';
import { Checkbox as ElCheckbox } from 'element-ui';
import { ElCheckbox } from 'element-plus';
import N8nInputLabel from '../N8nInputLabel';
export default defineComponent({
@ -46,7 +46,7 @@ export default defineComponent({
type: Boolean,
default: false,
},
value: {
modelValue: {
type: Boolean,
default: false,
},
@ -57,8 +57,8 @@ export default defineComponent({
},
},
methods: {
onChange(event: Event) {
this.$emit('input', event);
onUpdateModelValue(value: boolean) {
this.$emit('update:modelValue', value);
},
onLabelClick() {
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
class="el-checkbox__input"
>
<span
class="el-checkbox__inner"
/>
<input
aria-hidden="false"
class="el-checkbox__original"
type="checkbox"
value="Checkbox"
/>
<span
class="el-checkbox__inner"
/>
</span>
<span
class="el-checkbox__label"
>
<strong>
Bold text
</strong>
<div
class="container"
data-test-id="input-label"
@ -38,16 +41,21 @@ exports[`components > N8nCheckbox > should render with both child and label 1`]
<span
class="n8n-text size-medium regular"
>
Checkbox
<!---->
Checkbox
<!--v-if-->
</span>
</div>
<!---->
<!---->
<!---->
<!--v-if-->
<!--v-if-->
<!--v-if-->
</label>
</div>
<!---->
<!--v-if-->
</span>
</label>
</div>
@ -62,24 +70,27 @@ exports[`components > N8nCheckbox > should render with child 1`] = `
<span
class="el-checkbox__input"
>
<span
class="el-checkbox__inner"
/>
<input
aria-hidden="false"
class="el-checkbox__original"
type="checkbox"
value=""
/>
<span
class="el-checkbox__inner"
/>
</span>
<span
class="el-checkbox__label"
>
<strong>
Bold text
</strong>
<!---->
<!---->
<!--v-if-->
<!--v-if-->
</span>
</label>
</div>
@ -94,19 +105,22 @@ exports[`components > N8nCheckbox > should render with label 1`] = `
<span
class="el-checkbox__input"
>
<span
class="el-checkbox__inner"
/>
<input
aria-hidden="false"
class="el-checkbox__original"
type="checkbox"
value="Checkbox"
/>
<span
class="el-checkbox__inner"
/>
</span>
<span
class="el-checkbox__label"
>
<div
class="container"
data-test-id="input-label"
@ -120,16 +134,21 @@ exports[`components > N8nCheckbox > should render with label 1`] = `
<span
class="n8n-text size-medium regular"
>
Checkbox
<!---->
Checkbox
<!--v-if-->
</span>
</div>
<!---->
<!---->
<!---->
<!--v-if-->
<!--v-if-->
<!--v-if-->
</label>
</div>
<!---->
<!--v-if-->
</span>
</label>
</div>
@ -144,17 +163,22 @@ exports[`components > N8nCheckbox > should render without label and child conten
<span
class="el-checkbox__input"
>
<span
class="el-checkbox__inner"
/>
<input
aria-hidden="false"
class="el-checkbox__original"
type="checkbox"
value=""
/>
<span
class="el-checkbox__inner"
/>
</span>
<!---->
<span
class="el-checkbox__label"
>
<!--v-if-->
</span>
</label>
</div>
`;

View File

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

View File

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

View File

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

View File

@ -6,11 +6,15 @@ exports[`components > N8nColorPicker > should render with input 1`] = `
class="n8n-color-picker component"
data-v-dab78bb8=""
>
<div
class="el-color-picker el-color-picker--medium"
data-v-dab78bb8=""
aria-description="current color is . press enter to select a new color."
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
class="el-color-picker__trigger"
>
@ -20,122 +24,75 @@ exports[`components > N8nColorPicker > should render with input 1`] = `
<span
class="el-color-picker__color-inner"
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
class="el-color-hue-slider is-vertical"
style="float: right;"
<i
class="el-icon el-color-picker__icon is-icon-arrow-down"
style="display: none;"
>
<div
class="el-color-hue-slider__bar"
/>
<div
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;"
<svg
viewBox="0 0 1024 1024"
xmlns="http://www.w3.org/2000/svg"
>
<div />
</div>
</div>
</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"
<path
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"
fill="currentColor"
/>
<!---->
<!---->
<!---->
<!---->
</div>
</span>
<button
class="el-button el-color-dropdown__link-btn el-button--text el-button--mini"
type="button"
</svg>
</i>
<i
class="el-icon el-color-picker__empty is-icon-close"
>
<!---->
<!---->
<span>
清空
</span>
</button>
<button
class="el-button el-color-dropdown__btn el-button--default el-button--mini is-plain"
type="button"
>
<!---->
<!---->
<span>
确定
</span>
</button>
</div>
</div>
</transition-stub>
<svg
viewBox="0 0 1024 1024"
xmlns="http://www.w3.org/2000/svg"
>
<path
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"
fill="currentColor"
/>
</svg>
</i>
</span>
</span>
</div>
</div>
<!--teleport start-->
<!--teleport end-->
<div
class="el-input el-input--medium n8n-input input"
class="el-input el-input--medium n8n-input input input"
data-v-dab78bb8=""
>
<!---->
<input
autocomplete="off"
class="el-input__inner"
type="text"
/>
<!---->
<!---->
<!---->
<!---->
<!-- input -->
<!-- prepend slot -->
<!--v-if-->
<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>
</span>
</div>
@ -147,11 +104,15 @@ exports[`components > N8nColorPicker > should render without input 1`] = `
class="n8n-color-picker component"
data-v-dab78bb8=""
>
<div
class="el-color-picker el-color-picker--medium"
data-v-dab78bb8=""
aria-description="current color is . press enter to select a new color."
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
class="el-color-picker__trigger"
>
@ -161,109 +122,46 @@ exports[`components > N8nColorPicker > should render without input 1`] = `
<span
class="el-color-picker__color-inner"
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
class="el-color-hue-slider is-vertical"
style="float: right;"
<i
class="el-icon el-color-picker__icon is-icon-arrow-down"
style="display: none;"
>
<div
class="el-color-hue-slider__bar"
/>
<div
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;"
<svg
viewBox="0 0 1024 1024"
xmlns="http://www.w3.org/2000/svg"
>
<div />
</div>
</div>
</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"
<path
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"
fill="currentColor"
/>
<!---->
<!---->
<!---->
<!---->
</div>
</span>
<button
class="el-button el-color-dropdown__link-btn el-button--text el-button--mini"
type="button"
</svg>
</i>
<i
class="el-icon el-color-picker__empty is-icon-close"
>
<!---->
<!---->
<span>
清空
</span>
</button>
<button
class="el-button el-color-dropdown__btn el-button--default el-button--mini is-plain"
type="button"
>
<!---->
<!---->
<span>
确定
</span>
</button>
</div>
</div>
</transition-stub>
<svg
viewBox="0 0 1024 1024"
xmlns="http://www.w3.org/2000/svg"
>
<path
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"
fill="currentColor"
/>
</svg>
</i>
</span>
</span>
</div>
</div>
<!---->
<!--teleport start-->
<!--teleport end-->
<!--v-if-->
</span>
</div>
`;

View File

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

View File

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

View File

@ -10,12 +10,14 @@ describe('components', () => {
it('should render correctly', () => {
const wrapper = render(N8nDatatable, {
propsData: {
props: {
columns,
rows,
rowsPerPage,
},
stubs,
global: {
stubs,
},
});
expect(wrapper.container.querySelectorAll('thead tr').length).toEqual(1);
@ -28,12 +30,14 @@ describe('components', () => {
it('should add column classes', () => {
const wrapper = render(N8nDatatable, {
propsData: {
props: {
columns: columns.map((column) => ({ ...column, classes: ['example'] })),
rows,
rowsPerPage,
},
stubs,
global: {
stubs,
},
});
expect(wrapper.container.querySelectorAll('.example').length).toEqual(
@ -43,14 +47,16 @@ describe('components', () => {
it('should render row slot', () => {
const wrapper = render(N8nDatatable, {
propsData: {
props: {
columns,
rows,
rowsPerPage,
},
stubs,
scopedSlots: {
row: '<main><td v-for="column in props.columns" :key="column.id">Row slot</td></main>', // Wrapper is necessary for looping
global: {
stubs,
},
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\\">
<thead class=\\"datatableHeader\\">
<tr>
<th> ID </th>
<th> Name </th>
<th> Age </th>
<th> Action </th>
<th class=\\"\\">ID</th>
<th class=\\"\\">Name</th>
<th class=\\"\\">Age</th>
<th class=\\"\\">Action</th>
</tr>
</thead>
<tbody>
<tr>
<td><span>1</span></td>
<td><span>Richard Hendricks</span></td>
<td><span>29</span></td>
<td><button aria-live=\\"polite\\" class=\\"button button primary medium\\" column=\\"[object Object]\\">
<!----><span>Button 1</span></button></td>
<td class=\\"\\"><span>1</span></td>
<td class=\\"\\"><span>Richard Hendricks</span></td>
<td class=\\"\\"><span>29</span></td>
<td class=\\"\\"><button class=\\"button button primary medium\\" aria-live=\\"polite\\" column=\\"[object Object]\\">
<!--v-if--><span>Button 1</span>
</button></td>
</tr>
<tr>
<td><span>2</span></td>
<td><span>Bertram Gilfoyle</span></td>
<td><span>44</span></td>
<td><button aria-live=\\"polite\\" class=\\"button button primary medium\\" column=\\"[object Object]\\">
<!----><span>Button 2</span></button></td>
<td class=\\"\\"><span>2</span></td>
<td class=\\"\\"><span>Bertram Gilfoyle</span></td>
<td class=\\"\\"><span>44</span></td>
<td class=\\"\\"><button class=\\"button button primary medium\\" aria-live=\\"polite\\" column=\\"[object Object]\\">
<!--v-if--><span>Button 2</span>
</button></td>
</tr>
<tr>
<td><span>3</span></td>
<td><span>Dinesh Chugtai</span></td>
<td><span>31</span></td>
<td><button aria-live=\\"polite\\" class=\\"button button primary medium\\" column=\\"[object Object]\\">
<!----><span>Button 3</span></button></td>
<td class=\\"\\"><span>3</span></td>
<td class=\\"\\"><span>Dinesh Chugtai</span></td>
<td class=\\"\\"><span>31</span></td>
<td class=\\"\\"><button class=\\"button button primary medium\\" aria-live=\\"polite\\" column=\\"[object Object]\\">
<!--v-if--><span>Button 3</span>
</button></td>
</tr>
<tr>
<td><span>4</span></td>
<td><span>Jared Dunn </span></td>
<td><span>38</span></td>
<td><button aria-live=\\"polite\\" class=\\"button button primary medium\\" column=\\"[object Object]\\">
<!----><span>Button 4</span></button></td>
<td class=\\"\\"><span>4</span></td>
<td class=\\"\\"><span>Jared Dunn </span></td>
<td class=\\"\\"><span>38</span></td>
<td class=\\"\\"><button class=\\"button button primary medium\\" aria-live=\\"polite\\" column=\\"[object Object]\\">
<!--v-if--><span>Button 4</span>
</button></td>
</tr>
<tr>
<td><span>5</span></td>
<td><span>Richard Hendricks</span></td>
<td><span>29</span></td>
<td><button aria-live=\\"polite\\" class=\\"button button primary medium\\" column=\\"[object Object]\\">
<!----><span>Button 5</span></button></td>
<td class=\\"\\"><span>5</span></td>
<td class=\\"\\"><span>Richard Hendricks</span></td>
<td class=\\"\\"><span>29</span></td>
<td class=\\"\\"><button class=\\"button button primary medium\\" aria-live=\\"polite\\" column=\\"[object Object]\\">
<!--v-if--><span>Button 5</span>
</button></td>
</tr>
<tr>
<td><span>6</span></td>
<td><span>Bertram Gilfoyle</span></td>
<td><span>44</span></td>
<td><button aria-live=\\"polite\\" class=\\"button button primary medium\\" column=\\"[object Object]\\">
<!----><span>Button 6</span></button></td>
<td class=\\"\\"><span>6</span></td>
<td class=\\"\\"><span>Bertram Gilfoyle</span></td>
<td class=\\"\\"><span>44</span></td>
<td class=\\"\\"><button class=\\"button button primary medium\\" aria-live=\\"polite\\" column=\\"[object Object]\\">
<!--v-if--><span>Button 6</span>
</button></td>
</tr>
<tr>
<td><span>7</span></td>
<td><span>Dinesh Chugtai</span></td>
<td><span>31</span></td>
<td><button aria-live=\\"polite\\" class=\\"button button primary medium\\" column=\\"[object Object]\\">
<!----><span>Button 7</span></button></td>
<td class=\\"\\"><span>7</span></td>
<td class=\\"\\"><span>Dinesh Chugtai</span></td>
<td class=\\"\\"><span>31</span></td>
<td class=\\"\\"><button class=\\"button button primary medium\\" aria-live=\\"polite\\" column=\\"[object Object]\\">
<!--v-if--><span>Button 7</span>
</button></td>
</tr>
<tr>
<td><span>8</span></td>
<td><span>Jared Dunn </span></td>
<td><span>38</span></td>
<td><button aria-live=\\"polite\\" class=\\"button button primary medium\\" column=\\"[object Object]\\">
<!----><span>Button 8</span></button></td>
<td class=\\"\\"><span>8</span></td>
<td class=\\"\\"><span>Jared Dunn </span></td>
<td class=\\"\\"><span>38</span></td>
<td class=\\"\\"><button class=\\"button button primary medium\\" aria-live=\\"polite\\" column=\\"[object Object]\\">
<!--v-if--><span>Button 8</span>
</button></td>
</tr>
<tr>
<td><span>9</span></td>
<td><span>Richard Hendricks</span></td>
<td><span>29</span></td>
<td><button aria-live=\\"polite\\" class=\\"button button primary medium\\" column=\\"[object Object]\\">
<!----><span>Button 9</span></button></td>
<td class=\\"\\"><span>9</span></td>
<td class=\\"\\"><span>Richard Hendricks</span></td>
<td class=\\"\\"><span>29</span></td>
<td class=\\"\\"><button class=\\"button button primary medium\\" aria-live=\\"polite\\" column=\\"[object Object]\\">
<!--v-if--><span>Button 9</span>
</button></td>
</tr>
<tr>
<td><span>10</span></td>
<td><span>Bertram Gilfoyle</span></td>
<td><span>44</span></td>
<td><button aria-live=\\"polite\\" class=\\"button button primary medium\\" column=\\"[object Object]\\">
<!----><span>Button 10</span></button></td>
<td class=\\"\\"><span>10</span></td>
<td class=\\"\\"><span>Bertram Gilfoyle</span></td>
<td class=\\"\\"><span>44</span></td>
<td class=\\"\\"><button class=\\"button button primary medium\\" aria-live=\\"polite\\" column=\\"[object Object]\\">
<!--v-if--><span>Button 10</span>
</button></td>
</tr>
</tbody>
</table>
<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\\">
<n8n-select-stub value=\\"10\\" size=\\"mini\\" popperappendtobody=\\"true\\">
<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>
<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>
</div>
</div>
</div>"

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import N8nFormInput from './FormInput.vue';
import { action } from '@storybook/addon-actions';
import type { StoryFn } from '@storybook/vue';
import type { StoryFn } from '@storybook/vue3';
export default {
title: 'Modules/FormInput',
@ -9,18 +9,19 @@ export default {
};
const methods = {
onInput: action('input'),
onUpdateModelValue: action('update:modelValue'),
onFocus: action('focus'),
onChange: action('change'),
};
const Template: StoryFn = (args, { argTypes }) => ({
setup: () => ({ args }),
props: Object.keys(argTypes),
components: {
N8nFormInput,
},
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,
data() {

View File

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

View File

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

View File

@ -22,12 +22,12 @@
v-bind="input.properties"
:name="input.name"
:label="input.properties.label || ''"
:value="values[input.name]"
:modelValue="values[input.name]"
:data-test-id="input.name"
:showValidationWarnings="showValidationWarnings"
@input="(value) => onInput(input.name, value)"
:teleported="teleported"
@update:modelValue="(value) => onUpdateModelValue(input.name, value)"
@validate="(value) => onValidate(input.name, value)"
@change="(value) => onInput(input.name, value)"
@enter="onSubmit"
/>
</div>
@ -69,6 +69,10 @@ export default defineComponent({
default: '',
validator: (value: string): boolean => ['', 'xs', 's', 'm', 'm', 'l', 'xl'].includes(value),
},
teleported: {
type: Boolean,
default: true,
},
},
data() {
return {
@ -80,7 +84,10 @@ export default defineComponent({
mounted() {
this.inputs.forEach((input) => {
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: {
onInput(name: string, value: unknown) {
onUpdateModelValue(name: string, value: unknown) {
this.values = {
...this.values,
[name]: value,
};
this.$emit('input', { name, value });
this.$emit('update', { name, value });
},
onValidate(name: string, valid: boolean) {
this.$set(this.validity, name, valid);
this.validity = {
...this.validity,
[name]: valid,
};
},
onSubmit() {
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';
export default {
@ -8,24 +8,25 @@ export default {
size: {
control: {
type: 'select',
options: ['2xlarge', 'xlarge', 'large', 'medium', 'small'],
},
options: ['2xlarge', 'xlarge', 'large', 'medium', 'small'],
},
color: {
control: {
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 }) => ({
setup: () => ({ args }),
props: Object.keys(argTypes),
components: {
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({});

View File

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

View File

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

View File

@ -1,5 +1,5 @@
<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]" />
</n8n-text>
</template>

View File

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

View File

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

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