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 272213d5390c2..816b56b59ad40 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 c6a46aa3d4eed..bfd1cf651b875 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 82cf7da30aeb2..436c0b1b18d94 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 1788bac6e1877..f3b0f7e192ba3 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 04365662b28e4..05c40402f1c0a 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 300b25390d121..f4d516aaeff82 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 0000000000000..d7e05b0f5bf85 --- /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 0000000000000..14d3ca4d78334 --- /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 fc141304f324b..36a5d169e1ec9 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 40692ab32392c..5a5f2cb411878 100644 --- a/packages/editor-ui/src/components/JsEditor/JsEditor.vue +++ b/packages/editor-ui/src/components/JsEditor/JsEditor.vue @@ -1,5 +1,5 @@