mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-25 04:34:06 -08:00
feat(editor): Unify regular and trigger node creator panels (#5315)
* WIP: Merge TriggerHelperPanel with MainPanel * WIP: Implement switching between views * Remove logging * WIP: Rework search * Fix category toggling and search results display * Fix node item description * Sort actions based on the root view * Adjust personalisation modal, make trigger canvas node round * Linting fixes * Fix filtering of API options * Fix types and no result state * Cleanup * Linting fixes * Adjust mode prop for node creator tracking * Fix merging of core nodes and filtering of single placeholder actions * Lint fixes * Implement actions override, fix node creator view item spacing and increase click radius of trigger node icon * Fix keyboard view navigation * WIP: E2E Tests * Address product review * Minor fixes & cleanup * Fix tests * Some more test fixes * Add specs to check actions and panels * Update personalisation survey snapshot
This commit is contained in:
parent
561882f599
commit
9a1e7b52f7
|
@ -10,7 +10,7 @@ describe('Inline expression editor', () => {
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
WorkflowPage.actions.visit();
|
WorkflowPage.actions.visit();
|
||||||
WorkflowPage.actions.addInitialNodeToCanvas('Manual Trigger');
|
WorkflowPage.actions.addInitialNodeToCanvas('Manual');
|
||||||
WorkflowPage.actions.addNodeToCanvas('Hacker News');
|
WorkflowPage.actions.addNodeToCanvas('Hacker News');
|
||||||
WorkflowPage.actions.openNode('Hacker News');
|
WorkflowPage.actions.openNode('Hacker News');
|
||||||
WorkflowPage.actions.openInlineExpressionEditor();
|
WorkflowPage.actions.openInlineExpressionEditor();
|
||||||
|
@ -50,6 +50,7 @@ describe('Inline expression editor', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should resolve array resolvables', () => {
|
it('should resolve array resolvables', () => {
|
||||||
|
WorkflowPage.getters.inlineExpressionEditorInput().clear();
|
||||||
WorkflowPage.getters.inlineExpressionEditorInput().type('{{');
|
WorkflowPage.getters.inlineExpressionEditorInput().type('{{');
|
||||||
WorkflowPage.getters.inlineExpressionEditorInput().type('[1, 2, 3]');
|
WorkflowPage.getters.inlineExpressionEditorInput().type('[1, 2, 3]');
|
||||||
WorkflowPage.getters.inlineExpressionEditorOutput().contains(/^\[Array: \[1,2,3\]\]$/);
|
WorkflowPage.getters.inlineExpressionEditorOutput().contains(/^\[Array: \[1,2,3\]\]$/);
|
||||||
|
@ -63,8 +64,9 @@ describe('Inline expression editor', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should resolve $parameter[]', () => {
|
it('should resolve $parameter[]', () => {
|
||||||
|
WorkflowPage.getters.inlineExpressionEditorInput().clear();
|
||||||
WorkflowPage.getters.inlineExpressionEditorInput().type('{{');
|
WorkflowPage.getters.inlineExpressionEditorInput().type('{{');
|
||||||
WorkflowPage.getters.inlineExpressionEditorInput().type('$parameter["operation"]');
|
WorkflowPage.getters.inlineExpressionEditorInput().type('$parameter["operation"]');
|
||||||
WorkflowPage.getters.inlineExpressionEditorOutput().contains(/^get$/);
|
WorkflowPage.getters.inlineExpressionEditorOutput().contains(/^getAll$/);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -96,6 +96,7 @@ describe('Canvas Actions', () => {
|
||||||
|
|
||||||
it('should add merge node and test connections', () => {
|
it('should add merge node and test connections', () => {
|
||||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||||
|
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
||||||
for (let i = 0; i < 2; i++) {
|
for (let i = 0; i < 2; i++) {
|
||||||
WorkflowPage.actions.addNodeToCanvas(SET_NODE_NAME, true);
|
WorkflowPage.actions.addNodeToCanvas(SET_NODE_NAME, true);
|
||||||
WorkflowPage.getters.nodeViewBackground().click(600 + i * 100, 200, { force: true });
|
WorkflowPage.getters.nodeViewBackground().click(600 + i * 100, 200, { force: true });
|
||||||
|
@ -138,6 +139,7 @@ describe('Canvas Actions', () => {
|
||||||
|
|
||||||
it('should add nodes and check execution success', () => {
|
it('should add nodes and check execution success', () => {
|
||||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||||
|
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
||||||
for (let i = 0; i < 3; i++) {
|
for (let i = 0; i < 3; i++) {
|
||||||
WorkflowPage.actions.addNodeToCanvas(SET_NODE_NAME, true);
|
WorkflowPage.actions.addNodeToCanvas(SET_NODE_NAME, true);
|
||||||
}
|
}
|
||||||
|
@ -235,6 +237,7 @@ describe('Canvas Actions', () => {
|
||||||
|
|
||||||
it('should move node', () => {
|
it('should move node', () => {
|
||||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||||
|
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
||||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||||
WorkflowPage.actions.zoomToFit();
|
WorkflowPage.actions.zoomToFit();
|
||||||
cy.drag('[data-test-id="canvas-node"].jtk-drag-selected', [50, 150]);
|
cy.drag('[data-test-id="canvas-node"].jtk-drag-selected', [50, 150]);
|
||||||
|
@ -316,6 +319,7 @@ describe('Canvas Actions', () => {
|
||||||
|
|
||||||
it('should select nodes using arrow keys', () => {
|
it('should select nodes using arrow keys', () => {
|
||||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||||
|
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
||||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||||
cy.wait(500);
|
cy.wait(500);
|
||||||
cy.get('body').type('{leftArrow}');
|
cy.get('body').type('{leftArrow}');
|
||||||
|
@ -326,6 +330,7 @@ describe('Canvas Actions', () => {
|
||||||
|
|
||||||
it('should select nodes using shift and arrow keys', () => {
|
it('should select nodes using shift and arrow keys', () => {
|
||||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||||
|
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
||||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||||
cy.wait(500);
|
cy.wait(500);
|
||||||
cy.get('body').type('{shift}', { release: false }).type('{leftArrow}');
|
cy.get('body').type('{shift}', { release: false }).type('{leftArrow}');
|
||||||
|
@ -334,6 +339,7 @@ describe('Canvas Actions', () => {
|
||||||
|
|
||||||
it('should delete connections by pressing the delete button', () => {
|
it('should delete connections by pressing the delete button', () => {
|
||||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||||
|
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
||||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||||
WorkflowPage.getters.nodeConnections().first().realHover();
|
WorkflowPage.getters.nodeConnections().first().realHover();
|
||||||
cy.get('.connection-actions .delete').first().click({ force: true });
|
cy.get('.connection-actions .delete').first().click({ force: true });
|
||||||
|
@ -342,6 +348,7 @@ describe('Canvas Actions', () => {
|
||||||
|
|
||||||
it('should delete a connection by moving it away from endpoint', () => {
|
it('should delete a connection by moving it away from endpoint', () => {
|
||||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||||
|
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
||||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||||
cy.drag(WorkflowPage.getters.getEndpointSelector('input', CODE_NODE_NAME), [0, -100]);
|
cy.drag(WorkflowPage.getters.getEndpointSelector('input', CODE_NODE_NAME), [0, -100]);
|
||||||
WorkflowPage.getters.nodeConnections().should('have.length', 0);
|
WorkflowPage.getters.nodeConnections().should('have.length', 0);
|
||||||
|
@ -349,6 +356,7 @@ describe('Canvas Actions', () => {
|
||||||
|
|
||||||
it('should disable node by pressing the disable button', () => {
|
it('should disable node by pressing the disable button', () => {
|
||||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||||
|
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
||||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||||
WorkflowPage.getters
|
WorkflowPage.getters
|
||||||
.canvasNodes()
|
.canvasNodes()
|
||||||
|
@ -360,6 +368,7 @@ describe('Canvas Actions', () => {
|
||||||
|
|
||||||
it('should disable node using keyboard shortcut', () => {
|
it('should disable node using keyboard shortcut', () => {
|
||||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||||
|
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
||||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||||
WorkflowPage.getters.canvasNodes().last().click();
|
WorkflowPage.getters.canvasNodes().last().click();
|
||||||
WorkflowPage.actions.hitDisableNodeShortcut();
|
WorkflowPage.actions.hitDisableNodeShortcut();
|
||||||
|
@ -368,6 +377,7 @@ describe('Canvas Actions', () => {
|
||||||
|
|
||||||
it('should disable multiple nodes', () => {
|
it('should disable multiple nodes', () => {
|
||||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||||
|
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
||||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||||
cy.get('body').type('{esc}');
|
cy.get('body').type('{esc}');
|
||||||
cy.get('body').type('{esc}');
|
cy.get('body').type('{esc}');
|
||||||
|
@ -389,6 +399,7 @@ describe('Canvas Actions', () => {
|
||||||
|
|
||||||
it('should duplicate node', () => {
|
it('should duplicate node', () => {
|
||||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||||
|
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
||||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||||
WorkflowPage.getters
|
WorkflowPage.getters
|
||||||
.canvasNodes()
|
.canvasNodes()
|
||||||
|
|
|
@ -12,8 +12,7 @@ describe('Data pinning', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should be able to pin node output', () => {
|
it('Should be able to pin node output', () => {
|
||||||
workflowPage.actions.addInitialNodeToCanvas('Schedule Trigger');
|
workflowPage.actions.addInitialNodeToCanvas('Schedule Trigger', { keepNdvOpen: true});
|
||||||
workflowPage.getters.canvasNodes().first().dblclick();
|
|
||||||
ndv.getters.container().should('be.visible');
|
ndv.getters.container().should('be.visible');
|
||||||
ndv.getters.pinDataButton().should('not.exist');
|
ndv.getters.pinDataButton().should('not.exist');
|
||||||
ndv.getters.editPinnedDataButton().should('be.visible');
|
ndv.getters.editPinnedDataButton().should('be.visible');
|
||||||
|
@ -21,7 +20,9 @@ describe('Data pinning', () => {
|
||||||
ndv.actions.execute();
|
ndv.actions.execute();
|
||||||
|
|
||||||
ndv.getters.outputDataContainer().should('be.visible');
|
ndv.getters.outputDataContainer().should('be.visible');
|
||||||
ndv.getters.outputDataContainer().get('table').should('be.visible');
|
// We hover over the table to get rid of the pinning tooltip which would overlay the table
|
||||||
|
// slightly and cause the test to fail
|
||||||
|
ndv.getters.outputDataContainer().get('table').realHover().should('be.visible');
|
||||||
ndv.getters.outputTableRows().should('have.length', 2);
|
ndv.getters.outputTableRows().should('have.length', 2);
|
||||||
ndv.getters.outputTableHeaders().should('have.length.at.least', 10);
|
ndv.getters.outputTableHeaders().should('have.length.at.least', 10);
|
||||||
ndv.getters.outputTableHeaders().first().should('include.text', 'timestamp');
|
ndv.getters.outputTableHeaders().first().should('include.text', 'timestamp');
|
||||||
|
@ -42,8 +43,7 @@ describe('Data pinning', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should be be able to set pinned data', () => {
|
it('Should be be able to set pinned data', () => {
|
||||||
workflowPage.actions.addInitialNodeToCanvas('Schedule Trigger');
|
workflowPage.actions.addInitialNodeToCanvas('Schedule Trigger', { keepNdvOpen: true});
|
||||||
workflowPage.getters.canvasNodes().first().dblclick();
|
|
||||||
ndv.getters.container().should('be.visible');
|
ndv.getters.container().should('be.visible');
|
||||||
ndv.getters.pinDataButton().should('not.exist');
|
ndv.getters.pinDataButton().should('not.exist');
|
||||||
ndv.getters.editPinnedDataButton().should('be.visible');
|
ndv.getters.editPinnedDataButton().should('be.visible');
|
||||||
|
|
|
@ -26,7 +26,8 @@ describe('Data transformation expressions', () => {
|
||||||
|
|
||||||
ndv.getters.inlineExpressionEditorInput().clear().type(input);
|
ndv.getters.inlineExpressionEditorInput().clear().type(input);
|
||||||
ndv.actions.execute();
|
ndv.actions.execute();
|
||||||
ndv.getters.outputDataContainer().should('be.visible').contains(output);
|
ndv.getters.outputDataContainer().should('be.visible')
|
||||||
|
ndv.getters.outputDataContainer().contains(output);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('$json + n8n string methods', () => {
|
it('$json + n8n string methods', () => {
|
||||||
|
@ -40,7 +41,8 @@ describe('Data transformation expressions', () => {
|
||||||
|
|
||||||
ndv.getters.inlineExpressionEditorInput().clear().type(input);
|
ndv.getters.inlineExpressionEditorInput().clear().type(input);
|
||||||
ndv.actions.execute();
|
ndv.actions.execute();
|
||||||
ndv.getters.outputDataContainer().should('be.visible').contains(output);
|
ndv.getters.outputDataContainer().should('be.visible')
|
||||||
|
ndv.getters.outputDataContainer().contains(output);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('$json + native numeric methods', () => {
|
it('$json + native numeric methods', () => {
|
||||||
|
@ -54,7 +56,8 @@ describe('Data transformation expressions', () => {
|
||||||
|
|
||||||
ndv.getters.inlineExpressionEditorInput().clear().type(input);
|
ndv.getters.inlineExpressionEditorInput().clear().type(input);
|
||||||
ndv.actions.execute();
|
ndv.actions.execute();
|
||||||
ndv.getters.outputDataContainer().should('be.visible').contains(output);
|
ndv.getters.outputDataContainer().should('be.visible')
|
||||||
|
ndv.getters.outputDataContainer().contains(output);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('$json + n8n numeric methods', () => {
|
it('$json + n8n numeric methods', () => {
|
||||||
|
@ -68,7 +71,8 @@ describe('Data transformation expressions', () => {
|
||||||
|
|
||||||
ndv.getters.inlineExpressionEditorInput().clear().type(input);
|
ndv.getters.inlineExpressionEditorInput().clear().type(input);
|
||||||
ndv.actions.execute();
|
ndv.actions.execute();
|
||||||
ndv.getters.outputDataContainer().should('be.visible').contains(output);
|
ndv.getters.outputDataContainer().should('be.visible')
|
||||||
|
ndv.getters.outputDataContainer().contains(output);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('$json + native array methods', () => {
|
it('$json + native array methods', () => {
|
||||||
|
@ -82,7 +86,8 @@ describe('Data transformation expressions', () => {
|
||||||
|
|
||||||
ndv.getters.inlineExpressionEditorInput().clear().type(input);
|
ndv.getters.inlineExpressionEditorInput().clear().type(input);
|
||||||
ndv.actions.execute();
|
ndv.actions.execute();
|
||||||
ndv.getters.outputDataContainer().should('be.visible').contains(output);
|
ndv.getters.outputDataContainer().should('be.visible')
|
||||||
|
ndv.getters.outputDataContainer().contains(output);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('$json + n8n array methods', () => {
|
it('$json + n8n array methods', () => {
|
||||||
|
|
|
@ -1,8 +1,12 @@
|
||||||
import { WorkflowPage, NDV, CanvasNode } from '../pages';
|
import {
|
||||||
|
MANUAL_TRIGGER_NODE_NAME,
|
||||||
|
MANUAL_TRIGGER_NODE_DISPLAY_NAME,
|
||||||
|
SCHEDULE_TRIGGER_NODE_NAME,
|
||||||
|
} from './../constants';
|
||||||
|
import { WorkflowPage, NDV } from '../pages';
|
||||||
|
|
||||||
const workflowPage = new WorkflowPage();
|
const workflowPage = new WorkflowPage();
|
||||||
const ndv = new NDV();
|
const ndv = new NDV();
|
||||||
const canvasNode = new CanvasNode();
|
|
||||||
|
|
||||||
describe('Data mapping', () => {
|
describe('Data mapping', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
@ -20,7 +24,7 @@ describe('Data mapping', () => {
|
||||||
cy.fixture('Test_workflow-actions_paste-data.json').then((data) => {
|
cy.fixture('Test_workflow-actions_paste-data.json').then((data) => {
|
||||||
cy.get('body').paste(JSON.stringify(data));
|
cy.get('body').paste(JSON.stringify(data));
|
||||||
});
|
});
|
||||||
canvasNode.actions.openNode('Set');
|
workflowPage.actions.openNode('Set');
|
||||||
ndv.actions.executePrevious();
|
ndv.actions.executePrevious();
|
||||||
ndv.actions.switchInputMode('Table');
|
ndv.actions.switchInputMode('Table');
|
||||||
ndv.getters.inputDataContainer().get('table', { timeout: 10000 }).should('exist');
|
ndv.getters.inputDataContainer().get('table', { timeout: 10000 }).should('exist');
|
||||||
|
@ -42,7 +46,7 @@ describe('Data mapping', () => {
|
||||||
cy.get('body').paste(JSON.stringify(data));
|
cy.get('body').paste(JSON.stringify(data));
|
||||||
});
|
});
|
||||||
|
|
||||||
canvasNode.actions.openNode('Set');
|
workflowPage.actions.openNode('Set');
|
||||||
ndv.actions.switchInputMode('Table');
|
ndv.actions.switchInputMode('Table');
|
||||||
ndv.getters.inputDataContainer().get('table', { timeout: 10000 }).should('exist');
|
ndv.getters.inputDataContainer().get('table', { timeout: 10000 }).should('exist');
|
||||||
|
|
||||||
|
@ -87,7 +91,7 @@ describe('Data mapping', () => {
|
||||||
cy.get('body').paste(JSON.stringify(data));
|
cy.get('body').paste(JSON.stringify(data));
|
||||||
});
|
});
|
||||||
|
|
||||||
canvasNode.actions.openNode('Set');
|
workflowPage.actions.openNode('Set');
|
||||||
ndv.actions.switchInputMode('JSON');
|
ndv.actions.switchInputMode('JSON');
|
||||||
|
|
||||||
ndv.getters.inputDataContainer().should('exist').find('.json-data')
|
ndv.getters.inputDataContainer().should('exist').find('.json-data')
|
||||||
|
@ -115,7 +119,7 @@ describe('Data mapping', () => {
|
||||||
cy.get('body').paste(JSON.stringify(data));
|
cy.get('body').paste(JSON.stringify(data));
|
||||||
});
|
});
|
||||||
|
|
||||||
canvasNode.actions.openNode('Set');
|
workflowPage.actions.openNode('Set');
|
||||||
ndv.actions.clearParameterInput('value');
|
ndv.actions.clearParameterInput('value');
|
||||||
cy.get('body').type('{esc}');
|
cy.get('body').type('{esc}');
|
||||||
|
|
||||||
|
@ -142,22 +146,22 @@ describe('Data mapping', () => {
|
||||||
|
|
||||||
it('maps expressions from previous nodes', () => {
|
it('maps expressions from previous nodes', () => {
|
||||||
cy.createFixtureWorkflow('Test_workflow_3.json', `My test workflow`);
|
cy.createFixtureWorkflow('Test_workflow_3.json', `My test workflow`);
|
||||||
canvasNode.actions.openNode('Set1');
|
workflowPage.actions.openNode('Set1');
|
||||||
|
|
||||||
ndv.actions.selectInputNode('Schedule Trigger');
|
ndv.actions.selectInputNode(SCHEDULE_TRIGGER_NODE_NAME);
|
||||||
|
|
||||||
ndv.getters.inputDataContainer()
|
ndv.getters.inputDataContainer()
|
||||||
.find('span').contains('count')
|
.find('span').contains('count')
|
||||||
.realMouseDown();
|
.realMouseDown();
|
||||||
|
|
||||||
ndv.actions.mapToParameter('value');
|
ndv.actions.mapToParameter('value');
|
||||||
ndv.getters.inlineExpressionEditorInput().should('have.text', '{{ $node["Schedule Trigger"].json.input[0].count }}');
|
ndv.getters.inlineExpressionEditorInput().should('have.text', `{{ $node["${SCHEDULE_TRIGGER_NODE_NAME}"].json.input[0].count }}`);
|
||||||
ndv.getters.parameterExpressionPreview('value')
|
ndv.getters.parameterExpressionPreview('value')
|
||||||
.should('not.exist');
|
.should('not.exist');
|
||||||
|
|
||||||
ndv.actions.switchInputMode('Table');
|
ndv.actions.switchInputMode('Table');
|
||||||
ndv.actions.mapDataFromHeader(1, 'value');
|
ndv.actions.mapDataFromHeader(1, 'value');
|
||||||
ndv.getters.inlineExpressionEditorInput().should('have.text', '{{ $node["Schedule Trigger"].json.input[0].count }} {{ $node["Schedule Trigger"].json.input }}');
|
ndv.getters.inlineExpressionEditorInput().should('have.text', `{{ $node["${SCHEDULE_TRIGGER_NODE_NAME}"].json.input[0].count }} {{ $node["${SCHEDULE_TRIGGER_NODE_NAME}"].json.input }}`);
|
||||||
ndv.getters.parameterExpressionPreview('value')
|
ndv.getters.parameterExpressionPreview('value')
|
||||||
.should('not.exist');
|
.should('not.exist');
|
||||||
|
|
||||||
|
@ -175,8 +179,9 @@ describe('Data mapping', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('maps keys to path', () => {
|
it('maps keys to path', () => {
|
||||||
workflowPage.actions.addInitialNodeToCanvas('Manual Trigger', {keepNdvOpen: true});
|
workflowPage.actions.addInitialNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||||
|
workflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
||||||
|
workflowPage.actions.openNode(MANUAL_TRIGGER_NODE_DISPLAY_NAME);
|
||||||
ndv.actions.setPinnedData([
|
ndv.actions.setPinnedData([
|
||||||
{
|
{
|
||||||
input: [
|
input: [
|
||||||
|
@ -201,7 +206,7 @@ describe('Data mapping', () => {
|
||||||
ndv.actions.close();
|
ndv.actions.close();
|
||||||
|
|
||||||
workflowPage.actions.addNodeToCanvas('Item Lists');
|
workflowPage.actions.addNodeToCanvas('Item Lists');
|
||||||
canvasNode.actions.openNode('Item Lists');
|
workflowPage.actions.openNode('Item Lists');
|
||||||
|
|
||||||
ndv.getters.parameterInput('operation')
|
ndv.getters.parameterInput('operation')
|
||||||
.click()
|
.click()
|
||||||
|
|
|
@ -253,7 +253,7 @@ describe('Credentials', () => {
|
||||||
|
|
||||||
it('should render custom node with n8n credential', () => {
|
it('should render custom node with n8n credential', () => {
|
||||||
workflowPage.actions.visit();
|
workflowPage.actions.visit();
|
||||||
workflowPage.actions.addNodeToCanvas('Manual Trigger');
|
workflowPage.actions.addNodeToCanvas('Manual');
|
||||||
workflowPage.actions.addNodeToCanvas('E2E Node with native n8n credential', true, true);
|
workflowPage.actions.addNodeToCanvas('E2E Node with native n8n credential', true, true);
|
||||||
workflowPage.getters.nodeCredentialsLabel().click();
|
workflowPage.getters.nodeCredentialsLabel().click();
|
||||||
cy.contains('Create New Credential').click();
|
cy.contains('Create New Credential').click();
|
||||||
|
@ -263,7 +263,7 @@ describe('Credentials', () => {
|
||||||
|
|
||||||
it('should render custom node with custom credential', () => {
|
it('should render custom node with custom credential', () => {
|
||||||
workflowPage.actions.visit();
|
workflowPage.actions.visit();
|
||||||
workflowPage.actions.addNodeToCanvas('Manual Trigger');
|
workflowPage.actions.addNodeToCanvas('Manual');
|
||||||
workflowPage.actions.addNodeToCanvas('E2E Node with custom credential', true, true);
|
workflowPage.actions.addNodeToCanvas('E2E Node with custom credential', true, true);
|
||||||
workflowPage.getters.nodeCredentialsLabel().click();
|
workflowPage.getters.nodeCredentialsLabel().click();
|
||||||
cy.contains('Create New Credential').click();
|
cy.contains('Create New Credential').click();
|
||||||
|
|
|
@ -1,13 +1,16 @@
|
||||||
import { NodeCreator } from '../pages/features/node-creator';
|
import { NodeCreator } from '../pages/features/node-creator';
|
||||||
import { INodeTypeDescription } from 'n8n-workflow';
|
|
||||||
import { DEFAULT_USER_EMAIL, DEFAULT_USER_PASSWORD } from '../constants';
|
import { DEFAULT_USER_EMAIL, DEFAULT_USER_PASSWORD } from '../constants';
|
||||||
import { randFirstName, randLastName } from '@ngneat/falso';
|
import { randFirstName, randLastName } from '@ngneat/falso';
|
||||||
|
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
|
||||||
|
import { NDV } from '../pages/ndv';
|
||||||
|
|
||||||
const email = DEFAULT_USER_EMAIL;
|
const email = DEFAULT_USER_EMAIL;
|
||||||
const password = DEFAULT_USER_PASSWORD;
|
const password = DEFAULT_USER_PASSWORD;
|
||||||
const firstName = randFirstName();
|
const firstName = randFirstName();
|
||||||
const lastName = randLastName();
|
const lastName = randLastName();
|
||||||
const nodeCreatorFeature = new NodeCreator();
|
const nodeCreatorFeature = new NodeCreator();
|
||||||
|
const WorkflowPage = new WorkflowPageClass();
|
||||||
|
const NDVModal = new NDV();
|
||||||
|
|
||||||
describe('Node Creator', () => {
|
describe('Node Creator', () => {
|
||||||
before(() => {
|
before(() => {
|
||||||
|
@ -27,30 +30,22 @@ describe('Node Creator', () => {
|
||||||
|
|
||||||
nodeCreatorFeature.getters
|
nodeCreatorFeature.getters
|
||||||
.nodeCreator()
|
.nodeCreator()
|
||||||
.contains('When should this workflow run?')
|
.contains('Select a trigger')
|
||||||
.should('be.visible');
|
.should('be.visible');
|
||||||
|
|
||||||
nodeCreatorFeature.getters.nodeCreatorTabs().should('not.exist');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should see all tabs when opening via plus button', () => {
|
|
||||||
nodeCreatorFeature.actions.openNodeCreator();
|
|
||||||
nodeCreatorFeature.getters.nodeCreatorTabs().should('exist');
|
|
||||||
nodeCreatorFeature.getters.selectedTab().should('have.text', 'Trigger');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should navigate subcategory', () => {
|
it('should navigate subcategory', () => {
|
||||||
nodeCreatorFeature.actions.openNodeCreator();
|
nodeCreatorFeature.actions.openNodeCreator();
|
||||||
nodeCreatorFeature.getters.getCreatorItem('On App Event').click();
|
nodeCreatorFeature.getters.getCreatorItem('On app event').click();
|
||||||
nodeCreatorFeature.getters.activeSubcategory().should('have.text', 'On App Event');
|
nodeCreatorFeature.getters.activeSubcategory().should('have.text', 'On app event');
|
||||||
// Go back
|
// Go back
|
||||||
nodeCreatorFeature.getters.activeSubcategory().find('button').click();
|
nodeCreatorFeature.getters.activeSubcategory().find('button').click();
|
||||||
nodeCreatorFeature.getters.activeSubcategory().should('not.exist');
|
nodeCreatorFeature.getters.activeSubcategory().should('not.have.text', 'On app event');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should search for nodes', () => {
|
it('should search for nodes', () => {
|
||||||
nodeCreatorFeature.actions.openNodeCreator();
|
nodeCreatorFeature.actions.openNodeCreator();
|
||||||
nodeCreatorFeature.getters.selectedTab().should('have.text', 'Trigger');
|
|
||||||
|
|
||||||
nodeCreatorFeature.getters.searchBar().find('input').type('manual');
|
nodeCreatorFeature.getters.searchBar().find('input').type('manual');
|
||||||
nodeCreatorFeature.getters.creatorItem().should('have.length', 1);
|
nodeCreatorFeature.getters.creatorItem().should('have.length', 1);
|
||||||
|
@ -72,92 +67,63 @@ describe('Node Creator', () => {
|
||||||
nodeCreatorFeature.getters.creatorItem().should('have.length', 0);
|
nodeCreatorFeature.getters.creatorItem().should('have.length', 0);
|
||||||
|
|
||||||
nodeCreatorFeature.getters.searchBar().find('input').clear();
|
nodeCreatorFeature.getters.searchBar().find('input').clear();
|
||||||
nodeCreatorFeature.getters.getCreatorItem('On App Event').click();
|
nodeCreatorFeature.getters.getCreatorItem('On app event').click();
|
||||||
|
|
||||||
nodeCreatorFeature.getters.searchBar().find('input').clear().type('edit image');
|
nodeCreatorFeature.getters.searchBar().find('input').clear().type('edit image');
|
||||||
nodeCreatorFeature.getters.creatorItem().should('have.length', 0);
|
nodeCreatorFeature.getters.getCreatorItem('Results in other categories (1)').should('exist');
|
||||||
nodeCreatorFeature.getters
|
nodeCreatorFeature.getters.creatorItem().should('have.length', 2);
|
||||||
.noResults()
|
|
||||||
.should('exist')
|
|
||||||
.should('contain.text', 'To see all results, click here');
|
|
||||||
|
|
||||||
nodeCreatorFeature.getters.noResults().contains('click here').click();
|
|
||||||
nodeCreatorFeature.getters.nodeCreatorTabs().should('exist');
|
|
||||||
nodeCreatorFeature.getters.getCreatorItem('Edit Image').should('exist');
|
nodeCreatorFeature.getters.getCreatorItem('Edit Image').should('exist');
|
||||||
nodeCreatorFeature.getters.selectedTab().should('have.text', 'All');
|
nodeCreatorFeature.getters.searchBar().find('input').clear().type('edit image123123');
|
||||||
nodeCreatorFeature.getters.searchBar().find('button').click();
|
nodeCreatorFeature.getters.creatorItem().should('have.length', 0);
|
||||||
nodeCreatorFeature.getters.searchBar().find('input').should('be.empty');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should add manual trigger node', () => {
|
it('should check correct view panels', () => {
|
||||||
nodeCreatorFeature.getters.canvasAddButton().click();
|
nodeCreatorFeature.getters.canvasAddButton().click();
|
||||||
nodeCreatorFeature.getters.getCreatorItem('Manually').click();
|
WorkflowPage.actions.addNodeToCanvas('Manual', false);
|
||||||
|
|
||||||
// TODO: Replace once we have canvas feature utils
|
|
||||||
cy.get('span').contains('Back to canvas').click();
|
|
||||||
|
|
||||||
nodeCreatorFeature.getters.canvasAddButton().should('not.be.visible');
|
nodeCreatorFeature.getters.canvasAddButton().should('not.be.visible');
|
||||||
nodeCreatorFeature.getters.nodeCreator().should('not.exist');
|
nodeCreatorFeature.getters.nodeCreator().should('not.exist');
|
||||||
|
|
||||||
// TODO: Replace once we have canvas feature utils
|
// TODO: Replace once we have canvas feature utils
|
||||||
cy.get('div').contains('Add first step').should('exist');
|
cy.get('div').contains('Add first step').should('be.hidden');
|
||||||
|
nodeCreatorFeature.actions.openNodeCreator()
|
||||||
|
nodeCreatorFeature.getters
|
||||||
|
.nodeCreator()
|
||||||
|
.contains('What happens next?')
|
||||||
|
.should('be.visible');
|
||||||
|
|
||||||
|
nodeCreatorFeature.getters.getCreatorItem('Add another trigger').click();
|
||||||
|
nodeCreatorFeature.getters.nodeCreator().contains('Select a trigger').should('be.visible');
|
||||||
|
nodeCreatorFeature.getters.activeSubcategory().find('button').should('exist');
|
||||||
|
nodeCreatorFeature.getters.activeSubcategory().find('button').click();
|
||||||
|
nodeCreatorFeature.getters
|
||||||
|
.nodeCreator()
|
||||||
|
.contains('What happens next?')
|
||||||
|
.should('be.visible');
|
||||||
});
|
});
|
||||||
it('check if non-core nodes are rendered', () => {
|
|
||||||
cy.wait('@nodesIntercept').then((interception) => {
|
|
||||||
const nodes = interception.response?.body as INodeTypeDescription[];
|
|
||||||
|
|
||||||
const categorizedNodes = nodeCreatorFeature.actions.categorizeNodes(nodes);
|
it('should add node to canvas from actions panel', () => {
|
||||||
nodeCreatorFeature.actions.openNodeCreator();
|
const editImageNode = 'Edit Image';
|
||||||
nodeCreatorFeature.actions.selectTab('All');
|
nodeCreatorFeature.actions.openNodeCreator();
|
||||||
|
nodeCreatorFeature.getters.searchBar().find('input').clear().type(editImageNode);
|
||||||
const categories = Object.keys(categorizedNodes);
|
nodeCreatorFeature.getters.getCreatorItem(editImageNode).click();
|
||||||
categories.forEach((category: string) => {
|
nodeCreatorFeature.getters.activeSubcategory().should('have.text', editImageNode);
|
||||||
// Core Nodes contains subcategories which we'll test separately
|
nodeCreatorFeature.getters.getCreatorItem('Crop Image').click();
|
||||||
if (category === 'Core Nodes') return;
|
NDVModal.getters.parameterInput('operation').should('contain.text', 'Crop');
|
||||||
|
})
|
||||||
nodeCreatorFeature.actions.toggleCategory(category);
|
|
||||||
|
|
||||||
// Check if all nodes are present
|
|
||||||
nodeCreatorFeature.getters.nodeItemName().then(($elements) => {
|
|
||||||
const visibleNodes: string[] = [];
|
|
||||||
$elements.each((_, element) => {
|
|
||||||
visibleNodes.push(element.textContent?.trim() || '');
|
|
||||||
});
|
|
||||||
const visibleCategoryNodes = (categorizedNodes[category] as INodeTypeDescription[])
|
|
||||||
.filter((node) => !node.hidden)
|
|
||||||
.map((node) => node.displayName?.trim());
|
|
||||||
|
|
||||||
cy.wrap(visibleCategoryNodes).each((categoryNode: string) => {
|
|
||||||
expect(visibleNodes).to.include(categoryNode);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
nodeCreatorFeature.actions.toggleCategory(category);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render and select community node', () => {
|
it('should render and select community node', () => {
|
||||||
cy.intercept('GET', '/types/nodes.json').as('nodesIntercept');
|
cy.intercept('GET', '/types/nodes.json').as('nodesIntercept');
|
||||||
cy.wait('@nodesIntercept').then(() => {
|
cy.wait('@nodesIntercept').then(() => {
|
||||||
const customCategory = 'Custom Category';
|
|
||||||
const customNode = 'E2E Node';
|
const customNode = 'E2E Node';
|
||||||
const customNodeDescription = 'Demonstrate rendering of node';
|
|
||||||
|
|
||||||
nodeCreatorFeature.actions.openNodeCreator();
|
nodeCreatorFeature.actions.openNodeCreator();
|
||||||
nodeCreatorFeature.actions.selectTab('All');
|
nodeCreatorFeature.getters.searchBar().find('input').clear().type(customNode);
|
||||||
|
|
||||||
nodeCreatorFeature.getters.getCreatorItem(customCategory).should('exist');
|
|
||||||
|
|
||||||
nodeCreatorFeature.actions.toggleCategory(customCategory);
|
|
||||||
nodeCreatorFeature.getters
|
nodeCreatorFeature.getters
|
||||||
.getCreatorItem(customNode)
|
.getCreatorItem(customNode)
|
||||||
.findChildByTestId('node-creator-item-tooltip')
|
.findChildByTestId('node-creator-item-tooltip')
|
||||||
.should('exist');
|
.should('exist');
|
||||||
nodeCreatorFeature.getters
|
|
||||||
.getCreatorItem(customNode)
|
|
||||||
.contains(customNodeDescription)
|
|
||||||
.should('exist');
|
|
||||||
nodeCreatorFeature.actions.selectNode(customNode);
|
nodeCreatorFeature.actions.selectNode(customNode);
|
||||||
|
|
||||||
// TODO: Replace once we have canvas feature utils
|
// TODO: Replace once we have canvas feature utils
|
||||||
|
|
|
@ -17,7 +17,7 @@ describe('NDV', () => {
|
||||||
|
|
||||||
|
|
||||||
it('should show up when double clicked on a node and close when Back to canvas clicked', () => {
|
it('should show up when double clicked on a node and close when Back to canvas clicked', () => {
|
||||||
workflowPage.actions.addInitialNodeToCanvas('Manual Trigger');
|
workflowPage.actions.addInitialNodeToCanvas('Manual');
|
||||||
workflowPage.getters.canvasNodes().first().dblclick();
|
workflowPage.getters.canvasNodes().first().dblclick();
|
||||||
ndv.getters.container().should('be.visible');
|
ndv.getters.container().should('be.visible');
|
||||||
ndv.getters.backToCanvas().click();
|
ndv.getters.backToCanvas().click();
|
||||||
|
@ -67,8 +67,8 @@ describe('NDV', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show validation errors only after blur or re-opening of NDV', () => {
|
it('should show validation errors only after blur or re-opening of NDV', () => {
|
||||||
workflowPage.actions.addNodeToCanvas('Manual Trigger');
|
workflowPage.actions.addNodeToCanvas('Manual');
|
||||||
workflowPage.actions.addNodeToCanvas('Airtable', true, true);
|
workflowPage.actions.addNodeToCanvas('Airtable', true, true, 'Read data from a table');
|
||||||
ndv.getters.container().should('be.visible');
|
ndv.getters.container().should('be.visible');
|
||||||
cy.get('.has-issues').should('have.length', 0);
|
cy.get('.has-issues').should('have.length', 0);
|
||||||
ndv.getters.parameterInput('table').find('input').eq(1).focus().blur();
|
ndv.getters.parameterInput('table').find('input').eq(1).focus().blur();
|
||||||
|
|
|
@ -12,7 +12,7 @@ describe('Code node', () => {
|
||||||
|
|
||||||
it('should execute the placeholder in all-items mode successfully', () => {
|
it('should execute the placeholder in all-items mode successfully', () => {
|
||||||
WorkflowPage.actions.visit();
|
WorkflowPage.actions.visit();
|
||||||
WorkflowPage.actions.addInitialNodeToCanvas('Manual Trigger');
|
WorkflowPage.actions.addInitialNodeToCanvas('Manual');
|
||||||
WorkflowPage.actions.addNodeToCanvas('Code');
|
WorkflowPage.actions.addNodeToCanvas('Code');
|
||||||
WorkflowPage.actions.openNode('Code');
|
WorkflowPage.actions.openNode('Code');
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ describe('Code node', () => {
|
||||||
|
|
||||||
it('should execute the placeholder in each-item mode successfully', () => {
|
it('should execute the placeholder in each-item mode successfully', () => {
|
||||||
WorkflowPage.actions.visit();
|
WorkflowPage.actions.visit();
|
||||||
WorkflowPage.actions.addInitialNodeToCanvas('Manual Trigger');
|
WorkflowPage.actions.addInitialNodeToCanvas('Manual');
|
||||||
WorkflowPage.actions.addNodeToCanvas('Code');
|
WorkflowPage.actions.addNodeToCanvas('Code');
|
||||||
WorkflowPage.actions.openNode('Code');
|
WorkflowPage.actions.openNode('Code');
|
||||||
ndv.getters.parameterInput('mode').click();
|
ndv.getters.parameterInput('mode').click();
|
||||||
|
|
|
@ -14,7 +14,7 @@ describe('HTTP Request node', () => {
|
||||||
cy.visit(workflowsPage.url);
|
cy.visit(workflowsPage.url);
|
||||||
|
|
||||||
workflowsPage.actions.createWorkflowFromCard();
|
workflowsPage.actions.createWorkflowFromCard();
|
||||||
workflowPage.actions.addInitialNodeToCanvas('Manual Trigger');
|
workflowPage.actions.addInitialNodeToCanvas('Manual');
|
||||||
workflowPage.actions.addNodeToCanvas('HTTP Request');
|
workflowPage.actions.addNodeToCanvas('HTTP Request');
|
||||||
workflowPage.actions.openNode('HTTP Request');
|
workflowPage.actions.openNode('HTTP Request');
|
||||||
ndv.actions.typeIntoParameterInput('url', 'https://catfact.ninja/fact');
|
ndv.actions.typeIntoParameterInput('url', 'https://catfact.ninja/fact');
|
||||||
|
|
|
@ -10,13 +10,14 @@ describe('Expression editor modal', () => {
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
WorkflowPage.actions.visit();
|
WorkflowPage.actions.visit();
|
||||||
WorkflowPage.actions.addInitialNodeToCanvas('Manual Trigger');
|
WorkflowPage.actions.addInitialNodeToCanvas('Manual');
|
||||||
WorkflowPage.actions.addNodeToCanvas('Hacker News');
|
WorkflowPage.actions.addNodeToCanvas('Hacker News');
|
||||||
WorkflowPage.actions.openNode('Hacker News');
|
WorkflowPage.actions.openNode('Hacker News');
|
||||||
WorkflowPage.actions.openExpressionEditorModal();
|
WorkflowPage.actions.openExpressionEditorModal();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should resolve primitive resolvables', () => {
|
it('should resolve primitive resolvables', () => {
|
||||||
|
WorkflowPage.getters.expressionModalInput().clear();
|
||||||
WorkflowPage.getters.expressionModalInput().type('{{ 1 + 2');
|
WorkflowPage.getters.expressionModalInput().type('{{ 1 + 2');
|
||||||
WorkflowPage.getters.expressionModalOutput().contains(/^3$/);
|
WorkflowPage.getters.expressionModalOutput().contains(/^3$/);
|
||||||
WorkflowPage.getters.expressionModalInput().clear();
|
WorkflowPage.getters.expressionModalInput().clear();
|
||||||
|
@ -31,6 +32,7 @@ describe('Expression editor modal', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should resolve object resolvables', () => {
|
it('should resolve object resolvables', () => {
|
||||||
|
WorkflowPage.getters.expressionModalInput().clear();
|
||||||
WorkflowPage.getters
|
WorkflowPage.getters
|
||||||
.expressionModalInput()
|
.expressionModalInput()
|
||||||
.type('{{ { a : 1 }', { parseSpecialCharSequences: false });
|
.type('{{ { a : 1 }', { parseSpecialCharSequences: false });
|
||||||
|
@ -45,6 +47,7 @@ describe('Expression editor modal', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should resolve array resolvables', () => {
|
it('should resolve array resolvables', () => {
|
||||||
|
WorkflowPage.getters.expressionModalInput().clear();
|
||||||
WorkflowPage.getters.expressionModalInput().type('{{ [1, 2, 3]');
|
WorkflowPage.getters.expressionModalInput().type('{{ [1, 2, 3]');
|
||||||
WorkflowPage.getters.expressionModalOutput().contains(/^\[Array: \[1,2,3\]\]$/);
|
WorkflowPage.getters.expressionModalOutput().contains(/^\[Array: \[1,2,3\]\]$/);
|
||||||
|
|
||||||
|
@ -55,7 +58,8 @@ describe('Expression editor modal', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should resolve $parameter[]', () => {
|
it('should resolve $parameter[]', () => {
|
||||||
|
WorkflowPage.getters.expressionModalInput().clear();
|
||||||
WorkflowPage.getters.expressionModalInput().type('{{ $parameter["operation"]');
|
WorkflowPage.getters.expressionModalInput().type('{{ $parameter["operation"]');
|
||||||
WorkflowPage.getters.expressionModalOutput().contains(/^get$/);
|
WorkflowPage.getters.expressionModalOutput().contains(/^getAll$/);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,15 +0,0 @@
|
||||||
import { BasePage } from './base';
|
|
||||||
|
|
||||||
export class CanvasNode extends BasePage {
|
|
||||||
getters = {
|
|
||||||
nodes: () => cy.getByTestId('canvas-node'),
|
|
||||||
nodeByName: (nodeName: string) =>
|
|
||||||
this.getters.nodes().filter(`:contains("${nodeName}")`),
|
|
||||||
};
|
|
||||||
|
|
||||||
actions = {
|
|
||||||
openNode: (nodeName: string) => {
|
|
||||||
this.getters.nodeByName(nodeName).eq(0).dblclick();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -31,9 +31,6 @@ export class NodeCreator extends BasePage {
|
||||||
selectNode: (displayName: string) => {
|
selectNode: (displayName: string) => {
|
||||||
this.getters.getCreatorItem(displayName).click();
|
this.getters.getCreatorItem(displayName).click();
|
||||||
},
|
},
|
||||||
selectTab: (tab: string) => {
|
|
||||||
this.getters.nodeCreatorTabs().contains(tab).click();
|
|
||||||
},
|
|
||||||
toggleCategory: (category: string) => {
|
toggleCategory: (category: string) => {
|
||||||
this.getters.getCreatorItem(category).click();
|
this.getters.getCreatorItem(category).click();
|
||||||
},
|
},
|
||||||
|
|
|
@ -9,4 +9,3 @@ export * from './settings-users';
|
||||||
export * from './settings-log-streaming';
|
export * from './settings-log-streaming';
|
||||||
export * from './sidebar';
|
export * from './sidebar';
|
||||||
export * from './ndv';
|
export * from './ndv';
|
||||||
export * from './canvas-node';
|
|
||||||
|
|
|
@ -19,7 +19,7 @@ export class NDV extends BasePage {
|
||||||
digital: () => cy.getByTestId('ndv-run-data-display-mode'),
|
digital: () => cy.getByTestId('ndv-run-data-display-mode'),
|
||||||
pinDataButton: () => cy.getByTestId('ndv-pin-data'),
|
pinDataButton: () => cy.getByTestId('ndv-pin-data'),
|
||||||
editPinnedDataButton: () => cy.getByTestId('ndv-edit-pinned-data'),
|
editPinnedDataButton: () => cy.getByTestId('ndv-edit-pinned-data'),
|
||||||
pinnedDataEditor: () => this.getters.outputPanel().find('.monaco-editor'),
|
pinnedDataEditor: () => this.getters.outputPanel().find('.monaco-editor[role=code]'),
|
||||||
runDataPaneHeader: () => cy.getByTestId('run-data-pane-header'),
|
runDataPaneHeader: () => cy.getByTestId('run-data-pane-header'),
|
||||||
savePinnedDataButton: () => this.getters.runDataPaneHeader().find('button').contains('Save'),
|
savePinnedDataButton: () => this.getters.runDataPaneHeader().find('button').contains('Save'),
|
||||||
outputTableRows: () => this.getters.outputDataContainer().find('table tr'),
|
outputTableRows: () => this.getters.outputDataContainer().find('table tr'),
|
||||||
|
@ -71,10 +71,9 @@ export class NDV extends BasePage {
|
||||||
setPinnedData: (data: object) => {
|
setPinnedData: (data: object) => {
|
||||||
this.getters.editPinnedDataButton().click();
|
this.getters.editPinnedDataButton().click();
|
||||||
|
|
||||||
const editor = this.getters.pinnedDataEditor();
|
this.getters.pinnedDataEditor().click();
|
||||||
editor.click();
|
this.getters.pinnedDataEditor().type(`{selectall}{backspace}`);
|
||||||
editor.type(`{selectall}{backspace}`);
|
this.getters.pinnedDataEditor().type(JSON.stringify(data).replace(new RegExp('{', 'g'), '{{}'));
|
||||||
editor.type(JSON.stringify(data).replace(new RegExp('{', 'g'), '{{}'));
|
|
||||||
|
|
||||||
this.actions.savePinnedData();
|
this.actions.savePinnedData();
|
||||||
},
|
},
|
||||||
|
|
|
@ -24,7 +24,7 @@ export class WorkflowPage extends BasePage {
|
||||||
canvasPlusButton: () => cy.getByTestId('canvas-plus-button'),
|
canvasPlusButton: () => cy.getByTestId('canvas-plus-button'),
|
||||||
canvasNodes: () => cy.getByTestId('canvas-node'),
|
canvasNodes: () => cy.getByTestId('canvas-node'),
|
||||||
canvasNodeByName: (nodeName: string) =>
|
canvasNodeByName: (nodeName: string) =>
|
||||||
this.getters.canvasNodes().filter(`:contains("${nodeName}")`),
|
this.getters.canvasNodes().filter(`:contains(${nodeName})`),
|
||||||
getEndpointSelector: (type: 'input' | 'output' | 'plus', nodeName: string, index = 0) => {
|
getEndpointSelector: (type: 'input' | 'output' | 'plus', nodeName: string, index = 0) => {
|
||||||
return `[data-endpoint-name='${nodeName}'][data-endpoint-type='${type}'][data-input-index='${index}']`;
|
return `[data-endpoint-name='${nodeName}'][data-endpoint-type='${type}'][data-input-index='${index}']`;
|
||||||
},
|
},
|
||||||
|
@ -124,6 +124,7 @@ export class WorkflowPage extends BasePage {
|
||||||
nodeDisplayName: string,
|
nodeDisplayName: string,
|
||||||
plusButtonClick = true,
|
plusButtonClick = true,
|
||||||
preventNdvClose?: boolean,
|
preventNdvClose?: boolean,
|
||||||
|
action?: string,
|
||||||
) => {
|
) => {
|
||||||
if (plusButtonClick) {
|
if (plusButtonClick) {
|
||||||
this.getters.nodeCreatorPlusButton().click();
|
this.getters.nodeCreatorPlusButton().click();
|
||||||
|
@ -131,11 +132,21 @@ export class WorkflowPage extends BasePage {
|
||||||
|
|
||||||
this.getters.nodeCreatorSearchBar().type(nodeDisplayName);
|
this.getters.nodeCreatorSearchBar().type(nodeDisplayName);
|
||||||
this.getters.nodeCreatorSearchBar().type('{enter}');
|
this.getters.nodeCreatorSearchBar().type('{enter}');
|
||||||
|
cy.wait(500)
|
||||||
|
cy.get('body').then((body) => {
|
||||||
|
if(body.find('[data-test-id=node-creator]').length > 0) {
|
||||||
|
if(action) {
|
||||||
|
cy.contains(action).click()
|
||||||
|
} else {
|
||||||
|
cy.getByTestId('item-iterator-item').eq(1).click()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
if (!preventNdvClose) cy.get('body').type('{esc}');
|
if (!preventNdvClose) cy.get('body').type('{esc}');
|
||||||
},
|
},
|
||||||
openNode: (nodeTypeName: string) => {
|
openNode: (nodeTypeName: string) => {
|
||||||
this.getters.canvasNodeByName(nodeTypeName).dblclick();
|
this.getters.canvasNodeByName(nodeTypeName).first().dblclick();
|
||||||
},
|
},
|
||||||
openExpressionEditorModal: () => {
|
openExpressionEditorModal: () => {
|
||||||
cy.contains('Expression').invoke('show').click();
|
cy.contains('Expression').invoke('show').click();
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
<div>
|
<div>
|
||||||
<div :class="$style.details">
|
<div :class="$style.details">
|
||||||
<span :class="$style.name" v-text="title" data-test-id="node-creator-item-name" />
|
<span :class="$style.name" v-text="title" data-test-id="node-creator-item-name" />
|
||||||
<trigger-icon v-if="isTrigger" :class="$style.triggerIcon" />
|
<font-awesome-icon icon="bolt" v-if="isTrigger" size="xs" :class="$style.triggerIcon" />
|
||||||
<n8n-tooltip
|
<n8n-tooltip
|
||||||
v-if="!!$slots.tooltip"
|
v-if="!!$slots.tooltip"
|
||||||
placement="top"
|
placement="top"
|
||||||
|
@ -36,7 +36,6 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
|
||||||
import TriggerIcon from './TriggerIcon.vue';
|
|
||||||
import N8nTooltip from '../N8nTooltip';
|
import N8nTooltip from '../N8nTooltip';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
|
@ -60,14 +59,14 @@ defineEmits<{
|
||||||
align-items: center;
|
align-items: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
padding: 11px 8px 11px 0;
|
padding: var(--spacing-xs) var(--spacing-2xs) var(--spacing-xs) 0;
|
||||||
|
|
||||||
&.hasAction {
|
&.hasAction {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.creatorNode:hover .panelIcon {
|
.creatorNode:hover .panelIcon {
|
||||||
color: var(--color-text-light);
|
color: var(--action-arrow-color-hover, var(--color-text-light));
|
||||||
}
|
}
|
||||||
|
|
||||||
.panelIcon {
|
.panelIcon {
|
||||||
|
@ -76,7 +75,7 @@ defineEmits<{
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-left: var(--spacing-2xs);
|
margin-left: var(--spacing-2xs);
|
||||||
color: var(--color-text-lighter);
|
color: var(--action-arrow-color, var(--color-text-lighter));
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
|
@ -110,11 +109,12 @@ defineEmits<{
|
||||||
font-size: var(--font-size-2xs);
|
font-size: var(--font-size-2xs);
|
||||||
line-height: 1rem;
|
line-height: 1rem;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
color: var(--node-creator-description-colo, var(--color-text-base));
|
color: var(--node-creator-description-colos, var(--color-text-base));
|
||||||
}
|
}
|
||||||
|
|
||||||
.triggerIcon {
|
.triggerIcon {
|
||||||
margin-left: var(--spacing-2xs);
|
margin-left: var(--spacing-3xs);
|
||||||
|
color: var(--color-primary);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
|
@ -1,65 +0,0 @@
|
||||||
<template>
|
|
||||||
<span :class="$style.trigger">
|
|
||||||
<svg
|
|
||||||
width="36px"
|
|
||||||
height="36px"
|
|
||||||
viewBox="0 0 36 36"
|
|
||||||
version="1.1"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
|
||||||
>
|
|
||||||
<title>Trigger node</title>
|
|
||||||
<g
|
|
||||||
id="/integrations-(V1-feature)"
|
|
||||||
stroke="none"
|
|
||||||
stroke-width="1"
|
|
||||||
fill="none"
|
|
||||||
fill-rule="evenodd"
|
|
||||||
>
|
|
||||||
<g
|
|
||||||
id="Individual-node-view"
|
|
||||||
transform="translate(-304.000000, -137.000000)"
|
|
||||||
fill-rule="nonzero"
|
|
||||||
>
|
|
||||||
<g id="left-column" transform="translate(120.000000, 131.000000)">
|
|
||||||
<g id="trigger-badge" transform="translate(178.000000, 0.000000)">
|
|
||||||
<g id="trigger-icon" transform="translate(6.857143, 6.857143)">
|
|
||||||
<g id="Icon" transform="translate(8.571429, 0.000000)" fill="#FF6150">
|
|
||||||
<polygon
|
|
||||||
id="Icon-Path"
|
|
||||||
points="7.14285714 21.4285714 0 21.4285714 10 1.42857143 10 12.8571429 17.1428571 12.8571429 7.14285714 32.8571429"
|
|
||||||
></polygon>
|
|
||||||
</g>
|
|
||||||
<rect id="ViewBox" x="0" y="0" width="34.2857143" height="34.2857143"></rect>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
export default {
|
|
||||||
name: 'TriggerIcon',
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" module>
|
|
||||||
.trigger {
|
|
||||||
background-color: var(--trigger-icon-background-color, var(--color-background-xlight));
|
|
||||||
border: 1px solid var(--trigger-icon-border-color, var(--color-background-xlight));
|
|
||||||
border-radius: 4px;
|
|
||||||
height: 16px;
|
|
||||||
width: 16px;
|
|
||||||
display: inline-block;
|
|
||||||
vertical-align: middle;
|
|
||||||
line-height: 16px;
|
|
||||||
|
|
||||||
> svg {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -12,7 +12,7 @@
|
||||||
<!-- ElementUI tooltip is prone to memory-leaking so we only render it if we really need it -->
|
<!-- ElementUI tooltip is prone to memory-leaking so we only render it if we really need it -->
|
||||||
<n8n-tooltip placement="top" :disabled="!showTooltip" v-if="showTooltip">
|
<n8n-tooltip placement="top" :disabled="!showTooltip" v-if="showTooltip">
|
||||||
<template #content>{{ nodeTypeName }}</template>
|
<template #content>{{ nodeTypeName }}</template>
|
||||||
<div v-if="type !== 'unknown'" :class="$style['icon']">
|
<div v-if="type !== 'unknown'" :class="$style.icon">
|
||||||
<img v-if="type === 'file'" :src="src" :class="$style.nodeIconImage" />
|
<img v-if="type === 'file'" :src="src" :class="$style.nodeIconImage" />
|
||||||
<font-awesome-icon v-else :icon="name" :style="fontStyleData" />
|
<font-awesome-icon v-else :icon="name" :style="fontStyleData" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -103,11 +103,11 @@ export default Vue.extend({
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
.nodeIconWrapper {
|
.nodeIconWrapper {
|
||||||
width: 26px;
|
width: var(--node-icon-size, 26px);
|
||||||
height: 26px;
|
height: var(--node-icon-size, 26px);
|
||||||
border-radius: var(--border-radius-small);
|
border-radius: var(--border-radius-small);
|
||||||
color: #444;
|
color: var(--node-icon-color, #444);
|
||||||
line-height: 26px;
|
line-height: var(--node-icon-size, 26px);
|
||||||
font-size: 1.1em;
|
font-size: 1.1em;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 88 KiB |
|
@ -31,8 +31,8 @@ import {
|
||||||
IExecutionsSummary,
|
IExecutionsSummary,
|
||||||
IAbstractEventMessage,
|
IAbstractEventMessage,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { FAKE_DOOR_FEATURES } from './constants';
|
|
||||||
import { SignInType } from './constants';
|
import { SignInType } from './constants';
|
||||||
|
import { FAKE_DOOR_FEATURES, TRIGGER_NODE_FILTER, REGULAR_NODE_FILTER } from './constants';
|
||||||
import { BulkCommand, Undoable } from '@/models/history';
|
import { BulkCommand, Undoable } from '@/models/history';
|
||||||
|
|
||||||
export * from 'n8n-design-system/types';
|
export * from 'n8n-design-system/types';
|
||||||
|
@ -758,13 +758,15 @@ export interface ISubcategoryItemProps {
|
||||||
subcategory: string;
|
subcategory: string;
|
||||||
description: string;
|
description: string;
|
||||||
key?: string;
|
key?: string;
|
||||||
|
iconType: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
defaults?: INodeParameters;
|
defaults?: INodeParameters;
|
||||||
iconData?: {
|
}
|
||||||
type: string;
|
export interface ViewItemProps {
|
||||||
icon?: string;
|
withTopBorder: boolean;
|
||||||
fileBuffer?: string;
|
title: string;
|
||||||
};
|
description: string;
|
||||||
|
icon: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface INodeItemProps {
|
export interface INodeItemProps {
|
||||||
|
@ -779,10 +781,11 @@ export interface IActionItemProps {
|
||||||
|
|
||||||
export interface ICategoryItemProps {
|
export interface ICategoryItemProps {
|
||||||
expanded: boolean;
|
expanded: boolean;
|
||||||
|
category: string;
|
||||||
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateElementBase {
|
export interface CreateElementBase {
|
||||||
category: string;
|
|
||||||
key: string;
|
key: string;
|
||||||
includedByTrigger?: boolean;
|
includedByTrigger?: boolean;
|
||||||
includedByRegular?: boolean;
|
includedByRegular?: boolean;
|
||||||
|
@ -790,6 +793,7 @@ export interface CreateElementBase {
|
||||||
|
|
||||||
export interface NodeCreateElement extends CreateElementBase {
|
export interface NodeCreateElement extends CreateElementBase {
|
||||||
type: 'node';
|
type: 'node';
|
||||||
|
category?: string[];
|
||||||
properties: INodeItemProps;
|
properties: INodeItemProps;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -802,9 +806,14 @@ export interface SubcategoryCreateElement extends CreateElementBase {
|
||||||
type: 'subcategory';
|
type: 'subcategory';
|
||||||
properties: ISubcategoryItemProps;
|
properties: ISubcategoryItemProps;
|
||||||
}
|
}
|
||||||
|
export interface ViewCreateElement extends CreateElementBase {
|
||||||
|
type: 'view';
|
||||||
|
properties: ViewItemProps;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ActionCreateElement extends CreateElementBase {
|
export interface ActionCreateElement extends CreateElementBase {
|
||||||
type: 'action';
|
type: 'action';
|
||||||
|
category: string;
|
||||||
properties: IActionItemProps;
|
properties: IActionItemProps;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -812,6 +821,7 @@ export type INodeCreateElement =
|
||||||
| NodeCreateElement
|
| NodeCreateElement
|
||||||
| CategoryCreateElement
|
| CategoryCreateElement
|
||||||
| SubcategoryCreateElement
|
| SubcategoryCreateElement
|
||||||
|
| ViewCreateElement
|
||||||
| ActionCreateElement;
|
| ActionCreateElement;
|
||||||
|
|
||||||
export interface ICategoriesWithNodes {
|
export interface ICategoriesWithNodes {
|
||||||
|
@ -1110,13 +1120,13 @@ export type IFakeDoorLocation =
|
||||||
| 'credentialsModal'
|
| 'credentialsModal'
|
||||||
| 'workflowShareModal';
|
| 'workflowShareModal';
|
||||||
|
|
||||||
export type INodeFilterType = 'Regular' | 'Trigger' | 'All';
|
export type INodeFilterType = typeof REGULAR_NODE_FILTER | typeof TRIGGER_NODE_FILTER;
|
||||||
|
|
||||||
export interface INodeCreatorState {
|
export interface INodeCreatorState {
|
||||||
itemsFilter: string;
|
itemsFilter: string;
|
||||||
showTabs: boolean;
|
|
||||||
showScrim: boolean;
|
showScrim: boolean;
|
||||||
selectedType: INodeFilterType;
|
rootViewHistory: INodeFilterType[];
|
||||||
|
selectedView: INodeFilterType;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ISettingsState {
|
export interface ISettingsState {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="node-wrapper"
|
:class="{ 'node-wrapper': true, 'node-wrapper--trigger': isTriggerNode }"
|
||||||
:style="nodePosition"
|
:style="nodePosition"
|
||||||
:id="nodeId"
|
:id="nodeId"
|
||||||
data-test-id="canvas-node"
|
data-test-id="canvas-node"
|
||||||
|
@ -22,6 +22,9 @@
|
||||||
v-touch:start="touchStart"
|
v-touch:start="touchStart"
|
||||||
v-touch:end="touchEnd"
|
v-touch:end="touchEnd"
|
||||||
>
|
>
|
||||||
|
<i class="trigger-icon">
|
||||||
|
<font-awesome-icon icon="bolt" size="lg" v-if="isTriggerNode" />
|
||||||
|
</i>
|
||||||
<div
|
<div
|
||||||
v-if="!data.disabled"
|
v-if="!data.disabled"
|
||||||
:class="{ 'node-info-icon': true, 'shift-icon': shiftOutputCount }"
|
:class="{ 'node-info-icon': true, 'shift-icon': shiftOutputCount }"
|
||||||
|
@ -196,6 +199,7 @@ import { useWorkflowsStore } from '@/stores/workflows';
|
||||||
import { useNDVStore } from '@/stores/ndv';
|
import { useNDVStore } from '@/stores/ndv';
|
||||||
import { useNodeTypesStore } from '@/stores/nodeTypes';
|
import { useNodeTypesStore } from '@/stores/nodeTypes';
|
||||||
import { EnableNodeToggleCommand } from '@/models/history';
|
import { EnableNodeToggleCommand } from '@/models/history';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
|
||||||
|
|
||||||
export default mixins(
|
export default mixins(
|
||||||
externalHooks,
|
externalHooks,
|
||||||
|
@ -208,6 +212,7 @@ export default mixins(
|
||||||
name: 'Node',
|
name: 'Node',
|
||||||
components: {
|
components: {
|
||||||
TitledList,
|
TitledList,
|
||||||
|
FontAwesomeIcon,
|
||||||
NodeIcon,
|
NodeIcon,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
@ -673,7 +678,6 @@ export default mixins(
|
||||||
border: 2px solid var(--color-foreground-xdark);
|
border: 2px solid var(--color-foreground-xdark);
|
||||||
border-radius: var(--border-radius-large);
|
border-radius: var(--border-radius-large);
|
||||||
background-color: var(--color-background-xlight);
|
background-color: var(--color-background-xlight);
|
||||||
|
|
||||||
&.executing {
|
&.executing {
|
||||||
background-color: var(--color-primary-tint-3) !important;
|
background-color: var(--color-primary-tint-3) !important;
|
||||||
|
|
||||||
|
@ -793,6 +797,22 @@ export default mixins(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
&--trigger .node-default .node-box {
|
||||||
|
border-radius: 32px 8px 8px 32px;
|
||||||
|
}
|
||||||
|
.trigger-icon {
|
||||||
|
position: absolute;
|
||||||
|
right: 100%;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
margin: auto;
|
||||||
|
color: var(--color-primary);
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: fit-content;
|
||||||
|
// Increase click radius of the bolt icon
|
||||||
|
padding: var(--spacing-2xs);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-background {
|
.select-background {
|
||||||
|
@ -810,6 +830,10 @@ export default mixins(
|
||||||
top: -8px !important;
|
top: -8px !important;
|
||||||
height: 116px;
|
height: 116px;
|
||||||
width: 116px !important;
|
width: 116px !important;
|
||||||
|
|
||||||
|
.node-wrapper--trigger & {
|
||||||
|
border-radius: 36px 8px 8px 36px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.disabled-linethrough {
|
.disabled-linethrough {
|
||||||
|
|
|
@ -5,111 +5,78 @@
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
data-test-id="categorized-items"
|
data-test-id="categorized-items"
|
||||||
:class="$style.categorizedItems"
|
:class="$style.categorizedItems"
|
||||||
:key="`${activeSubcategoryTitle}_transition`"
|
:key="`${activeSubcategoryTitle + selectedViewType}_transition`"
|
||||||
@keydown.capture="nodeFilterKeyDown"
|
@keydown.capture="nodeFilterKeyDown"
|
||||||
>
|
>
|
||||||
<div v-if="$slots.header">
|
|
||||||
<slot name="header" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
:class="$style.subcategoryHeader"
|
:class="{
|
||||||
v-if="activeSubcategory"
|
[$style.header]: true,
|
||||||
|
[$style.headerWithBackground]: activeSubcategory,
|
||||||
|
}"
|
||||||
data-test-id="categorized-items-subcategory"
|
data-test-id="categorized-items-subcategory"
|
||||||
>
|
>
|
||||||
<button :class="$style.subcategoryBackButton" @click="onSubcategoryClose">
|
<button
|
||||||
|
:class="$style.backButton"
|
||||||
|
@click="onBackButton"
|
||||||
|
v-if="isViewNavigated || activeSubcategory"
|
||||||
|
>
|
||||||
<font-awesome-icon :class="$style.subcategoryBackIcon" icon="arrow-left" size="2x" />
|
<font-awesome-icon :class="$style.subcategoryBackIcon" icon="arrow-left" size="2x" />
|
||||||
</button>
|
</button>
|
||||||
<node-icon
|
<div v-if="isRootView && $slots.header">
|
||||||
v-if="showSubcategoryIcon && activeSubcategory.properties.nodeType"
|
<slot name="header" />
|
||||||
:class="$style.nodeIcon"
|
</div>
|
||||||
:nodeType="activeSubcategory.properties.nodeType"
|
<template v-if="activeSubcategory">
|
||||||
:size="16"
|
<n8n-node-icon
|
||||||
:shrink="false"
|
:class="$style.nodeIcon"
|
||||||
/>
|
v-if="showSubcategoryIcon && activeSubcategory.properties.icon"
|
||||||
<span v-text="activeSubcategoryTitle" />
|
:type="activeSubcategory.properties.iconType || 'unknown'"
|
||||||
|
:src="activeSubcategory.properties.icon"
|
||||||
|
:name="activeSubcategory.properties.icon"
|
||||||
|
:color="activeSubcategory.properties.color"
|
||||||
|
:circle="false"
|
||||||
|
:showTooltip="false"
|
||||||
|
:size="16"
|
||||||
|
/>
|
||||||
|
<span v-text="activeSubcategoryTitle" />
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="isRootView && $slots.description"
|
||||||
|
:class="{
|
||||||
|
[$style.description]: true,
|
||||||
|
[$style.descriptionOffset]: isViewNavigated || activeSubcategory,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<slot name="description" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<search-bar
|
<search-bar
|
||||||
v-if="alwaysShowSearch || isSearchVisible"
|
v-if="alwaysShowSearch || isSearchVisible"
|
||||||
:key="nodeCreatorStore.selectedType"
|
:key="nodeCreatorStore.selectedView"
|
||||||
:value="nodeCreatorStore.itemsFilter"
|
:value="searchFilter"
|
||||||
:placeholder="
|
:placeholder="
|
||||||
searchPlaceholder
|
searchPlaceholder
|
||||||
? searchPlaceholder
|
? searchPlaceholder
|
||||||
: $locale.baseText('nodeCreator.searchBar.searchNodes')
|
: $locale.baseText('nodeCreator.searchBar.searchNodes')
|
||||||
"
|
"
|
||||||
ref="searchBar"
|
|
||||||
@input="onNodeFilterChange"
|
@input="onNodeFilterChange"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<template v-if="searchFilter.length > 0 && filteredNodeTypes.length === 0">
|
<div :class="$style.scrollable" ref="scrollableContainer">
|
||||||
<no-results
|
|
||||||
data-test-id="categorized-no-results"
|
|
||||||
:showRequest="
|
|
||||||
!$slots.noResultsTitle && !$slots.noResultsAction && filteredAllNodeTypes.length === 0
|
|
||||||
"
|
|
||||||
:show-icon="
|
|
||||||
!$slots.noResultsTitle && !$slots.noResultsAction && filteredAllNodeTypes.length === 0
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<template v-if="$slots.noResultsTitle" #title>
|
|
||||||
<slot name="noResultsTitle" />
|
|
||||||
</template>
|
|
||||||
<!-- There are results in other sub-categories/tabs -->
|
|
||||||
<template v-else-if="filteredAllNodeTypes.length > 0" #title>
|
|
||||||
<p v-html="$locale.baseText('nodeCreator.noResults.clickToSeeResults')" />
|
|
||||||
</template>
|
|
||||||
<template v-else #title>
|
|
||||||
<p v-text="$locale.baseText('nodeCreator.noResults.weDidntMakeThatYet')" />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-if="$slots.noResultsAction" #action>
|
|
||||||
<slot name="noResultsAction" />
|
|
||||||
</template>
|
|
||||||
<template v-else-if="filteredAllNodeTypes.length === 0" #action>
|
|
||||||
{{ $locale.baseText('nodeCreator.noResults.dontWorryYouCanProbablyDoItWithThe') }}
|
|
||||||
<n8n-link
|
|
||||||
@click="selectHttpRequest"
|
|
||||||
v-if="[REGULAR_NODE_FILTER, ALL_NODE_FILTER].includes(nodeCreatorStore.selectedType)"
|
|
||||||
>
|
|
||||||
{{ $locale.baseText('nodeCreator.noResults.httpRequest') }}
|
|
||||||
</n8n-link>
|
|
||||||
<template v-if="nodeCreatorStore.selectedType === ALL_NODE_FILTER">
|
|
||||||
{{ $locale.baseText('nodeCreator.noResults.or') }}
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<n8n-link
|
|
||||||
@click="selectWebhook"
|
|
||||||
v-if="[TRIGGER_NODE_FILTER, ALL_NODE_FILTER].includes(nodeCreatorStore.selectedType)"
|
|
||||||
>
|
|
||||||
{{ $locale.baseText('nodeCreator.noResults.webhook') }}
|
|
||||||
</n8n-link>
|
|
||||||
{{ $locale.baseText('nodeCreator.noResults.node') }}
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<n8n-link
|
|
||||||
@click="selectWebhook"
|
|
||||||
v-if="[TRIGGER_NODE_FILTER, ALL_NODE_FILTER].includes(nodeCreatorStore.selectedType)"
|
|
||||||
>
|
|
||||||
{{ $locale.baseText('nodeCreator.noResults.webhook') }}
|
|
||||||
{{ $locale.baseText('nodeCreator.noResults.node') }}
|
|
||||||
</n8n-link>
|
|
||||||
</no-results>
|
|
||||||
</template>
|
|
||||||
<div :class="$style.scrollable" ref="scrollableContainer" v-else>
|
|
||||||
<item-iterator
|
<item-iterator
|
||||||
:elements="searchFilter.length === 0 ? renderedItems : filteredNodeTypes"
|
:elements="searchFilter.length === 0 ? renderedItems : mergedFilteredNodes"
|
||||||
:activeIndex="activeSubcategory ? activeSubcategoryIndex : activeIndex"
|
:activeIndex="activeSubcategory ? activeSubcategoryIndex : activeIndex"
|
||||||
:with-actions-getter="withActionsGetter"
|
:with-actions-getter="withActionsGetter"
|
||||||
:lazyRender="lazyRender"
|
:with-description-getter="withDescriptionGetter"
|
||||||
:enable-global-categories-counter="enableGlobalCategoriesCounter"
|
:lazyRender="true"
|
||||||
@selected="selected"
|
@selected="selected"
|
||||||
@actionsOpen="$listeners.actionsOpen"
|
@actionsOpen="$listeners.actionsOpen"
|
||||||
@nodeTypeSelected="$listeners.nodeTypeSelected"
|
@nodeTypeSelected="$listeners.nodeTypeSelected"
|
||||||
>
|
/>
|
||||||
</item-iterator>
|
<div v-if="searchFilter.length > 0 && mergedFilteredNodes.length === 0">
|
||||||
<div :class="$style.footer" v-if="$slots.footer">
|
<slot name="noResults" />
|
||||||
|
</div>
|
||||||
|
<div :class="$style.footer" v-else-if="$slots.footer">
|
||||||
<slot name="footer" />
|
<slot name="footer" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -121,7 +88,6 @@
|
||||||
import {
|
import {
|
||||||
computed,
|
computed,
|
||||||
reactive,
|
reactive,
|
||||||
onMounted,
|
|
||||||
watch,
|
watch,
|
||||||
getCurrentInstance,
|
getCurrentInstance,
|
||||||
toRefs,
|
toRefs,
|
||||||
|
@ -130,30 +96,19 @@ import {
|
||||||
nextTick,
|
nextTick,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import camelcase from 'lodash.camelcase';
|
import camelcase from 'lodash.camelcase';
|
||||||
|
|
||||||
import { externalHooks } from '@/mixins/externalHooks';
|
import { externalHooks } from '@/mixins/externalHooks';
|
||||||
import useGlobalLinkActions from '@/composables/useGlobalLinkActions';
|
|
||||||
import { INodeTypeDescription } from 'n8n-workflow';
|
import { INodeTypeDescription } from 'n8n-workflow';
|
||||||
import ItemIterator from './ItemIterator.vue';
|
import ItemIterator from './ItemIterator.vue';
|
||||||
import NoResults from './NoResults.vue';
|
|
||||||
import SearchBar from './SearchBar.vue';
|
import SearchBar from './SearchBar.vue';
|
||||||
import NodeIcon from '@/components/NodeIcon.vue';
|
|
||||||
import {
|
import {
|
||||||
INodeCreateElement,
|
INodeCreateElement,
|
||||||
ISubcategoryItemProps,
|
ISubcategoryItemProps,
|
||||||
ICategoryItemProps,
|
ICategoryItemProps,
|
||||||
ICategoriesWithNodes,
|
|
||||||
SubcategoryCreateElement,
|
SubcategoryCreateElement,
|
||||||
NodeCreateElement,
|
NodeCreateElement,
|
||||||
|
CategoryCreateElement,
|
||||||
|
INodeItemProps,
|
||||||
} from '@/Interface';
|
} from '@/Interface';
|
||||||
import {
|
|
||||||
WEBHOOK_NODE_TYPE,
|
|
||||||
HTTP_REQUEST_NODE_TYPE,
|
|
||||||
ALL_NODE_FILTER,
|
|
||||||
TRIGGER_NODE_FILTER,
|
|
||||||
REGULAR_NODE_FILTER,
|
|
||||||
NODE_TYPE_COUNT_MAPPER,
|
|
||||||
} from '@/constants';
|
|
||||||
import { BaseTextKey } from '@/plugins/i18n';
|
import { BaseTextKey } from '@/plugins/i18n';
|
||||||
import { sublimeSearch, matchesNodeType, matchesSelectType } from '@/utils';
|
import { sublimeSearch, matchesNodeType, matchesSelectType } from '@/utils';
|
||||||
import { useWorkflowsStore } from '@/stores/workflows';
|
import { useWorkflowsStore } from '@/stores/workflows';
|
||||||
|
@ -161,45 +116,38 @@ import { useRootStore } from '@/stores/n8nRootStore';
|
||||||
import { useNodeCreatorStore } from '@/stores/nodeCreator';
|
import { useNodeCreatorStore } from '@/stores/nodeCreator';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
flatten?: boolean;
|
|
||||||
filterByType?: boolean;
|
|
||||||
showSubcategoryIcon?: boolean;
|
showSubcategoryIcon?: boolean;
|
||||||
alwaysShowSearch?: boolean;
|
alwaysShowSearch?: boolean;
|
||||||
expandAllCategories?: boolean;
|
hideOtherCategoryItems?: boolean;
|
||||||
enableGlobalCategoriesCounter?: boolean;
|
|
||||||
lazyRender?: boolean;
|
lazyRender?: boolean;
|
||||||
searchPlaceholder?: string;
|
searchPlaceholder?: string;
|
||||||
withActionsGetter?: (element: NodeCreateElement) => boolean;
|
withActionsGetter?: (element: NodeCreateElement) => boolean;
|
||||||
|
withDescriptionGetter?: (element: NodeCreateElement) => boolean;
|
||||||
searchItems?: INodeCreateElement[];
|
searchItems?: INodeCreateElement[];
|
||||||
excludedSubcategories?: string[];
|
|
||||||
firstLevelItems?: INodeCreateElement[];
|
firstLevelItems?: INodeCreateElement[];
|
||||||
initialActiveCategories?: string[];
|
|
||||||
initialActiveIndex?: number;
|
|
||||||
categorizedItems: INodeCreateElement[];
|
categorizedItems: INodeCreateElement[];
|
||||||
allItems: INodeCreateElement[];
|
|
||||||
categoriesWithNodes: ICategoriesWithNodes;
|
|
||||||
subcategoryOverride?: SubcategoryCreateElement | undefined;
|
subcategoryOverride?: SubcategoryCreateElement | undefined;
|
||||||
|
allItems?: INodeCreateElement[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const OTHER_RESULT_CATEGORY = 'searchAll';
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
filterByType: true,
|
allItems: () => [],
|
||||||
searchItems: () => [],
|
searchItems: () => [],
|
||||||
excludedSubcategories: () => [],
|
|
||||||
firstLevelItems: () => [],
|
firstLevelItems: () => [],
|
||||||
initialActiveCategories: () => [],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(event: 'subcategoryClose', value: INodeCreateElement[]): void;
|
(event: 'subcategoryClose', value: INodeCreateElement[]): void;
|
||||||
(event: 'onSubcategorySelected', value: INodeCreateElement): void;
|
(event: 'onSubcategorySelected', value: INodeCreateElement): void;
|
||||||
(event: 'nodeTypeSelected', value: string[]): void;
|
(event: 'nodeTypeSelected', value: string[]): void;
|
||||||
|
|
||||||
(event: 'actionSelected', value: INodeCreateElement): void;
|
(event: 'actionSelected', value: INodeCreateElement): void;
|
||||||
(event: 'actionsOpen', value: INodeTypeDescription): void;
|
(event: 'actionsOpen', value: INodeTypeDescription): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const instance = getCurrentInstance();
|
const instance = getCurrentInstance();
|
||||||
const { registerCustomAction, unregisterCustomAction } = useGlobalLinkActions();
|
|
||||||
|
|
||||||
const { $externalHooks } = new externalHooks();
|
const { $externalHooks } = new externalHooks();
|
||||||
|
|
||||||
const { defaultLocale } = useRootStore();
|
const { defaultLocale } = useRootStore();
|
||||||
|
@ -207,7 +155,7 @@ const { workflowId } = useWorkflowsStore();
|
||||||
const nodeCreatorStore = useNodeCreatorStore();
|
const nodeCreatorStore = useNodeCreatorStore();
|
||||||
|
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
activeCategory: props.initialActiveCategories,
|
activeCategories: [] as string[],
|
||||||
// Keep track of activated subcategories so we could traverse back more than one level
|
// Keep track of activated subcategories so we could traverse back more than one level
|
||||||
activeSubcategoryHistory: [] as Array<{
|
activeSubcategoryHistory: [] as Array<{
|
||||||
scrollPosition: number;
|
scrollPosition: number;
|
||||||
|
@ -215,22 +163,23 @@ const state = reactive({
|
||||||
activeIndex: number;
|
activeIndex: number;
|
||||||
filter: string;
|
filter: string;
|
||||||
}>,
|
}>,
|
||||||
activeIndex: props.initialActiveIndex || 0,
|
activeIndex: 0,
|
||||||
activeSubcategoryIndex: 0,
|
activeSubcategoryIndex: 0,
|
||||||
ALL_NODE_FILTER,
|
|
||||||
TRIGGER_NODE_FILTER,
|
|
||||||
REGULAR_NODE_FILTER,
|
|
||||||
mainPanelContainer: null as HTMLElement | null,
|
mainPanelContainer: null as HTMLElement | null,
|
||||||
transitionDirection: 'in',
|
transitionDirection: 'in',
|
||||||
});
|
});
|
||||||
const searchBar = ref<InstanceType<typeof SearchBar>>();
|
const searchBar = ref<InstanceType<typeof SearchBar>>();
|
||||||
const scrollableContainer = ref<InstanceType<typeof HTMLElement>>();
|
const scrollableContainer = ref<InstanceType<typeof HTMLElement>>();
|
||||||
|
|
||||||
const activeSubcategory = computed<INodeCreateElement | null>(
|
const activeSubcategory = computed<INodeCreateElement | null>(() => {
|
||||||
() =>
|
return (
|
||||||
state.activeSubcategoryHistory[state.activeSubcategoryHistory.length - 1]?.subcategory || null,
|
state.activeSubcategoryHistory[state.activeSubcategoryHistory.length - 1]?.subcategory || null
|
||||||
);
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const categoriesKeys = computed(() =>
|
||||||
|
props.categorizedItems.filter((item) => item.type === 'category').map((item) => item.key),
|
||||||
|
);
|
||||||
const activeSubcategoryTitle = computed<string>(() => {
|
const activeSubcategoryTitle = computed<string>(() => {
|
||||||
if (!activeSubcategory.value || !activeSubcategory.value.properties) return '';
|
if (!activeSubcategory.value || !activeSubcategory.value.properties) return '';
|
||||||
|
|
||||||
|
@ -252,12 +201,7 @@ const activeSubcategoryTitle = computed<string>(() => {
|
||||||
|
|
||||||
const searchFilter = computed<string>(() => nodeCreatorStore.itemsFilter.toLowerCase().trim());
|
const searchFilter = computed<string>(() => nodeCreatorStore.itemsFilter.toLowerCase().trim());
|
||||||
|
|
||||||
const matchedTypeNodes = computed<INodeCreateElement[]>(() => {
|
const selectedViewType = computed(() => nodeCreatorStore.selectedView);
|
||||||
if (!props.filterByType) return props.searchItems;
|
|
||||||
return props.searchItems.filter((el: INodeCreateElement) =>
|
|
||||||
matchesSelectType(el, nodeCreatorStore.selectedType),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const filteredNodeTypes = computed<INodeCreateElement[]>(() => {
|
const filteredNodeTypes = computed<INodeCreateElement[]>(() => {
|
||||||
const filter = searchFilter.value;
|
const filter = searchFilter.value;
|
||||||
|
@ -267,121 +211,151 @@ const filteredNodeTypes = computed<INodeCreateElement[]>(() => {
|
||||||
returnItems = props.searchItems.filter((el: INodeCreateElement) => {
|
returnItems = props.searchItems.filter((el: INodeCreateElement) => {
|
||||||
return (
|
return (
|
||||||
filter &&
|
filter &&
|
||||||
matchesSelectType(el, nodeCreatorStore.selectedType) &&
|
matchesSelectType(el, nodeCreatorStore.selectedView) &&
|
||||||
matchesNodeType(el, filter)
|
matchesNodeType(el, filter)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const matchingNodes = props.filterByType
|
const matchingNodes =
|
||||||
? props.searchItems.filter((el) => matchesSelectType(el, nodeCreatorStore.selectedType))
|
subcategorizedItems.value.length > 0 ? subcategorizedItems.value : props.searchItems;
|
||||||
: props.searchItems;
|
|
||||||
|
|
||||||
const matchedCategorizedNodes = sublimeSearch<INodeCreateElement>(filter, matchingNodes, [
|
returnItems = getFilteredNodes(matchingNodes);
|
||||||
{ key: 'properties.nodeType.displayName', weight: 2 },
|
|
||||||
{ key: 'properties.nodeType.codex.alias', weight: 1 },
|
|
||||||
]);
|
|
||||||
returnItems = matchedCategorizedNodes.map(({ item }) => item);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return returnItems;
|
return returnItems;
|
||||||
});
|
});
|
||||||
|
|
||||||
const filteredAllNodeTypes = computed<INodeCreateElement[]>(() => {
|
const isViewNavigated = computed(() => nodeCreatorStore.rootViewHistory.length > 1);
|
||||||
if (filteredNodeTypes.value.length > 0) return [];
|
|
||||||
|
|
||||||
const matchedAllNodex = props.allItems.filter((el: INodeCreateElement) => {
|
const globalFilteredNodeTypes = computed<INodeCreateElement[]>(() => {
|
||||||
return searchFilter.value && el.type === 'node' && matchesNodeType(el, searchFilter.value);
|
const result = getFilteredNodes(props.allItems).reduce((acc, item) => {
|
||||||
});
|
if (acc.find((el) => trimTriggerNodeName(el.key) === trimTriggerNodeName(item.key))) {
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
|
||||||
return matchedAllNodex;
|
return [...acc, item];
|
||||||
|
}, [] as INodeCreateElement[]);
|
||||||
|
|
||||||
|
return result;
|
||||||
});
|
});
|
||||||
|
|
||||||
const categorized = computed<INodeCreateElement[]>(() => {
|
const otherCategoryNodes = computed(() => {
|
||||||
return props.categorizedItems.reduce((accu: INodeCreateElement[], el: INodeCreateElement) => {
|
const nodes = [];
|
||||||
if (
|
|
||||||
el.type === 'subcategory' &&
|
|
||||||
(props.excludedSubcategories || []).includes(
|
|
||||||
(el.properties as ISubcategoryItemProps).subcategory,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return accu;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (el.type !== 'category' && !state.activeCategory.includes(el.category)) {
|
// Get diff of nodes between `globalFilteredNodeTypes` and `filteredNodeTypes`
|
||||||
return accu;
|
for (const node of globalFilteredNodeTypes.value) {
|
||||||
}
|
const isNodeInFiltered = filteredNodeTypes.value.find(
|
||||||
|
(el) => trimTriggerNodeName(el.key) === trimTriggerNodeName(node.key),
|
||||||
|
);
|
||||||
|
|
||||||
if (!matchesSelectType(el, nodeCreatorStore.selectedType)) {
|
if (!isNodeInFiltered) nodes.push(node);
|
||||||
return accu;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (el.type === 'category') {
|
return nodes;
|
||||||
accu.push({
|
|
||||||
...el,
|
|
||||||
properties: {
|
|
||||||
expanded: state.activeCategory.includes(el.category),
|
|
||||||
},
|
|
||||||
} as INodeCreateElement);
|
|
||||||
return accu;
|
|
||||||
}
|
|
||||||
|
|
||||||
accu.push(el);
|
|
||||||
return accu;
|
|
||||||
}, []);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const mergedFilteredNodes = computed<INodeCreateElement[]>(() => {
|
||||||
|
if (props.hideOtherCategoryItems) return filteredNodeTypes.value;
|
||||||
|
|
||||||
|
const isExpanded = state.activeCategories.includes(OTHER_RESULT_CATEGORY);
|
||||||
|
const searchCategory: CategoryCreateElement = {
|
||||||
|
type: 'category',
|
||||||
|
key: OTHER_RESULT_CATEGORY,
|
||||||
|
properties: {
|
||||||
|
category: OTHER_RESULT_CATEGORY,
|
||||||
|
name: `Results in other categories (${otherCategoryNodes.value.length})`,
|
||||||
|
expanded: isExpanded,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const nodeTypes = [...filteredNodeTypes.value];
|
||||||
|
|
||||||
|
if (otherCategoryNodes.value.length > 0) {
|
||||||
|
nodeTypes.push(searchCategory);
|
||||||
|
}
|
||||||
|
if (isExpanded) {
|
||||||
|
nodeTypes.push(...otherCategoryNodes.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return nodeTypes;
|
||||||
|
});
|
||||||
|
|
||||||
|
const isRootView = computed(() => activeSubcategory.value === null);
|
||||||
|
|
||||||
const subcategorizedItems = computed<INodeCreateElement[]>(() => {
|
const subcategorizedItems = computed<INodeCreateElement[]>(() => {
|
||||||
if (!activeSubcategory.value) return [];
|
if (!activeSubcategory.value) return [];
|
||||||
|
|
||||||
const category = activeSubcategory.value.category;
|
const items = props.searchItems.filter((el: INodeCreateElement) => {
|
||||||
const subcategory = (activeSubcategory.value.properties as ISubcategoryItemProps).subcategory;
|
if (!activeSubcategory.value) return false;
|
||||||
|
|
||||||
// If no category is set, we use all categorized nodes
|
const subcategories = Object.values(
|
||||||
const nodes = category
|
(el.properties as INodeItemProps).nodeType.codex?.subcategories || {},
|
||||||
? props.categoriesWithNodes[category][subcategory].nodes
|
).flat();
|
||||||
: categorized.value;
|
return subcategories.includes(activeSubcategory.value.key);
|
||||||
|
});
|
||||||
|
|
||||||
return nodes.filter((el: INodeCreateElement) =>
|
return items.filter((el: INodeCreateElement) =>
|
||||||
matchesSelectType(el, nodeCreatorStore.selectedType),
|
matchesSelectType(el, nodeCreatorStore.selectedView),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const filteredCategorizedItems = computed<INodeCreateElement[]>(() => {
|
||||||
|
let categoriesCount = 0;
|
||||||
|
const reducedItems = props.categorizedItems.reduce(
|
||||||
|
(acc: INodeCreateElement[], el: INodeCreateElement) => {
|
||||||
|
if (el.type === 'category') {
|
||||||
|
el.properties.expanded = state.activeCategories.includes(el.key);
|
||||||
|
categoriesCount++;
|
||||||
|
return [...acc, el];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (el.type === 'action' && state.activeCategories.includes(el.category)) {
|
||||||
|
return [...acc, el];
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
// If there is only one category we don't show it
|
||||||
|
if (categoriesCount <= 1)
|
||||||
|
return reducedItems.filter((el: INodeCreateElement) => el.type !== 'category');
|
||||||
|
|
||||||
|
return reducedItems;
|
||||||
|
});
|
||||||
|
|
||||||
const renderedItems = computed<INodeCreateElement[]>(() => {
|
const renderedItems = computed<INodeCreateElement[]>(() => {
|
||||||
if (props.firstLevelItems.length > 0 && activeSubcategory.value === null)
|
if (props.firstLevelItems.length > 0 && activeSubcategory.value === null)
|
||||||
return props.firstLevelItems;
|
return props.firstLevelItems;
|
||||||
if (props.flatten) return matchedTypeNodes.value;
|
|
||||||
if (subcategorizedItems.value.length === 0) return categorized.value;
|
|
||||||
|
|
||||||
const isSingleCategory =
|
// If active subcategory is * then we show all items
|
||||||
subcategorizedItems.value.filter((item) => item.type === 'category').length === 1;
|
if (activeSubcategory.value?.key === '*') return props.searchItems;
|
||||||
return isSingleCategory ? subcategorizedItems.value.slice(1) : subcategorizedItems.value;
|
// Otherwise we show only items that match the subcategory
|
||||||
|
if (subcategorizedItems.value.length > 0) return subcategorizedItems.value;
|
||||||
|
|
||||||
|
// Finally if none of the above is true we show the categorized items
|
||||||
|
return filteredCategorizedItems.value;
|
||||||
});
|
});
|
||||||
|
|
||||||
const isSearchVisible = computed<boolean>(() => {
|
const isSearchVisible = computed<boolean>(() => {
|
||||||
if (subcategorizedItems.value.length === 0) return true;
|
if (subcategorizedItems.value.length === 0) return true;
|
||||||
|
|
||||||
let totalItems = 0;
|
return subcategorizedItems.value.length > 9;
|
||||||
for (const item of subcategorizedItems.value) {
|
|
||||||
// Category contains many nodes so we need to count all of them
|
|
||||||
// for the current selectedType
|
|
||||||
if (item.type === 'category') {
|
|
||||||
const categoryItems = props.categoriesWithNodes[item.key];
|
|
||||||
const categoryItemsCount = Object.values(categoryItems)?.[0];
|
|
||||||
const countKeys = NODE_TYPE_COUNT_MAPPER[nodeCreatorStore.selectedType];
|
|
||||||
|
|
||||||
for (const countKey of countKeys) {
|
|
||||||
totalItems += categoryItemsCount[countKey as 'triggerCount' | 'regularCount'];
|
|
||||||
}
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// If it's not category, it must be just a node item so we count it as 1
|
|
||||||
totalItems += 1;
|
|
||||||
}
|
|
||||||
return totalItems > 9;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Methods
|
// Methods
|
||||||
|
function trimTriggerNodeName(nodeName: string) {
|
||||||
|
return nodeName.toLowerCase().replace('trigger', '');
|
||||||
|
}
|
||||||
|
function getFilteredNodes(items: INodeCreateElement[]) {
|
||||||
|
// In order to support the old search we need to remove the 'trigger' part
|
||||||
|
const trimmedFilter = searchFilter.value.toLowerCase().replace('trigger', '');
|
||||||
|
return (
|
||||||
|
sublimeSearch<INodeCreateElement>(trimmedFilter, items, [
|
||||||
|
{ key: 'properties.nodeType.displayName', weight: 2 },
|
||||||
|
{ key: 'properties.nodeType.codex.alias', weight: 1 },
|
||||||
|
]) || []
|
||||||
|
).map(({ item }) => item);
|
||||||
|
}
|
||||||
function getScrollTop() {
|
function getScrollTop() {
|
||||||
return scrollableContainer.value?.scrollTop || 0;
|
return scrollableContainer.value?.scrollTop || 0;
|
||||||
}
|
}
|
||||||
|
@ -390,26 +364,10 @@ function setScrollTop(scrollTop: number) {
|
||||||
scrollableContainer.value.scrollTop = scrollTop;
|
scrollableContainer.value.scrollTop = scrollTop;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function switchToAllTabAndFilter() {
|
|
||||||
const currentFilter = nodeCreatorStore.itemsFilter;
|
|
||||||
nodeCreatorStore.setShowTabs(true);
|
|
||||||
nodeCreatorStore.setSelectedType(ALL_NODE_FILTER);
|
|
||||||
state.activeSubcategoryHistory = [];
|
|
||||||
|
|
||||||
nextTick(() => onNodeFilterChange(currentFilter));
|
|
||||||
}
|
|
||||||
|
|
||||||
function onNodeFilterChange(filter: string) {
|
function onNodeFilterChange(filter: string) {
|
||||||
nodeCreatorStore.setFilter(filter);
|
nodeCreatorStore.setFilter(filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectWebhook() {
|
|
||||||
emit('nodeTypeSelected', [WEBHOOK_NODE_TYPE]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectHttpRequest() {
|
|
||||||
emit('nodeTypeSelected', [HTTP_REQUEST_NODE_TYPE]);
|
|
||||||
}
|
|
||||||
function nodeFilterKeyDown(e: KeyboardEvent) {
|
function nodeFilterKeyDown(e: KeyboardEvent) {
|
||||||
// We only want to propagate 'Escape' as it closes the node-creator and
|
// We only want to propagate 'Escape' as it closes the node-creator and
|
||||||
// 'Tab' which toggles it
|
// 'Tab' which toggles it
|
||||||
|
@ -438,7 +396,7 @@ function nodeFilterKeyDown(e: KeyboardEvent) {
|
||||||
) {
|
) {
|
||||||
selected(activeNodeType);
|
selected(activeNodeType);
|
||||||
} else if (e.key === 'ArrowLeft') {
|
} else if (e.key === 'ArrowLeft') {
|
||||||
onSubcategoryClose();
|
onBackButton();
|
||||||
} else if (
|
} else if (
|
||||||
e.key === 'ArrowRight' &&
|
e.key === 'ArrowRight' &&
|
||||||
activeNodeType?.type === 'category' &&
|
activeNodeType?.type === 'category' &&
|
||||||
|
@ -466,6 +424,8 @@ function nodeFilterKeyDown(e: KeyboardEvent) {
|
||||||
selected(activeNodeType);
|
selected(activeNodeType);
|
||||||
} else if (e.key === 'ArrowRight' && activeNodeType?.type === 'subcategory') {
|
} else if (e.key === 'ArrowRight' && activeNodeType?.type === 'subcategory') {
|
||||||
selected(activeNodeType);
|
selected(activeNodeType);
|
||||||
|
} else if (e.key === 'ArrowRight' && activeNodeType?.type === 'view') {
|
||||||
|
selected(activeNodeType);
|
||||||
} else if (
|
} else if (
|
||||||
e.key === 'ArrowRight' &&
|
e.key === 'ArrowRight' &&
|
||||||
activeNodeType?.type === 'category' &&
|
activeNodeType?.type === 'category' &&
|
||||||
|
@ -478,20 +438,29 @@ function nodeFilterKeyDown(e: KeyboardEvent) {
|
||||||
(activeNodeType.properties as ICategoryItemProps).expanded
|
(activeNodeType.properties as ICategoryItemProps).expanded
|
||||||
) {
|
) {
|
||||||
selected(activeNodeType);
|
selected(activeNodeType);
|
||||||
|
} else if (e.key === 'ArrowLeft' && isViewNavigated.value) {
|
||||||
|
onBackButton();
|
||||||
} else if (e.key === 'ArrowRight' && ['node', 'action'].includes(activeNodeType?.type)) {
|
} else if (e.key === 'ArrowRight' && ['node', 'action'].includes(activeNodeType?.type)) {
|
||||||
selected(activeNodeType);
|
selected(activeNodeType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function selected(element: INodeCreateElement) {
|
function selected(element: INodeCreateElement) {
|
||||||
const typeHandler = {
|
const typeHandler = {
|
||||||
category: () => onCategorySelected(element.category),
|
category: () => onCategorySelected(element),
|
||||||
subcategory: () => onSubcategorySelected(element),
|
subcategory: () => onSubcategorySelected(element),
|
||||||
node: () => onNodeSelected(element as NodeCreateElement),
|
node: () => onNodeSelected(element as NodeCreateElement),
|
||||||
action: () => onActionSelected(element),
|
action: () => onActionSelected(element),
|
||||||
|
view: () => onViewSelected(element),
|
||||||
};
|
};
|
||||||
|
|
||||||
typeHandler[element.type]();
|
typeHandler[element.type]();
|
||||||
}
|
}
|
||||||
|
function onViewSelected(view: Record<string, any>) {
|
||||||
|
state.transitionDirection = 'in';
|
||||||
|
state.activeIndex = 0;
|
||||||
|
nodeCreatorStore.setSelectedView(view.key);
|
||||||
|
nodeCreatorStore.setFilter('');
|
||||||
|
}
|
||||||
|
|
||||||
function onNodeSelected(element: NodeCreateElement) {
|
function onNodeSelected(element: NodeCreateElement) {
|
||||||
const hasActions = (element.properties.nodeType?.actions?.length || 0) > 0;
|
const hasActions = (element.properties.nodeType?.actions?.length || 0) > 0;
|
||||||
|
@ -502,20 +471,19 @@ function onNodeSelected(element: NodeCreateElement) {
|
||||||
emit('nodeTypeSelected', [element.key]);
|
emit('nodeTypeSelected', [element.key]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onCategorySelected(category: string) {
|
function onCategorySelected(element: CategoryCreateElement) {
|
||||||
if (state.activeCategory.includes(category)) {
|
const categoryKey = element.properties.category;
|
||||||
state.activeCategory = state.activeCategory.filter((active: string) => active !== category);
|
if (state.activeCategories.includes(categoryKey)) {
|
||||||
|
state.activeCategories = state.activeCategories.filter(
|
||||||
|
(active: string) => active !== categoryKey,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
state.activeCategory = [...state.activeCategory, category];
|
state.activeCategories = [...state.activeCategories, categoryKey];
|
||||||
instance?.proxy.$telemetry.trackNodesPanel('nodeCreateList.onCategoryExpanded', {
|
instance?.proxy.$telemetry.trackNodesPanel('nodeCreateList.onCategoryExpanded', {
|
||||||
category_name: category,
|
category_name: categoryKey,
|
||||||
workflow_id: workflowId,
|
workflow_id: workflowId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
state.activeIndex = categorized.value.findIndex(
|
|
||||||
(el: INodeCreateElement) => el.category === category,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
function onActionSelected(element: INodeCreateElement) {
|
function onActionSelected(element: INodeCreateElement) {
|
||||||
emit('actionSelected', element);
|
emit('actionSelected', element);
|
||||||
|
@ -533,7 +501,6 @@ function onSubcategorySelected(selected: INodeCreateElement, track = true) {
|
||||||
});
|
});
|
||||||
nodeCreatorStore.setFilter('');
|
nodeCreatorStore.setFilter('');
|
||||||
emit('onSubcategorySelected', selected);
|
emit('onSubcategorySelected', selected);
|
||||||
nodeCreatorStore.setShowTabs(false);
|
|
||||||
state.activeSubcategoryIndex = 0;
|
state.activeSubcategoryIndex = 0;
|
||||||
|
|
||||||
if (track) {
|
if (track) {
|
||||||
|
@ -544,8 +511,14 @@ function onSubcategorySelected(selected: INodeCreateElement, track = true) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onSubcategoryClose() {
|
async function onBackButton() {
|
||||||
state.transitionDirection = 'out';
|
state.transitionDirection = 'out';
|
||||||
|
// Switching views
|
||||||
|
if (isRootView.value && isViewNavigated.value) {
|
||||||
|
nodeCreatorStore.closeCurrentView();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const poppedSubCategory = state.activeSubcategoryHistory.pop();
|
const poppedSubCategory = state.activeSubcategoryHistory.pop();
|
||||||
onNodeFilterChange(poppedSubCategory?.filter || '');
|
onNodeFilterChange(poppedSubCategory?.filter || '');
|
||||||
await nextTick();
|
await nextTick();
|
||||||
|
@ -556,40 +529,30 @@ async function onSubcategoryClose() {
|
||||||
await nextTick();
|
await nextTick();
|
||||||
setScrollTop(poppedSubCategory?.scrollPosition || 0);
|
setScrollTop(poppedSubCategory?.scrollPosition || 0);
|
||||||
state.activeSubcategoryIndex = poppedSubCategory?.activeIndex || 0;
|
state.activeSubcategoryIndex = poppedSubCategory?.activeIndex || 0;
|
||||||
|
|
||||||
if (!nodeCreatorStore.showScrim && state.activeSubcategoryHistory.length === 0) {
|
|
||||||
nodeCreatorStore.setShowTabs(true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.expandAllCategories,
|
|
||||||
(expandAll) => {
|
|
||||||
if (expandAll) state.activeCategory = Object.keys(props.categoriesWithNodes);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.subcategoryOverride,
|
() => props.subcategoryOverride,
|
||||||
(subcategory) => {
|
(subcategory) => {
|
||||||
if (subcategory) onSubcategorySelected(subcategory, false);
|
if (subcategory) onSubcategorySelected(subcategory, false);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
watch(
|
||||||
onMounted(() => {
|
() => props.categorizedItems,
|
||||||
registerCustomAction('showAllNodeCreatorNodes', switchToAllTabAndFilter);
|
() => {
|
||||||
});
|
state.activeCategories = [...categoriesKeys.value, OTHER_RESULT_CATEGORY];
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
nodeCreatorStore.setFilter('');
|
nodeCreatorStore.setFilter('');
|
||||||
unregisterCustomAction('showAllNodeCreatorNodes');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(filteredNodeTypes, (returnItems) => {
|
watch(filteredNodeTypes, (returnItems) => {
|
||||||
$externalHooks().run('nodeCreateList.filteredNodeTypesComputed', {
|
$externalHooks().run('nodeCreateList.filteredNodeTypesComputed', {
|
||||||
nodeFilter: nodeCreatorStore.itemsFilter,
|
nodeFilter: nodeCreatorStore.itemsFilter,
|
||||||
result: returnItems,
|
result: returnItems,
|
||||||
selectedType: nodeCreatorStore.selectedType,
|
selectedType: nodeCreatorStore.selectedView,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -609,13 +572,13 @@ watch(
|
||||||
$externalHooks().run('nodeCreateList.nodeFilterChanged', {
|
$externalHooks().run('nodeCreateList.nodeFilterChanged', {
|
||||||
oldValue,
|
oldValue,
|
||||||
newValue,
|
newValue,
|
||||||
selectedType: nodeCreatorStore.selectedType,
|
selectedType: nodeCreatorStore.selectedView,
|
||||||
filteredNodes: filteredNodeTypes.value,
|
filteredNodes: filteredNodeTypes.value,
|
||||||
});
|
});
|
||||||
instance?.proxy.$telemetry.trackNodesPanel('nodeCreateList.nodeFilterChanged', {
|
instance?.proxy.$telemetry.trackNodesPanel('nodeCreateList.nodeFilterChanged', {
|
||||||
oldValue,
|
oldValue,
|
||||||
newValue,
|
newValue,
|
||||||
selectedType: nodeCreatorStore.selectedType,
|
selectedType: nodeCreatorStore.selectedView,
|
||||||
filteredNodes: filteredNodeTypes.value,
|
filteredNodes: filteredNodeTypes.value,
|
||||||
workflow_id: workflowId,
|
workflow_id: workflowId,
|
||||||
});
|
});
|
||||||
|
@ -650,6 +613,7 @@ const { activeSubcategoryIndex, activeIndex, mainPanelContainer } = toRefs(state
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
.nodeIcon {
|
.nodeIcon {
|
||||||
|
--node-icon-size: 16px;
|
||||||
margin-right: var(--spacing-s);
|
margin-right: var(--spacing-s);
|
||||||
}
|
}
|
||||||
.categorizedItems {
|
.categorizedItems {
|
||||||
|
@ -675,31 +639,39 @@ const { activeSubcategoryIndex, activeIndex, mainPanelContainer } = toRefs(state
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
margin-top: -1px;
|
margin-top: -1px;
|
||||||
}
|
}
|
||||||
.subcategoryHeader {
|
.header {
|
||||||
border-bottom: $node-creator-border-color solid 1px;
|
|
||||||
height: 50px;
|
|
||||||
background-color: $node-creator-subcategory-panel-header-bacground-color;
|
|
||||||
|
|
||||||
font-size: var(--font-size-l);
|
font-size: var(--font-size-l);
|
||||||
font-weight: var(--font-weight-bold);
|
font-weight: var(--font-weight-bold);
|
||||||
line-height: var(--font-line-height-compact);
|
line-height: var(--font-line-height-compact);
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 11px 15px;
|
padding: var(--spacing-s) var(--spacing-s) var(--spacing-2xs);
|
||||||
}
|
|
||||||
|
|
||||||
.subcategoryBackButton {
|
&.headerWithBackground {
|
||||||
|
border-bottom: $node-creator-border-color solid 1px;
|
||||||
|
height: 50px;
|
||||||
|
background-color: $node-creator-subcategory-panel-header-bacground-color;
|
||||||
|
padding: var(--spacing-s) var(--spacing-s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.description {
|
||||||
|
padding: 0 var(--spacing-s) var(--spacing-2xs) var(--spacing-s);
|
||||||
|
margin-top: -4px;
|
||||||
|
}
|
||||||
|
.descriptionOffset {
|
||||||
|
margin-left: calc(var(--spacing-xl) + var(--spacing-4xs));
|
||||||
|
}
|
||||||
|
.backButton {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: var(--spacing-s) 0;
|
padding: 0 var(--spacing-xs) 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.subcategoryBackIcon {
|
.subcategoryBackIcon {
|
||||||
color: $node-creator-arrow-color;
|
color: $node-creator-arrow-color;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
margin-right: var(--spacing-s);
|
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,34 +1,18 @@
|
||||||
<template>
|
<template>
|
||||||
<div :class="$style.category">
|
<div :class="$style.category">
|
||||||
<span :class="$style.name">
|
<span :class="$style.name" v-text="item.name" />
|
||||||
{{ renderCategoryName(item.category) }}{{ count !== undefined ? ` (${count})` : '' }}
|
<font-awesome-icon v-if="item.expanded" icon="chevron-down" :class="$style.arrow" />
|
||||||
</span>
|
|
||||||
<font-awesome-icon v-if="isExpanded" icon="chevron-down" :class="$style.arrow" />
|
|
||||||
<font-awesome-icon :class="$style.arrow" icon="chevron-up" v-else />
|
<font-awesome-icon :class="$style.arrow" icon="chevron-up" v-else />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, getCurrentInstance } from 'vue';
|
import { ICategoryItemProps } from '@/Interface';
|
||||||
import camelcase from 'lodash.camelcase';
|
|
||||||
import { CategoryName } from '@/plugins/i18n';
|
|
||||||
import { INodeCreateElement, ICategoryItemProps } from '@/Interface';
|
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
item: INodeCreateElement;
|
item: ICategoryItemProps;
|
||||||
count?: number;
|
|
||||||
}
|
|
||||||
const props = defineProps<Props>();
|
|
||||||
const instance = getCurrentInstance();
|
|
||||||
|
|
||||||
const isExpanded = computed<boolean>(() => (props.item.properties as ICategoryItemProps).expanded);
|
|
||||||
|
|
||||||
function renderCategoryName(categoryName: string) {
|
|
||||||
const camelCasedCategoryName = camelcase(categoryName) as CategoryName;
|
|
||||||
const key = `nodeCreator.categoryNames.${camelCasedCategoryName}` as const;
|
|
||||||
|
|
||||||
return instance?.proxy.$locale.exists(key) ? instance?.proxy.$locale.baseText(key) : categoryName;
|
|
||||||
}
|
}
|
||||||
|
defineProps<Props>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
|
|
|
@ -20,18 +20,15 @@
|
||||||
ref="iteratorItems"
|
ref="iteratorItems"
|
||||||
@click="wrappedEmit('selected', item)"
|
@click="wrappedEmit('selected', item)"
|
||||||
>
|
>
|
||||||
<category-item
|
<category-item v-if="item.type === 'category'" :item="item.properties" />
|
||||||
v-if="item.type === 'category'"
|
|
||||||
:item="item"
|
|
||||||
:count="enableGlobalCategoriesCounter ? getCategoryCount(item) : undefined"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<subcategory-item v-else-if="item.type === 'subcategory'" :item="item" />
|
<subcategory-item v-else-if="item.type === 'subcategory'" :item="item.properties" />
|
||||||
|
|
||||||
<node-item
|
<node-item
|
||||||
v-else-if="item.type === 'node'"
|
v-else-if="item.type === 'node'"
|
||||||
:nodeType="item.properties.nodeType"
|
:nodeType="item.properties.nodeType"
|
||||||
:allow-actions="withActionsGetter && withActionsGetter(item)"
|
:allow-actions="withActionsGetter && withActionsGetter(item)"
|
||||||
|
:allow-description="withDescriptionGetter && withDescriptionGetter(item)"
|
||||||
@dragstart="wrappedEmit('dragstart', item, $event)"
|
@dragstart="wrappedEmit('dragstart', item, $event)"
|
||||||
@dragend="wrappedEmit('dragend', item, $event)"
|
@dragend="wrappedEmit('dragend', item, $event)"
|
||||||
@nodeTypeSelected="$listeners.nodeTypeSelected"
|
@nodeTypeSelected="$listeners.nodeTypeSelected"
|
||||||
|
@ -45,6 +42,8 @@
|
||||||
@dragstart="wrappedEmit('dragstart', item, $event)"
|
@dragstart="wrappedEmit('dragstart', item, $event)"
|
||||||
@dragend="wrappedEmit('dragend', item, $event)"
|
@dragend="wrappedEmit('dragend', item, $event)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<view-item v-else-if="item.type === 'view'" :view="item.properties" />
|
||||||
</div>
|
</div>
|
||||||
<aside
|
<aside
|
||||||
v-for="item in elements.length"
|
v-for="item in elements.length"
|
||||||
|
@ -58,15 +57,13 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { INodeCreateElement, CategoryCreateElement, NodeCreateElement } from '@/Interface';
|
import { INodeCreateElement, NodeCreateElement } from '@/Interface';
|
||||||
import NodeItem from './NodeItem.vue';
|
import NodeItem from './NodeItem.vue';
|
||||||
import SubcategoryItem from './SubcategoryItem.vue';
|
import SubcategoryItem from './SubcategoryItem.vue';
|
||||||
import CategoryItem from './CategoryItem.vue';
|
import CategoryItem from './CategoryItem.vue';
|
||||||
import ActionItem from './ActionItem.vue';
|
import ActionItem from './ActionItem.vue';
|
||||||
|
import ViewItem from './ViewItem.vue';
|
||||||
import { reactive, toRefs, onMounted, watch, onUnmounted, ref } from 'vue';
|
import { reactive, toRefs, onMounted, watch, onUnmounted, ref } from 'vue';
|
||||||
import { useNodeTypesStore } from '@/stores/nodeTypes';
|
|
||||||
import { useNodeCreatorStore } from '@/stores/nodeCreator';
|
|
||||||
import { NODE_TYPE_COUNT_MAPPER } from '@/constants';
|
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
elements: INodeCreateElement[];
|
elements: INodeCreateElement[];
|
||||||
|
@ -74,6 +71,7 @@ export interface Props {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
lazyRender?: boolean;
|
lazyRender?: boolean;
|
||||||
withActionsGetter?: (element: NodeCreateElement) => boolean;
|
withActionsGetter?: (element: NodeCreateElement) => boolean;
|
||||||
|
withDescriptionGetter?: (element: NodeCreateElement) => boolean;
|
||||||
enableGlobalCategoriesCounter?: boolean;
|
enableGlobalCategoriesCounter?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -102,25 +100,6 @@ function wrappedEmit(
|
||||||
|
|
||||||
emit((event as 'selected') || 'dragstart' || 'dragend', element, $e);
|
emit((event as 'selected') || 'dragstart' || 'dragend', element, $e);
|
||||||
}
|
}
|
||||||
function getCategoryCount(item: CategoryCreateElement) {
|
|
||||||
const { categoriesWithNodes } = useNodeTypesStore();
|
|
||||||
|
|
||||||
const currentCategory = categoriesWithNodes[item.category];
|
|
||||||
const subcategories = Object.keys(currentCategory);
|
|
||||||
|
|
||||||
// We need to sum subcategories count for the curent nodeType view
|
|
||||||
// to get the total count of category
|
|
||||||
const count = subcategories.reduce((accu: number, subcategory: string) => {
|
|
||||||
const countKeys = NODE_TYPE_COUNT_MAPPER[useNodeCreatorStore().selectedType];
|
|
||||||
|
|
||||||
for (const countKey of countKeys) {
|
|
||||||
accu += currentCategory[subcategory][countKey as 'triggerCount' | 'regularCount'];
|
|
||||||
}
|
|
||||||
|
|
||||||
return accu;
|
|
||||||
}, 0);
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lazy render large items lists to prevent the browser from freezing
|
// Lazy render large items lists to prevent the browser from freezing
|
||||||
// when loading many items.
|
// when loading many items.
|
||||||
|
|
|
@ -1,100 +1,408 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="container" ref="mainPanelContainer">
|
<div :class="{ [$style.mainPanel]: true, [$style.isRoot]: isRoot }">
|
||||||
<div class="main-panel">
|
<CategorizedItems
|
||||||
<trigger-helper-panel
|
:subcategoryOverride="nodeAppSubcategory"
|
||||||
v-if="nodeCreatorStore.selectedType === TRIGGER_NODE_FILTER"
|
:alwaysShowSearch="isActionsActive"
|
||||||
@nodeTypeSelected="$listeners.nodeTypeSelected"
|
:hideOtherCategoryItems="isActionsActive"
|
||||||
>
|
:categorizedItems="computedCategorizedItems"
|
||||||
<template #header>
|
:searchItems="searchItems"
|
||||||
<type-selector />
|
:withActionsGetter="shouldShowNodeActions"
|
||||||
</template>
|
:withDescriptionGetter="shouldShowNodeDescription"
|
||||||
</trigger-helper-panel>
|
:firstLevelItems="firstLevelItems"
|
||||||
<categorized-items
|
:showSubcategoryIcon="isActionsActive"
|
||||||
v-else
|
:allItems="transformCreateElements(mergedAppNodes)"
|
||||||
enable-global-categories-counter
|
:searchPlaceholder="searchPlaceholder"
|
||||||
:categorizedItems="categorizedItems"
|
@subcategoryClose="onSubcategoryClose"
|
||||||
:categoriesWithNodes="categoriesWithNodes"
|
@onSubcategorySelected="onSubcategorySelected"
|
||||||
:searchItems="searchItems"
|
@nodeTypeSelected="onNodeTypeSelected"
|
||||||
:excludedSubcategories="[OTHER_TRIGGER_NODES_SUBCATEGORY]"
|
@actionsOpen="setActiveActionsNodeType"
|
||||||
:initialActiveCategories="[CORE_NODES_CATEGORY]"
|
@actionSelected="onActionSelected"
|
||||||
:allItems="categorizedItems"
|
>
|
||||||
@nodeTypeSelected="$listeners.nodeTypeSelected"
|
<template #noResults>
|
||||||
@actionsOpen="() => {}"
|
<no-results
|
||||||
>
|
data-test-id="categorized-no-results"
|
||||||
<template #header>
|
:showRequest="!isActionsActive"
|
||||||
<type-selector />
|
:show-icon="!isActionsActive"
|
||||||
</template>
|
>
|
||||||
</categorized-items>
|
<template #title v-if="!isActionsActive">
|
||||||
</div>
|
<p v-text="$locale.baseText('nodeCreator.noResults.weDidntMakeThatYet')" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-if="isActionsActive" #action>
|
||||||
|
<p
|
||||||
|
v-if="containsAPIAction"
|
||||||
|
v-html="getCustomAPICallHintLocale('apiCallNoResult')"
|
||||||
|
class="clickable"
|
||||||
|
@click.stop="addHttpNode(true)"
|
||||||
|
/>
|
||||||
|
<p v-else v-text="$locale.baseText('nodeCreator.noResults.noMatchingActions')" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else #action>
|
||||||
|
{{ $locale.baseText('nodeCreator.noResults.dontWorryYouCanProbablyDoItWithThe') }}
|
||||||
|
<n8n-link v-if="[REGULAR_NODE_FILTER].includes(selectedView)" @click="addHttpNode">
|
||||||
|
{{ $locale.baseText('nodeCreator.noResults.httpRequest') }}
|
||||||
|
</n8n-link>
|
||||||
|
|
||||||
|
<n8n-link v-if="[TRIGGER_NODE_FILTER].includes(selectedView)" @click="addWebHookNode()">
|
||||||
|
{{ $locale.baseText('nodeCreator.noResults.webhook') }}
|
||||||
|
</n8n-link>
|
||||||
|
{{ $locale.baseText('nodeCreator.noResults.node') }}
|
||||||
|
</template>
|
||||||
|
</no-results>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #header>
|
||||||
|
<p
|
||||||
|
v-if="isRoot && activeView && activeView.title"
|
||||||
|
v-text="activeView.title"
|
||||||
|
:class="$style.title"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #description>
|
||||||
|
<p
|
||||||
|
v-if="isRoot && activeView && activeView.description"
|
||||||
|
v-text="activeView.description"
|
||||||
|
:class="$style.description"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #footer v-if="activeNodeActions && containsAPIAction">
|
||||||
|
<span
|
||||||
|
v-html="getCustomAPICallHintLocale('apiCall')"
|
||||||
|
class="clickable"
|
||||||
|
@click.stop="addHttpNode(true)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</CategorizedItems>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { watch, getCurrentInstance, onMounted, onUnmounted } from 'vue';
|
import { reactive, toRefs, getCurrentInstance, computed, onUnmounted, ref } from 'vue';
|
||||||
import { externalHooks } from '@/mixins/externalHooks';
|
import {
|
||||||
import TriggerHelperPanel from './TriggerHelperPanel.vue';
|
INodeTypeDescription,
|
||||||
|
INodeActionTypeDescription,
|
||||||
|
INodeTypeNameVersion,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
import {
|
||||||
|
INodeCreateElement,
|
||||||
|
NodeCreateElement,
|
||||||
|
IActionItemProps,
|
||||||
|
SubcategoryCreateElement,
|
||||||
|
IUpdateInformation,
|
||||||
|
} from '@/Interface';
|
||||||
import {
|
import {
|
||||||
ALL_NODE_FILTER,
|
|
||||||
TRIGGER_NODE_FILTER,
|
|
||||||
OTHER_TRIGGER_NODES_SUBCATEGORY,
|
|
||||||
CORE_NODES_CATEGORY,
|
CORE_NODES_CATEGORY,
|
||||||
|
WEBHOOK_NODE_TYPE,
|
||||||
|
EMAIL_IMAP_NODE_TYPE,
|
||||||
|
CUSTOM_API_CALL_NAME,
|
||||||
|
HTTP_REQUEST_NODE_TYPE,
|
||||||
|
STICKY_NODE_TYPE,
|
||||||
|
REGULAR_NODE_FILTER,
|
||||||
|
TRIGGER_NODE_FILTER,
|
||||||
|
N8N_NODE_TYPE,
|
||||||
} from '@/constants';
|
} from '@/constants';
|
||||||
import CategorizedItems from './CategorizedItems.vue';
|
import CategorizedItems from './CategorizedItems.vue';
|
||||||
import TypeSelector from './TypeSelector.vue';
|
|
||||||
import { INodeCreateElement } from '@/Interface';
|
|
||||||
import { useWorkflowsStore } from '@/stores/workflows';
|
|
||||||
import { useNodeCreatorStore } from '@/stores/nodeCreator';
|
import { useNodeCreatorStore } from '@/stores/nodeCreator';
|
||||||
|
import { getCategoriesWithNodes, getCategorizedList } from '@/utils';
|
||||||
|
import { externalHooks } from '@/mixins/externalHooks';
|
||||||
import { useNodeTypesStore } from '@/stores/nodeTypes';
|
import { useNodeTypesStore } from '@/stores/nodeTypes';
|
||||||
|
import { BaseTextKey } from '@/plugins/i18n';
|
||||||
export interface Props {
|
import NoResults from './NoResults.vue';
|
||||||
searchItems?: INodeCreateElement[];
|
import { useRootStore } from '@/stores/n8nRootStore';
|
||||||
}
|
import useMainPanelView from './useMainPanelView';
|
||||||
|
|
||||||
withDefaults(defineProps<Props>(), {
|
|
||||||
searchItems: () => [],
|
|
||||||
});
|
|
||||||
|
|
||||||
const instance = getCurrentInstance();
|
const instance = getCurrentInstance();
|
||||||
const { $externalHooks } = new externalHooks();
|
|
||||||
const { workflowId } = useWorkflowsStore();
|
|
||||||
const nodeCreatorStore = useNodeCreatorStore();
|
|
||||||
const { categorizedItems, categoriesWithNodes } = useNodeTypesStore();
|
|
||||||
|
|
||||||
watch(
|
const emit = defineEmits({
|
||||||
() => nodeCreatorStore.selectedType,
|
nodeTypeSelected: (nodeTypes: string[]) => true,
|
||||||
(newValue, oldValue) => {
|
});
|
||||||
$externalHooks().run('nodeCreateList.selectedTypeChanged', {
|
|
||||||
oldValue,
|
const state = reactive({
|
||||||
newValue,
|
isRoot: true,
|
||||||
});
|
selectedSubcategory: '',
|
||||||
instance?.proxy.$telemetry.trackNodesPanel('nodeCreateList.selectedTypeChanged', {
|
activeNodeActions: null as INodeTypeDescription | null,
|
||||||
old_filter: oldValue,
|
});
|
||||||
new_filter: newValue,
|
const { baseUrl } = useRootStore();
|
||||||
workflow_id: workflowId,
|
const { $externalHooks } = new externalHooks();
|
||||||
});
|
const {
|
||||||
},
|
mergedAppNodes,
|
||||||
|
getActionData,
|
||||||
|
getNodeTypesWithManualTrigger,
|
||||||
|
setAddedNodeActionParameters,
|
||||||
|
} = useNodeCreatorStore();
|
||||||
|
const { activeView } = useMainPanelView();
|
||||||
|
const telemetry = instance?.proxy.$telemetry;
|
||||||
|
const { isTriggerNode } = useNodeTypesStore();
|
||||||
|
const containsAPIAction = computed(
|
||||||
|
() =>
|
||||||
|
state.activeNodeActions?.properties.some((p) =>
|
||||||
|
p.options?.find((o) => o.name === CUSTOM_API_CALL_NAME),
|
||||||
|
) === true,
|
||||||
);
|
);
|
||||||
|
|
||||||
onMounted(() => {
|
const selectedView = computed(() => useNodeCreatorStore().selectedView);
|
||||||
$externalHooks().run('nodeCreateList.mounted');
|
const computedCategorizedItems = computed(() => {
|
||||||
// Make sure tabs are visible on mount
|
if (isActionsActive.value) {
|
||||||
nodeCreatorStore.setShowTabs(true);
|
return sortActions(getCategorizedList(computedCategoriesWithNodes.value, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
return getCategorizedList(computedCategoriesWithNodes.value, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const nodeAppSubcategory = computed<SubcategoryCreateElement | undefined>(() => {
|
||||||
|
if (!state.activeNodeActions) return undefined;
|
||||||
|
|
||||||
|
const icon = state.activeNodeActions.iconUrl
|
||||||
|
? `${baseUrl}${state.activeNodeActions.iconUrl}`
|
||||||
|
: state.activeNodeActions.icon?.split(':')[1];
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'subcategory',
|
||||||
|
key: state.activeNodeActions.name,
|
||||||
|
properties: {
|
||||||
|
subcategory: state.activeNodeActions.displayName,
|
||||||
|
description: '',
|
||||||
|
iconType: state.activeNodeActions.iconUrl ? 'file' : 'icon',
|
||||||
|
icon,
|
||||||
|
color: state.activeNodeActions.defaults.color,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const searchPlaceholder = computed(() => {
|
||||||
|
const nodeNameTitle = state.activeNodeActions?.displayName?.trim() as string;
|
||||||
|
const actionsSearchPlaceholder = instance?.proxy.$locale.baseText(
|
||||||
|
'nodeCreator.actionsCategory.searchActions',
|
||||||
|
{ interpolate: { nodeNameTitle } },
|
||||||
|
);
|
||||||
|
|
||||||
|
return isActionsActive.value ? actionsSearchPlaceholder : undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
const filteredMergedAppNodes = computed(() => {
|
||||||
|
const WHITELISTED_APP_CORE_NODES = [EMAIL_IMAP_NODE_TYPE, WEBHOOK_NODE_TYPE];
|
||||||
|
|
||||||
|
if (isAppEventSubcategory.value)
|
||||||
|
return mergedAppNodes.filter((node) => {
|
||||||
|
const isTrigger = isTriggerNode(node.name);
|
||||||
|
const isRegularNode = !isTrigger;
|
||||||
|
const isStickyNode = node.name === STICKY_NODE_TYPE;
|
||||||
|
const isCoreNode =
|
||||||
|
node.codex?.categories?.includes(CORE_NODES_CATEGORY) &&
|
||||||
|
!WHITELISTED_APP_CORE_NODES.includes(node.name);
|
||||||
|
const hasActions = (node.actions || []).length > 0;
|
||||||
|
|
||||||
|
// Never show core nodes and sticky node in the Apps subcategory
|
||||||
|
if (isCoreNode || isStickyNode) return false;
|
||||||
|
|
||||||
|
// Only show nodes without action within their view
|
||||||
|
if (!hasActions) {
|
||||||
|
return isRegularNode
|
||||||
|
? selectedView.value === REGULAR_NODE_FILTER
|
||||||
|
: selectedView.value === TRIGGER_NODE_FILTER;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
return mergedAppNodes;
|
||||||
|
});
|
||||||
|
|
||||||
|
const computedCategoriesWithNodes = computed(() => {
|
||||||
|
if (!state.activeNodeActions) return getCategoriesWithNodes(filteredMergedAppNodes.value);
|
||||||
|
|
||||||
|
return getCategoriesWithNodes(selectedNodeActions.value, state.activeNodeActions.displayName);
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedNodeActions = computed<INodeActionTypeDescription[]>(
|
||||||
|
() => state.activeNodeActions?.actions ?? [],
|
||||||
|
);
|
||||||
|
const isAppEventSubcategory = computed(() => state.selectedSubcategory === '*');
|
||||||
|
const isActionsActive = computed(() => state.activeNodeActions !== null);
|
||||||
|
const firstLevelItems = computed(() => (isRoot.value ? activeView.value.items : []));
|
||||||
|
|
||||||
|
const searchItems = computed<INodeCreateElement[]>(() => {
|
||||||
|
return state.activeNodeActions
|
||||||
|
? transformCreateElements(selectedNodeActions.value)
|
||||||
|
: transformCreateElements(filteredMergedAppNodes.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// If the user is in the root view, we want to show trigger nodes first
|
||||||
|
// otherwise we want to show them last
|
||||||
|
function sortActions(nodeCreateElements: INodeCreateElement[]): INodeCreateElement[] {
|
||||||
|
const elements = {
|
||||||
|
trigger: [] as INodeCreateElement[],
|
||||||
|
regular: [] as INodeCreateElement[],
|
||||||
|
};
|
||||||
|
|
||||||
|
nodeCreateElements.forEach((el) => {
|
||||||
|
const isTriggersCategory = el.type === 'category' && el.key === 'Triggers';
|
||||||
|
const isTriggerAction = el.type === 'action' && el.category === 'Triggers';
|
||||||
|
|
||||||
|
elements[isTriggersCategory || isTriggerAction ? 'trigger' : 'regular'].push(el);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (selectedView.value === TRIGGER_NODE_FILTER) {
|
||||||
|
return [...elements.trigger, ...elements.regular];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...elements.regular, ...elements.trigger];
|
||||||
|
}
|
||||||
|
|
||||||
|
function transformCreateElements(
|
||||||
|
createElements: Array<INodeTypeDescription | INodeActionTypeDescription>,
|
||||||
|
): INodeCreateElement[] {
|
||||||
|
const sorted = [...createElements];
|
||||||
|
|
||||||
|
sorted.sort((a, b) => {
|
||||||
|
const textA = a.displayName.toLowerCase();
|
||||||
|
const textB = b.displayName.toLowerCase();
|
||||||
|
return textA < textB ? -1 : textA > textB ? 1 : 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
return sorted.map((nodeType) => {
|
||||||
|
// N8n node is a special case since it's the only core node that is both trigger and regular
|
||||||
|
// if we have more cases like this we should add more robust logic
|
||||||
|
const isN8nNode = nodeType.name.includes(N8N_NODE_TYPE);
|
||||||
|
return {
|
||||||
|
type: 'node',
|
||||||
|
category: nodeType.codex?.categories,
|
||||||
|
key: nodeType.name,
|
||||||
|
properties: {
|
||||||
|
nodeType,
|
||||||
|
subcategory: state.activeNodeActions?.displayName ?? '',
|
||||||
|
},
|
||||||
|
includedByTrigger: isN8nNode || nodeType.group.includes('trigger'),
|
||||||
|
includedByRegular: isN8nNode || !nodeType.group.includes('trigger'),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onNodeTypeSelected(nodeTypes: string[]) {
|
||||||
|
emit(
|
||||||
|
'nodeTypeSelected',
|
||||||
|
nodeTypes.length === 1 ? getNodeTypesWithManualTrigger(nodeTypes[0]) : nodeTypes,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
function getCustomAPICallHintLocale(key: string) {
|
||||||
|
if (!state.activeNodeActions) return '';
|
||||||
|
|
||||||
|
const nodeNameTitle = state.activeNodeActions.displayName;
|
||||||
|
return instance?.proxy.$locale.baseText(`nodeCreator.actionsList.${key}` as BaseTextKey, {
|
||||||
|
interpolate: { nodeNameTitle },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setActiveActionsNodeType(nodeType: INodeTypeDescription | null) {
|
||||||
|
state.activeNodeActions = nodeType;
|
||||||
|
|
||||||
|
if (nodeType) trackActionsView();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onActionSelected(actionCreateElement: INodeCreateElement) {
|
||||||
|
const action = (actionCreateElement.properties as IActionItemProps).nodeType;
|
||||||
|
const actionUpdateData = getActionData(action);
|
||||||
|
emit('nodeTypeSelected', getNodeTypesWithManualTrigger(actionUpdateData.key));
|
||||||
|
setAddedNodeActionParameters(actionUpdateData, telemetry);
|
||||||
|
}
|
||||||
|
function addWebHookNode() {
|
||||||
|
emit('nodeTypeSelected', [WEBHOOK_NODE_TYPE]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addHttpNode(isAction: boolean) {
|
||||||
|
const updateData = {
|
||||||
|
name: '',
|
||||||
|
key: HTTP_REQUEST_NODE_TYPE,
|
||||||
|
value: {
|
||||||
|
authentication: 'predefinedCredentialType',
|
||||||
|
},
|
||||||
|
} as IUpdateInformation;
|
||||||
|
|
||||||
|
emit('nodeTypeSelected', [HTTP_REQUEST_NODE_TYPE]);
|
||||||
|
if (isAction) {
|
||||||
|
setAddedNodeActionParameters(updateData, telemetry, false);
|
||||||
|
|
||||||
|
const app_identifier = state.activeNodeActions?.name;
|
||||||
|
$externalHooks().run('nodeCreateList.onActionsCustmAPIClicked', { app_identifier });
|
||||||
|
telemetry?.trackNodesPanel('nodeCreateList.onActionsCustmAPIClicked', { app_identifier });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSubcategorySelected(subcategory: INodeCreateElement) {
|
||||||
|
state.isRoot = false;
|
||||||
|
state.selectedSubcategory = subcategory.key;
|
||||||
|
}
|
||||||
|
function onSubcategoryClose(activeSubcategories: INodeCreateElement[]) {
|
||||||
|
if (isActionsActive.value === true) setActiveActionsNodeType(null);
|
||||||
|
|
||||||
|
state.isRoot = activeSubcategories.length === 0;
|
||||||
|
state.selectedSubcategory = activeSubcategories[activeSubcategories.length - 1]?.key ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldShowNodeDescription(node: NodeCreateElement) {
|
||||||
|
return (node.category || []).includes(CORE_NODES_CATEGORY);
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldShowNodeActions(node: INodeCreateElement) {
|
||||||
|
if (state.isRoot && useNodeCreatorStore().itemsFilter === '') return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function trackActionsView() {
|
||||||
|
const trigger_action_count = selectedNodeActions.value.filter((action) =>
|
||||||
|
action.name.toLowerCase().includes('trigger'),
|
||||||
|
).length;
|
||||||
|
|
||||||
|
const trackingPayload = {
|
||||||
|
app_identifier: state.activeNodeActions?.name,
|
||||||
|
actions: selectedNodeActions.value.map((action) => action.displayName),
|
||||||
|
regular_action_count: selectedNodeActions.value.length - trigger_action_count,
|
||||||
|
trigger_action_count,
|
||||||
|
};
|
||||||
|
|
||||||
|
$externalHooks().run('nodeCreateList.onViewActions', trackingPayload);
|
||||||
|
telemetry?.trackNodesPanel('nodeCreateList.onViewActions', trackingPayload);
|
||||||
|
}
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
nodeCreatorStore.setSelectedType(ALL_NODE_FILTER);
|
useNodeCreatorStore().resetRootViewHistory();
|
||||||
$externalHooks().run('nodeCreateList.destroyed');
|
|
||||||
instance?.proxy.$telemetry.trackNodesPanel('nodeCreateList.destroyed', {
|
|
||||||
workflow_id: workflowId,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
const { isRoot, activeNodeActions } = toRefs(state);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" module>
|
||||||
.container {
|
.mainPanel {
|
||||||
|
--node-icon-color: var(--color-text-base);
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
// Remove node item border on the root level
|
||||||
|
&.isRoot {
|
||||||
|
--node-item-border: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.main-panel {
|
.itemCreator {
|
||||||
height: 100%;
|
height: calc(100% - 120px);
|
||||||
|
padding-top: 1px;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: visible;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: var(--font-size-l);
|
||||||
|
line-height: var(--font-line-height-xloose);
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
color: var(--color-text-dark);
|
||||||
|
}
|
||||||
|
.description {
|
||||||
|
font-size: var(--font-size-s);
|
||||||
|
line-height: var(--font-line-height-loose);
|
||||||
|
color: var(--color-text-base);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -14,20 +14,17 @@
|
||||||
@mouseup="onMouseUp"
|
@mouseup="onMouseUp"
|
||||||
data-test-id="node-creator"
|
data-test-id="node-creator"
|
||||||
>
|
>
|
||||||
<main-panel @nodeTypeSelected="$listeners.nodeTypeSelected" :searchItems="searchItems" />
|
<main-panel @nodeTypeSelected="$listeners.nodeTypeSelected" />
|
||||||
</div>
|
</div>
|
||||||
</slide-transition>
|
</slide-transition>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, watch, reactive, toRefs } from 'vue';
|
import { watch, reactive, toRefs } from 'vue';
|
||||||
|
|
||||||
import { INodeCreateElement } from '@/Interface';
|
|
||||||
import SlideTransition from '@/components/transitions/SlideTransition.vue';
|
import SlideTransition from '@/components/transitions/SlideTransition.vue';
|
||||||
|
|
||||||
import MainPanel from './MainPanel.vue';
|
import MainPanel from './MainPanel.vue';
|
||||||
import { useNodeTypesStore } from '@/stores/nodeTypes';
|
|
||||||
import { useNodeCreatorStore } from '@/stores/nodeCreator';
|
import { useNodeCreatorStore } from '@/stores/nodeCreator';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
|
@ -46,28 +43,6 @@ const state = reactive({
|
||||||
mousedownInsideEvent: null as MouseEvent | null,
|
mousedownInsideEvent: null as MouseEvent | null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const visibleNodeTypes = computed(() => useNodeTypesStore().visibleNodeTypes);
|
|
||||||
const searchItems = computed<INodeCreateElement[]>(() => {
|
|
||||||
const sorted = [...visibleNodeTypes.value];
|
|
||||||
sorted.sort((a, b) => {
|
|
||||||
const textA = a.displayName.toLowerCase();
|
|
||||||
const textB = b.displayName.toLowerCase();
|
|
||||||
return textA < textB ? -1 : textA > textB ? 1 : 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
return sorted.map((nodeType) => ({
|
|
||||||
type: 'node',
|
|
||||||
category: '',
|
|
||||||
key: `${nodeType.name}`,
|
|
||||||
properties: {
|
|
||||||
nodeType,
|
|
||||||
subcategory: '',
|
|
||||||
},
|
|
||||||
includedByTrigger: nodeType.group.includes('trigger'),
|
|
||||||
includedByRegular: !nodeType.group.includes('trigger'),
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
function onClickOutside(event: Event) {
|
function onClickOutside(event: Event) {
|
||||||
// We need to prevent cases where user would click inside the node creator
|
// We need to prevent cases where user would click inside the node creator
|
||||||
// and try to drag undraggable element. In that case the click event would
|
// and try to drag undraggable element. In that case the click event would
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
@dragend="onDragEnd"
|
@dragend="onDragEnd"
|
||||||
@click.stop="onClick"
|
@click.stop="onClick"
|
||||||
:class="$style.nodeItem"
|
:class="$style.nodeItem"
|
||||||
:description="allowActions ? undefined : description"
|
:description="allowDescription ? description : ''"
|
||||||
:title="displayName"
|
:title="displayName"
|
||||||
:isTrigger="!allowActions && isTriggerNode"
|
:isTrigger="!allowActions && isTriggerNode"
|
||||||
:show-action-arrow="showActionArrow"
|
:show-action-arrow="showActionArrow"
|
||||||
|
@ -53,11 +53,13 @@ export interface Props {
|
||||||
nodeType: INodeTypeDescription;
|
nodeType: INodeTypeDescription;
|
||||||
active?: boolean;
|
active?: boolean;
|
||||||
allowActions?: boolean;
|
allowActions?: boolean;
|
||||||
|
allowDescription?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
active: false,
|
active: false,
|
||||||
allowActions: false,
|
allowActions: false,
|
||||||
|
allowDescription: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
|
|
@ -80,7 +80,7 @@ defineExpose({
|
||||||
.searchContainer {
|
.searchContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
padding: var(--spacing-s) var(--spacing-xs);
|
padding: 0 var(--spacing-xs);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin: var(--search-margin, var(--spacing-s));
|
margin: var(--search-margin, var(--spacing-s));
|
||||||
filter: drop-shadow(0px 2px 5px rgba(46, 46, 50, 0.04));
|
filter: drop-shadow(0px 2px 5px rgba(46, 46, 50, 0.04));
|
||||||
|
|
|
@ -1,97 +1,38 @@
|
||||||
<template>
|
<template>
|
||||||
<div :class="{ [$style.subcategory]: true, [$style.subcategoryWithIcon]: hasIcon }">
|
<n8n-node-creator-node
|
||||||
<node-icon v-if="hasIcon" :class="$style.subcategoryIcon" :nodeType="itemProperties" />
|
:class="$style.subCategory"
|
||||||
<div :class="$style.details">
|
:title="$locale.baseText(`nodeCreator.subcategoryNames.${subcategoryName}`)"
|
||||||
<div :class="$style.title">
|
:isTrigger="false"
|
||||||
{{ $locale.baseText(`nodeCreator.subcategoryNames.${subcategoryName}`) }}
|
:description="$locale.baseText(`nodeCreator.subcategoryDescriptions.${subcategoryName}`)"
|
||||||
</div>
|
:showActionArrow="true"
|
||||||
<div v-if="item.properties.description" :class="$style.description">
|
>
|
||||||
{{ $locale.baseText(`nodeCreator.subcategoryDescriptions.${subcategoryName}`) }}
|
<template #icon>
|
||||||
</div>
|
<n8n-node-icon type="icon" :name="item.icon" :circle="false" :showTooltip="false" />
|
||||||
</div>
|
</template>
|
||||||
<div :class="$style.action">
|
</n8n-node-creator-node>
|
||||||
<font-awesome-icon :class="$style.arrow" icon="arrow-right" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import Vue, { PropType } from 'vue';
|
import { ISubcategoryItemProps } from '@/Interface';
|
||||||
import camelcase from 'lodash.camelcase';
|
import camelcase from 'lodash.camelcase';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
export interface Props {
|
||||||
|
item: ISubcategoryItemProps;
|
||||||
|
}
|
||||||
|
|
||||||
import NodeIcon from '@/components/NodeIcon.vue';
|
const props = defineProps<Props>();
|
||||||
import { INodeCreateElement, ISubcategoryItemProps } from '@/Interface';
|
const subcategoryName = computed(() => camelcase(props.item.subcategory));
|
||||||
export default Vue.extend({
|
|
||||||
components: {
|
|
||||||
NodeIcon,
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
item: {
|
|
||||||
type: Object as PropType<INodeCreateElement>,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
itemProperties(): ISubcategoryItemProps {
|
|
||||||
return this.item.properties as ISubcategoryItemProps;
|
|
||||||
},
|
|
||||||
subcategoryName(): string {
|
|
||||||
return camelcase(this.itemProperties.subcategory);
|
|
||||||
},
|
|
||||||
hasIcon(): boolean {
|
|
||||||
return this.itemProperties.icon !== undefined || this.itemProperties.iconData !== undefined;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
.subcategoryIcon {
|
.subCategory {
|
||||||
min-width: 26px;
|
--action-arrow-color: var(--color-text-light);
|
||||||
max-width: 26px;
|
|
||||||
margin-right: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subcategory {
|
|
||||||
display: flex;
|
|
||||||
padding: 11px 16px 11px 30px;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subcategoryWithIcon {
|
|
||||||
margin-left: 15px;
|
margin-left: 15px;
|
||||||
margin-right: 12px;
|
margin-right: 12px;
|
||||||
padding: 11px 8px 11px 0;
|
|
||||||
}
|
}
|
||||||
|
.withTopBorder {
|
||||||
.details {
|
border-top: 1px solid var(--color-foreground-base);
|
||||||
flex-grow: 1;
|
margin-top: var(--spacing-m);
|
||||||
margin-right: 4px;
|
padding-top: var(--spacing-l);
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: var(--font-weight-bold);
|
|
||||||
line-height: 16px;
|
|
||||||
margin-bottom: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.description {
|
|
||||||
font-size: var(--font-size-2xs);
|
|
||||||
line-height: 16px;
|
|
||||||
font-weight: 400;
|
|
||||||
color: $node-creator-description-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-left: var(--spacing-2xs);
|
|
||||||
}
|
|
||||||
|
|
||||||
.arrow {
|
|
||||||
font-size: 12px;
|
|
||||||
width: 12px;
|
|
||||||
color: $node-creator-arrow-color;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,449 +0,0 @@
|
||||||
<template>
|
|
||||||
<div :class="{ [$style.triggerHelperContainer]: true, [$style.isRoot]: isRoot }">
|
|
||||||
<categorized-items
|
|
||||||
:expandAllCategories="isActionsActive"
|
|
||||||
:subcategoryOverride="nodeAppSubcategory"
|
|
||||||
:alwaysShowSearch="isActionsActive"
|
|
||||||
:categorizedItems="computedCategorizedItems"
|
|
||||||
:categoriesWithNodes="computedCategoriesWithNodes"
|
|
||||||
:initialActiveIndex="0"
|
|
||||||
:searchItems="searchItems"
|
|
||||||
:withActionsGetter="shouldShowNodeActions"
|
|
||||||
:firstLevelItems="firstLevelItems"
|
|
||||||
:showSubcategoryIcon="isActionsActive"
|
|
||||||
:flatten="!isActionsActive && isAppEventSubcategory"
|
|
||||||
:filterByType="false"
|
|
||||||
:lazyRender="true"
|
|
||||||
:allItems="allNodes"
|
|
||||||
:searchPlaceholder="searchPlaceholder"
|
|
||||||
ref="categorizedItemsRef"
|
|
||||||
@subcategoryClose="onSubcategoryClose"
|
|
||||||
@onSubcategorySelected="onSubcategorySelected"
|
|
||||||
@nodeTypeSelected="onNodeTypeSelected"
|
|
||||||
@actionsOpen="setActiveActionsNodeType"
|
|
||||||
@actionSelected="onActionSelected"
|
|
||||||
>
|
|
||||||
<template #noResultsTitle v-if="isActionsActive">
|
|
||||||
<i />
|
|
||||||
</template>
|
|
||||||
<template #noResultsAction v-if="isActionsActive">
|
|
||||||
<p
|
|
||||||
v-if="containsAPIAction"
|
|
||||||
v-html="getCustomAPICallHintLocale('apiCallNoResult')"
|
|
||||||
class="clickable"
|
|
||||||
@click.stop="addHttpNode"
|
|
||||||
/>
|
|
||||||
<p v-else v-text="$locale.baseText('nodeCreator.noResults.noMatchingActions')" />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #header>
|
|
||||||
<slot name="header" />
|
|
||||||
<p
|
|
||||||
v-if="isRoot"
|
|
||||||
v-text="$locale.baseText('nodeCreator.triggerHelperPanel.title')"
|
|
||||||
:class="$style.title"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
<template #footer v-if="activeNodeActions && containsAPIAction">
|
|
||||||
<slot name="footer" />
|
|
||||||
<span
|
|
||||||
v-html="getCustomAPICallHintLocale('apiCall')"
|
|
||||||
class="clickable"
|
|
||||||
@click.stop="addHttpNode"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</categorized-items>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { reactive, toRefs, getCurrentInstance, computed, onMounted, ref } from 'vue';
|
|
||||||
import {
|
|
||||||
INodeTypeDescription,
|
|
||||||
INodeActionTypeDescription,
|
|
||||||
INodeTypeNameVersion,
|
|
||||||
} from 'n8n-workflow';
|
|
||||||
import {
|
|
||||||
INodeCreateElement,
|
|
||||||
IActionItemProps,
|
|
||||||
SubcategoryCreateElement,
|
|
||||||
IUpdateInformation,
|
|
||||||
} from '@/Interface';
|
|
||||||
import {
|
|
||||||
CORE_NODES_CATEGORY,
|
|
||||||
WEBHOOK_NODE_TYPE,
|
|
||||||
OTHER_TRIGGER_NODES_SUBCATEGORY,
|
|
||||||
EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE,
|
|
||||||
MANUAL_TRIGGER_NODE_TYPE,
|
|
||||||
SCHEDULE_TRIGGER_NODE_TYPE,
|
|
||||||
EMAIL_IMAP_NODE_TYPE,
|
|
||||||
CUSTOM_API_CALL_NAME,
|
|
||||||
HTTP_REQUEST_NODE_TYPE,
|
|
||||||
STICKY_NODE_TYPE,
|
|
||||||
} from '@/constants';
|
|
||||||
import CategorizedItems from './CategorizedItems.vue';
|
|
||||||
import { useNodeCreatorStore } from '@/stores/nodeCreator';
|
|
||||||
import { getCategoriesWithNodes, getCategorizedList } from '@/utils';
|
|
||||||
import { externalHooks } from '@/mixins/externalHooks';
|
|
||||||
import { useNodeTypesStore } from '@/stores/nodeTypes';
|
|
||||||
import { BaseTextKey } from '@/plugins/i18n';
|
|
||||||
|
|
||||||
const instance = getCurrentInstance();
|
|
||||||
const items: INodeCreateElement[] = [
|
|
||||||
{
|
|
||||||
key: '*',
|
|
||||||
type: 'subcategory',
|
|
||||||
title: instance?.proxy.$locale.baseText('nodeCreator.subcategoryNames.appTriggerNodes'),
|
|
||||||
properties: {
|
|
||||||
subcategory: 'App Trigger Nodes',
|
|
||||||
description: instance?.proxy.$locale.baseText(
|
|
||||||
'nodeCreator.subcategoryDescriptions.appTriggerNodes',
|
|
||||||
),
|
|
||||||
icon: 'fa:satellite-dish',
|
|
||||||
defaults: {
|
|
||||||
color: '#7D838F',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: SCHEDULE_TRIGGER_NODE_TYPE,
|
|
||||||
type: 'node',
|
|
||||||
properties: {
|
|
||||||
nodeType: {
|
|
||||||
group: [],
|
|
||||||
name: SCHEDULE_TRIGGER_NODE_TYPE,
|
|
||||||
displayName: instance?.proxy.$locale.baseText(
|
|
||||||
'nodeCreator.triggerHelperPanel.scheduleTriggerDisplayName',
|
|
||||||
),
|
|
||||||
description: instance?.proxy.$locale.baseText(
|
|
||||||
'nodeCreator.triggerHelperPanel.scheduleTriggerDescription',
|
|
||||||
),
|
|
||||||
icon: 'fa:clock',
|
|
||||||
defaults: {
|
|
||||||
color: '#7D838F',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: WEBHOOK_NODE_TYPE,
|
|
||||||
type: 'node',
|
|
||||||
properties: {
|
|
||||||
nodeType: {
|
|
||||||
group: [],
|
|
||||||
name: WEBHOOK_NODE_TYPE,
|
|
||||||
displayName: instance?.proxy.$locale.baseText(
|
|
||||||
'nodeCreator.triggerHelperPanel.webhookTriggerDisplayName',
|
|
||||||
),
|
|
||||||
description: instance?.proxy.$locale.baseText(
|
|
||||||
'nodeCreator.triggerHelperPanel.webhookTriggerDescription',
|
|
||||||
),
|
|
||||||
iconData: {
|
|
||||||
type: 'file',
|
|
||||||
icon: 'webhook',
|
|
||||||
fileBuffer: '/static/webhook-icon.svg',
|
|
||||||
},
|
|
||||||
defaults: {
|
|
||||||
color: '#7D838F',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: MANUAL_TRIGGER_NODE_TYPE,
|
|
||||||
type: 'node',
|
|
||||||
properties: {
|
|
||||||
nodeType: {
|
|
||||||
group: [],
|
|
||||||
name: MANUAL_TRIGGER_NODE_TYPE,
|
|
||||||
displayName: instance?.proxy.$locale.baseText(
|
|
||||||
'nodeCreator.triggerHelperPanel.manualTriggerDisplayName',
|
|
||||||
),
|
|
||||||
description: instance?.proxy.$locale.baseText(
|
|
||||||
'nodeCreator.triggerHelperPanel.manualTriggerDescription',
|
|
||||||
),
|
|
||||||
icon: 'fa:mouse-pointer',
|
|
||||||
defaults: {
|
|
||||||
color: '#7D838F',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE,
|
|
||||||
type: 'node',
|
|
||||||
properties: {
|
|
||||||
nodeType: {
|
|
||||||
group: [],
|
|
||||||
name: EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE,
|
|
||||||
displayName: instance?.proxy.$locale.baseText(
|
|
||||||
'nodeCreator.triggerHelperPanel.workflowTriggerDisplayName',
|
|
||||||
),
|
|
||||||
description: instance?.proxy.$locale.baseText(
|
|
||||||
'nodeCreator.triggerHelperPanel.workflowTriggerDescription',
|
|
||||||
),
|
|
||||||
icon: 'fa:sign-out-alt',
|
|
||||||
defaults: {
|
|
||||||
color: '#7D838F',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'subcategory',
|
|
||||||
key: OTHER_TRIGGER_NODES_SUBCATEGORY,
|
|
||||||
category: CORE_NODES_CATEGORY,
|
|
||||||
properties: {
|
|
||||||
subcategory: OTHER_TRIGGER_NODES_SUBCATEGORY,
|
|
||||||
description: instance?.proxy.$locale.baseText(
|
|
||||||
'nodeCreator.subcategoryDescriptions.otherTriggerNodes',
|
|
||||||
),
|
|
||||||
icon: 'fa:folder-open',
|
|
||||||
defaults: {
|
|
||||||
color: '#7D838F',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const emit = defineEmits({
|
|
||||||
nodeTypeSelected: (nodeTypes: string[]) => true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const state = reactive({
|
|
||||||
isRoot: true,
|
|
||||||
selectedSubcategory: '',
|
|
||||||
activeNodeActions: null as INodeTypeDescription | null,
|
|
||||||
latestNodeData: null as INodeTypeDescription | null,
|
|
||||||
});
|
|
||||||
const categorizedItemsRef = ref<InstanceType<typeof CategorizedItems>>();
|
|
||||||
|
|
||||||
const { $externalHooks } = new externalHooks();
|
|
||||||
const {
|
|
||||||
mergedAppNodes,
|
|
||||||
setShowTabs,
|
|
||||||
getActionData,
|
|
||||||
getNodeTypesWithManualTrigger,
|
|
||||||
setAddedNodeActionParameters,
|
|
||||||
} = useNodeCreatorStore();
|
|
||||||
|
|
||||||
const { getNodeType } = useNodeTypesStore();
|
|
||||||
|
|
||||||
const telemetry = instance?.proxy.$telemetry;
|
|
||||||
const { categorizedItems: allNodes, isTriggerNode } = useNodeTypesStore();
|
|
||||||
const containsAPIAction = computed(
|
|
||||||
() =>
|
|
||||||
activeNodeActions.value?.properties.some((p) =>
|
|
||||||
p.options?.find((o) => o.name === CUSTOM_API_CALL_NAME),
|
|
||||||
) === true,
|
|
||||||
);
|
|
||||||
|
|
||||||
const computedCategorizedItems = computed(() =>
|
|
||||||
getCategorizedList(computedCategoriesWithNodes.value, true),
|
|
||||||
);
|
|
||||||
|
|
||||||
const nodeAppSubcategory = computed<SubcategoryCreateElement | undefined>(() => {
|
|
||||||
if (!state.activeNodeActions) return undefined;
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: 'subcategory',
|
|
||||||
properties: {
|
|
||||||
subcategory: state.activeNodeActions.displayName,
|
|
||||||
nodeType: {
|
|
||||||
description: '',
|
|
||||||
key: state.activeNodeActions.name,
|
|
||||||
iconUrl: state.activeNodeActions.iconUrl,
|
|
||||||
icon: state.activeNodeActions.icon,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
const searchPlaceholder = computed(() => {
|
|
||||||
const nodeNameTitle = state.activeNodeActions?.displayName?.trim() as string;
|
|
||||||
const actionsSearchPlaceholder = instance?.proxy.$locale.baseText(
|
|
||||||
'nodeCreator.actionsCategory.searchActions',
|
|
||||||
{ interpolate: { nodeNameTitle } },
|
|
||||||
);
|
|
||||||
|
|
||||||
return isActionsActive.value ? actionsSearchPlaceholder : undefined;
|
|
||||||
});
|
|
||||||
|
|
||||||
const filteredMergedAppNodes = computed(() => {
|
|
||||||
const WHITELISTED_APP_CORE_NODES = [EMAIL_IMAP_NODE_TYPE, WEBHOOK_NODE_TYPE];
|
|
||||||
|
|
||||||
if (isAppEventSubcategory.value)
|
|
||||||
return mergedAppNodes.filter((node) => {
|
|
||||||
const isRegularNode = !isTriggerNode(node.name);
|
|
||||||
const isStickyNode = node.name === STICKY_NODE_TYPE;
|
|
||||||
const isCoreNode =
|
|
||||||
node.codex?.categories?.includes(CORE_NODES_CATEGORY) &&
|
|
||||||
!WHITELISTED_APP_CORE_NODES.includes(node.name);
|
|
||||||
const hasActions = (node.actions || []).length > 0;
|
|
||||||
|
|
||||||
if (isRegularNode && !hasActions) return false;
|
|
||||||
return !isCoreNode && !isStickyNode;
|
|
||||||
});
|
|
||||||
|
|
||||||
return mergedAppNodes;
|
|
||||||
});
|
|
||||||
|
|
||||||
const computedCategoriesWithNodes = computed(() => {
|
|
||||||
if (!state.activeNodeActions) return getCategoriesWithNodes(filteredMergedAppNodes.value, []);
|
|
||||||
|
|
||||||
return getCategoriesWithNodes(selectedNodeActions.value, [], state.activeNodeActions.displayName);
|
|
||||||
});
|
|
||||||
|
|
||||||
const selectedNodeActions = computed<INodeActionTypeDescription[]>(
|
|
||||||
() => state.activeNodeActions?.actions ?? [],
|
|
||||||
);
|
|
||||||
const isAppEventSubcategory = computed(() => state.selectedSubcategory === '*');
|
|
||||||
const isActionsActive = computed(() => state.activeNodeActions !== null);
|
|
||||||
const firstLevelItems = computed(() => (isRoot.value ? items : []));
|
|
||||||
|
|
||||||
const isSearchActive = computed(() => useNodeCreatorStore().itemsFilter !== '');
|
|
||||||
const searchItems = computed<INodeCreateElement[]>(() => {
|
|
||||||
const sorted = state.activeNodeActions
|
|
||||||
? [...selectedNodeActions.value]
|
|
||||||
: [...filteredMergedAppNodes.value];
|
|
||||||
sorted.sort((a, b) => {
|
|
||||||
const textA = a.displayName.toLowerCase();
|
|
||||||
const textB = b.displayName.toLowerCase();
|
|
||||||
return textA < textB ? -1 : textA > textB ? 1 : 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
return sorted.map((nodeType) => ({
|
|
||||||
type: 'node',
|
|
||||||
category: '',
|
|
||||||
key: nodeType.name,
|
|
||||||
properties: {
|
|
||||||
nodeType,
|
|
||||||
subcategory: state.activeNodeActions ? state.activeNodeActions.displayName : '',
|
|
||||||
},
|
|
||||||
includedByTrigger: nodeType.group.includes('trigger'),
|
|
||||||
includedByRegular: !nodeType.group.includes('trigger'),
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
function onNodeTypeSelected(nodeTypes: string[]) {
|
|
||||||
emit(
|
|
||||||
'nodeTypeSelected',
|
|
||||||
nodeTypes.length === 1 ? getNodeTypesWithManualTrigger(nodeTypes[0]) : nodeTypes,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
function getCustomAPICallHintLocale(key: string) {
|
|
||||||
if (!state.activeNodeActions) return '';
|
|
||||||
|
|
||||||
const nodeNameTitle = state.activeNodeActions.displayName;
|
|
||||||
return instance?.proxy.$locale.baseText(`nodeCreator.actionsList.${key}` as BaseTextKey, {
|
|
||||||
interpolate: { nodeNameTitle },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function setActiveActionsNodeType(nodeType: INodeTypeDescription | null) {
|
|
||||||
state.activeNodeActions = nodeType;
|
|
||||||
setShowTabs(false);
|
|
||||||
|
|
||||||
if (nodeType) trackActionsView();
|
|
||||||
}
|
|
||||||
|
|
||||||
function onActionSelected(actionCreateElement: INodeCreateElement) {
|
|
||||||
const action = (actionCreateElement.properties as IActionItemProps).nodeType;
|
|
||||||
const actionUpdateData = getActionData(action);
|
|
||||||
emit('nodeTypeSelected', getNodeTypesWithManualTrigger(actionUpdateData.key));
|
|
||||||
setAddedNodeActionParameters(actionUpdateData, telemetry);
|
|
||||||
}
|
|
||||||
function addHttpNode() {
|
|
||||||
const app_identifier = state.activeNodeActions?.name;
|
|
||||||
let nodeCredentialType = '';
|
|
||||||
const nodeType = app_identifier ? getNodeType(app_identifier) : null;
|
|
||||||
|
|
||||||
if (nodeType && nodeType.credentials && nodeType.credentials.length > 0) {
|
|
||||||
nodeCredentialType = nodeType.credentials[0].name;
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateData = {
|
|
||||||
name: '',
|
|
||||||
key: HTTP_REQUEST_NODE_TYPE,
|
|
||||||
value: {
|
|
||||||
authentication: 'predefinedCredentialType',
|
|
||||||
nodeCredentialType,
|
|
||||||
},
|
|
||||||
} as IUpdateInformation;
|
|
||||||
|
|
||||||
emit('nodeTypeSelected', [MANUAL_TRIGGER_NODE_TYPE, HTTP_REQUEST_NODE_TYPE]);
|
|
||||||
setAddedNodeActionParameters(updateData, telemetry, false);
|
|
||||||
|
|
||||||
$externalHooks().run('nodeCreateList.onActionsCustmAPIClicked', { app_identifier });
|
|
||||||
telemetry?.trackNodesPanel('nodeCreateList.onActionsCustmAPIClicked', { app_identifier });
|
|
||||||
}
|
|
||||||
|
|
||||||
function onSubcategorySelected(subcategory: INodeCreateElement) {
|
|
||||||
state.isRoot = false;
|
|
||||||
state.selectedSubcategory = subcategory.key;
|
|
||||||
}
|
|
||||||
function onSubcategoryClose(activeSubcategories: INodeCreateElement[]) {
|
|
||||||
if (isActionsActive.value === true) setActiveActionsNodeType(null);
|
|
||||||
|
|
||||||
state.isRoot = activeSubcategories.length === 0;
|
|
||||||
state.selectedSubcategory = activeSubcategories[activeSubcategories.length - 1]?.key ?? '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function shouldShowNodeActions(node: INodeCreateElement) {
|
|
||||||
if (isAppEventSubcategory.value) return true;
|
|
||||||
if (state.isRoot && !isSearchActive.value) return false;
|
|
||||||
// Do not show actions for core category when searching
|
|
||||||
if (node.type === 'node')
|
|
||||||
return !node.properties.nodeType.codex?.categories?.includes(CORE_NODES_CATEGORY);
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function trackActionsView() {
|
|
||||||
const trigger_action_count = selectedNodeActions.value.filter((action) =>
|
|
||||||
action.name.toLowerCase().includes('trigger'),
|
|
||||||
).length;
|
|
||||||
|
|
||||||
const trackingPayload = {
|
|
||||||
app_identifier: state.activeNodeActions?.name,
|
|
||||||
actions: selectedNodeActions.value.map((action) => action.displayName),
|
|
||||||
regular_action_count: selectedNodeActions.value.length - trigger_action_count,
|
|
||||||
trigger_action_count,
|
|
||||||
};
|
|
||||||
|
|
||||||
$externalHooks().run('nodeCreateList.onViewActions', trackingPayload);
|
|
||||||
telemetry?.trackNodesPanel('nodeCreateList.onViewActions', trackingPayload);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { isRoot, activeNodeActions } = toRefs(state);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" module>
|
|
||||||
.triggerHelperContainer {
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
// Remove node item border on the root level
|
|
||||||
&.isRoot {
|
|
||||||
--node-item-border: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.itemCreator {
|
|
||||||
height: calc(100% - 120px);
|
|
||||||
padding-top: 1px;
|
|
||||||
overflow-y: auto;
|
|
||||||
overflow-x: visible;
|
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
font-size: var(--font-size-l);
|
|
||||||
line-height: var(--font-line-height-xloose);
|
|
||||||
font-weight: var(--font-weight-bold);
|
|
||||||
color: var(--color-text-dark);
|
|
||||||
padding: var(--spacing-s) var(--spacing-s) var(--spacing-3xs);
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,59 +0,0 @@
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
class="type-selector"
|
|
||||||
v-if="nodeCreatorStore.showTabs"
|
|
||||||
data-test-id="node-creator-type-selector"
|
|
||||||
>
|
|
||||||
<el-tabs
|
|
||||||
stretch
|
|
||||||
:value="nodeCreatorStore.selectedType"
|
|
||||||
@input="nodeCreatorStore.setSelectedType"
|
|
||||||
>
|
|
||||||
<el-tab-pane
|
|
||||||
:label="$locale.baseText('nodeCreator.mainPanel.all')"
|
|
||||||
:name="ALL_NODE_FILTER"
|
|
||||||
></el-tab-pane>
|
|
||||||
<el-tab-pane
|
|
||||||
:label="$locale.baseText('nodeCreator.mainPanel.regular')"
|
|
||||||
:name="REGULAR_NODE_FILTER"
|
|
||||||
></el-tab-pane>
|
|
||||||
<el-tab-pane
|
|
||||||
:label="$locale.baseText('nodeCreator.mainPanel.trigger')"
|
|
||||||
:name="TRIGGER_NODE_FILTER"
|
|
||||||
></el-tab-pane>
|
|
||||||
</el-tabs>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ALL_NODE_FILTER, REGULAR_NODE_FILTER, TRIGGER_NODE_FILTER } from '@/constants';
|
|
||||||
import { useNodeCreatorStore } from '@/stores/nodeCreator';
|
|
||||||
|
|
||||||
const nodeCreatorStore = useNodeCreatorStore();
|
|
||||||
</script>
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
::v-deep .el-tabs__item {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
::v-deep .el-tabs__active-bar {
|
|
||||||
height: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::v-deep .el-tabs__nav-wrap::after {
|
|
||||||
height: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.type-selector {
|
|
||||||
text-align: center;
|
|
||||||
background-color: $node-creator-select-background-color;
|
|
||||||
::v-deep .el-tabs > div {
|
|
||||||
margin-bottom: 0;
|
|
||||||
z-index: 1;
|
|
||||||
|
|
||||||
.el-tabs__nav {
|
|
||||||
height: 43px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
<template>
|
||||||
|
<n8n-node-creator-node
|
||||||
|
:class="{
|
||||||
|
[$style.view]: true,
|
||||||
|
[$style.withTopBorder]: view.withTopBorder,
|
||||||
|
}"
|
||||||
|
:title="view.title"
|
||||||
|
:isTrigger="false"
|
||||||
|
:description="view.description"
|
||||||
|
:showActionArrow="true"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<n8n-node-icon
|
||||||
|
type="icon"
|
||||||
|
:name="view.icon"
|
||||||
|
:circle="false"
|
||||||
|
:showTooltip="false"
|
||||||
|
></n8n-node-icon>
|
||||||
|
</template>
|
||||||
|
</n8n-node-creator-node>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ViewItemProps } from '@/Interface';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
view: ViewItemProps;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<Props>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.view {
|
||||||
|
--action-arrow-color: var(--color-text-light);
|
||||||
|
margin-left: 15px;
|
||||||
|
margin-right: 12px;
|
||||||
|
padding: 11px 4px 11px 0;
|
||||||
|
}
|
||||||
|
.withTopBorder {
|
||||||
|
border-top: 1px solid var(--color-foreground-base);
|
||||||
|
margin-top: var(--spacing-xs);
|
||||||
|
padding-top: var(--spacing-l);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,198 @@
|
||||||
|
import { getCurrentInstance, computed } from 'vue';
|
||||||
|
import {
|
||||||
|
CORE_NODES_CATEGORY,
|
||||||
|
WEBHOOK_NODE_TYPE,
|
||||||
|
OTHER_TRIGGER_NODES_SUBCATEGORY,
|
||||||
|
EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE,
|
||||||
|
MANUAL_TRIGGER_NODE_TYPE,
|
||||||
|
SCHEDULE_TRIGGER_NODE_TYPE,
|
||||||
|
REGULAR_NODE_FILTER,
|
||||||
|
TRANSFORM_DATA_SUBCATEGORY,
|
||||||
|
FILES_SUBCATEGORY,
|
||||||
|
FLOWS_CONTROL_SUBCATEGORY,
|
||||||
|
HELPERS_SUBCATEGORY,
|
||||||
|
TRIGGER_NODE_FILTER,
|
||||||
|
} from '@/constants';
|
||||||
|
import { useNodeCreatorStore } from '@/stores/nodeCreator';
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
const instance = getCurrentInstance();
|
||||||
|
const nodeCreatorStore = useNodeCreatorStore();
|
||||||
|
|
||||||
|
const VIEWS = [
|
||||||
|
{
|
||||||
|
value: REGULAR_NODE_FILTER,
|
||||||
|
title: instance?.proxy.$locale.baseText('nodeCreator.triggerHelperPanel.whatHappensNext'),
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
key: '*',
|
||||||
|
type: 'subcategory',
|
||||||
|
properties: {
|
||||||
|
subcategory: 'App Regular Nodes',
|
||||||
|
icon: 'globe',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'subcategory',
|
||||||
|
key: TRANSFORM_DATA_SUBCATEGORY,
|
||||||
|
category: CORE_NODES_CATEGORY,
|
||||||
|
properties: {
|
||||||
|
subcategory: TRANSFORM_DATA_SUBCATEGORY,
|
||||||
|
icon: 'pen',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'subcategory',
|
||||||
|
key: HELPERS_SUBCATEGORY,
|
||||||
|
category: CORE_NODES_CATEGORY,
|
||||||
|
properties: {
|
||||||
|
subcategory: HELPERS_SUBCATEGORY,
|
||||||
|
icon: 'toolbox',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'subcategory',
|
||||||
|
key: FLOWS_CONTROL_SUBCATEGORY,
|
||||||
|
category: CORE_NODES_CATEGORY,
|
||||||
|
properties: {
|
||||||
|
subcategory: FLOWS_CONTROL_SUBCATEGORY,
|
||||||
|
icon: 'code-branch',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'subcategory',
|
||||||
|
key: FILES_SUBCATEGORY,
|
||||||
|
category: CORE_NODES_CATEGORY,
|
||||||
|
properties: {
|
||||||
|
subcategory: FILES_SUBCATEGORY,
|
||||||
|
icon: 'file-alt',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: TRIGGER_NODE_FILTER,
|
||||||
|
type: 'view',
|
||||||
|
properties: {
|
||||||
|
title: instance?.proxy.$locale.baseText(
|
||||||
|
'nodeCreator.triggerHelperPanel.addAnotherTrigger',
|
||||||
|
),
|
||||||
|
icon: 'bolt',
|
||||||
|
withTopBorder: true,
|
||||||
|
description: instance?.proxy.$locale.baseText(
|
||||||
|
'nodeCreator.triggerHelperPanel.addAnotherTriggerDescription',
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: TRIGGER_NODE_FILTER,
|
||||||
|
title: instance?.proxy.$locale.baseText('nodeCreator.triggerHelperPanel.selectATrigger'),
|
||||||
|
description: instance?.proxy.$locale.baseText(
|
||||||
|
'nodeCreator.triggerHelperPanel.selectATriggerDescription',
|
||||||
|
),
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
key: '*',
|
||||||
|
type: 'subcategory',
|
||||||
|
properties: {
|
||||||
|
subcategory: 'App Trigger Nodes',
|
||||||
|
icon: 'satellite-dish',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: SCHEDULE_TRIGGER_NODE_TYPE,
|
||||||
|
type: 'node',
|
||||||
|
category: [CORE_NODES_CATEGORY],
|
||||||
|
properties: {
|
||||||
|
nodeType: {
|
||||||
|
group: [],
|
||||||
|
name: SCHEDULE_TRIGGER_NODE_TYPE,
|
||||||
|
displayName: instance?.proxy.$locale.baseText(
|
||||||
|
'nodeCreator.triggerHelperPanel.scheduleTriggerDisplayName',
|
||||||
|
),
|
||||||
|
description: instance?.proxy.$locale.baseText(
|
||||||
|
'nodeCreator.triggerHelperPanel.scheduleTriggerDescription',
|
||||||
|
),
|
||||||
|
icon: 'fa:clock',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: WEBHOOK_NODE_TYPE,
|
||||||
|
type: 'node',
|
||||||
|
category: [CORE_NODES_CATEGORY],
|
||||||
|
properties: {
|
||||||
|
nodeType: {
|
||||||
|
group: [],
|
||||||
|
name: WEBHOOK_NODE_TYPE,
|
||||||
|
displayName: instance?.proxy.$locale.baseText(
|
||||||
|
'nodeCreator.triggerHelperPanel.webhookTriggerDisplayName',
|
||||||
|
),
|
||||||
|
description: instance?.proxy.$locale.baseText(
|
||||||
|
'nodeCreator.triggerHelperPanel.webhookTriggerDescription',
|
||||||
|
),
|
||||||
|
iconData: {
|
||||||
|
type: 'file',
|
||||||
|
icon: 'webhook',
|
||||||
|
fileBuffer: '/static/webhook-icon.svg',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: MANUAL_TRIGGER_NODE_TYPE,
|
||||||
|
type: 'node',
|
||||||
|
category: [CORE_NODES_CATEGORY],
|
||||||
|
properties: {
|
||||||
|
nodeType: {
|
||||||
|
group: [],
|
||||||
|
name: MANUAL_TRIGGER_NODE_TYPE,
|
||||||
|
displayName: instance?.proxy.$locale.baseText(
|
||||||
|
'nodeCreator.triggerHelperPanel.manualTriggerDisplayName',
|
||||||
|
),
|
||||||
|
description: instance?.proxy.$locale.baseText(
|
||||||
|
'nodeCreator.triggerHelperPanel.manualTriggerDescription',
|
||||||
|
),
|
||||||
|
icon: 'fa:mouse-pointer',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE,
|
||||||
|
type: 'node',
|
||||||
|
category: [CORE_NODES_CATEGORY],
|
||||||
|
properties: {
|
||||||
|
nodeType: {
|
||||||
|
group: [],
|
||||||
|
name: EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE,
|
||||||
|
displayName: instance?.proxy.$locale.baseText(
|
||||||
|
'nodeCreator.triggerHelperPanel.workflowTriggerDisplayName',
|
||||||
|
),
|
||||||
|
description: instance?.proxy.$locale.baseText(
|
||||||
|
'nodeCreator.triggerHelperPanel.workflowTriggerDescription',
|
||||||
|
),
|
||||||
|
icon: 'fa:sign-out-alt',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'subcategory',
|
||||||
|
key: OTHER_TRIGGER_NODES_SUBCATEGORY,
|
||||||
|
category: CORE_NODES_CATEGORY,
|
||||||
|
properties: {
|
||||||
|
subcategory: OTHER_TRIGGER_NODES_SUBCATEGORY,
|
||||||
|
icon: 'folder-open',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const activeView = computed(() => {
|
||||||
|
return VIEWS.find((v) => v.value === nodeCreatorStore.selectedView) || VIEWS[0];
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
activeView,
|
||||||
|
};
|
||||||
|
};
|
|
@ -1,12 +1,8 @@
|
||||||
<template>
|
<template>
|
||||||
<Modal
|
<Modal
|
||||||
:name="PERSONALIZATION_MODAL_KEY"
|
:name="PERSONALIZATION_MODAL_KEY"
|
||||||
:title="
|
:title="$locale.baseText('personalizationModal.customizeN8n')"
|
||||||
!submitted
|
:subtitle="$locale.baseText('personalizationModal.theseQuestionsHelpUs')"
|
||||||
? $locale.baseText('personalizationModal.customizeN8n')
|
|
||||||
: $locale.baseText('personalizationModal.thanks')
|
|
||||||
"
|
|
||||||
:subtitle="!submitted ? $locale.baseText('personalizationModal.theseQuestionsHelpUs') : ''"
|
|
||||||
:centerTitle="true"
|
:centerTitle="true"
|
||||||
:showClose="false"
|
:showClose="false"
|
||||||
:eventBus="modalBus"
|
:eventBus="modalBus"
|
||||||
|
@ -17,11 +13,7 @@
|
||||||
@enter="onSave"
|
@enter="onSave"
|
||||||
>
|
>
|
||||||
<template #content>
|
<template #content>
|
||||||
<div v-if="submitted" :class="$style.submittedContainer">
|
<div :class="$style.container">
|
||||||
<img :class="$style.demoImage" :src="rootStore.baseUrl + 'suggestednodes.png'" />
|
|
||||||
<n8n-text>{{ $locale.baseText('personalizationModal.lookOutForThingsMarked') }}</n8n-text>
|
|
||||||
</div>
|
|
||||||
<div :class="$style.container" v-else>
|
|
||||||
<n8n-form-inputs
|
<n8n-form-inputs
|
||||||
:inputs="survey"
|
:inputs="survey"
|
||||||
:columnView="true"
|
:columnView="true"
|
||||||
|
@ -33,16 +25,9 @@
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div>
|
<div>
|
||||||
<n8n-button
|
<n8n-button
|
||||||
v-if="submitted"
|
|
||||||
@click="closeDialog"
|
|
||||||
:label="$locale.baseText('personalizationModal.getStarted')"
|
|
||||||
float="right"
|
|
||||||
/>
|
|
||||||
<n8n-button
|
|
||||||
v-else
|
|
||||||
@click="onSave"
|
@click="onSave"
|
||||||
:loading="isSaving"
|
:loading="isSaving"
|
||||||
:label="$locale.baseText('personalizationModal.continue')"
|
:label="$locale.baseText('personalizationModal.getStarted')"
|
||||||
float="right"
|
float="right"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -156,7 +141,6 @@ export default mixins(showMessage, workflowHelpers).extend({
|
||||||
name: 'PersonalizationModal',
|
name: 'PersonalizationModal',
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
submitted: false,
|
|
||||||
isSaving: false,
|
isSaving: false,
|
||||||
PERSONALIZATION_MODAL_KEY,
|
PERSONALIZATION_MODAL_KEY,
|
||||||
otherWorkAreaFieldVisible: false,
|
otherWorkAreaFieldVisible: false,
|
||||||
|
@ -646,12 +630,12 @@ export default mixins(showMessage, workflowHelpers).extend({
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.fetchOnboardingPrompt();
|
await this.fetchOnboardingPrompt();
|
||||||
this.submitted = true;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.$showError(e, 'Error while submitting results');
|
this.$showError(e, 'Error while submitting results');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$data.isSaving = false;
|
this.$data.isSaving = false;
|
||||||
|
this.closeDialog();
|
||||||
},
|
},
|
||||||
async fetchOnboardingPrompt() {
|
async fetchOnboardingPrompt() {
|
||||||
if (
|
if (
|
||||||
|
@ -695,17 +679,4 @@ export default mixins(showMessage, workflowHelpers).extend({
|
||||||
margin-bottom: var(--spacing-m);
|
margin-bottom: var(--spacing-m);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.submittedContainer {
|
|
||||||
* {
|
|
||||||
margin-bottom: var(--spacing-2xs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.demoImage {
|
|
||||||
border-radius: var(--border-radius-large);
|
|
||||||
border: var(--border-base);
|
|
||||||
width: 100%;
|
|
||||||
height: 140px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -274,7 +274,7 @@ exports[`PersonalizationModal.vue > should render correctly 1`] = `
|
||||||
</div>
|
</div>
|
||||||
<div class=\\"footer\\">
|
<div class=\\"footer\\">
|
||||||
<div><button aria-disabled=\\"false\\" aria-busy=\\"false\\" aria-live=\\"polite\\" class=\\"button button primary medium float-right\\">
|
<div><button aria-disabled=\\"false\\" aria-busy=\\"false\\" aria-live=\\"polite\\" class=\\"button button primary medium float-right\\">
|
||||||
<!----><span>Continue</span></button></div>
|
<!----><span>Get started</span></button></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!---->
|
<!---->
|
||||||
|
|
|
@ -103,6 +103,7 @@ export const JIRA_TRIGGER_NODE_TYPE = 'n8n-nodes-base.jiraTrigger';
|
||||||
export const MICROSOFT_EXCEL_NODE_TYPE = 'n8n-nodes-base.microsoftExcel';
|
export const MICROSOFT_EXCEL_NODE_TYPE = 'n8n-nodes-base.microsoftExcel';
|
||||||
export const MANUAL_TRIGGER_NODE_TYPE = 'n8n-nodes-base.manualTrigger';
|
export const MANUAL_TRIGGER_NODE_TYPE = 'n8n-nodes-base.manualTrigger';
|
||||||
export const MICROSOFT_TEAMS_NODE_TYPE = 'n8n-nodes-base.microsoftTeams';
|
export const MICROSOFT_TEAMS_NODE_TYPE = 'n8n-nodes-base.microsoftTeams';
|
||||||
|
export const N8N_NODE_TYPE = 'n8n-nodes-base.n8n';
|
||||||
export const NO_OP_NODE_TYPE = 'n8n-nodes-base.noOp';
|
export const NO_OP_NODE_TYPE = 'n8n-nodes-base.noOp';
|
||||||
export const STICKY_NODE_TYPE = 'n8n-nodes-base.stickyNote';
|
export const STICKY_NODE_TYPE = 'n8n-nodes-base.stickyNote';
|
||||||
export const NOTION_TRIGGER_NODE_TYPE = 'n8n-nodes-base.notionTrigger';
|
export const NOTION_TRIGGER_NODE_TYPE = 'n8n-nodes-base.notionTrigger';
|
||||||
|
@ -168,6 +169,10 @@ export const UNCATEGORIZED_CATEGORY = 'Miscellaneous';
|
||||||
export const UNCATEGORIZED_SUBCATEGORY = 'Helpers';
|
export const UNCATEGORIZED_SUBCATEGORY = 'Helpers';
|
||||||
export const PERSONALIZED_CATEGORY = 'Suggested Nodes';
|
export const PERSONALIZED_CATEGORY = 'Suggested Nodes';
|
||||||
export const OTHER_TRIGGER_NODES_SUBCATEGORY = 'Other Trigger Nodes';
|
export const OTHER_TRIGGER_NODES_SUBCATEGORY = 'Other Trigger Nodes';
|
||||||
|
export const TRANSFORM_DATA_SUBCATEGORY = 'Data Transformation';
|
||||||
|
export const FILES_SUBCATEGORY = 'Files';
|
||||||
|
export const FLOWS_CONTROL_SUBCATEGORY = 'Flow';
|
||||||
|
export const HELPERS_SUBCATEGORY = 'Helpers';
|
||||||
|
|
||||||
export const REQUEST_NODE_FORM_URL = 'https://n8n-community.typeform.com/to/K1fBVTZ3';
|
export const REQUEST_NODE_FORM_URL = 'https://n8n-community.typeform.com/to/K1fBVTZ3';
|
||||||
|
|
||||||
|
|
|
@ -677,10 +677,10 @@
|
||||||
"node.discovery.pinData.canvas": "You can pin this output instead of waiting for a test event. Open node to do so.",
|
"node.discovery.pinData.canvas": "You can pin this output instead of waiting for a test event. Open node to do so.",
|
||||||
"node.discovery.pinData.ndv": "You can pin this output instead of waiting for a test event.",
|
"node.discovery.pinData.ndv": "You can pin this output instead of waiting for a test event.",
|
||||||
"nodeBase.clickToAddNodeOrDragToConnect": "Click to add node<br />or drag to connect",
|
"nodeBase.clickToAddNodeOrDragToConnect": "Click to add node<br />or drag to connect",
|
||||||
"nodeCreator.actionsCategory.operations": "Operations",
|
"nodeCreator.actionsCategory.actions": "Actions",
|
||||||
"nodeCreator.actionsCategory.onNewEvent": "On new {event} event",
|
"nodeCreator.actionsCategory.onNewEvent": "On new {event} event",
|
||||||
"nodeCreator.actionsCategory.onEvent": "On {event}",
|
"nodeCreator.actionsCategory.onEvent": "On {event}",
|
||||||
"nodeCreator.actionsCategory.recommended": "Recommended",
|
"nodeCreator.actionsCategory.triggers": "Triggers",
|
||||||
"nodeCreator.actionsCategory.searchActions": "Search {nodeNameTitle} Actions...",
|
"nodeCreator.actionsCategory.searchActions": "Search {nodeNameTitle} Actions...",
|
||||||
"nodeCreator.actionsList.apiCall": "Didn't find the right event? Make a <a data-action='addHttpNode'>custom {nodeNameTitle} API call</a>",
|
"nodeCreator.actionsList.apiCall": "Didn't find the right event? Make a <a data-action='addHttpNode'>custom {nodeNameTitle} API call</a>",
|
||||||
"nodeCreator.actionsList.apiCallNoResult": "Nothing found — try making a <a data-action='addHttpNode'>custom {nodeNameTitle} API call</a>",
|
"nodeCreator.actionsList.apiCallNoResult": "Nothing found — try making a <a data-action='addHttpNode'>custom {nodeNameTitle} API call</a>",
|
||||||
|
@ -712,26 +712,33 @@
|
||||||
"nodeCreator.noResults.webhook": "Webhook",
|
"nodeCreator.noResults.webhook": "Webhook",
|
||||||
"nodeCreator.searchBar.searchNodes": "Search nodes...",
|
"nodeCreator.searchBar.searchNodes": "Search nodes...",
|
||||||
"nodeCreator.subcategoryDescriptions.appTriggerNodes": "Runs the flow when something happens in an app like Telegram, Notion or Airtable",
|
"nodeCreator.subcategoryDescriptions.appTriggerNodes": "Runs the flow when something happens in an app like Telegram, Notion or Airtable",
|
||||||
"nodeCreator.subcategoryDescriptions.dataTransformation": "Manipulate data fields, run code",
|
"nodeCreator.subcategoryDescriptions.appRegularNodes": "Do something in an app or service like Google Sheets, Telegram or Notion",
|
||||||
"nodeCreator.subcategoryDescriptions.files": "Work with CSV, XML, text, images etc.",
|
"nodeCreator.subcategoryDescriptions.dataTransformation": "Manipulate data, run JavaScript code, etc.",
|
||||||
"nodeCreator.subcategoryDescriptions.flow": "Branches, core triggers, merge data",
|
"nodeCreator.subcategoryDescriptions.files": "CSV, XLS, XML, text, images, etc.",
|
||||||
"nodeCreator.subcategoryDescriptions.helpers": "HTTP Requests (API calls), date and time, scrape HTML",
|
"nodeCreator.subcategoryDescriptions.flow": "IF, Switch, Wait, Compare and Merge data, etc.",
|
||||||
"nodeCreator.subcategoryDescriptions.otherTriggerNodes": "Runs the flow on workflow errors, file changes, and more",
|
"nodeCreator.subcategoryDescriptions.helpers": "HTTP Requests (API Calls), date and time, scrape HTML, RSS, SSH, etc.",
|
||||||
"nodeCreator.subcategoryNames.appTriggerNodes": "On App Event",
|
"nodeCreator.subcategoryDescriptions.otherTriggerNodes": "Runs the flow on workflow errors, file changes, etc.",
|
||||||
"nodeCreator.subcategoryNames.dataTransformation": "Data Transformation",
|
"nodeCreator.subcategoryNames.appTriggerNodes": "On app event",
|
||||||
|
"nodeCreator.subcategoryNames.appRegularNodes": "Action in an app",
|
||||||
|
"nodeCreator.subcategoryNames.dataTransformation": "Data transformation",
|
||||||
"nodeCreator.subcategoryNames.files": "Files",
|
"nodeCreator.subcategoryNames.files": "Files",
|
||||||
"nodeCreator.subcategoryNames.flow": "Flow",
|
"nodeCreator.subcategoryNames.flow": "Flow",
|
||||||
"nodeCreator.subcategoryNames.helpers": "Helpers",
|
"nodeCreator.subcategoryNames.helpers": "Helpers",
|
||||||
"nodeCreator.subcategoryNames.otherTriggerNodes": "Other ways...",
|
"nodeCreator.subcategoryNames.otherTriggerNodes": "Other ways...",
|
||||||
"nodeCreator.subcategoryTitles.otherTriggerNodes": "Other Trigger Nodes",
|
"nodeCreator.subcategoryTitles.otherTriggerNodes": "Other Trigger Nodes",
|
||||||
|
"nodeCreator.triggerHelperPanel.addAnotherTrigger": "Add another trigger",
|
||||||
|
"nodeCreator.triggerHelperPanel.addAnotherTriggerDescription": "Triggers start your workflow. Workflows can have multiple triggers.",
|
||||||
"nodeCreator.triggerHelperPanel.title": "When should this workflow run?",
|
"nodeCreator.triggerHelperPanel.title": "When should this workflow run?",
|
||||||
"nodeCreator.triggerHelperPanel.scheduleTriggerDisplayName": "On a Schedule",
|
"nodeCreator.triggerHelperPanel.scheduleTriggerDisplayName": "On a schedule",
|
||||||
"nodeCreator.triggerHelperPanel.scheduleTriggerDescription": "Runs the flow every day, hour, or custom interval",
|
"nodeCreator.triggerHelperPanel.scheduleTriggerDescription": "Runs the flow every day, hour, or custom interval",
|
||||||
"nodeCreator.triggerHelperPanel.webhookTriggerDisplayName": "On Webhook Call",
|
"nodeCreator.triggerHelperPanel.webhookTriggerDisplayName": "On webhook call",
|
||||||
"nodeCreator.triggerHelperPanel.webhookTriggerDescription": "Runs the flow when another app sends a webhook",
|
"nodeCreator.triggerHelperPanel.webhookTriggerDescription": "Runs the flow when another app sends a webhook",
|
||||||
"nodeCreator.triggerHelperPanel.manualTriggerDisplayName": "Manually",
|
"nodeCreator.triggerHelperPanel.manualTriggerDisplayName": "Manually",
|
||||||
"nodeCreator.triggerHelperPanel.manualTriggerDescription": "Runs the flow on clicking a button in n8n",
|
"nodeCreator.triggerHelperPanel.manualTriggerDescription": "Runs the flow on clicking a button in n8n",
|
||||||
"nodeCreator.triggerHelperPanel.workflowTriggerDisplayName": "When Called By Another Workflow",
|
"nodeCreator.triggerHelperPanel.whatHappensNext": "What happens next?",
|
||||||
|
"nodeCreator.triggerHelperPanel.selectATrigger": "Select a trigger",
|
||||||
|
"nodeCreator.triggerHelperPanel.selectATriggerDescription": "A trigger is a step that starts your workflow",
|
||||||
|
"nodeCreator.triggerHelperPanel.workflowTriggerDisplayName": "When called by another workflow",
|
||||||
"nodeCreator.triggerHelperPanel.workflowTriggerDescription": "Runs the flow when called by the Execute Workflow node from a different workflow",
|
"nodeCreator.triggerHelperPanel.workflowTriggerDescription": "Runs the flow when called by the Execute Workflow node from a different workflow",
|
||||||
"nodeCredentials.createNew": "Create New Credential",
|
"nodeCredentials.createNew": "Create New Credential",
|
||||||
"nodeCredentials.credentialFor": "Credential for {credentialType}",
|
"nodeCredentials.credentialFor": "Credential for {credentialType}",
|
||||||
|
@ -929,7 +936,6 @@
|
||||||
"personalizationModal.it": "IT",
|
"personalizationModal.it": "IT",
|
||||||
"personalizationModal.legal": "Legal",
|
"personalizationModal.legal": "Legal",
|
||||||
"personalizationModal.lessThan20People": "Less than 20 people",
|
"personalizationModal.lessThan20People": "Less than 20 people",
|
||||||
"personalizationModal.lookOutForThingsMarked": "Look out for things marked with a ✨. They are personalized to make n8n more relevant to you.",
|
|
||||||
"personalizationModal.managedServiceProvider": "Managed service provider",
|
"personalizationModal.managedServiceProvider": "Managed service provider",
|
||||||
"personalizationModal.manufacturing": "Manufacturing",
|
"personalizationModal.manufacturing": "Manufacturing",
|
||||||
"personalizationModal.marketing": "Marketing",
|
"personalizationModal.marketing": "Marketing",
|
||||||
|
|
|
@ -11,6 +11,7 @@ import {
|
||||||
faArrowRight,
|
faArrowRight,
|
||||||
faAt,
|
faAt,
|
||||||
faBan,
|
faBan,
|
||||||
|
faBolt,
|
||||||
faBook,
|
faBook,
|
||||||
faBoxOpen,
|
faBoxOpen,
|
||||||
faBug,
|
faBug,
|
||||||
|
@ -46,6 +47,7 @@ import {
|
||||||
faExternalLinkAlt,
|
faExternalLinkAlt,
|
||||||
faExchangeAlt,
|
faExchangeAlt,
|
||||||
faFile,
|
faFile,
|
||||||
|
faFileAlt,
|
||||||
faFileArchive,
|
faFileArchive,
|
||||||
faFileCode,
|
faFileCode,
|
||||||
faFileDownload,
|
faFileDownload,
|
||||||
|
@ -112,6 +114,7 @@ import {
|
||||||
faThumbtack,
|
faThumbtack,
|
||||||
faTimes,
|
faTimes,
|
||||||
faTimesCircle,
|
faTimesCircle,
|
||||||
|
faToolbox,
|
||||||
faTrash,
|
faTrash,
|
||||||
faUndo,
|
faUndo,
|
||||||
faUnlink,
|
faUnlink,
|
||||||
|
@ -140,6 +143,7 @@ addIcon(faArrowLeft);
|
||||||
addIcon(faArrowRight);
|
addIcon(faArrowRight);
|
||||||
addIcon(faAt);
|
addIcon(faAt);
|
||||||
addIcon(faBan);
|
addIcon(faBan);
|
||||||
|
addIcon(faBolt);
|
||||||
addIcon(faBook);
|
addIcon(faBook);
|
||||||
addIcon(faBoxOpen);
|
addIcon(faBoxOpen);
|
||||||
addIcon(faBug);
|
addIcon(faBug);
|
||||||
|
@ -176,6 +180,7 @@ addIcon(faExpandAlt);
|
||||||
addIcon(faExternalLinkAlt);
|
addIcon(faExternalLinkAlt);
|
||||||
addIcon(faExchangeAlt);
|
addIcon(faExchangeAlt);
|
||||||
addIcon(faFile);
|
addIcon(faFile);
|
||||||
|
addIcon(faFileAlt);
|
||||||
addIcon(faFileArchive);
|
addIcon(faFileArchive);
|
||||||
addIcon(faFileCode);
|
addIcon(faFileCode);
|
||||||
addIcon(faFileDownload);
|
addIcon(faFileDownload);
|
||||||
|
@ -243,6 +248,7 @@ addIcon(faThLarge);
|
||||||
addIcon(faThumbtack);
|
addIcon(faThumbtack);
|
||||||
addIcon(faTimes);
|
addIcon(faTimes);
|
||||||
addIcon(faTimesCircle);
|
addIcon(faTimesCircle);
|
||||||
|
addIcon(faToolbox);
|
||||||
addIcon(faTrash);
|
addIcon(faTrash);
|
||||||
addIcon(faUndo);
|
addIcon(faUndo);
|
||||||
addIcon(faUnlink);
|
addIcon(faUnlink);
|
||||||
|
|
|
@ -14,16 +14,14 @@ import {
|
||||||
STORES,
|
STORES,
|
||||||
MANUAL_TRIGGER_NODE_TYPE,
|
MANUAL_TRIGGER_NODE_TYPE,
|
||||||
CORE_NODES_CATEGORY,
|
CORE_NODES_CATEGORY,
|
||||||
CALENDLY_TRIGGER_NODE_TYPE,
|
|
||||||
TRIGGER_NODE_FILTER,
|
TRIGGER_NODE_FILTER,
|
||||||
WEBHOOK_NODE_TYPE,
|
|
||||||
STICKY_NODE_TYPE,
|
STICKY_NODE_TYPE,
|
||||||
} from '@/constants';
|
} from '@/constants';
|
||||||
import { useNodeTypesStore } from '@/stores/nodeTypes';
|
import { useNodeTypesStore } from '@/stores/nodeTypes';
|
||||||
import { useWorkflowsStore } from './workflows';
|
import { useWorkflowsStore } from './workflows';
|
||||||
import { CUSTOM_API_CALL_KEY, ALL_NODE_FILTER } from '@/constants';
|
import { CUSTOM_API_CALL_KEY, ALL_NODE_FILTER } from '@/constants';
|
||||||
import { INodeCreatorState, INodeFilterType, IUpdateInformation } from '@/Interface';
|
import { INodeCreatorState, INodeFilterType, IUpdateInformation } from '@/Interface';
|
||||||
import { i18n } from '@/plugins/i18n';
|
import { BaseTextKey, i18n } from '@/plugins/i18n';
|
||||||
import { externalHooks } from '@/mixins/externalHooks';
|
import { externalHooks } from '@/mixins/externalHooks';
|
||||||
import { Telemetry } from '@/plugins/telemetry';
|
import { Telemetry } from '@/plugins/telemetry';
|
||||||
|
|
||||||
|
@ -42,7 +40,7 @@ const customNodeActionsParsers: {
|
||||||
(categoryItem): INodeActionTypeDescription => ({
|
(categoryItem): INodeActionTypeDescription => ({
|
||||||
...getNodeTypeBase(
|
...getNodeTypeBase(
|
||||||
nodeTypeDescription,
|
nodeTypeDescription,
|
||||||
i18n.baseText('nodeCreator.actionsCategory.recommended'),
|
i18n.baseText('nodeCreator.actionsCategory.triggers'),
|
||||||
),
|
),
|
||||||
actionKey: categoryItem.value as string,
|
actionKey: categoryItem.value as string,
|
||||||
displayName: i18n.baseText('nodeCreator.actionsCategory.onEvent', {
|
displayName: i18n.baseText('nodeCreator.actionsCategory.onEvent', {
|
||||||
|
@ -81,7 +79,9 @@ function getNodeTypeBase(nodeTypeDescription: INodeTypeDescription, category: st
|
||||||
iconUrl: nodeTypeDescription.iconUrl,
|
iconUrl: nodeTypeDescription.iconUrl,
|
||||||
icon: nodeTypeDescription.icon,
|
icon: nodeTypeDescription.icon,
|
||||||
version: [1],
|
version: [1],
|
||||||
defaults: {},
|
defaults: {
|
||||||
|
...nodeTypeDescription.defaults,
|
||||||
|
},
|
||||||
inputs: [],
|
inputs: [],
|
||||||
outputs: [],
|
outputs: [],
|
||||||
properties: [],
|
properties: [],
|
||||||
|
@ -104,10 +104,7 @@ function operationsCategory(
|
||||||
);
|
);
|
||||||
|
|
||||||
const items = filteredOutItems.map((item: INodePropertyOptions) => ({
|
const items = filteredOutItems.map((item: INodePropertyOptions) => ({
|
||||||
...getNodeTypeBase(
|
...getNodeTypeBase(nodeTypeDescription, i18n.baseText('nodeCreator.actionsCategory.actions')),
|
||||||
nodeTypeDescription,
|
|
||||||
i18n.baseText('nodeCreator.actionsCategory.operations'),
|
|
||||||
),
|
|
||||||
actionKey: item.value as string,
|
actionKey: item.value as string,
|
||||||
displayName: item.action ?? startCase(item.name),
|
displayName: item.action ?? startCase(item.name),
|
||||||
description: item.description ?? '',
|
description: item.description ?? '',
|
||||||
|
@ -123,9 +120,7 @@ function operationsCategory(
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
function recommendedCategory(
|
function triggersCategory(nodeTypeDescription: INodeTypeDescription): INodeActionTypeDescription[] {
|
||||||
nodeTypeDescription: INodeTypeDescription,
|
|
||||||
): INodeActionTypeDescription[] {
|
|
||||||
const matchingKeys = ['event', 'events', 'trigger on'];
|
const matchingKeys = ['event', 'events', 'trigger on'];
|
||||||
const isTrigger = nodeTypeDescription.displayName?.toLowerCase().includes('trigger');
|
const isTrigger = nodeTypeDescription.displayName?.toLowerCase().includes('trigger');
|
||||||
const matchedProperty = nodeTypeDescription.properties.find((property) =>
|
const matchedProperty = nodeTypeDescription.properties.find((property) =>
|
||||||
|
@ -141,7 +136,7 @@ function recommendedCategory(
|
||||||
{
|
{
|
||||||
...getNodeTypeBase(
|
...getNodeTypeBase(
|
||||||
nodeTypeDescription,
|
nodeTypeDescription,
|
||||||
i18n.baseText('nodeCreator.actionsCategory.recommended'),
|
i18n.baseText('nodeCreator.actionsCategory.triggers'),
|
||||||
),
|
),
|
||||||
actionKey: PLACEHOLDER_RECOMMENDED_ACTION_KEY,
|
actionKey: PLACEHOLDER_RECOMMENDED_ACTION_KEY,
|
||||||
displayName: i18n.baseText('nodeCreator.actionsCategory.onNewEvent', {
|
displayName: i18n.baseText('nodeCreator.actionsCategory.onNewEvent', {
|
||||||
|
@ -166,7 +161,7 @@ function recommendedCategory(
|
||||||
filteredOutItems.map((categoryItem: INodePropertyOptions) => ({
|
filteredOutItems.map((categoryItem: INodePropertyOptions) => ({
|
||||||
...getNodeTypeBase(
|
...getNodeTypeBase(
|
||||||
nodeTypeDescription,
|
nodeTypeDescription,
|
||||||
i18n.baseText('nodeCreator.actionsCategory.recommended'),
|
i18n.baseText('nodeCreator.actionsCategory.triggers'),
|
||||||
),
|
),
|
||||||
actionKey: categoryItem.value as string,
|
actionKey: categoryItem.value as string,
|
||||||
displayName:
|
displayName:
|
||||||
|
@ -247,19 +242,26 @@ function resourceCategories(
|
||||||
export const useNodeCreatorStore = defineStore(STORES.NODE_CREATOR, {
|
export const useNodeCreatorStore = defineStore(STORES.NODE_CREATOR, {
|
||||||
state: (): INodeCreatorState => ({
|
state: (): INodeCreatorState => ({
|
||||||
itemsFilter: '',
|
itemsFilter: '',
|
||||||
showTabs: true,
|
|
||||||
showScrim: false,
|
showScrim: false,
|
||||||
selectedType: ALL_NODE_FILTER,
|
selectedView: TRIGGER_NODE_FILTER,
|
||||||
|
rootViewHistory: [],
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
setShowTabs(isVisible: boolean) {
|
|
||||||
this.showTabs = isVisible;
|
|
||||||
},
|
|
||||||
setShowScrim(isVisible: boolean) {
|
setShowScrim(isVisible: boolean) {
|
||||||
this.showScrim = isVisible;
|
this.showScrim = isVisible;
|
||||||
},
|
},
|
||||||
setSelectedType(selectedNodeType: INodeFilterType) {
|
setSelectedView(selectedNodeType: INodeFilterType) {
|
||||||
this.selectedType = selectedNodeType;
|
this.selectedView = selectedNodeType;
|
||||||
|
if (!this.rootViewHistory.includes(selectedNodeType)) {
|
||||||
|
this.rootViewHistory.push(selectedNodeType);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
closeCurrentView() {
|
||||||
|
this.rootViewHistory.pop();
|
||||||
|
this.selectedView = this.rootViewHistory[this.rootViewHistory.length - 1];
|
||||||
|
},
|
||||||
|
resetRootViewHistory() {
|
||||||
|
this.rootViewHistory = [];
|
||||||
},
|
},
|
||||||
setFilter(search: string) {
|
setFilter(search: string) {
|
||||||
this.itemsFilter = search;
|
this.itemsFilter = search;
|
||||||
|
@ -295,16 +297,11 @@ export const useNodeCreatorStore = defineStore(STORES.NODE_CREATOR, {
|
||||||
visibleNodesWithActions(): INodeTypeDescription[] {
|
visibleNodesWithActions(): INodeTypeDescription[] {
|
||||||
const nodes = deepCopy(useNodeTypesStore().visibleNodeTypes);
|
const nodes = deepCopy(useNodeTypesStore().visibleNodeTypes);
|
||||||
const nodesWithActions = nodes.map((node) => {
|
const nodesWithActions = nodes.map((node) => {
|
||||||
const isCoreNode = node.codex?.categories?.includes(CORE_NODES_CATEGORY);
|
node.actions = [
|
||||||
// Core nodes shouldn't support actions
|
...triggersCategory(node),
|
||||||
node.actions = [];
|
|
||||||
if (isCoreNode) return node;
|
|
||||||
|
|
||||||
node.actions.push(
|
|
||||||
...recommendedCategory(node),
|
|
||||||
...operationsCategory(node),
|
...operationsCategory(node),
|
||||||
...resourceCategories(node),
|
...resourceCategories(node),
|
||||||
);
|
];
|
||||||
|
|
||||||
return node;
|
return node;
|
||||||
});
|
});
|
||||||
|
@ -322,20 +319,15 @@ export const useNodeCreatorStore = defineStore(STORES.NODE_CREATOR, {
|
||||||
})
|
})
|
||||||
.reduce((acc: Record<string, INodeTypeDescription>, node: INodeTypeDescription) => {
|
.reduce((acc: Record<string, INodeTypeDescription>, node: INodeTypeDescription) => {
|
||||||
const clonedNode = deepCopy(node);
|
const clonedNode = deepCopy(node);
|
||||||
const isCoreNode = node.codex?.categories?.includes(CORE_NODES_CATEGORY);
|
|
||||||
const actions = node.actions || [];
|
const actions = node.actions || [];
|
||||||
// Do not merge core nodes
|
// Do not merge core nodes
|
||||||
const normalizedName = isCoreNode
|
const normalizedName = node.name.toLowerCase().replace('trigger', '');
|
||||||
? node.name
|
|
||||||
: node.name.toLowerCase().replace('trigger', '');
|
|
||||||
const existingNode = acc[normalizedName];
|
const existingNode = acc[normalizedName];
|
||||||
|
|
||||||
if (existingNode) existingNode.actions?.push(...actions);
|
if (existingNode) existingNode.actions?.push(...actions);
|
||||||
else acc[normalizedName] = clonedNode;
|
else acc[normalizedName] = clonedNode;
|
||||||
|
|
||||||
if (!isCoreNode) {
|
acc[normalizedName].displayName = node.displayName.replace('Trigger', '');
|
||||||
acc[normalizedName].displayName = node.displayName.replace('Trigger', '');
|
|
||||||
}
|
|
||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
|
@ -355,7 +347,7 @@ export const useNodeCreatorStore = defineStore(STORES.NODE_CREATOR, {
|
||||||
const { workflowTriggerNodes } = useWorkflowsStore();
|
const { workflowTriggerNodes } = useWorkflowsStore();
|
||||||
const isTrigger = useNodeTypesStore().isTriggerNode(nodeType);
|
const isTrigger = useNodeTypesStore().isTriggerNode(nodeType);
|
||||||
const workflowContainsTrigger = workflowTriggerNodes.length > 0;
|
const workflowContainsTrigger = workflowTriggerNodes.length > 0;
|
||||||
const isTriggerPanel = useNodeCreatorStore().selectedType === TRIGGER_NODE_FILTER;
|
const isTriggerPanel = useNodeCreatorStore().selectedView === TRIGGER_NODE_FILTER;
|
||||||
const isStickyNode = nodeType === STICKY_NODE_TYPE;
|
const isStickyNode = nodeType === STICKY_NODE_TYPE;
|
||||||
|
|
||||||
const nodeTypes =
|
const nodeTypes =
|
||||||
|
|
|
@ -4,7 +4,6 @@ import { useNodeTypesStore } from './../stores/nodeTypes';
|
||||||
import { INodeCredentialDescription } from './../../../workflow/src/Interfaces';
|
import { INodeCredentialDescription } from './../../../workflow/src/Interfaces';
|
||||||
import {
|
import {
|
||||||
CORE_NODES_CATEGORY,
|
CORE_NODES_CATEGORY,
|
||||||
RECOMMENDED_CATEGORY,
|
|
||||||
CUSTOM_NODES_CATEGORY,
|
CUSTOM_NODES_CATEGORY,
|
||||||
SUBCATEGORY_DESCRIPTIONS,
|
SUBCATEGORY_DESCRIPTIONS,
|
||||||
UNCATEGORIZED_CATEGORY,
|
UNCATEGORIZED_CATEGORY,
|
||||||
|
@ -72,7 +71,7 @@ const addNodeToCategory = (
|
||||||
}
|
}
|
||||||
accu[category][subcategory].nodes.push({
|
accu[category][subcategory].nodes.push({
|
||||||
type: nodeType.actionKey ? 'action' : 'node',
|
type: nodeType.actionKey ? 'action' : 'node',
|
||||||
key: `${category}_${nodeType.name}`,
|
key: `${nodeType.name}`,
|
||||||
category,
|
category,
|
||||||
properties: {
|
properties: {
|
||||||
nodeType,
|
nodeType,
|
||||||
|
@ -85,17 +84,12 @@ const addNodeToCategory = (
|
||||||
|
|
||||||
export const getCategoriesWithNodes = (
|
export const getCategoriesWithNodes = (
|
||||||
nodeTypes: INodeTypeDescription[],
|
nodeTypes: INodeTypeDescription[],
|
||||||
personalizedNodeTypes: string[],
|
|
||||||
uncategorizedSubcategory = UNCATEGORIZED_SUBCATEGORY,
|
uncategorizedSubcategory = UNCATEGORIZED_SUBCATEGORY,
|
||||||
): ICategoriesWithNodes => {
|
): ICategoriesWithNodes => {
|
||||||
const sorted = [...nodeTypes].sort((a: INodeTypeDescription, b: INodeTypeDescription) =>
|
const sorted = [...nodeTypes].sort((a: INodeTypeDescription, b: INodeTypeDescription) =>
|
||||||
a.displayName > b.displayName ? 1 : -1,
|
a.displayName > b.displayName ? 1 : -1,
|
||||||
);
|
);
|
||||||
const result = sorted.reduce((accu: ICategoriesWithNodes, nodeType: INodeTypeDescription) => {
|
const result = sorted.reduce((accu: ICategoriesWithNodes, nodeType: INodeTypeDescription) => {
|
||||||
if (personalizedNodeTypes.includes(nodeType.name)) {
|
|
||||||
addNodeToCategory(accu, nodeType, PERSONALIZED_CATEGORY, uncategorizedSubcategory);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!nodeType.codex || !nodeType.codex.categories) {
|
if (!nodeType.codex || !nodeType.codex.categories) {
|
||||||
addNodeToCategory(accu, nodeType, UNCATEGORIZED_CATEGORY, uncategorizedSubcategory);
|
addNodeToCategory(accu, nodeType, UNCATEGORIZED_CATEGORY, uncategorizedSubcategory);
|
||||||
return accu;
|
return accu;
|
||||||
|
@ -125,14 +119,12 @@ const getCategories = (categoriesWithNodes: ICategoriesWithNodes): string[] => {
|
||||||
CUSTOM_NODES_CATEGORY,
|
CUSTOM_NODES_CATEGORY,
|
||||||
UNCATEGORIZED_CATEGORY,
|
UNCATEGORIZED_CATEGORY,
|
||||||
PERSONALIZED_CATEGORY,
|
PERSONALIZED_CATEGORY,
|
||||||
RECOMMENDED_CATEGORY,
|
|
||||||
];
|
];
|
||||||
const categories = Object.keys(categoriesWithNodes);
|
const categories = Object.keys(categoriesWithNodes);
|
||||||
const sorted = categories.filter((category: string) => !excludeFromSort.includes(category));
|
const sorted = categories.filter((category: string) => !excludeFromSort.includes(category));
|
||||||
sorted.sort();
|
sorted.sort();
|
||||||
|
|
||||||
return [
|
return [
|
||||||
RECOMMENDED_CATEGORY,
|
|
||||||
CORE_NODES_CATEGORY,
|
CORE_NODES_CATEGORY,
|
||||||
CUSTOM_NODES_CATEGORY,
|
CUSTOM_NODES_CATEGORY,
|
||||||
PERSONALIZED_CATEGORY,
|
PERSONALIZED_CATEGORY,
|
||||||
|
@ -155,8 +147,9 @@ export const getCategorizedList = (
|
||||||
const categoryEl: INodeCreateElement = {
|
const categoryEl: INodeCreateElement = {
|
||||||
type: 'category',
|
type: 'category',
|
||||||
key: category,
|
key: category,
|
||||||
category,
|
|
||||||
properties: {
|
properties: {
|
||||||
|
category,
|
||||||
|
name: category,
|
||||||
expanded: categoryIsExpanded,
|
expanded: categoryIsExpanded,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -179,7 +172,6 @@ export const getCategorizedList = (
|
||||||
const subcategoryEl: INodeCreateElement = {
|
const subcategoryEl: INodeCreateElement = {
|
||||||
type: 'subcategory',
|
type: 'subcategory',
|
||||||
key: `${category}_${subcategory}`,
|
key: `${category}_${subcategory}`,
|
||||||
category,
|
|
||||||
properties: {
|
properties: {
|
||||||
subcategory,
|
subcategory,
|
||||||
description: SUBCATEGORY_DESCRIPTIONS[category][subcategory],
|
description: SUBCATEGORY_DESCRIPTIONS[category][subcategory],
|
||||||
|
@ -277,15 +269,15 @@ export const executionDataToJson = (inputData: INodeExecutionData[]): IDataObjec
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
export const matchesSelectType = (el: INodeCreateElement, selectedType: string) => {
|
export const matchesSelectType = (el: INodeCreateElement, selectedView: string) => {
|
||||||
if (selectedType === REGULAR_NODE_FILTER && el.includedByRegular) {
|
if (selectedView === REGULAR_NODE_FILTER && el.includedByRegular) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (selectedType === TRIGGER_NODE_FILTER && el.includedByTrigger) {
|
if (selectedView === TRIGGER_NODE_FILTER && el.includedByTrigger) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return selectedType === ALL_NODE_FILTER;
|
return selectedView === ALL_NODE_FILTER;
|
||||||
};
|
};
|
||||||
|
|
||||||
const matchesAlias = (nodeType: INodeTypeDescription, filter: string): boolean => {
|
const matchesAlias = (nodeType: INodeTypeDescription, filter: string): boolean => {
|
||||||
|
|
|
@ -196,6 +196,8 @@ import {
|
||||||
TRIGGER_NODE_FILTER,
|
TRIGGER_NODE_FILTER,
|
||||||
EnterpriseEditionFeature,
|
EnterpriseEditionFeature,
|
||||||
POSTHOG_ASSUMPTION_TEST,
|
POSTHOG_ASSUMPTION_TEST,
|
||||||
|
REGULAR_NODE_FILTER,
|
||||||
|
MANUAL_TRIGGER_NODE_TYPE,
|
||||||
} from '@/constants';
|
} from '@/constants';
|
||||||
import { copyPaste } from '@/mixins/copyPaste';
|
import { copyPaste } from '@/mixins/copyPaste';
|
||||||
import { externalHooks } from '@/mixins/externalHooks';
|
import { externalHooks } from '@/mixins/externalHooks';
|
||||||
|
@ -752,10 +754,9 @@ export default mixins(
|
||||||
},
|
},
|
||||||
showTriggerCreator(source: string) {
|
showTriggerCreator(source: string) {
|
||||||
if (this.createNodeActive) return;
|
if (this.createNodeActive) return;
|
||||||
this.nodeCreatorStore.setSelectedType(TRIGGER_NODE_FILTER);
|
this.nodeCreatorStore.setSelectedView(TRIGGER_NODE_FILTER);
|
||||||
this.nodeCreatorStore.setShowScrim(true);
|
this.nodeCreatorStore.setShowScrim(true);
|
||||||
this.onToggleNodeCreator({ source, createNodeActive: true });
|
this.onToggleNodeCreator({ source, createNodeActive: true });
|
||||||
this.$nextTick(() => this.nodeCreatorStore.setShowTabs(false));
|
|
||||||
},
|
},
|
||||||
async openExecution(executionId: string) {
|
async openExecution(executionId: string) {
|
||||||
this.startLoading();
|
this.startLoading();
|
||||||
|
@ -1799,6 +1800,7 @@ export default mixins(
|
||||||
options: AddNodeOptions = {},
|
options: AddNodeOptions = {},
|
||||||
showDetail = true,
|
showDetail = true,
|
||||||
trackHistory = false,
|
trackHistory = false,
|
||||||
|
isAutoAdd = false,
|
||||||
) {
|
) {
|
||||||
const nodeTypeData: INodeTypeDescription | null =
|
const nodeTypeData: INodeTypeDescription | null =
|
||||||
this.nodeTypesStore.getNodeType(nodeTypeName);
|
this.nodeTypesStore.getNodeType(nodeTypeName);
|
||||||
|
@ -1920,6 +1922,7 @@ export default mixins(
|
||||||
this.$externalHooks().run('nodeView.addNodeButton', { nodeTypeName });
|
this.$externalHooks().run('nodeView.addNodeButton', { nodeTypeName });
|
||||||
const trackProperties: ITelemetryTrackProperties = {
|
const trackProperties: ITelemetryTrackProperties = {
|
||||||
node_type: nodeTypeName,
|
node_type: nodeTypeName,
|
||||||
|
is_auto_add: isAutoAdd,
|
||||||
workflow_id: this.workflowsStore.workflowId,
|
workflow_id: this.workflowsStore.workflowId,
|
||||||
drag_and_drop: options.dragAndDrop,
|
drag_and_drop: options.dragAndDrop,
|
||||||
};
|
};
|
||||||
|
@ -2005,6 +2008,7 @@ export default mixins(
|
||||||
options: AddNodeOptions = {},
|
options: AddNodeOptions = {},
|
||||||
showDetail = true,
|
showDetail = true,
|
||||||
trackHistory = false,
|
trackHistory = false,
|
||||||
|
isAutoAdd = false,
|
||||||
) {
|
) {
|
||||||
if (!this.editAllowedCheck()) {
|
if (!this.editAllowedCheck()) {
|
||||||
return;
|
return;
|
||||||
|
@ -2016,7 +2020,13 @@ export default mixins(
|
||||||
|
|
||||||
this.historyStore.startRecordingUndo();
|
this.historyStore.startRecordingUndo();
|
||||||
|
|
||||||
const newNodeData = await this.injectNode(nodeTypeName, options, showDetail, trackHistory);
|
const newNodeData = await this.injectNode(
|
||||||
|
nodeTypeName,
|
||||||
|
options,
|
||||||
|
showDetail,
|
||||||
|
trackHistory,
|
||||||
|
isAutoAdd,
|
||||||
|
);
|
||||||
if (!newNodeData) {
|
if (!newNodeData) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -3674,12 +3684,14 @@ export default mixins(
|
||||||
if (createNodeActive === this.createNodeActive) return;
|
if (createNodeActive === this.createNodeActive) return;
|
||||||
|
|
||||||
// Default to the trigger tab in node creator if there's no trigger node yet
|
// Default to the trigger tab in node creator if there's no trigger node yet
|
||||||
if (!this.containsTrigger) this.nodeCreatorStore.setSelectedType(TRIGGER_NODE_FILTER);
|
this.nodeCreatorStore.setSelectedView(
|
||||||
|
this.containsTrigger ? REGULAR_NODE_FILTER : TRIGGER_NODE_FILTER,
|
||||||
|
);
|
||||||
|
|
||||||
this.createNodeActive = createNodeActive;
|
this.createNodeActive = createNodeActive;
|
||||||
|
|
||||||
const mode =
|
const mode =
|
||||||
this.nodeCreatorStore.selectedType === TRIGGER_NODE_FILTER ? 'trigger' : 'default';
|
this.nodeCreatorStore.selectedView === TRIGGER_NODE_FILTER ? 'trigger' : 'regular';
|
||||||
this.$externalHooks().run('nodeView.createNodeActiveChanged', {
|
this.$externalHooks().run('nodeView.createNodeActiveChanged', {
|
||||||
source,
|
source,
|
||||||
mode,
|
mode,
|
||||||
|
@ -3697,11 +3709,14 @@ export default mixins(
|
||||||
dragAndDrop: boolean,
|
dragAndDrop: boolean,
|
||||||
) {
|
) {
|
||||||
nodeTypes.forEach(({ nodeTypeName, position }, index) => {
|
nodeTypes.forEach(({ nodeTypeName, position }, index) => {
|
||||||
|
const isManualTrigger = nodeTypeName === MANUAL_TRIGGER_NODE_TYPE;
|
||||||
|
const openNDV = !isManualTrigger && (nodeTypes.length === 1 || index > 0);
|
||||||
this.addNode(
|
this.addNode(
|
||||||
nodeTypeName,
|
nodeTypeName,
|
||||||
{ position, dragAndDrop },
|
{ position, dragAndDrop },
|
||||||
nodeTypes.length === 1 || index > 0,
|
openNDV,
|
||||||
true,
|
true,
|
||||||
|
nodeTypes.length > 1 && index < 1,
|
||||||
);
|
);
|
||||||
if (index === 0) return;
|
if (index === 0) return;
|
||||||
// If there's more than one node, we want to connect them
|
// If there's more than one node, we want to connect them
|
||||||
|
@ -3717,7 +3732,7 @@ export default mixins(
|
||||||
this.connectTwoNodes(previouslyAddedNode.name, 0, lastAddedNode.name, 0),
|
this.connectTwoNodes(previouslyAddedNode.name, 0, lastAddedNode.name, 0),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Position the added node to the right side of the previsouly added one
|
// Position the added node to the right side of the previously added one
|
||||||
lastAddedNode.position = [
|
lastAddedNode.position = [
|
||||||
previouslyAddedNode.position[0] + NodeViewUtils.NODE_SIZE * 2,
|
previouslyAddedNode.position[0] + NodeViewUtils.NODE_SIZE * 2,
|
||||||
previouslyAddedNode.position[1],
|
previouslyAddedNode.position[1],
|
||||||
|
|
|
@ -22,56 +22,67 @@ const nodeOperations: INodePropertyOptions[] = [
|
||||||
name: 'Blur',
|
name: 'Blur',
|
||||||
value: 'blur',
|
value: 'blur',
|
||||||
description: 'Adds a blur to the image and so makes it less sharp',
|
description: 'Adds a blur to the image and so makes it less sharp',
|
||||||
|
action: 'Blur Image',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Border',
|
name: 'Border',
|
||||||
value: 'border',
|
value: 'border',
|
||||||
description: 'Adds a border to the image',
|
description: 'Adds a border to the image',
|
||||||
|
action: 'Border Image',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Composite',
|
name: 'Composite',
|
||||||
value: 'composite',
|
value: 'composite',
|
||||||
description: 'Composite image on top of another one',
|
description: 'Composite image on top of another one',
|
||||||
|
action: 'Composite Image',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Create',
|
name: 'Create',
|
||||||
value: 'create',
|
value: 'create',
|
||||||
description: 'Create a new image',
|
description: 'Create a new image',
|
||||||
|
action: 'Create Image',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Crop',
|
name: 'Crop',
|
||||||
value: 'crop',
|
value: 'crop',
|
||||||
description: 'Crops the image',
|
description: 'Crops the image',
|
||||||
|
action: 'Crop Image',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Draw',
|
name: 'Draw',
|
||||||
value: 'draw',
|
value: 'draw',
|
||||||
description: 'Draw on image',
|
description: 'Draw on image',
|
||||||
|
action: 'Draw Image',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Rotate',
|
name: 'Rotate',
|
||||||
value: 'rotate',
|
value: 'rotate',
|
||||||
description: 'Rotate image',
|
description: 'Rotate image',
|
||||||
|
action: 'Rotate Image',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Resize',
|
name: 'Resize',
|
||||||
value: 'resize',
|
value: 'resize',
|
||||||
description: 'Change the size of image',
|
description: 'Change the size of image',
|
||||||
|
action: 'Resize Image',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Shear',
|
name: 'Shear',
|
||||||
value: 'shear',
|
value: 'shear',
|
||||||
description: 'Shear image along the X or Y axis',
|
description: 'Shear image along the X or Y axis',
|
||||||
|
action: 'Shear Image',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Text',
|
name: 'Text',
|
||||||
value: 'text',
|
value: 'text',
|
||||||
description: 'Adds text to image',
|
description: 'Adds text to image',
|
||||||
|
action: 'Apply Text to Image',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Transparent',
|
name: 'Transparent',
|
||||||
value: 'transparent',
|
value: 'transparent',
|
||||||
description: 'Make a color in image transparent',
|
description: 'Make a color in image transparent',
|
||||||
|
action: 'Add Transparency to Image',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -54,6 +54,7 @@ const versionDescription: INodeTypeDescription = {
|
||||||
{
|
{
|
||||||
name: 'Send',
|
name: 'Send',
|
||||||
value: 'send',
|
value: 'send',
|
||||||
|
action: 'Send an Email',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
|
@ -17,6 +17,6 @@
|
||||||
},
|
},
|
||||||
"alias": ["Workflow", "Execution"],
|
"alias": ["Workflow", "Execution"],
|
||||||
"subcategories": {
|
"subcategories": {
|
||||||
"Core Nodes": ["Helpers"]
|
"Core Nodes": ["Helpers", "Flow", "Other Trigger Nodes"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@ export class N8n implements INodeType {
|
||||||
group: ['transform'],
|
group: ['transform'],
|
||||||
version: 1,
|
version: 1,
|
||||||
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
||||||
description: 'Consume n8n API',
|
description: 'Handle events and perform actions on your n8n instance',
|
||||||
defaults: {
|
defaults: {
|
||||||
name: 'n8n',
|
name: 'n8n',
|
||||||
},
|
},
|
||||||
|
|
|
@ -11,6 +11,6 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"subcategories": {
|
"subcategories": {
|
||||||
"Core Nodes": ["Flow", "Other Trigger Nodes"]
|
"Core Nodes": ["Flow", "Other Trigger Nodes", "Helpers"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ export class N8nTrigger implements INodeType {
|
||||||
icon: 'file:n8nTrigger.svg',
|
icon: 'file:n8nTrigger.svg',
|
||||||
group: ['trigger'],
|
group: ['trigger'],
|
||||||
version: 1,
|
version: 1,
|
||||||
description: 'Handle events from your n8n instance',
|
description: 'Handle events and perform actions on your n8n instance',
|
||||||
eventTriggerDescription: '',
|
eventTriggerDescription: '',
|
||||||
mockManualExecution: true,
|
mockManualExecution: true,
|
||||||
defaults: {
|
defaults: {
|
||||||
|
|
|
@ -69,7 +69,7 @@ export class SpreadsheetFile implements INodeType {
|
||||||
name: 'Write to File',
|
name: 'Write to File',
|
||||||
value: 'toFile',
|
value: 'toFile',
|
||||||
description: 'Writes the workflow data to a spreadsheet file',
|
description: 'Writes the workflow data to a spreadsheet file',
|
||||||
action: 'Write the workflow data to a spreadsheet file',
|
action: 'Write data to a spreadsheet file',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
default: 'fromFile',
|
default: 'fromFile',
|
||||||
|
|
Loading…
Reference in a new issue