From a2f21a76159e40de97c84c7604d3039d7e9a522e Mon Sep 17 00:00:00 2001 From: Shireen Missi <94372015+ShireenMissi@users.noreply.github.com> Date: Wed, 23 Jul 2025 14:32:28 +0100 Subject: [PATCH] feat(editor): Add settings icons to the node on canvas (#15467) --- .../components/N8nIcon/custom/Continue.svg | 10 ++ .../components/N8nIcon/custom/EmptyOutput.svg | 10 ++ .../src/components/N8nIcon/custom/Retry.svg | 10 ++ .../src/components/N8nIcon/custom/RunOnce.svg | 3 + .../src/components/N8nIcon/icons.ts | 8 ++ .../frontend/@n8n/i18n/src/locales/en.json | 14 ++ .../src/components/NodeSettingsHint.vue | 123 ++++++++++++++++++ .../editor-ui/src/components/RunData.vue | 40 +++++- .../__snapshots__/InputPanel.test.ts.snap | 1 + .../elements/nodes/CanvasNodeRenderer.test.ts | 5 + .../render-types/CanvasNodeDefault.test.ts | 5 + .../nodes/render-types/CanvasNodeDefault.vue | 10 ++ .../CanvasNodeDefault.test.ts.snap | 26 ++++ .../parts/CanvasNodeSettingsIcons.vue | 108 +++++++++++++++ 14 files changed, 371 insertions(+), 2 deletions(-) create mode 100644 packages/frontend/@n8n/design-system/src/components/N8nIcon/custom/Continue.svg create mode 100644 packages/frontend/@n8n/design-system/src/components/N8nIcon/custom/EmptyOutput.svg create mode 100644 packages/frontend/@n8n/design-system/src/components/N8nIcon/custom/Retry.svg create mode 100644 packages/frontend/@n8n/design-system/src/components/N8nIcon/custom/RunOnce.svg create mode 100644 packages/frontend/editor-ui/src/components/NodeSettingsHint.vue create mode 100644 packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/parts/CanvasNodeSettingsIcons.vue diff --git a/packages/frontend/@n8n/design-system/src/components/N8nIcon/custom/Continue.svg b/packages/frontend/@n8n/design-system/src/components/N8nIcon/custom/Continue.svg new file mode 100644 index 0000000000..ded4110c33 --- /dev/null +++ b/packages/frontend/@n8n/design-system/src/components/N8nIcon/custom/Continue.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/packages/frontend/@n8n/design-system/src/components/N8nIcon/custom/EmptyOutput.svg b/packages/frontend/@n8n/design-system/src/components/N8nIcon/custom/EmptyOutput.svg new file mode 100644 index 0000000000..3af2081ffc --- /dev/null +++ b/packages/frontend/@n8n/design-system/src/components/N8nIcon/custom/EmptyOutput.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/packages/frontend/@n8n/design-system/src/components/N8nIcon/custom/Retry.svg b/packages/frontend/@n8n/design-system/src/components/N8nIcon/custom/Retry.svg new file mode 100644 index 0000000000..0973d4cc1a --- /dev/null +++ b/packages/frontend/@n8n/design-system/src/components/N8nIcon/custom/Retry.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/packages/frontend/@n8n/design-system/src/components/N8nIcon/custom/RunOnce.svg b/packages/frontend/@n8n/design-system/src/components/N8nIcon/custom/RunOnce.svg new file mode 100644 index 0000000000..1a984bb000 --- /dev/null +++ b/packages/frontend/@n8n/design-system/src/components/N8nIcon/custom/RunOnce.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/frontend/@n8n/design-system/src/components/N8nIcon/icons.ts b/packages/frontend/@n8n/design-system/src/components/N8nIcon/icons.ts index 5b54360541..18eff4e3c9 100644 --- a/packages/frontend/@n8n/design-system/src/components/N8nIcon/icons.ts +++ b/packages/frontend/@n8n/design-system/src/components/N8nIcon/icons.ts @@ -1,8 +1,12 @@ import Binary from './custom/binary.svg'; import BoltFilled from './custom/bolt-filled.svg'; +import Continue from './custom/Continue.svg'; +import EmptyOutput from './custom/EmptyOutput.svg'; import GripLinesVertical from './custom/grip-lines-vertical.svg'; import Json from './custom/json.svg'; import PopOut from './custom/pop-out.svg'; +import Retry from './custom/Retry.svg'; +import RunOnce from './custom/RunOnce.svg'; import Schema from './custom/schema.svg'; import Spinner from './custom/spinner.svg'; import StatusCanceled from './custom/status-canceled.svg'; @@ -406,6 +410,10 @@ export const updatedIconSet = { 'status-unknown': StatusUnknown, 'status-warning': StatusWarning, 'vector-square': VectorSquare, + 'continue-on-error': Continue, + 'always-output-data': EmptyOutput, + 'retry-on-fail': Retry, + 'execute-once': RunOnce, schema: Schema, json: Json, binary: Binary, diff --git a/packages/frontend/@n8n/i18n/src/locales/en.json b/packages/frontend/@n8n/i18n/src/locales/en.json index 60ba91e9a1..3200886bab 100644 --- a/packages/frontend/@n8n/i18n/src/locales/en.json +++ b/packages/frontend/@n8n/i18n/src/locales/en.json @@ -1252,6 +1252,11 @@ "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.executionError.openNode": "Open node", + "node.settings.continuesOnError": "Execution will continue even if the node fails", + "node.settings.continuesOnError.title": "Continue On Fail", + "node.settings.retriesOnFailure": "This node will automatically retry if it fails", + "node.settings.executeOnce": "This node executes only once, no matter how many input items there are", + "node.settings.alwaysOutputData": "This node will output an empty item if nothing would normally be returned", "nodeBase.clickToAddNodeOrDragToConnect": "Click to add node \n or drag to connect", "nodeCreator.actionsPlaceholderNode.scheduleTrigger": "On a Schedule", "nodeCreator.actionsPlaceholderNode.webhook": "On a Webhook call", @@ -2429,6 +2434,15 @@ "ndv.search.noMatchSchema.description": "To search field values, switch to table or JSON view. {link}", "ndv.search.noMatchSchema.description.link": "Clear filter", "ndv.search.items": "{matched} of {count} item | {matched} of {count} items", + "ndv.nodeHints.disabled": "This node is disabled, and will simply pass the input through", + "ndv.nodeHints.alwaysOutputData": "This node will output an empty item if nothing would normally be returned", + "ndv.nodeHints.alwaysOutputData.short": "output an empty item if nothing would normally be returned", + "ndv.nodeHints.executeOnce": "This node will execute only once, no matter how many input items there are", + "ndv.nodeHints.executeOnce.short": "execute only once, no matter how many input items there are", + "ndv.nodeHints.retryOnFail": "This node will automatically retry if it fails", + "ndv.nodeHints.retryOnFail.short": "automatically retry if it fails", + "ndv.nodeHints.continueOnError": "Execution will continue even if the node fails", + "ndv.nodeHints.continueOnError.short": "continue executing even if the node fails", "updatesPanel.andIs": "and is", "updatesPanel.behindTheLatest": "behind the latest and greatest n8n", "updatesPanel.howToUpdateYourN8nVersion": "How to update your n8n version", diff --git a/packages/frontend/editor-ui/src/components/NodeSettingsHint.vue b/packages/frontend/editor-ui/src/components/NodeSettingsHint.vue new file mode 100644 index 0000000000..83d53f41b3 --- /dev/null +++ b/packages/frontend/editor-ui/src/components/NodeSettingsHint.vue @@ -0,0 +1,123 @@ + + + + + diff --git a/packages/frontend/editor-ui/src/components/RunData.vue b/packages/frontend/editor-ui/src/components/RunData.vue index 658985b965..83e3b701d3 100644 --- a/packages/frontend/editor-ui/src/components/RunData.vue +++ b/packages/frontend/editor-ui/src/components/RunData.vue @@ -2,6 +2,7 @@ import { ViewableMimeTypes } from '@n8n/api-types'; import { useStorage } from '@/composables/useStorage'; import { saveAs } from 'file-saver'; +import NodeSettingsHint from '@/components/NodeSettingsHint.vue'; import type { IBinaryData, IConnectedNode, @@ -818,7 +819,6 @@ function getNodeHints(): NodeHint[] { return []; } - function onItemHover(itemIndex: number | null) { if (itemIndex === null) { emit('itemHover', null); @@ -1575,7 +1575,7 @@ defineExpose({ enterEditMode }); - + should render 1`] = ` + diff --git a/packages/frontend/editor-ui/src/components/canvas/elements/nodes/CanvasNodeRenderer.test.ts b/packages/frontend/editor-ui/src/components/canvas/elements/nodes/CanvasNodeRenderer.test.ts index ae36476792..175c4cd392 100644 --- a/packages/frontend/editor-ui/src/components/canvas/elements/nodes/CanvasNodeRenderer.test.ts +++ b/packages/frontend/editor-ui/src/components/canvas/elements/nodes/CanvasNodeRenderer.test.ts @@ -4,12 +4,17 @@ import { createCanvasNodeProvide, createCanvasProvide } from '@/__tests__/data'; import { createTestingPinia } from '@pinia/testing'; import { setActivePinia } from 'pinia'; import { CanvasNodeRenderType } from '@/types'; +import { useWorkflowsStore } from '@/stores/workflows.store'; +import { createTestWorkflowObject } from '@/__tests__/mocks'; const renderComponent = createComponentRenderer(CanvasNodeRenderer); beforeEach(() => { const pinia = createTestingPinia(); setActivePinia(pinia); + const workflowsStore = useWorkflowsStore(); + const workflowObject = createTestWorkflowObject(workflowsStore.workflow); + workflowsStore.getCurrentWorkflow = vi.fn().mockReturnValue(workflowObject); }); describe('CanvasNodeRenderer', () => { diff --git a/packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeDefault.test.ts b/packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeDefault.test.ts index 3683265105..142114de0c 100644 --- a/packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeDefault.test.ts +++ b/packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeDefault.test.ts @@ -6,6 +6,8 @@ import { createTestingPinia } from '@pinia/testing'; import { setActivePinia } from 'pinia'; import { CanvasConnectionMode, CanvasNodeRenderType } from '@/types'; import { fireEvent } from '@testing-library/vue'; +import { useWorkflowsStore } from '@/stores/workflows.store'; +import { createTestWorkflowObject } from '@/__tests__/mocks'; const renderComponent = createComponentRenderer(CanvasNodeDefault, { global: { @@ -18,6 +20,9 @@ const renderComponent = createComponentRenderer(CanvasNodeDefault, { beforeEach(() => { const pinia = createTestingPinia(); setActivePinia(pinia); + const workflowsStore = useWorkflowsStore(); + const workflowObject = createTestWorkflowObject(workflowsStore.workflow); + workflowsStore.getCurrentWorkflow = vi.fn().mockReturnValue(workflowObject); }); describe('CanvasNodeDefault', () => { diff --git a/packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeDefault.vue b/packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeDefault.vue index 3963f54516..872a577e29 100644 --- a/packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeDefault.vue +++ b/packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeDefault.vue @@ -5,6 +5,8 @@ import { useI18n } from '@n8n/i18n'; import { useCanvasNode } from '@/composables/useCanvasNode'; import type { CanvasNodeDefaultRender } from '@/types'; import { useCanvas } from '@/composables/useCanvas'; +import CanvasNodeSettingsIcons from '@/components/canvas/elements/nodes/render-types/parts/CanvasNodeSettingsIcons.vue'; +import { useNodeHelpers } from '@/composables/useNodeHelpers'; import { calculateNodeSize } from '@/utils/nodeViewUtils'; import ExperimentalInPlaceNodeSettings from '@/components/canvas/experimental/components/ExperimentalEmbeddedNodeDetails.vue'; @@ -43,6 +45,7 @@ const { mainOutputs, mainOutputConnections, mainInputs, mainInputConnections, no connections, }); +const nodeHelpers = useNodeHelpers(); const renderOptions = computed(() => render.value.options as CanvasNodeDefaultRender['options']); const classes = computed(() => { @@ -153,6 +156,13 @@ function onActivate(event: MouseEvent) { :disabled="isDisabled" :class="$style.icon" /> +
diff --git a/packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/__snapshots__/CanvasNodeDefault.test.ts.snap b/packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/__snapshots__/CanvasNodeDefault.test.ts.snap index d6920c0938..4476565ba6 100644 --- a/packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/__snapshots__/CanvasNodeDefault.test.ts.snap +++ b/packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/__snapshots__/CanvasNodeDefault.test.ts.snap @@ -25,6 +25,14 @@ exports[`CanvasNodeDefault > configurable > should render configurable node corr
+
+ + + + +
configuration > should render configurable configur
+
@@ -116,6 +125,7 @@ exports[`CanvasNodeDefault > configuration > should render configuration node co
+
@@ -160,6 +170,14 @@ exports[`CanvasNodeDefault > should render node correctly 1`] = `
+
+ + + + +
trigger > should render trigger node correctly 1`]
+
+ + + + +
+import { computed } from 'vue'; +import { useCanvasNode } from '@/composables/useCanvasNode'; +import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers'; +import { useI18n } from '@n8n/i18n'; +import { N8nIcon } from '@n8n/design-system'; + +const { name } = useCanvasNode(); +const i18n = useI18n(); +const workflowHelpers = useWorkflowHelpers(); + +const workflow = computed(() => workflowHelpers.getCurrentWorkflow()); +const node = computed(() => workflow.value.getNode(name.value)); + + + + +