From 4d222ac19d943b69fd9f87abe5e5c5f5141eed8d Mon Sep 17 00:00:00 2001 From: Michael Kret <88898367+michael-radency@users.noreply.github.com> Date: Wed, 14 Aug 2024 16:20:02 +0300 Subject: [PATCH] feat(AI Transform Node): New node (#10405) Co-authored-by: Giulio Andreini Co-authored-by: Shireen Missi <94372015+ShireenMissi@users.noreply.github.com> --- .../ManualChatTrigger.node.ts | 4 +- .../dynamicNodeParameters.controller.ts | 26 +- packages/cli/src/requests.ts | 6 + .../services/dynamicNodeParameters.service.ts | 27 +- packages/editor-ui/src/Interface.ts | 5 + packages/editor-ui/src/api/nodeTypes.ts | 13 + .../ButtonParameter/ButtonParameter.vue | 281 ++++++++++++++++++ .../src/components/ButtonParameter/utils.ts | 46 +++ .../CodeNodeEditor/CodeNodeEditor.vue | 12 +- .../src/components/JsEditor/JsEditor.vue | 32 +- .../src/components/Node/NodeCreator/utils.ts | 8 + .../components/Node/NodeCreator/viewsData.ts | 11 +- .../src/components/ParameterInput.vue | 9 +- .../src/components/ParameterInputList.vue | 26 +- .../__tests__/ButtonParameter.test.ts | 136 +++++++++ packages/editor-ui/src/constants.ts | 7 +- .../editor-ui/src/stores/nodeTypes.store.ts | 7 + .../editor-ui/src/stores/posthog.store.ts | 15 +- .../nodes/AiTransform/AiTransform.node.json | 18 ++ .../nodes/AiTransform/AiTransform.node.ts | 148 +++++++++ .../nodes/AiTransform/aitransform.svg | 10 + packages/nodes-base/package.json | 1 + packages/workflow/src/Constants.ts | 9 +- packages/workflow/src/Interfaces.ts | 21 +- 24 files changed, 839 insertions(+), 39 deletions(-) create mode 100644 packages/editor-ui/src/components/ButtonParameter/ButtonParameter.vue create mode 100644 packages/editor-ui/src/components/ButtonParameter/utils.ts create mode 100644 packages/editor-ui/src/components/__tests__/ButtonParameter.test.ts create mode 100644 packages/nodes-base/nodes/AiTransform/AiTransform.node.json create mode 100644 packages/nodes-base/nodes/AiTransform/AiTransform.node.ts create mode 100644 packages/nodes-base/nodes/AiTransform/aitransform.svg diff --git a/packages/@n8n/nodes-langchain/nodes/trigger/ManualChatTrigger/ManualChatTrigger.node.ts b/packages/@n8n/nodes-langchain/nodes/trigger/ManualChatTrigger/ManualChatTrigger.node.ts index 272213d539..816b56b59a 100644 --- a/packages/@n8n/nodes-langchain/nodes/trigger/ManualChatTrigger/ManualChatTrigger.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/trigger/ManualChatTrigger/ManualChatTrigger.node.ts @@ -50,7 +50,9 @@ export class ManualChatTrigger implements INodeType { name: 'openChat', type: 'button', typeOptions: { - action: 'openChat', + buttonConfig: { + action: 'openChat', + }, }, default: '', }, diff --git a/packages/cli/src/controllers/dynamicNodeParameters.controller.ts b/packages/cli/src/controllers/dynamicNodeParameters.controller.ts index c6a46aa3d4..bfd1cf651b 100644 --- a/packages/cli/src/controllers/dynamicNodeParameters.controller.ts +++ b/packages/cli/src/controllers/dynamicNodeParameters.controller.ts @@ -1,4 +1,4 @@ -import type { INodePropertyOptions } from 'n8n-workflow'; +import type { INodePropertyOptions, NodeParameterValueType } from 'n8n-workflow'; import { Post, RestController } from '@/decorators'; import { getBase } from '@/WorkflowExecuteAdditionalData'; @@ -92,4 +92,28 @@ export class DynamicNodeParametersController { credentials, ); } + + @Post('/action-result') + async getActionResult( + req: DynamicNodeParametersRequest.ActionResult, + ): Promise { + const { currentNodeParameters, nodeTypeAndVersion, path, credentials, handler, payload } = + req.body; + + const additionalData = await getBase(req.user.id, currentNodeParameters); + + if (handler) { + return await this.service.getActionResult( + handler, + path, + additionalData, + nodeTypeAndVersion, + currentNodeParameters, + payload, + credentials, + ); + } + + return; + } } diff --git a/packages/cli/src/requests.ts b/packages/cli/src/requests.ts index 82cf7da30a..436c0b1b18 100644 --- a/packages/cli/src/requests.ts +++ b/packages/cli/src/requests.ts @@ -420,6 +420,12 @@ export declare namespace DynamicNodeParametersRequest { type ResourceMapperFields = BaseRequest<{ methodName: string; }>; + + /** POST /dynamic-node-parameters/action-result */ + type ActionResult = BaseRequest<{ + handler: string; + payload: IDataObject | string | undefined; + }>; } // ---------------------------------- diff --git a/packages/cli/src/services/dynamicNodeParameters.service.ts b/packages/cli/src/services/dynamicNodeParameters.service.ts index 1788bac6e1..f3b0f7e192 100644 --- a/packages/cli/src/services/dynamicNodeParameters.service.ts +++ b/packages/cli/src/services/dynamicNodeParameters.service.ts @@ -15,6 +15,8 @@ import type { INodeCredentials, INodeParameters, INodeTypeNameVersion, + NodeParameterValueType, + IDataObject, } from 'n8n-workflow'; import { Workflow, RoutingNode, ApplicationError } from 'n8n-workflow'; import { NodeExecuteFunctions } from 'n8n-core'; @@ -156,6 +158,24 @@ export class DynamicNodeParametersService { return method.call(thisArgs); } + /** Returns the result of the action handler */ + async getActionResult( + handler: string, + path: string, + additionalData: IWorkflowExecuteAdditionalData, + nodeTypeAndVersion: INodeTypeNameVersion, + currentNodeParameters: INodeParameters, + payload: IDataObject | string | undefined, + credentials?: INodeCredentials, + ): Promise { + const nodeType = this.getNodeType(nodeTypeAndVersion); + const method = this.getMethod('actionHandler', handler, nodeType); + const workflow = this.getWorkflow(nodeTypeAndVersion, currentNodeParameters, credentials); + const thisArgs = this.getThisArg(path, additionalData, workflow); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return method.call(thisArgs, payload); + } + private getMethod( type: 'resourceMapping', methodName: string, @@ -175,9 +195,14 @@ export class DynamicNodeParametersService { methodName: string, nodeType: INodeType, ): (this: ILoadOptionsFunctions) => Promise; + private getMethod( + type: 'actionHandler', + methodName: string, + nodeType: INodeType, + ): (this: ILoadOptionsFunctions, payload?: string) => Promise; private getMethod( - type: 'resourceMapping' | 'listSearch' | 'loadOptions', + type: 'resourceMapping' | 'listSearch' | 'loadOptions' | 'actionHandler', methodName: string, nodeType: INodeType, ) { diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts index 04365662b2..05c40402f1 100644 --- a/packages/editor-ui/src/Interface.ts +++ b/packages/editor-ui/src/Interface.ts @@ -1553,6 +1553,11 @@ export declare namespace DynamicNodeParameters { interface ResourceMapperFieldsRequest extends BaseRequest { methodName: string; } + + interface ActionResultRequest extends BaseRequest { + handler: string; + payload: IDataObject | string | undefined; + } } export interface EnvironmentVariable { diff --git a/packages/editor-ui/src/api/nodeTypes.ts b/packages/editor-ui/src/api/nodeTypes.ts index 300b25390d..f4d516aaef 100644 --- a/packages/editor-ui/src/api/nodeTypes.ts +++ b/packages/editor-ui/src/api/nodeTypes.ts @@ -5,6 +5,7 @@ import type { INodePropertyOptions, INodeTypeDescription, INodeTypeNameVersion, + NodeParameterValueType, ResourceMapperFields, } from 'n8n-workflow'; import axios from 'axios'; @@ -57,3 +58,15 @@ export async function getResourceMapperFields( sendData, ); } + +export async function getNodeParameterActionResult( + context: IRestApiContext, + sendData: DynamicNodeParameters.ActionResultRequest, +): Promise { + return await makeRestApiRequest( + context, + 'POST', + '/dynamic-node-parameters/action-result', + sendData, + ); +} diff --git a/packages/editor-ui/src/components/ButtonParameter/ButtonParameter.vue b/packages/editor-ui/src/components/ButtonParameter/ButtonParameter.vue new file mode 100644 index 0000000000..d7e05b0f5b --- /dev/null +++ b/packages/editor-ui/src/components/ButtonParameter/ButtonParameter.vue @@ -0,0 +1,281 @@ + + + + + diff --git a/packages/editor-ui/src/components/ButtonParameter/utils.ts b/packages/editor-ui/src/components/ButtonParameter/utils.ts new file mode 100644 index 0000000000..14d3ca4d78 --- /dev/null +++ b/packages/editor-ui/src/components/ButtonParameter/utils.ts @@ -0,0 +1,46 @@ +import type { Schema } from '@/Interface'; +import type { INodeExecutionData } from 'n8n-workflow'; +import { useWorkflowsStore } from '@/stores/workflows.store'; +import { useNDVStore } from '@/stores/ndv.store'; +import { useDataSchema } from '@/composables/useDataSchema'; +import { executionDataToJson } from '@/utils/nodeTypesUtils'; + +export function getParentNodes() { + const activeNode = useNDVStore().activeNode; + const { getCurrentWorkflow, getNodeByName } = useWorkflowsStore(); + const workflow = getCurrentWorkflow(); + + if (!activeNode || !workflow) return []; + + return workflow + .getParentNodesByDepth(activeNode?.name) + .filter(({ name }, i, nodes) => { + return name !== activeNode.name && nodes.findIndex((node) => node.name === name) === i; + }) + .map((n) => getNodeByName(n.name)) + .filter((n) => n !== null); +} + +export function getSchemas() { + const parentNodes = getParentNodes(); + const parentNodesNames = parentNodes.map((node) => node?.name); + const { getSchemaForExecutionData, getInputDataWithPinned } = useDataSchema(); + const parentNodesSchemas: Array<{ nodeName: string; schema: Schema }> = parentNodes + .map((node) => { + const inputData: INodeExecutionData[] = getInputDataWithPinned(node); + + return { + nodeName: node?.name || '', + schema: getSchemaForExecutionData(executionDataToJson(inputData), true), + }; + }) + .filter((node) => node.schema?.value.length > 0); + + const inputSchema = parentNodesSchemas.shift(); + + return { + parentNodesNames, + inputSchema, + parentNodesSchemas, + }; +} diff --git a/packages/editor-ui/src/components/CodeNodeEditor/CodeNodeEditor.vue b/packages/editor-ui/src/components/CodeNodeEditor/CodeNodeEditor.vue index fc141304f3..36a5d169e1 100644 --- a/packages/editor-ui/src/components/CodeNodeEditor/CodeNodeEditor.vue +++ b/packages/editor-ui/src/components/CodeNodeEditor/CodeNodeEditor.vue @@ -60,13 +60,12 @@ import jsParser from 'prettier/plugins/babel'; import * as estree from 'prettier/plugins/estree'; import { type Ref, computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'; -import { ASK_AI_EXPERIMENT, CODE_NODE_TYPE } from '@/constants'; +import { CODE_NODE_TYPE } from '@/constants'; import { codeNodeEditorEventBus } from '@/event-bus'; import { useRootStore } from '@/stores/root.store'; import { usePostHog } from '@/stores/posthog.store'; import { useMessage } from '@/composables/useMessage'; -import { useSettingsStore } from '@/stores/settings.store'; import AskAI from './AskAI/AskAI.vue'; import { readOnlyEditorExtensions, writableEditorExtensions } from './baseExtensions'; import { useCompleter } from './completer'; @@ -114,7 +113,6 @@ const { autocompletionExtension } = useCompleter(() => props.mode, editor); const { createLinter } = useLinter(() => props.mode, editor); const rootStore = useRootStore(); -const settingsStore = useSettingsStore(); const posthog = usePostHog(); const i18n = useI18n(); const telemetry = useTelemetry(); @@ -194,13 +192,7 @@ onBeforeUnmount(() => { }); const aiEnabled = computed(() => { - const isAiExperimentEnabled = [ASK_AI_EXPERIMENT.gpt3, ASK_AI_EXPERIMENT.gpt4].includes( - (posthog.getVariant(ASK_AI_EXPERIMENT.name) ?? '') as string, - ); - - return ( - isAiExperimentEnabled && settingsStore.settings.ai.enabled && props.language === 'javaScript' - ); + return posthog.isAiEnabled() && props.language === 'javaScript'; }); const placeholder = computed(() => { diff --git a/packages/editor-ui/src/components/JsEditor/JsEditor.vue b/packages/editor-ui/src/components/JsEditor/JsEditor.vue index 40692ab323..5a5f2cb411 100644 --- a/packages/editor-ui/src/components/JsEditor/JsEditor.vue +++ b/packages/editor-ui/src/components/JsEditor/JsEditor.vue @@ -1,5 +1,5 @@