diff --git a/cypress/e2e/4-node-creator.cy.ts b/cypress/e2e/4-node-creator.cy.ts
index e11dc9b3f2..f3efd655b2 100644
--- a/cypress/e2e/4-node-creator.cy.ts
+++ b/cypress/e2e/4-node-creator.cy.ts
@@ -70,11 +70,20 @@ describe('Node Creator', () => {
.should('exist')
.should('contain.text', 'We didn\'t make that... yet');
+ nodeCreatorFeature.getters.searchBar().find('input').clear().type('edit image');
+ nodeCreatorFeature.getters.creatorItem().should('have.length', 1);
+
+ nodeCreatorFeature.getters.searchBar().find('input').clear().type('this node totally does not exist');
+ nodeCreatorFeature.getters.creatorItem().should('have.length', 0);
+
+ nodeCreatorFeature.getters.searchBar().find('input').clear()
+ nodeCreatorFeature.getters.getCreatorItem('On App Event').click();
+
nodeCreatorFeature.getters.searchBar().find('input').clear().type('edit image');
nodeCreatorFeature.getters.creatorItem().should('have.length', 0);
nodeCreatorFeature.getters.noResults()
.should('exist')
- .should('contain.text', 'To see results, click here');
+ .should('contain.text', 'To see all results, click here');
nodeCreatorFeature.getters.noResults().contains('click here').click();
nodeCreatorFeature.getters.nodeCreatorTabs().should('exist');
@@ -85,6 +94,7 @@ describe('Node Creator', () => {
})
it('should add manual trigger node', () => {
+ cy.get('.el-loading-mask').should('not.exist');
nodeCreatorFeature.getters.canvasAddButton().click();
nodeCreatorFeature.getters.getCreatorItem('Manually').click();
@@ -95,7 +105,7 @@ describe('Node Creator', () => {
nodeCreatorFeature.getters.nodeCreator().should('not.exist');
// TODO: Replace once we have canvas feature utils
- cy.get('div').contains("On clicking 'execute'").should('exist');
+ cy.get('div').contains("Add first step").should('exist');
})
it('check if non-core nodes are rendered', () => {
cy.wait('@nodesIntercept').then((interception) => {
@@ -144,7 +154,7 @@ describe('Node Creator', () => {
nodeCreatorFeature.getters.getCreatorItem(customCategory).should('exist');
nodeCreatorFeature.actions.toggleCategory(customCategory);
- nodeCreatorFeature.getters.getCreatorItem(customNode).findChildByTestId('node-item-community-tooltip').should('exist');
+ nodeCreatorFeature.getters.getCreatorItem(customNode).findChildByTestId('node-creator-item-tooltip').should('exist');
nodeCreatorFeature.getters.getCreatorItem(customNode).contains(customNodeDescription).should('exist');
nodeCreatorFeature.actions.selectNode(customNode);
diff --git a/cypress/e2e/7-workflow-actions.cy.ts b/cypress/e2e/7-workflow-actions.cy.ts
index 495614d569..b7d903b9ba 100644
--- a/cypress/e2e/7-workflow-actions.cy.ts
+++ b/cypress/e2e/7-workflow-actions.cy.ts
@@ -66,7 +66,7 @@ describe('Workflow Actions', () => {
it('should add more tags', () => {
WorkflowPage.getters.newTagLink().click();
WorkflowPage.actions.addTags(TEST_WF_TAGS);
- WorkflowPage.getters.workflowTagElements().first().click();
+ WorkflowPage.getters.firstWorkflowTagElement().click();
WorkflowPage.actions.addTags(['Another one']);
WorkflowPage.getters.workflowTagElements().should('have.length', TEST_WF_TAGS.length + 1);
});
@@ -74,7 +74,7 @@ describe('Workflow Actions', () => {
it('should remove tags by clicking X in tag', () => {
WorkflowPage.getters.newTagLink().click();
WorkflowPage.actions.addTags(TEST_WF_TAGS);
- WorkflowPage.getters.workflowTagElements().first().click();
+ WorkflowPage.getters.firstWorkflowTagElement().click();
WorkflowPage.getters.workflowTagsContainer().find('.el-tag__close').first().click();
cy.get('body').type('{enter}');
WorkflowPage.getters.workflowTagElements().should('have.length', TEST_WF_TAGS.length - 1);
@@ -83,7 +83,7 @@ describe('Workflow Actions', () => {
it('should remove tags from dropdown', () => {
WorkflowPage.getters.newTagLink().click();
WorkflowPage.actions.addTags(TEST_WF_TAGS);
- WorkflowPage.getters.workflowTagElements().first().click();
+ WorkflowPage.getters.firstWorkflowTagElement().click();
WorkflowPage.getters.workflowTagsDropdown().find('li').first().click();
cy.get('body').type('{enter}');
WorkflowPage.getters.workflowTagElements().should('have.length', TEST_WF_TAGS.length - 1);
diff --git a/cypress/pages/features/node-creator.ts b/cypress/pages/features/node-creator.ts
index 54dc9790ba..dad6f6ebe5 100644
--- a/cypress/pages/features/node-creator.ts
+++ b/cypress/pages/features/node-creator.ts
@@ -16,12 +16,13 @@ export class NodeCreator extends BasePage {
creatorItem: () => cy.getByTestId('item-iterator-item'),
communityNodeTooltip: () => cy.getByTestId('node-item-community-tooltip'),
noResults: () => cy.getByTestId('categorized-no-results'),
- nodeItemName: () => cy.getByTestId('node-item-name'),
+ nodeItemName: () => cy.getByTestId('node-creator-item-name'),
activeSubcategory: () => cy.getByTestId('categorized-items-subcategory'),
expandedCategories: () => this.getters.creatorItem().find('>div').filter('.active').invoke('text'),
};
actions = {
openNodeCreator: () => {
+ cy.get('.el-loading-mask').should('not.exist');
this.getters.plusButton().click();
this.getters.nodeCreator().should('be.visible')
},
diff --git a/cypress/pages/workflow.ts b/cypress/pages/workflow.ts
index 77001f2fc0..3e0369ec7b 100644
--- a/cypress/pages/workflow.ts
+++ b/cypress/pages/workflow.ts
@@ -10,6 +10,7 @@ export class WorkflowPage extends BasePage {
workflowTagsContainer: () => cy.getByTestId('workflow-tags-container'),
workflowTagsInput: () => this.getters.workflowTagsContainer().then(($el) => cy.wrap($el.find('input').first())),
workflowTagElements: () => cy.get('[data-test-id="workflow-tags-container"] span.tags > span'),
+ firstWorkflowTagElement: () => cy.get('[data-test-id="workflow-tags-container"] span.tags > span:nth-child(1)'),
workflowTagsDropdown: () => cy.getByTestId('workflow-tags-dropdown'),
newTagLink: () => cy.getByTestId('new-tag-link'),
saveButton: () => cy.getByTestId('workflow-save-button'),
@@ -43,12 +44,14 @@ export class WorkflowPage extends BasePage {
addInitialNodeToCanvas: (nodeDisplayName: string) => {
this.getters.canvasPlusButton().click();
this.getters.nodeCreatorSearchBar().type(nodeDisplayName);
- this.getters.nodeCreatorSearchBar().type('{enter}{esc}');
+ this.getters.nodeCreatorSearchBar().type('{enter}');
+ cy.get('body').type('{esc}');
},
addNodeToCanvas: (nodeDisplayName: string) => {
this.getters.nodeCreatorPlusButton().click();
this.getters.nodeCreatorSearchBar().type(nodeDisplayName);
- this.getters.nodeCreatorSearchBar().type('{enter}{esc}');
+ this.getters.nodeCreatorSearchBar().type('{enter}');
+ cy.get('body').type('{esc}');
},
openNodeNdv: (nodeTypeName: string) => {
this.getters.canvasNodeByName(nodeTypeName).dblclick();
diff --git a/packages/design-system/src/components/N8nNodeCreatorNode/NodeCreatorNode.stories.ts b/packages/design-system/src/components/N8nNodeCreatorNode/NodeCreatorNode.stories.ts
new file mode 100644
index 0000000000..7991eccca4
--- /dev/null
+++ b/packages/design-system/src/components/N8nNodeCreatorNode/NodeCreatorNode.stories.ts
@@ -0,0 +1,58 @@
+/* tslint:disable:variable-name */
+import N8nNodeCreatorNode from './NodeCreatorNode.vue';
+import { StoryFn } from '@storybook/vue';
+
+export default {
+ title: 'Modules/Node Creator Node',
+ component: N8nNodeCreatorNode,
+};
+
+const DefaultTemplate: StoryFn = (args, { argTypes }) => ({
+ props: Object.keys(argTypes),
+ components: {
+ N8nNodeCreatorNode,
+ },
+ template: `
+
+
+
+
+
+ `,
+});
+
+export const WithTitle = DefaultTemplate.bind({});
+WithTitle.args = {
+ title: 'Node with title',
+ tooltipHtml: 'Bold tooltip',
+ description:
+ 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean et vehicula ipsum, eu facilisis lacus. Aliquam commodo vel elit eget mollis. Quisque ac elit non purus iaculis placerat. Quisque fringilla ultrices nisi sed porta.',
+};
+
+const PanelTemplate: StoryFn = (args, { argTypes }) => ({
+ props: Object.keys(argTypes),
+ components: {
+ N8nNodeCreatorNode,
+ },
+ data() {
+ return {
+ isPanelActive: false,
+ };
+ },
+ template: `
+
+
+
+
+
+ Lorem ipsum dolor sit amet
+
+
+
+ `,
+});
+export const WithPanel = PanelTemplate.bind({});
+WithPanel.args = {
+ title: 'Node with panel',
+ isTrigger: true,
+};
diff --git a/packages/design-system/src/components/N8nNodeCreatorNode/NodeCreatorNode.vue b/packages/design-system/src/components/N8nNodeCreatorNode/NodeCreatorNode.vue
new file mode 100644
index 0000000000..4e0aef14a9
--- /dev/null
+++ b/packages/design-system/src/components/N8nNodeCreatorNode/NodeCreatorNode.vue
@@ -0,0 +1,125 @@
+
+
+
+
+
+
+
+
+
diff --git a/packages/design-system/src/components/N8nNodeCreatorNode/TriggerIcon.vue b/packages/design-system/src/components/N8nNodeCreatorNode/TriggerIcon.vue
new file mode 100644
index 0000000000..dda6b227e7
--- /dev/null
+++ b/packages/design-system/src/components/N8nNodeCreatorNode/TriggerIcon.vue
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+
+
+
diff --git a/packages/design-system/src/components/N8nNodeCreatorNode/index.ts b/packages/design-system/src/components/N8nNodeCreatorNode/index.ts
new file mode 100644
index 0000000000..657f0d26f2
--- /dev/null
+++ b/packages/design-system/src/components/N8nNodeCreatorNode/index.ts
@@ -0,0 +1,3 @@
+import NodeCreatorNode from './NodeCreatorNode.vue';
+
+export default NodeCreatorNode;
diff --git a/packages/design-system/src/components/N8nNodeIcon/NodeIcon.vue b/packages/design-system/src/components/N8nNodeIcon/NodeIcon.vue
index 79234705d1..80948018a3 100644
--- a/packages/design-system/src/components/N8nNodeIcon/NodeIcon.vue
+++ b/packages/design-system/src/components/N8nNodeIcon/NodeIcon.vue
@@ -2,24 +2,35 @@
-
+
+
{{ nodeTypeName }}
-
+
-
+
{{ nodeTypeName ? nodeTypeName.charAt(0) : '?' }}
?
+
+
+
+
+
+
+ {{ nodeTypeName ? nodeTypeName.charAt(0) : '?' }}
+ ?
+
+
@@ -91,7 +102,7 @@ export default Vue.extend({
diff --git a/packages/editor-ui/src/components/Node/NodeCreator/CategorizedItems.vue b/packages/editor-ui/src/components/Node/NodeCreator/CategorizedItems.vue
index 9d6b50b62b..ae18d680d3 100644
--- a/packages/editor-ui/src/components/Node/NodeCreator/CategorizedItems.vue
+++ b/packages/editor-ui/src/components/Node/NodeCreator/CategorizedItems.vue
@@ -1,15 +1,14 @@
-
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
-
+
-
-
-
-
+
+
-
+
+
+
+
{{ $locale.baseText('nodeCreator.noResults.dontWorryYouCanProbablyDoItWithThe') }}
-
+
{{ $locale.baseText('nodeCreator.noResults.httpRequest') }}
{{ $locale.baseText('nodeCreator.noResults.or') }}
-
+
{{ $locale.baseText('nodeCreator.noResults.webhook') }}
- {{ $locale.baseText('nodeCreator.noResults.node') }}
+ {{ $locale.baseText('nodeCreator.noResults.node') }}
-
+
+
+ {{ $locale.baseText('nodeCreator.noResults.webhook') }}
+ {{ $locale.baseText('nodeCreator.noResults.node') }}
+
+
+
+
-
diff --git a/packages/editor-ui/src/components/Node/NodeCreator/ItemIterator.vue b/packages/editor-ui/src/components/Node/NodeCreator/ItemIterator.vue
index bf0396cd5a..06e402b5f3 100644
--- a/packages/editor-ui/src/components/Node/NodeCreator/ItemIterator.vue
+++ b/packages/editor-ui/src/components/Node/NodeCreator/ItemIterator.vue
@@ -1,7 +1,6 @@
-
-
-
diff --git a/packages/editor-ui/src/components/Node/NodeCreator/MainPanel.vue b/packages/editor-ui/src/components/Node/NodeCreator/MainPanel.vue
index 3d363927ac..0c06a61cf3 100644
--- a/packages/editor-ui/src/components/Node/NodeCreator/MainPanel.vue
+++ b/packages/editor-ui/src/components/Node/NodeCreator/MainPanel.vue
@@ -5,9 +5,8 @@
>
$emit('nodeTypeSelected', nodeType)"
+ v-if="nodeCreatorStore.selectedType === TRIGGER_NODE_FILTER"
+ @nodeTypeSelected="$listeners.nodeTypeSelected"
>
@@ -15,10 +14,15 @@
$emit('nodeTypeSelected', nodeType)"
+ :allItems="categorizedItems"
+ @nodeTypeSelected="$listeners.nodeTypeSelected"
+ @actionsOpen="() => {}"
>
@@ -28,73 +32,58 @@
-
+
-
-
diff --git a/packages/editor-ui/src/components/Node/NodeCreator/SearchBar.vue b/packages/editor-ui/src/components/Node/NodeCreator/SearchBar.vue
index 960077f458..ea89914378 100644
--- a/packages/editor-ui/src/components/Node/NodeCreator/SearchBar.vue
+++ b/packages/editor-ui/src/components/Node/NodeCreator/SearchBar.vue
@@ -5,12 +5,14 @@
@@ -21,50 +23,56 @@
-
@@ -74,7 +82,7 @@ export default mixins(externalHooks).extend({
height: 40px;
padding: var(--spacing-s) var(--spacing-xs);
align-items: center;
- margin: var(--spacing-s);
+ margin: var(--search-margin, var(--spacing-s));
filter: drop-shadow(0px 2px 5px rgba(46, 46, 50, 0.04));
border: 1px solid $node-creator-border-color;
diff --git a/packages/editor-ui/src/components/Node/NodeCreator/SubcategoryItem.vue b/packages/editor-ui/src/components/Node/NodeCreator/SubcategoryItem.vue
index 6467b7ac3e..feaaeb381b 100644
--- a/packages/editor-ui/src/components/Node/NodeCreator/SubcategoryItem.vue
+++ b/packages/editor-ui/src/components/Node/NodeCreator/SubcategoryItem.vue
@@ -56,6 +56,7 @@ export default Vue.extend({
.subcategory {
display: flex;
padding: 11px 16px 11px 30px;
+ user-select: none;
}
.subcategoryWithIcon {
diff --git a/packages/editor-ui/src/components/Node/NodeCreator/TriggerHelperPanel.vue b/packages/editor-ui/src/components/Node/NodeCreator/TriggerHelperPanel.vue
index 9e6b461441..9a80d31885 100644
--- a/packages/editor-ui/src/components/Node/NodeCreator/TriggerHelperPanel.vue
+++ b/packages/editor-ui/src/components/Node/NodeCreator/TriggerHelperPanel.vue
@@ -1,167 +1,364 @@
$emit('nodeTypeSelected', nodeType)"
+ :expandAllCategories="isActionsActive"
+ :subcategoryOverride="nodeAppSubcategory"
+ :alwaysShowSearch="isActionsActive"
+ :categorizedItems="computedCategorizedItems"
+ :categoriesWithNodes="computedCategoriesWithNodes"
:initialActiveIndex="0"
:searchItems="searchItems"
- :firstLevelItems="isRoot ? items : []"
- :excludedCategories="isRoot ? [] : [CORE_NODES_CATEGORY]"
- :initialActiveCategories="[COMMUNICATION_CATEGORY]"
+ :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"
>
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
-
diff --git a/packages/editor-ui/src/composables/useGlobalLinkActions.ts b/packages/editor-ui/src/composables/useGlobalLinkActions.ts
new file mode 100644
index 0000000000..5ac3abe2ed
--- /dev/null
+++ b/packages/editor-ui/src/composables/useGlobalLinkActions.ts
@@ -0,0 +1,59 @@
+/**
+ * Creates event listeners for `data-action` attribute to allow for actions to be called from locale without using
+ * unsafe onclick attribute
+ */
+import { reactive, del, computed, onMounted, onUnmounted, getCurrentInstance } from 'vue';
+
+const state = reactive({
+ customActions: {} as Record,
+});
+
+export default () => {
+ function registerCustomAction(key: string, action: Function) {
+ state.customActions[key] = action;
+ }
+ function unregisterCustomAction(key: string) {
+ del(state.customActions, key);
+ }
+ function delegateClick(e: MouseEvent) {
+ const clickedElement = e.target;
+ if (!(clickedElement instanceof Element) || clickedElement.tagName !== 'A') return;
+
+ const actionAttribute = clickedElement.getAttribute('data-action');
+ if(actionAttribute && typeof availableActions.value[actionAttribute] === 'function') {
+ e.preventDefault();
+ availableActions.value[actionAttribute]();
+ }
+ }
+
+ function reload() {
+ if (window.top) {
+ window.top.location.reload();
+ } else {
+ window.location.reload();
+ }
+ }
+
+ const availableActions = computed<{[key: string]: Function}>(() => ({
+ reload,
+ ...state.customActions,
+ }));
+
+ onMounted(() => {
+ const instance = getCurrentInstance();
+ window.addEventListener('click', delegateClick);
+ instance?.proxy.$root.$on('registerGlobalLinkAction', registerCustomAction);
+ });
+
+ onUnmounted(() => {
+ const instance = getCurrentInstance();
+ window.removeEventListener('click', delegateClick);
+ instance?.proxy.$root.$off('registerGlobalLinkAction', registerCustomAction);
+ });
+
+ return {
+ registerCustomAction,
+ unregisterCustomAction,
+ };
+};
+
diff --git a/packages/editor-ui/src/constants.ts b/packages/editor-ui/src/constants.ts
index 9f98d9d196..84b69bc57d 100644
--- a/packages/editor-ui/src/constants.ts
+++ b/packages/editor-ui/src/constants.ts
@@ -7,6 +7,7 @@ export const PLACEHOLDER_FILLED_AT_EXECUTION_TIME = '[filled at execution time]'
// parameter input
export const CUSTOM_API_CALL_KEY = '__CUSTOM_API_CALL__';
+export const CUSTOM_API_CALL_NAME = 'Custom API Call';
// workflows
export const PLACEHOLDER_EMPTY_WORKFLOW_ID = '__EMPTY__';
@@ -79,10 +80,12 @@ export const CRON_NODE_TYPE = 'n8n-nodes-base.cron';
export const CLEARBIT_NODE_TYPE = 'n8n-nodes-base.clearbit';
export const FUNCTION_NODE_TYPE = 'n8n-nodes-base.function';
export const GITHUB_TRIGGER_NODE_TYPE = 'n8n-nodes-base.githubTrigger';
+export const GIT_NODE_TYPE = 'n8n-nodes-base.git';
export const GOOGLE_SHEETS_NODE_TYPE = 'n8n-nodes-base.googleSheets';
export const ERROR_TRIGGER_NODE_TYPE = 'n8n-nodes-base.errorTrigger';
export const ELASTIC_SECURITY_NODE_TYPE = 'n8n-nodes-base.elasticSecurity';
export const EMAIL_SEND_NODE_TYPE = 'n8n-nodes-base.emailSend';
+export const EMAIL_IMAP_NODE_TYPE = 'n8n-nodes-base.emailReadImap';
export const EXECUTE_COMMAND_NODE_TYPE = 'n8n-nodes-base.executeCommand';
export const HTTP_REQUEST_NODE_TYPE = 'n8n-nodes-base.httpRequest';
export const HUBSPOT_TRIGGER_NODE_TYPE = 'n8n-nodes-base.hubspotTrigger';
@@ -139,6 +142,7 @@ export const PIN_DATA_NODE_TYPES_DENYLIST = [
export const CORE_NODES_CATEGORY = 'Core Nodes';
export const COMMUNICATION_CATEGORY = 'Communication';
export const CUSTOM_NODES_CATEGORY = 'Custom Nodes';
+export const RECOMMENDED_CATEGORY = 'Recommended';
export const SUBCATEGORY_DESCRIPTIONS: {
[category: string]: { [subcategory: string]: string };
} = {
diff --git a/packages/editor-ui/src/mixins/globalLinkActions.ts b/packages/editor-ui/src/mixins/globalLinkActions.ts
deleted file mode 100644
index 9225fbeaf7..0000000000
--- a/packages/editor-ui/src/mixins/globalLinkActions.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-/**
- * Creates event listeners for `data-action` attribute to allow for actions to be called from locale without using
- * unsafe onclick attribute
- */
- import Vue from 'vue';
-
- export const globalLinkActions = Vue.extend({
- data(): {[key: string]: {[key: string]: Function}} {
- return {
- customActions: {},
- };
- },
- mounted() {
- window.addEventListener('click', this.delegateClick);
- this.$root.$on('registerGlobalLinkAction', this.registerCustomAction);
- },
- destroyed() {
- window.removeEventListener('click', this.delegateClick);
- this.$root.$off('registerGlobalLinkAction', this.registerCustomAction);
- },
- computed: {
- availableActions(): {[key: string]: Function} {
- return {
- reload: this.reload,
- ...this.customActions,
- };
- },
- },
- methods: {
- registerCustomAction(key: string, action: Function) {
- this.customActions[key] = action;
- },
- unregisterCustomAction(key: string) {
- Vue.delete(this.customActions, key);
- },
- delegateClick(e: MouseEvent) {
- const clickedElement = e.target;
- if (!(clickedElement instanceof Element) || clickedElement.tagName !== 'A') return;
-
- const actionAttribute = clickedElement.getAttribute('data-action');
- if(actionAttribute && typeof this.availableActions[actionAttribute] === 'function') {
- e.preventDefault();
- this.availableActions[actionAttribute]();
- }
- },
- reload() {
- if (window.top) {
- window.top.location.reload();
- } else {
- window.location.reload();
- }
- },
- },
- });
-
diff --git a/packages/editor-ui/src/plugins/i18n/locales/en.json b/packages/editor-ui/src/plugins/i18n/locales/en.json
index d0ed0acbf9..92044d2291 100644
--- a/packages/editor-ui/src/plugins/i18n/locales/en.json
+++ b/packages/editor-ui/src/plugins/i18n/locales/en.json
@@ -661,6 +661,13 @@
"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.",
"nodeBase.clickToAddNodeOrDragToConnect": "Click to add node
or drag to connect",
+ "nodeCreator.actionsCategory.operations": "Operations",
+ "nodeCreator.actionsCategory.onNewEvent": "On new {event} event",
+ "nodeCreator.actionsCategory.onEvent": "On {event}",
+ "nodeCreator.actionsCategory.recommended": "Recommended",
+ "nodeCreator.actionsCategory.searchActions": "Search {nodeNameTitle} Actions...",
+ "nodeCreator.actionsList.apiCall": "Didn't find the right event? Make a custom {nodeNameTitle} API call",
+ "nodeCreator.actionsList.apiCallNoResult": "Nothing found — try making a custom {nodeNameTitle} API call",
"nodeCreator.categoryNames.analytics": "Analytics",
"nodeCreator.categoryNames.communication": "Communication",
"nodeCreator.categoryNames.coreNodes": "Core Nodes",
@@ -684,7 +691,8 @@
"nodeCreator.noResults.requestTheNode": "Request the node",
"nodeCreator.noResults.wantUsToMakeItFaster": "Want us to make it faster?",
"nodeCreator.noResults.weDidntMakeThatYet": "We didn't make that... yet",
- "nodeCreator.noResults.clickToSeeResults": "To see results, click here",
+ "nodeCreator.noResults.noMatchingActions": "No actions matching your results",
+ "nodeCreator.noResults.clickToSeeResults": "To see all results, click here",
"nodeCreator.noResults.webhook": "Webhook",
"nodeCreator.searchBar.searchNodes": "Search nodes...",
"nodeCreator.subcategoryDescriptions.appTriggerNodes": "Runs the flow when something happens in an app like Telegram, Notion or Airtable",
diff --git a/packages/editor-ui/src/plugins/telemetry/index.ts b/packages/editor-ui/src/plugins/telemetry/index.ts
index 3ef00e6569..59fcc126cf 100644
--- a/packages/editor-ui/src/plugins/telemetry/index.ts
+++ b/packages/editor-ui/src/plugins/telemetry/index.ts
@@ -12,7 +12,6 @@ import { useSettingsStore } from "@/stores/settings";
import { useRootStore } from "@/stores/n8nRootStore";
export class Telemetry {
-
private pageEventQueue: Array<{route: Route}>;
private previousPath: string;
@@ -153,14 +152,28 @@ export class Telemetry {
break;
case 'nodeCreateList.onCategoryExpanded':
properties.is_subcategory = false;
+ properties.nodes_panel_session_id = this.userNodesPanelSession.sessionId;
this.track('User viewed node category', properties);
break;
+ case 'nodeCreateList.onViewActions':
+ properties.nodes_panel_session_id = this.userNodesPanelSession.sessionId;
+ this.track('User viewed node actions', properties);
+ break;
+ case 'nodeCreateList.onActionsCustmAPIClicked':
+ properties.nodes_panel_session_id = this.userNodesPanelSession.sessionId;
+ this.track('User clicked custom API from node actions', properties);
+ break;
+ case 'nodeCreateList.addAction':
+ properties.nodes_panel_session_id = this.userNodesPanelSession.sessionId;
+ this.track('User added action', properties);
+ break;
case 'nodeCreateList.onSubcategorySelected':
const selectedProperties = (properties.selected as IDataObject).properties as IDataObject;
if(selectedProperties && selectedProperties.subcategory) {
properties.category_name = selectedProperties.subcategory;
}
properties.is_subcategory = true;
+ properties.nodes_panel_session_id = this.userNodesPanelSession.sessionId;
delete properties.selected;
this.track('User viewed node category', properties);
break;
diff --git a/packages/editor-ui/src/shims.d.ts b/packages/editor-ui/src/shims.d.ts
index eb1c83fcdd..113b1ebaaa 100644
--- a/packages/editor-ui/src/shims.d.ts
+++ b/packages/editor-ui/src/shims.d.ts
@@ -18,4 +18,11 @@ declare global {
[elem: string]: any;
}
}
+
+ interface Array {
+ findLast(
+ predicate: (value: T, index: number, obj: T[]) => unknown,
+ thisArg?: any
+ ): T
+ }
}
diff --git a/packages/editor-ui/src/stores/nodeCreator.ts b/packages/editor-ui/src/stores/nodeCreator.ts
index 3c5b4cec38..240eb69289 100644
--- a/packages/editor-ui/src/stores/nodeCreator.ts
+++ b/packages/editor-ui/src/stores/nodeCreator.ts
@@ -1,6 +1,192 @@
-import { ALL_NODE_FILTER, STORES } from "@/constants";
-import { INodeCreatorState } from "@/Interface";
+import startCase from 'lodash.startCase';
import { defineStore } from "pinia";
+import { INodePropertyCollection, INodePropertyOptions, IDataObject, INodeProperties, INodeTypeDescription, deepCopy, INodeParameters, INodeActionTypeDescription } from 'n8n-workflow';
+import { STORES, MANUAL_TRIGGER_NODE_TYPE, CORE_NODES_CATEGORY, CALENDLY_TRIGGER_NODE_TYPE, TRIGGER_NODE_FILTER } from "@/constants";
+import { useNodeTypesStore } from '@/stores/nodeTypes';
+import { useWorkflowsStore } from './workflows';
+import { CUSTOM_API_CALL_KEY, ALL_NODE_FILTER } from '@/constants';
+import { INodeCreatorState, INodeFilterType, IUpdateInformation } from '@/Interface';
+import { i18n } from '@/plugins/i18n';
+import { externalHooks } from '@/mixins/externalHooks';
+import { Telemetry } from '@/plugins/telemetry';
+
+const PLACEHOLDER_RECOMMENDED_ACTION_KEY = 'placeholder_recommended';
+
+const customNodeActionsParsers: {[key: string]: (matchedProperty: INodeProperties, nodeTypeDescription: INodeTypeDescription) => INodeActionTypeDescription[] | undefined} = {
+ ['n8n-nodes-base.hubspotTrigger']: (matchedProperty, nodeTypeDescription) => {
+ const collection = matchedProperty?.options?.[0] as INodePropertyCollection;
+
+ return (collection?.values[0]?.options as INodePropertyOptions[])?.map((categoryItem): INodeActionTypeDescription => ({
+ ...getNodeTypeBase(nodeTypeDescription, i18n.baseText('nodeCreator.actionsCategory.recommended')),
+ actionKey: categoryItem.value as string,
+ displayName: i18n.baseText('nodeCreator.actionsCategory.onEvent', {
+ interpolate: {event: startCase(categoryItem.name)},
+ }),
+ description: categoryItem.description || '',
+ displayOptions: matchedProperty.displayOptions,
+ values: { eventsUi: { eventValues: [{ name: categoryItem.value }] } },
+ }));
+ },
+};
+
+function filterSinglePlaceholderAction(actions: INodeActionTypeDescription[]) {
+ return actions.filter((action: INodeActionTypeDescription, _: number, arr: INodeActionTypeDescription[]) => {
+ const isPlaceholderTriggerAction = action.actionKey === PLACEHOLDER_RECOMMENDED_ACTION_KEY;
+ return !isPlaceholderTriggerAction || (isPlaceholderTriggerAction && arr.length > 1);
+ });
+}
+
+function getNodeTypeBase(nodeTypeDescription: INodeTypeDescription, category: string) {
+ return {
+ name: nodeTypeDescription.name,
+ group: ['trigger'],
+ codex: {
+ categories: [category],
+ subcategories: {
+ [nodeTypeDescription.displayName]: [category],
+ },
+ },
+ iconUrl: nodeTypeDescription.iconUrl,
+ icon: nodeTypeDescription.icon,
+ version: [1],
+ defaults: {},
+ inputs: [],
+ outputs: [],
+ properties: [],
+ };
+}
+
+function operationsCategory(nodeTypeDescription: INodeTypeDescription): INodeActionTypeDescription[] {
+ if (!!nodeTypeDescription.properties.find((property) => property.name === 'resource')) return [];
+
+ const matchedProperty = nodeTypeDescription.properties
+ .find((property) =>property.name?.toLowerCase() === 'operation');
+
+ if (!matchedProperty || !matchedProperty.options) return [];
+
+ const filteredOutItems = (matchedProperty.options as INodePropertyOptions[]).filter(
+ (categoryItem: INodePropertyOptions) => !['*', '', ' '].includes(categoryItem.name),
+ );
+
+ const items = filteredOutItems.map((item: INodePropertyOptions) => ({
+ ...getNodeTypeBase(nodeTypeDescription, i18n.baseText('nodeCreator.actionsCategory.operations')),
+ actionKey: item.value as string,
+ displayName: item.action ?? startCase(item.name),
+ description: item.description ?? '',
+ displayOptions: matchedProperty.displayOptions,
+ values: {
+ [matchedProperty.name]: matchedProperty.type === 'multiOptions' ? [item.value] : item.value,
+ },
+ }));
+
+ // Do not return empty category
+ if (items.length === 0) return [];
+
+ return items;
+}
+
+function recommendedCategory(nodeTypeDescription: INodeTypeDescription): INodeActionTypeDescription[] {
+ const matchingKeys = ['event', 'events', 'trigger on'];
+ const isTrigger = nodeTypeDescription.displayName?.toLowerCase().includes('trigger');
+ const matchedProperty = nodeTypeDescription.properties.find((property) =>
+ matchingKeys.includes(property.displayName?.toLowerCase()),
+ );
+
+ if (!isTrigger) return [];
+
+ // Inject placeholder action if no events are available
+ // so user is able to add node to the canvas from the actions panel
+ if (!matchedProperty || !matchedProperty.options) {
+ return [{
+ ...getNodeTypeBase(nodeTypeDescription, i18n.baseText('nodeCreator.actionsCategory.recommended')),
+ actionKey: PLACEHOLDER_RECOMMENDED_ACTION_KEY,
+ displayName: i18n.baseText('nodeCreator.actionsCategory.onNewEvent', {
+ interpolate: {event: nodeTypeDescription.displayName.replace('Trigger', '').trimEnd()},
+ }),
+ description: '',
+ }];
+ }
+
+ const filteredOutItems = (matchedProperty.options as INodePropertyOptions[]).filter(
+ (categoryItem: INodePropertyOptions) => !['*', '', ' '].includes(categoryItem.name),
+ );
+
+ const customParsedItem = customNodeActionsParsers[nodeTypeDescription.name]?.(matchedProperty, nodeTypeDescription);
+
+ const items =
+ customParsedItem ??
+ filteredOutItems.map((categoryItem: INodePropertyOptions) => ({
+ ...getNodeTypeBase(nodeTypeDescription, i18n.baseText('nodeCreator.actionsCategory.recommended')),
+ actionKey: categoryItem.value as string,
+ displayName: i18n.baseText('nodeCreator.actionsCategory.onEvent', {
+ interpolate: {event: startCase(categoryItem.name)},
+ }),
+ description: categoryItem.description || '',
+ displayOptions: matchedProperty.displayOptions,
+ values: {
+ [matchedProperty.name]:
+ matchedProperty.type === 'multiOptions' ? [categoryItem.value] : categoryItem.value,
+ },
+ }));
+
+ return items;
+}
+
+function resourceCategories(nodeTypeDescription: INodeTypeDescription): INodeActionTypeDescription[] {
+ const transformedNodes: INodeActionTypeDescription[] = [];
+ const matchedProperties = nodeTypeDescription.properties.filter((property) =>property.displayName?.toLowerCase() === 'resource');
+
+ matchedProperties.forEach((property) => {
+ (property.options as INodePropertyOptions[] || [])
+ .filter((option) => option.value !== CUSTOM_API_CALL_KEY)
+ .forEach((resourceOption, i, options) => {
+ const isSingleResource = options.length === 1;
+
+ // Match operations for the resource by checking if displayOptions matches or contains the resource name
+ const operations = nodeTypeDescription.properties.find(
+ (operation) =>
+ operation.name === 'operation' &&
+ (operation.displayOptions?.show?.resource?.includes(resourceOption.value) ||
+ isSingleResource),
+ );
+
+ if (!operations?.options) return;
+
+ const items = (operations.options as INodePropertyOptions[] || []).map(
+ (operationOption) => {
+ const displayName =
+ operationOption.action ??
+ `${resourceOption.name} ${startCase(operationOption.name)}`;
+
+ // We need to manually populate displayOptions as they are not present in the node description
+ // if the resource has only one option
+ const displayOptions = isSingleResource
+ ? { show: { resource: [(options as INodePropertyOptions[])[0]?.value] } }
+ : operations?.displayOptions;
+
+ return {
+ ...getNodeTypeBase(nodeTypeDescription, resourceOption.name),
+ actionKey: operationOption.value as string,
+ description: operationOption?.description ?? '',
+ displayOptions,
+ values: {
+ operation:
+ operations?.type === 'multiOptions'
+ ? [operationOption.value]
+ : operationOption.value,
+ },
+ displayName,
+ group: ['trigger'],
+ };
+ },
+ );
+
+ transformedNodes.push(...items);
+ });
+ });
+
+ return transformedNodes;
+}
export const useNodeCreatorStore = defineStore(STORES.NODE_CREATOR, {
state: (): INodeCreatorState => ({
@@ -9,4 +195,112 @@ export const useNodeCreatorStore = defineStore(STORES.NODE_CREATOR, {
showScrim: false,
selectedType: ALL_NODE_FILTER,
}),
+ actions: {
+ setShowTabs(isVisible: boolean) {
+ this.showTabs = isVisible;
+ },
+ setShowScrim(isVisible: boolean) {
+ this.showScrim = isVisible;
+ },
+ setSelectedType(selectedNodeType: INodeFilterType) {
+ this.selectedType = selectedNodeType;
+ },
+ setFilter(search: string) {
+ this.itemsFilter = search;
+ },
+ setAddedNodeActionParameters (action: IUpdateInformation, telemetry?: Telemetry, track = true) {
+ const { $onAction: onWorkflowStoreAction } = useWorkflowsStore();
+ const storeWatcher = onWorkflowStoreAction(({ name, after, store: { setLastNodeParameters }, args }) => {
+ if (name !== 'addNode' || args[0].type !== action.key) return;
+ after(() => {
+ setLastNodeParameters(action);
+ if(track) this.trackActionSelected(action, telemetry);
+ storeWatcher();
+ });
+ });
+
+ return storeWatcher;
+ },
+ trackActionSelected (action: IUpdateInformation, telemetry?: Telemetry) {
+ const { $externalHooks } = new externalHooks();
+
+ const payload = {
+ node_type: action.key,
+ action: action.name,
+ resource: (action.value as INodeParameters).resource || '',
+ };
+ $externalHooks().run('nodeCreateList.addAction', payload);
+ telemetry?.trackNodesPanel('nodeCreateList.addAction', payload);
+ },
+ },
+ getters: {
+ visibleNodesWithActions(): INodeTypeDescription[] {
+ const nodes = deepCopy(useNodeTypesStore().visibleNodeTypes);
+ const nodesWithActions = nodes.map((node) => {
+ const isCoreNode = node.codex?.categories?.includes(CORE_NODES_CATEGORY);
+ // Core nodes shouldn't support actions
+ node.actions = [];
+ if(isCoreNode) return node;
+
+ node.actions.push(
+ ...recommendedCategory(node),
+ ...operationsCategory(node),
+ ...resourceCategories(node),
+ );
+
+ return node;
+ });
+ return nodesWithActions;
+ },
+ mergedAppNodes(): INodeTypeDescription[] {
+ const mergedNodes = this.visibleNodesWithActions.reduce((acc: Record, node: INodeTypeDescription) => {
+
+ const clonedNode = deepCopy(node);
+ const isCoreNode = node.codex?.categories?.includes(CORE_NODES_CATEGORY);
+ const actions = node.actions || [];
+ // Do not merge core nodes
+ const normalizedName = isCoreNode ? node.name : node.name.toLowerCase().replace('trigger', '');
+ const existingNode = acc[normalizedName];
+
+ if(existingNode) existingNode.actions?.push(...actions);
+ else acc[normalizedName] = clonedNode;
+
+ if(!isCoreNode) acc[normalizedName].displayName = node.displayName.replace('Trigger', '');
+
+ acc[normalizedName].actions = filterSinglePlaceholderAction(acc[normalizedName].actions || []);
+ return acc;
+ }, {});
+ return Object.values(mergedNodes);
+ },
+ getNodeTypesWithManualTrigger: () => (nodeType?: string): string[] => {
+ if(!nodeType) return [];
+
+ const { workflowTriggerNodes } = useWorkflowsStore();
+ const isTriggerAction = nodeType.toLocaleLowerCase().includes('trigger');
+ const workflowContainsTrigger = workflowTriggerNodes.length > 0;
+ const isTriggerPanel = useNodeCreatorStore().selectedType === TRIGGER_NODE_FILTER;
+
+ const nodeTypes = !isTriggerAction && !workflowContainsTrigger && isTriggerPanel
+ ? [MANUAL_TRIGGER_NODE_TYPE, nodeType]
+ : [nodeType];
+
+ return nodeTypes;
+ },
+
+ getActionData: () => (actionItem: INodeActionTypeDescription): IUpdateInformation => {
+ const displayOptions = actionItem.displayOptions ;
+
+ const displayConditions = Object.keys(displayOptions?.show || {})
+ .reduce((acc: IDataObject, showCondition: string) => {
+ acc[showCondition] = displayOptions?.show?.[showCondition]?.[0];
+ return acc;
+ }, {});
+
+ return {
+ name: actionItem.displayName,
+ key: actionItem.name as string,
+ value: { ...actionItem.values , ...displayConditions} as INodeParameters,
+ };
+ },
+ },
});
diff --git a/packages/editor-ui/src/stores/nodeTypes.ts b/packages/editor-ui/src/stores/nodeTypes.ts
index fdd4a2b930..d536ee3c96 100644
--- a/packages/editor-ui/src/stores/nodeTypes.ts
+++ b/packages/editor-ui/src/stores/nodeTypes.ts
@@ -9,7 +9,7 @@ import Vue from "vue";
import { useCredentialsStore } from "./credentials";
import { useRootStore } from "./n8nRootStore";
import { useUsersStore } from "./users";
-
+import { useNodeCreatorStore } from './nodeCreator';
function getNodeVersions(nodeType: INodeTypeDescription) {
return Array.isArray(nodeType.version) ? nodeType.version : [nodeType.version];
}
@@ -79,17 +79,21 @@ export const useNodeTypesStore = defineStore(STORES.NODE_TYPES, {
}
for (const version of newNodeVersions) {
+ // Node exists with the same name
if (acc[newNodeType.name]) {
- acc[newNodeType.name][version] = newNodeType;
+ acc[newNodeType.name][version] = Object.assign(acc[newNodeType.name][version] ?? {}, newNodeType);
} else {
- acc[newNodeType.name] = { [version]: newNodeType };
+ acc[newNodeType.name] = Object.assign(acc[newNodeType.name] ?? {}, { [version]: newNodeType });
}
}
return acc;
}, { ...this.nodeTypes });
-
Vue.set(this, 'nodeTypes', nodeTypes);
+
+ // Trigger compute of mergedAppNodes getter so it's ready when user opens the node creator
+ // tslint:disable-next-line: no-unused-expression
+ useNodeCreatorStore().mergedAppNodes;
},
removeNodeTypes(nodeTypesToRemove: INodeTypeDescription[]): void {
this.nodeTypes = nodeTypesToRemove.reduce(
@@ -97,7 +101,7 @@ export const useNodeTypesStore = defineStore(STORES.NODE_TYPES, {
this.nodeTypes,
);
},
- async getNodesInformation(nodeInfos: INodeTypeNameVersion[]): Promise {
+ async getNodesInformation(nodeInfos: INodeTypeNameVersion[], replace = true): Promise {
const rootStore = useRootStore();
const nodesInformation = await getNodesInformation(rootStore.getRestApiContext, nodeInfos);
@@ -111,7 +115,9 @@ export const useNodeTypesStore = defineStore(STORES.NODE_TYPES, {
);
}
});
- this.setNodeTypes(nodesInformation);
+ if(replace) this.setNodeTypes(nodesInformation);
+
+ return nodesInformation;
},
async getFullNodesProperties(nodesToBeFetched: INodeTypeNameVersion[]): Promise {
const credentialsStore = useCredentialsStore();
diff --git a/packages/editor-ui/src/stores/workflows.ts b/packages/editor-ui/src/stores/workflows.ts
index 46ef5ac727..c410d15b5b 100644
--- a/packages/editor-ui/src/stores/workflows.ts
+++ b/packages/editor-ui/src/stores/workflows.ts
@@ -21,7 +21,7 @@ import {
IWorkflowsMap,
WorkflowsState,
} from "@/Interface";
-import {defineStore} from "pinia";
+import { defineStore } from "pinia";
import {
deepCopy,
IConnection,
@@ -40,7 +40,7 @@ import {
} from 'n8n-workflow';
import Vue from "vue";
-import {useRootStore} from "./n8nRootStore";
+import { useRootStore } from "./n8nRootStore";
import {
getActiveWorkflows,
getCurrentExecutions,
@@ -50,7 +50,7 @@ import {
} from "@/api/workflows";
import {useUIStore} from "./ui";
import {dataPinningEventBus} from "@/event-bus/data-pinning-event-bus";
-import {isJsonKeyObject, getPairedItemsMapping, stringSizeInBytes} from "@/utils";
+import {isJsonKeyObject, getPairedItemsMapping, stringSizeInBytes, isObjectLiteral} from "@/utils";
import {useNDVStore} from "./ndv";
import {useNodeTypesStore} from "./nodeTypes";
import {useWorkflowsEEStore} from "@/stores/workflows.ee";
@@ -720,7 +720,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, {
Vue.set(node, updateInformation.key, updateInformation.value);
},
- setNodeParameters(updateInformation: IUpdateInformation): void {
+ setNodeParameters(updateInformation: IUpdateInformation, append?: boolean): void {
// Find the node that should be updated
const node = this.workflow.nodes.find(node => {
return node.name === updateInformation.name;
@@ -732,7 +732,11 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, {
const uiStore = useUIStore();
uiStore.stateIsDirty = true;
- Vue.set(node, 'parameters', updateInformation.value);
+ const newParameters = !!append && isObjectLiteral(updateInformation.value)
+ ? {...node.parameters, ...updateInformation.value }
+ : updateInformation.value;
+
+ Vue.set(node, 'parameters', newParameters);
if (!this.nodeMetadata[node.name]) {
Vue.set(this.nodeMetadata, node.name, {});
@@ -740,6 +744,12 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, {
Vue.set(this.nodeMetadata[node.name], 'parametersLastUpdatedAt', Date.now());
},
+ setLastNodeParameters(updateInformation: IUpdateInformation) {
+ const latestNode = this.workflow.nodes.findLast((node) => node.type === updateInformation.key) as INodeUi;
+
+ if(latestNode) this.setNodeParameters({...updateInformation, name: latestNode.name}, true);
+ },
+
addNodeExecutionData(pushData: IPushDataNodeExecuteAfter): void {
if (this.workflowExecutionData === null || !this.workflowExecutionData.data) {
throw new Error('The "workflowExecutionData" is not initialized!');
diff --git a/packages/editor-ui/src/utils/nodeTypesUtils.ts b/packages/editor-ui/src/utils/nodeTypesUtils.ts
index af6afc15c1..80029c3d0f 100644
--- a/packages/editor-ui/src/utils/nodeTypesUtils.ts
+++ b/packages/editor-ui/src/utils/nodeTypesUtils.ts
@@ -1,5 +1,6 @@
import {
CORE_NODES_CATEGORY,
+ RECOMMENDED_CATEGORY,
CUSTOM_NODES_CATEGORY,
SUBCATEGORY_DESCRIPTIONS,
UNCATEGORIZED_CATEGORY,
@@ -13,7 +14,7 @@ import {
MAPPING_PARAMS,
} from '@/constants';
import { INodeCreateElement, ICategoriesWithNodes, INodeUi, ITemplatesNode, INodeItemProps } from '@/Interface';
-import { IDataObject, INodeExecutionData, INodeProperties, INodeTypeDescription, NodeParameterValueType } from 'n8n-workflow';
+import { IDataObject, INodeExecutionData, INodeProperties, INodeTypeDescription, INodeActionTypeDescription, NodeParameterValueType } from 'n8n-workflow';
import { isResourceLocatorValue, isJsonKeyObject } from '@/utils';
/*
@@ -25,7 +26,7 @@ const CRED_KEYWORDS_TO_FILTER = ['API', 'OAuth1', 'OAuth2'];
const NODE_KEYWORDS_TO_FILTER = ['Trigger'];
const COMMUNITY_PACKAGE_NAME_REGEX = /(@\w+\/)?n8n-nodes-(?!base\b)\b\w+/g;
-const addNodeToCategory = (accu: ICategoriesWithNodes, nodeType: INodeTypeDescription, category: string, subcategory: string) => {
+const addNodeToCategory = (accu: ICategoriesWithNodes, nodeType: INodeTypeDescription | INodeActionTypeDescription, category: string, subcategory: string) => {
if (!accu[category]) {
accu[category] = {};
}
@@ -44,7 +45,7 @@ const addNodeToCategory = (accu: ICategoriesWithNodes, nodeType: INodeTypeDescri
accu[category][subcategory].regularCount++;
}
accu[category][subcategory].nodes.push({
- type: 'node',
+ type: nodeType.actionKey ? 'action' : 'node' ,
key: `${category}_${nodeType.name}`,
category,
properties: {
@@ -56,30 +57,25 @@ const addNodeToCategory = (accu: ICategoriesWithNodes, nodeType: INodeTypeDescri
});
};
-export const getCategoriesWithNodes = (nodeTypes: INodeTypeDescription[], personalizedNodeTypes: string[]): ICategoriesWithNodes => {
+export const getCategoriesWithNodes = (nodeTypes: INodeTypeDescription[], personalizedNodeTypes: string[], uncategorizedSubcategory = UNCATEGORIZED_SUBCATEGORY): ICategoriesWithNodes => {
const sorted = [...nodeTypes].sort((a: INodeTypeDescription, b: INodeTypeDescription) => a.displayName > b.displayName? 1 : -1);
- return sorted.reduce(
+ const result = sorted.reduce(
(accu: ICategoriesWithNodes, nodeType: INodeTypeDescription) => {
if (personalizedNodeTypes.includes(nodeType.name)) {
- addNodeToCategory(accu, nodeType, PERSONALIZED_CATEGORY, UNCATEGORIZED_SUBCATEGORY);
+ addNodeToCategory(accu, nodeType, PERSONALIZED_CATEGORY, uncategorizedSubcategory);
}
if (!nodeType.codex || !nodeType.codex.categories) {
- addNodeToCategory(accu, nodeType, UNCATEGORIZED_CATEGORY, UNCATEGORIZED_SUBCATEGORY);
+ addNodeToCategory(accu, nodeType, UNCATEGORIZED_CATEGORY, uncategorizedSubcategory);
return accu;
}
nodeType.codex.categories.forEach((_category: string) => {
const category = _category.trim();
- const subcategories =
- nodeType.codex &&
- nodeType.codex.subcategories &&
- nodeType.codex.subcategories[category]
- ? nodeType.codex.subcategories[category]
- : null;
+ const subcategories = nodeType?.codex?.subcategories?.[category] ?? null;
if(subcategories === null || subcategories.length === 0) {
- addNodeToCategory(accu, nodeType, category, UNCATEGORIZED_SUBCATEGORY);
+ addNodeToCategory(accu, nodeType, category, uncategorizedSubcategory);
return;
}
@@ -92,10 +88,11 @@ export const getCategoriesWithNodes = (nodeTypes: INodeTypeDescription[], person
},
{},
);
+ return result;
};
const getCategories = (categoriesWithNodes: ICategoriesWithNodes): string[] => {
- const excludeFromSort = [CORE_NODES_CATEGORY, CUSTOM_NODES_CATEGORY, UNCATEGORIZED_CATEGORY, PERSONALIZED_CATEGORY];
+ const excludeFromSort = [CORE_NODES_CATEGORY, CUSTOM_NODES_CATEGORY, UNCATEGORIZED_CATEGORY, PERSONALIZED_CATEGORY, RECOMMENDED_CATEGORY];
const categories = Object.keys(categoriesWithNodes);
const sorted = categories.filter(
(category: string) =>
@@ -103,13 +100,13 @@ const getCategories = (categoriesWithNodes: ICategoriesWithNodes): string[] => {
);
sorted.sort();
- return [CORE_NODES_CATEGORY, CUSTOM_NODES_CATEGORY, PERSONALIZED_CATEGORY, ...sorted, UNCATEGORIZED_CATEGORY];
+ return [RECOMMENDED_CATEGORY, CORE_NODES_CATEGORY, CUSTOM_NODES_CATEGORY, PERSONALIZED_CATEGORY, ...sorted, UNCATEGORIZED_CATEGORY];
};
-export const getCategorizedList = (categoriesWithNodes: ICategoriesWithNodes): INodeCreateElement[] => {
+export const getCategorizedList = (categoriesWithNodes: ICategoriesWithNodes, categoryIsExpanded = false): INodeCreateElement[] => {
const categories = getCategories(categoriesWithNodes);
- return categories.reduce(
+ const result = categories.reduce(
(accu: INodeCreateElement[], category: string) => {
if (!categoriesWithNodes[category]) {
return accu;
@@ -120,7 +117,7 @@ export const getCategorizedList = (categoriesWithNodes: ICategoriesWithNodes): I
key: category,
category,
properties: {
- expanded: false,
+ expanded: categoryIsExpanded,
},
};
@@ -170,6 +167,7 @@ export const getCategorizedList = (categoriesWithNodes: ICategoriesWithNodes): I
},
[],
);
+ return result;
};
export function getAppNameFromCredType(name: string) {
diff --git a/packages/editor-ui/src/views/CanvasAddButton.vue b/packages/editor-ui/src/views/CanvasAddButton.vue
index 7834880372..4f4549734c 100644
--- a/packages/editor-ui/src/views/CanvasAddButton.vue
+++ b/packages/editor-ui/src/views/CanvasAddButton.vue
@@ -1,6 +1,6 @@
-
-
+
+
@@ -12,38 +12,23 @@
-