From 0f345681d91492e015d3126218ef7afa8c35ccce Mon Sep 17 00:00:00 2001 From: Eugene Date: Wed, 29 Jan 2025 18:00:22 +0300 Subject: [PATCH 001/171] chore: Add basic telemetry events to workflow evaluation (no-changelog) (#12890) Co-authored-by: Oleg Ivaniv --- .../__tests__/test-runner.service.ee.test.ts | 12 ++++++++++++ .../test-runner/test-runner.service.ee.ts | 14 +++++++++++++- .../TestDefinition/TestDefinitionEditView.vue | 15 +++++++++++++++ 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/evaluation.ee/test-runner/__tests__/test-runner.service.ee.test.ts b/packages/cli/src/evaluation.ee/test-runner/__tests__/test-runner.service.ee.test.ts index 026b5d2eb8..f82b09a8e1 100644 --- a/packages/cli/src/evaluation.ee/test-runner/__tests__/test-runner.service.ee.test.ts +++ b/packages/cli/src/evaluation.ee/test-runner/__tests__/test-runner.service.ee.test.ts @@ -18,6 +18,7 @@ import type { TestRunRepository } from '@/databases/repositories/test-run.reposi import type { WorkflowRepository } from '@/databases/repositories/workflow.repository'; import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials'; import { NodeTypes } from '@/node-types'; +import type { Telemetry } from '@/telemetry'; import type { WorkflowRunner } from '@/workflow-runner'; import { mockInstance, mockLogger } from '@test/mocking'; import { mockNodeTypesData } from '@test-integration/utils/node-types-data'; @@ -131,6 +132,7 @@ function mockEvaluationExecutionData(metrics: Record) { const errorReporter = mock(); const logger = mockLogger(); +const telemetry = mock(); async function mockLongExecutionPromise(data: IRun, delay: number): Promise { return await new Promise((resolve) => { @@ -182,6 +184,7 @@ describe('TestRunnerService', () => { test('should create an instance of TestRunnerService', async () => { const testRunnerService = new TestRunnerService( logger, + telemetry, workflowRepository, workflowRunner, executionRepository, @@ -198,6 +201,7 @@ describe('TestRunnerService', () => { test('should create and run test cases from past executions', async () => { const testRunnerService = new TestRunnerService( logger, + telemetry, workflowRepository, workflowRunner, executionRepository, @@ -237,6 +241,7 @@ describe('TestRunnerService', () => { test('should run both workflow under test and evaluation workflow', async () => { const testRunnerService = new TestRunnerService( logger, + telemetry, workflowRepository, workflowRunner, executionRepository, @@ -339,6 +344,7 @@ describe('TestRunnerService', () => { test('should properly count passed and failed executions', async () => { const testRunnerService = new TestRunnerService( logger, + telemetry, workflowRepository, workflowRunner, executionRepository, @@ -398,6 +404,7 @@ describe('TestRunnerService', () => { test('should properly count failed test executions', async () => { const testRunnerService = new TestRunnerService( logger, + telemetry, workflowRepository, workflowRunner, executionRepository, @@ -453,6 +460,7 @@ describe('TestRunnerService', () => { test('should properly count failed evaluations', async () => { const testRunnerService = new TestRunnerService( logger, + telemetry, workflowRepository, workflowRunner, executionRepository, @@ -512,6 +520,7 @@ describe('TestRunnerService', () => { test('should specify correct start nodes when running workflow under test', async () => { const testRunnerService = new TestRunnerService( logger, + telemetry, workflowRepository, workflowRunner, executionRepository, @@ -587,6 +596,7 @@ describe('TestRunnerService', () => { test('should properly choose trigger and start nodes', async () => { const testRunnerService = new TestRunnerService( logger, + telemetry, workflowRepository, workflowRunner, executionRepository, @@ -613,6 +623,7 @@ describe('TestRunnerService', () => { test('should properly choose trigger and start nodes 2', async () => { const testRunnerService = new TestRunnerService( logger, + telemetry, workflowRepository, workflowRunner, executionRepository, @@ -644,6 +655,7 @@ describe('TestRunnerService', () => { test('should cancel test run', async () => { const testRunnerService = new TestRunnerService( logger, + telemetry, workflowRepository, workflowRunner, executionRepository, diff --git a/packages/cli/src/evaluation.ee/test-runner/test-runner.service.ee.ts b/packages/cli/src/evaluation.ee/test-runner/test-runner.service.ee.ts index a594e15c05..f2928f0b91 100644 --- a/packages/cli/src/evaluation.ee/test-runner/test-runner.service.ee.ts +++ b/packages/cli/src/evaluation.ee/test-runner/test-runner.service.ee.ts @@ -23,6 +23,7 @@ import { TestMetricRepository } from '@/databases/repositories/test-metric.repos import { TestRunRepository } from '@/databases/repositories/test-run.repository.ee'; import { WorkflowRepository } from '@/databases/repositories/workflow.repository'; import { NodeTypes } from '@/node-types'; +import { Telemetry } from '@/telemetry'; import { getRunData } from '@/workflow-execute-additional-data'; import { WorkflowRunner } from '@/workflow-runner'; @@ -43,6 +44,7 @@ export class TestRunnerService { constructor( private readonly logger: Logger, + private readonly telemetry: Telemetry, private readonly workflowRepository: WorkflowRepository, private readonly workflowRunner: WorkflowRunner, private readonly executionRepository: ExecutionRepository, @@ -268,12 +270,22 @@ export class TestRunnerService { const testMetricNames = await this.getTestMetricNames(test.id); // 2. Run over all the test cases + const pastExecutionIds = pastExecutions.map((e) => e.id); + await this.testRunRepository.markAsRunning(testRun.id, pastExecutions.length); + this.telemetry.track('User runs test', { + user_id: user.id, + test_id: test.id, + run_id: testRun.id, + executions_ids: pastExecutionIds, + workflow_id: test.workflowId, + evaluation_workflow_id: test.evaluationWorkflowId, + }); // Object to collect the results of the evaluation workflow executions const metrics = new EvaluationMetrics(testMetricNames); - for (const { id: pastExecutionId } of pastExecutions) { + for (const pastExecutionId of pastExecutionIds) { if (abortSignal.aborted) { this.logger.debug('Test run was cancelled', { testId: test.id, diff --git a/packages/editor-ui/src/views/TestDefinition/TestDefinitionEditView.vue b/packages/editor-ui/src/views/TestDefinition/TestDefinitionEditView.vue index 55d0b92297..95a49ea2aa 100644 --- a/packages/editor-ui/src/views/TestDefinition/TestDefinitionEditView.vue +++ b/packages/editor-ui/src/views/TestDefinition/TestDefinitionEditView.vue @@ -14,6 +14,8 @@ import type { TestMetricRecord, TestRunRecord } from '@/api/testDefinition.ee'; import { useUIStore } from '@/stores/ui.store'; import { useTestDefinitionStore } from '@/stores/testDefinition.store.ee'; import ConfigSection from '@/components/TestDefinition/EditDefinition/sections/ConfigSection.vue'; +import { useTelemetry } from '@/composables/useTelemetry'; +import { useRootStore } from '@/stores/root.store'; import { useExecutionsStore } from '@/stores/executions.store'; import { useWorkflowsStore } from '@/stores/workflows.store'; import type { IPinData } from 'n8n-workflow'; @@ -30,6 +32,7 @@ const toast = useToast(); const testDefinitionStore = useTestDefinitionStore(); const tagsStore = useAnnotationTagsStore(); const uiStore = useUIStore(); +const telemetry = useTelemetry(); const executionsStore = useExecutionsStore(); const workflowStore = useWorkflowsStore(); @@ -97,6 +100,18 @@ async function onSaveTest() { name: VIEWS.TEST_DEFINITION_EDIT, params: { testId: savedTest.id }, }); + + telemetry.track( + 'User created test', + { + test_id: savedTest.id, + workflow_id: currentWorkflowId.value, + session_id: useRootStore().pushRef, + }, + { + withPostHog: true, + }, + ); } } catch (e: unknown) { toast.showError(e, locale.baseText('testDefinition.edit.testSaveFailed')); From 0da1114981978e371b216bdabc0c3bbdceeefa09 Mon Sep 17 00:00:00 2001 From: Dana <152518854+dana-gill@users.noreply.github.com> Date: Wed, 29 Jan 2025 16:56:47 +0100 Subject: [PATCH 002/171] feat(n8n Form Node): Add Hidden Fields (#12803) --- .../cli/templates/form-trigger.handlebars | 4 ++ packages/nodes-base/nodes/Form/Form.node.ts | 10 +++- .../nodes/Form/common.descriptions.ts | 39 +++++++++++++- .../nodes/Form/test/Form.node.test.ts | 51 +++++++++++++++++- .../nodes-base/nodes/Form/test/utils.test.ts | 53 ++++++++++++++++++- packages/nodes-base/nodes/Form/utils.ts | 8 +++ packages/workflow/src/Interfaces.ts | 2 + 7 files changed, 161 insertions(+), 6 deletions(-) diff --git a/packages/cli/templates/form-trigger.handlebars b/packages/cli/templates/form-trigger.handlebars index 6bad1a02d8..21e10d86a9 100644 --- a/packages/cli/templates/form-trigger.handlebars +++ b/packages/cli/templates/form-trigger.handlebars @@ -377,6 +377,10 @@ {{/if}} + {{#if isHidden}} + + {{/if}} + {{#if isTextarea}}
diff --git a/packages/nodes-base/nodes/Form/Form.node.ts b/packages/nodes-base/nodes/Form/Form.node.ts index 735cbc4065..03f27219fe 100644 --- a/packages/nodes-base/nodes/Form/Form.node.ts +++ b/packages/nodes-base/nodes/Form/Form.node.ts @@ -266,7 +266,15 @@ export class Form extends Node { }); } } else { - fields = context.getNodeParameter('formFields.values', []) as FormFieldsParameter; + fields = (context.getNodeParameter('formFields.values', []) as FormFieldsParameter).map( + (field) => { + if (field.fieldType === 'hiddenField') { + field.fieldLabel = field.fieldName as string; + } + + return field; + }, + ); } const method = context.getRequestObject().method; diff --git a/packages/nodes-base/nodes/Form/common.descriptions.ts b/packages/nodes-base/nodes/Form/common.descriptions.ts index 67cf3515d1..9a3fab2e0f 100644 --- a/packages/nodes-base/nodes/Form/common.descriptions.ts +++ b/packages/nodes-base/nodes/Form/common.descriptions.ts @@ -64,6 +64,11 @@ export const formFields: INodeProperties = { placeholder: 'e.g. What is your name?', description: 'Label that appears above the input field', required: true, + displayOptions: { + hide: { + fieldType: ['hiddenField'], + }, + }, }, { displayName: 'Element Type', @@ -92,6 +97,10 @@ export const formFields: INodeProperties = { name: 'File', value: 'file', }, + { + name: 'Hidden Field', + value: 'hiddenField', + }, { name: 'Number', value: 'number', @@ -119,7 +128,33 @@ export const formFields: INodeProperties = { default: '', displayOptions: { hide: { - fieldType: ['dropdown', 'date', 'file', 'html'], + fieldType: ['dropdown', 'date', 'file', 'html', 'hiddenField'], + }, + }, + }, + { + displayName: 'Field Name', + name: 'fieldName', + description: + 'The name of the field, used in input attributes and referenced by the workflow', + type: 'string', + default: '', + displayOptions: { + show: { + fieldType: ['hiddenField'], + }, + }, + }, + { + displayName: 'Field Value', + name: 'fieldValue', + description: + 'Input value can be set here or will be passed as a query parameter via Field Name if no value is set', + type: 'string', + default: '', + displayOptions: { + show: { + fieldType: ['hiddenField'], }, }, }, @@ -242,7 +277,7 @@ export const formFields: INodeProperties = { 'Whether to require the user to enter a value for this field before submitting the form', displayOptions: { hide: { - fieldType: ['html'], + fieldType: ['html', 'hiddenField'], }, }, }, diff --git a/packages/nodes-base/nodes/Form/test/Form.node.test.ts b/packages/nodes-base/nodes/Form/test/Form.node.test.ts index 9dd0e4bbf2..537804dea3 100644 --- a/packages/nodes-base/nodes/Form/test/Form.node.test.ts +++ b/packages/nodes-base/nodes/Form/test/Form.node.test.ts @@ -106,7 +106,16 @@ describe('Form Node', () => { mockWebhookFunctions.getNodeParameter.mockImplementation((paramName: string) => { if (paramName === 'operation') return 'page'; if (paramName === 'useJson') return false; - if (paramName === 'formFields.values') return [{ fieldLabel: 'test' }]; + if (paramName === 'formFields.values') + return [ + { fieldLabel: 'test' }, + { + fieldName: 'Powerpuff Girl', + fieldValue: 'Blossom', + fieldType: 'hiddenField', + fieldLabel: '', + }, + ]; if (paramName === 'options') { return { formTitle: 'Form Title', @@ -121,7 +130,42 @@ describe('Form Node', () => { await form.webhook(mockWebhookFunctions); - expect(mockResponseObject.render).toHaveBeenCalledWith('form-trigger', expect.any(Object)); + expect(mockResponseObject.render).toHaveBeenCalledWith('form-trigger', { + appendAttribution: 'test', + buttonLabel: 'Form Button', + formDescription: 'Form Description', + formDescriptionMetadata: 'Form Description', + formFields: [ + { + id: 'field-0', + errorId: 'error-field-0', + label: 'test', + inputRequired: '', + defaultValue: '', + isInput: true, + placeholder: undefined, + type: undefined, + }, + { + id: 'field-1', + errorId: 'error-field-1', + label: 'Powerpuff Girl', + inputRequired: '', + defaultValue: '', + placeholder: undefined, + hiddenName: 'Powerpuff Girl', + hiddenValue: 'Blossom', + isHidden: true, + }, + ], + formSubmittedText: 'Your response has been recorded', + formTitle: 'Form Title', + n8nWebsiteLink: 'https://n8n.io/?utm_source=n8n-internal&utm_medium=form-trigger', + testRun: true, + useResponseData: false, + validForm: true, + formSubmittedHeader: undefined, + }); }); it('should return form data for POST request', async () => { @@ -182,6 +226,7 @@ describe('Form Node', () => { if (paramName === 'completionTitle') return 'Test Title'; if (paramName === 'completionMessage') return 'Test Message'; if (paramName === 'redirectUrl') return ''; + if (paramName === 'formFields.values') return []; return {}; }); mockWebhookFunctions.getParentNodes.mockReturnValue([ @@ -225,6 +270,8 @@ describe('Form Node', () => { if (paramName === 'completionTitle') return 'Test Title'; if (paramName === 'completionMessage') return 'Test Message'; if (paramName === 'redirectUrl') return 'https://n8n.io'; + if (paramName === 'formFields.values') return []; + return {}; }); mockWebhookFunctions.getParentNodes.mockReturnValue([ diff --git a/packages/nodes-base/nodes/Form/test/utils.test.ts b/packages/nodes-base/nodes/Form/test/utils.test.ts index 4d1bf39c36..a6130cfb3c 100644 --- a/packages/nodes-base/nodes/Form/test/utils.test.ts +++ b/packages/nodes-base/nodes/Form/test/utils.test.ts @@ -110,6 +110,12 @@ describe('FormTrigger, formWebhook', () => { html: '
Test HTML
', requiredField: false, }, + { + fieldName: 'Powerpuff Girl', + fieldValue: 'Blossom', + fieldType: 'hiddenField', + fieldLabel: '', + }, ]; executeFunctions.getNodeParameter.calledWith('formFields.values').mockReturnValue(formFields); @@ -174,6 +180,17 @@ describe('FormTrigger, formWebhook', () => { html: '
Test HTML
', isHtml: true, }, + { + id: 'field-5', + errorId: 'error-field-5', + hiddenName: 'Powerpuff Girl', + hiddenValue: 'Blossom', + label: 'Powerpuff Girl', + isHidden: true, + inputRequired: '', + defaultValue: '', + placeholder: undefined, + }, ], formSubmittedText: 'Your response has been recorded', formTitle: 'Test Form', @@ -300,9 +317,21 @@ describe('FormTrigger, prepareFormData', () => { acceptFileTypes: '.jpg,.png', multipleFiles: true, }, + { + fieldLabel: 'username', + fieldName: 'username', + fieldValue: 'powerpuffgirl125', + fieldType: 'hiddenField', + }, + { + fieldLabel: 'villain', + fieldName: 'villain', + fieldValue: 'Mojo Dojo', + fieldType: 'hiddenField', + }, ]; - const query = { Name: 'John Doe', Email: 'john@example.com' }; + const query = { Name: 'John Doe', Email: 'john@example.com', villain: 'princess morbucks' }; const result = prepareFormData({ formTitle: 'Test Form', @@ -368,6 +397,28 @@ describe('FormTrigger, prepareFormData', () => { acceptFileTypes: '.jpg,.png', multipleFiles: 'multiple', }, + { + id: 'field-4', + errorId: 'error-field-4', + label: 'username', + inputRequired: '', + defaultValue: '', + placeholder: undefined, + hiddenName: 'username', + hiddenValue: 'powerpuffgirl125', + isHidden: true, + }, + { + id: 'field-5', + errorId: 'error-field-5', + label: 'villain', + inputRequired: '', + defaultValue: 'princess morbucks', + placeholder: undefined, + hiddenName: 'villain', + isHidden: true, + hiddenValue: 'princess morbucks', + }, ], useResponseData: true, appendAttribution: true, diff --git a/packages/nodes-base/nodes/Form/utils.ts b/packages/nodes-base/nodes/Form/utils.ts index 2510be56ac..ff10c69d08 100644 --- a/packages/nodes-base/nodes/Form/utils.ts +++ b/packages/nodes-base/nodes/Form/utils.ts @@ -167,6 +167,11 @@ export function prepareFormData({ } else if (fieldType === 'html') { input.isHtml = true; input.html = field.html as string; + } else if (fieldType === 'hiddenField') { + input.isHidden = true; + input.hiddenName = field.fieldName as string; + input.hiddenValue = + input.defaultValue === '' ? (field.fieldValue as string) : input.defaultValue; } else { input.isInput = true; input.type = fieldType as 'text' | 'number' | 'date' | 'email'; @@ -432,6 +437,9 @@ export async function formWebhook( if (field.fieldType === 'html') { field.html = sanitizeHtml(field.html as string); } + if (field.fieldType === 'hiddenField') { + field.fieldLabel = field.fieldName as string; + } return field; }, ); diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index ff0b1ad973..0d67b07f37 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -2693,6 +2693,8 @@ export type FormFieldsParameter = Array<{ formatDate?: string; html?: string; placeholder?: string; + fieldName?: string; + fieldValue?: string; }>; export type FieldTypeMap = { From 6258f0c9dd02eece1acfe149c46e5caa8b5e243c Mon Sep 17 00:00:00 2001 From: Mariana-na <135712885+Mariana-na@users.noreply.github.com> Date: Thu, 30 Jan 2025 09:57:33 +0000 Subject: [PATCH 003/171] ci: Remove lefthook path to make it accessible on Windows (#12929) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ --- lefthook.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lefthook.yml b/lefthook.yml index aa17417824..b6aac6e069 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -2,14 +2,14 @@ pre-commit: commands: biome_check: glob: 'packages/**/*.{js,ts,json}' - run: ./node_modules/.bin/biome check --write --no-errors-on-unmatched --files-ignore-unknown=true --colors=off {staged_files} + run: pnpm biome check --write --no-errors-on-unmatched --files-ignore-unknown=true --colors=off {staged_files} stage_fixed: true skip: - merge - rebase prettier_check: glob: 'packages/**/*.{vue,yml,md,css,scss}' - run: ./node_modules/.bin/prettier --write --ignore-unknown --no-error-on-unmatched-pattern {staged_files} + run: pnpm prettier --write --ignore-unknown --no-error-on-unmatched-pattern {staged_files} stage_fixed: true skip: - merge From 0d8a544975f72724db931778d7e3ace8a12b6cfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20G=C3=B3mez=20Morales?= Date: Thu, 30 Jan 2025 15:15:03 +0100 Subject: [PATCH 004/171] fix(editor): SchemaView renders duplicate structures properly (#12943) --- .../src/composables/useDataSchema.test.ts | 23 +++++++++++++++++++ .../src/composables/useDataSchema.ts | 8 ++++--- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/packages/editor-ui/src/composables/useDataSchema.test.ts b/packages/editor-ui/src/composables/useDataSchema.test.ts index 212f767675..b98290e135 100644 --- a/packages/editor-ui/src/composables/useDataSchema.test.ts +++ b/packages/editor-ui/src/composables/useDataSchema.test.ts @@ -684,4 +684,27 @@ describe('useFlattenSchema', () => { }).length, ).toBe(3); }); + + it('items ids should be unique', () => { + const { flattenSchema } = useFlattenSchema(); + const schema: Schema = { + path: '', + type: 'object', + value: [ + { + key: 'index', + type: 'number', + value: '0', + path: '.index', + }, + ], + }; + const node1 = { name: 'First Node', type: 'any' }; + const node2 = { name: 'Second Node', type: 'any' }; + + const node1Schema = flattenSchema({ schema, node: node1, depth: 1 }); + const node2Schema = flattenSchema({ schema, node: node2, depth: 1 }); + + expect(node1Schema[0].id).not.toBe(node2Schema[0].id); + }); }); diff --git a/packages/editor-ui/src/composables/useDataSchema.ts b/packages/editor-ui/src/composables/useDataSchema.ts index 8896f8dd7e..9b06485713 100644 --- a/packages/editor-ui/src/composables/useDataSchema.ts +++ b/packages/editor-ui/src/composables/useDataSchema.ts @@ -282,6 +282,8 @@ export const useFlattenSchema = () => { path: schema.path, }); + const id = `${node.name}-${expression}`; + if (Array.isArray(schema.value)) { const items: RenderItem[] = []; @@ -293,14 +295,14 @@ export const useFlattenSchema = () => { depth, level, icon: getIconBySchemaType(schema.type), - id: expression, + id, collapsable: true, nodeType: node.type, type: 'item', }); } - if (closedNodes.value.has(expression)) { + if (closedNodes.value.has(id)) { return items; } @@ -327,7 +329,7 @@ export const useFlattenSchema = () => { level, depth, value: shorten(schema.value, 600, 0), - id: expression, + id, icon: getIconBySchemaType(schema.type), collapsable: false, nodeType: node.type, From 9590e5d58b8964de9ce901bf07b537926d18b6b7 Mon Sep 17 00:00:00 2001 From: Michael Kret <88898367+michael-radency@users.noreply.github.com> Date: Thu, 30 Jan 2025 16:16:42 +0200 Subject: [PATCH 005/171] feat: Human in the loop section (#12883) Co-authored-by: Dana <152518854+dana-gill@users.noreply.github.com> Co-authored-by: Jonathan Bennetts --- cypress/e2e/4-node-creator.cy.ts | 9 +++++ .../Node/NodeCreator/ItemTypes/NodeItem.vue | 7 +++- .../Node/NodeCreator/Modes/NodesMode.vue | 17 +++++++-- .../Node/NodeCreator/NodesListPanel.test.ts | 2 +- .../src/components/Node/NodeCreator/utils.ts | 18 ++++++++-- .../components/Node/NodeCreator/viewsData.ts | 35 ++++++++++++++++--- packages/editor-ui/src/constants.ts | 2 ++ .../src/plugins/i18n/locales/en.json | 3 ++ packages/editor-ui/src/plugins/icons/index.ts | 2 ++ .../nodes/EmailSend/EmailSend.node.json | 5 ++- .../nodes/Google/Chat/GoogleChat.node.json | 4 +-- .../nodes/Google/Gmail/Gmail.node.json | 5 ++- .../Outlook/MicrosoftOutlook.node.json | 5 ++- .../nodes-base/nodes/Slack/Slack.node.json | 5 ++- .../nodes/Telegram/Telegram.node.json | 5 ++- 15 files changed, 106 insertions(+), 18 deletions(-) diff --git a/cypress/e2e/4-node-creator.cy.ts b/cypress/e2e/4-node-creator.cy.ts index a33f16156f..5636179166 100644 --- a/cypress/e2e/4-node-creator.cy.ts +++ b/cypress/e2e/4-node-creator.cy.ts @@ -571,4 +571,13 @@ describe('Node Creator', () => { addVectorStoreToolToParent('In-Memory Vector Store', AGENT_NODE_NAME); }); + + it('should insert node to canvas with sendAndWait operation selected', () => { + nodeCreatorFeature.getters.canvasAddButton().click(); + WorkflowPage.actions.addNodeToCanvas('Manual', false); + nodeCreatorFeature.actions.openNodeCreator(); + cy.contains('Human in the loop').click(); + nodeCreatorFeature.getters.getCreatorItem('Slack').click(); + cy.contains('Send and Wait for Response').should('exist'); + }); }); diff --git a/packages/editor-ui/src/components/Node/NodeCreator/ItemTypes/NodeItem.vue b/packages/editor-ui/src/components/Node/NodeCreator/ItemTypes/NodeItem.vue index b982e4ecee..904c58133d 100644 --- a/packages/editor-ui/src/components/Node/NodeCreator/ItemTypes/NodeItem.vue +++ b/packages/editor-ui/src/components/Node/NodeCreator/ItemTypes/NodeItem.vue @@ -6,6 +6,7 @@ import { CREDENTIAL_ONLY_NODE_PREFIX, DEFAULT_SUBCATEGORY, DRAG_EVENT_DATA_KEY, + HITL_SUBCATEGORY, } from '@/constants'; import { isCommunityPackageName } from '@/utils/nodeTypesUtils'; @@ -44,6 +45,9 @@ const draggablePosition = ref({ x: -100, y: -100 }); const draggableDataTransfer = ref(null as Element | null); const description = computed(() => { + if (isSendAndWaitCategory.value) { + return ''; + } if ( props.subcategory === DEFAULT_SUBCATEGORY && !props.nodeType.name.startsWith(CREDENTIAL_ONLY_NODE_PREFIX) @@ -56,7 +60,8 @@ const description = computed(() => { fallback: props.nodeType.description, }); }); -const showActionArrow = computed(() => hasActions.value); +const showActionArrow = computed(() => hasActions.value && !isSendAndWaitCategory.value); +const isSendAndWaitCategory = computed(() => activeViewStack.subcategory === HITL_SUBCATEGORY); const dataTestId = computed(() => hasActions.value ? 'node-creator-action-item' : 'node-creator-node-item', ); diff --git a/packages/editor-ui/src/components/Node/NodeCreator/Modes/NodesMode.vue b/packages/editor-ui/src/components/Node/NodeCreator/Modes/NodesMode.vue index d7ed3d87f0..1cba28c4ed 100644 --- a/packages/editor-ui/src/components/Node/NodeCreator/Modes/NodesMode.vue +++ b/packages/editor-ui/src/components/Node/NodeCreator/Modes/NodesMode.vue @@ -1,7 +1,12 @@