mirror of
https://github.com/n8n-io/n8n.git
synced 2025-11-20 17:46:34 +00:00
feat(editor): show community tools
This commit is contained in:
parent
2661162238
commit
3dc317acc0
@ -5,10 +5,16 @@ export const validateNodeName = (name: string): string | undefined => {
|
||||
const regexScoped = /^@([a-z0-9]+(?:-[a-z0-9]+)*)\/n8n-nodes-([a-z0-9]+(?:-[a-z0-9]+)*)$/;
|
||||
// 2. Matches 'n8n-nodes-anything'
|
||||
const regexUnscoped = /^n8n-nodes-([a-z0-9]+(?:-[a-z0-9]+)*)$/;
|
||||
// 3. Matches 'anythingTool'
|
||||
const regexTool = /^.+\wTool$/;
|
||||
|
||||
if (!regexScoped.test(name) && !regexUnscoped.test(name)) {
|
||||
return "Must start with 'n8n-nodes-' or '@org/n8n-nodes-'. Examples: n8n-nodes-my-app, @mycompany/n8n-nodes-my-app";
|
||||
}
|
||||
|
||||
if (regexTool.test(name)) {
|
||||
return "Please remove the 'Tool' suffix from the node name. Examples: n8n-nodes-my-app, @mycompany/n8n-nodes-my-app";
|
||||
}
|
||||
return;
|
||||
};
|
||||
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import type { CommunityNodeType } from '@n8n/api-types';
|
||||
import { Logger, inProduction } from '@n8n/backend-common';
|
||||
import { inProduction, Logger } from '@n8n/backend-common';
|
||||
import { Service } from '@n8n/di';
|
||||
import { ensureError } from 'n8n-workflow';
|
||||
import { ensureError, NodeConnectionTypes } from 'n8n-workflow';
|
||||
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { getCommunityNodeTypes, StrapiCommunityNodeType } from './community-node-types-utils';
|
||||
import { CommunityPackagesConfig } from './community-packages.config';
|
||||
import { CommunityPackagesService } from './community-packages.service';
|
||||
@ -58,9 +59,36 @@ export class CommunityNodeTypesService {
|
||||
|
||||
this.communityNodeTypes = new Map(nodeTypes.map((nodeType) => [nodeType.name, nodeType]));
|
||||
|
||||
this.createAiTools();
|
||||
|
||||
this.lastUpdateTimestamp = Date.now();
|
||||
}
|
||||
|
||||
private createAiTools() {
|
||||
const usableAsTools = Array.from(this.communityNodeTypes.values()).filter(
|
||||
// TODO: remove "|| true"
|
||||
(nodeType) => nodeType.nodeDescription.usableAsTool || true,
|
||||
);
|
||||
for (const nodeType of usableAsTools) {
|
||||
const clonedNodeType = cloneDeep(nodeType);
|
||||
clonedNodeType.name += 'Tool';
|
||||
clonedNodeType.nodeDescription.name += 'Tool';
|
||||
clonedNodeType.nodeDescription.inputs = [];
|
||||
clonedNodeType.nodeDescription.outputs = [NodeConnectionTypes.AiTool];
|
||||
clonedNodeType.nodeDescription.displayName += ' Tool';
|
||||
clonedNodeType.nodeDescription.codex = {
|
||||
categories: ['AI'],
|
||||
subcategories: {
|
||||
AI: ['Tools'],
|
||||
Tools: clonedNodeType.nodeDescription.codex?.subcategories?.Tools ?? ['Other Tools'],
|
||||
},
|
||||
resources: clonedNodeType.nodeDescription.codex?.resources ?? {},
|
||||
};
|
||||
|
||||
this.communityNodeTypes.set(clonedNodeType.nodeDescription.name, clonedNodeType);
|
||||
}
|
||||
}
|
||||
|
||||
private resetCommunityNodeTypes() {
|
||||
this.communityNodeTypes = new Map();
|
||||
}
|
||||
|
||||
@ -131,6 +131,7 @@ import { useTemplatesStore } from '@/features/workflows/templates/templates.stor
|
||||
import { tryToParseNumber } from '@/app/utils/typesUtils';
|
||||
import { isValidNodeConnectionType } from '@/app/utils/typeGuards';
|
||||
import { useParentFolder } from '@/features/core/folders/composables/useParentFolder';
|
||||
import { removePreviewToken } from '@/features/shared/nodeCreator/nodeCreator.utils';
|
||||
|
||||
type AddNodeData = Partial<INodeUi> & {
|
||||
type: string;
|
||||
@ -1020,7 +1021,7 @@ export function useCanvasOperations() {
|
||||
node.name ??
|
||||
nodeHelpers.getDefaultNodeName(node) ??
|
||||
(nodeTypeDescription.defaults.name as string);
|
||||
const type = nodeTypeDescription.name;
|
||||
const type = node.type ?? nodeTypeDescription.name;
|
||||
const typeVersion = node.typeVersion;
|
||||
const position =
|
||||
options.forcePosition && node.position
|
||||
@ -1043,8 +1044,11 @@ export function useCanvasOperations() {
|
||||
};
|
||||
|
||||
resolveNodeName(nodeData);
|
||||
resolveNodeParameters(nodeData, nodeTypeDescription);
|
||||
resolveNodeWebhook(nodeData, nodeTypeDescription);
|
||||
|
||||
if (nodeTypesStore.getIsNodeInstalled(nodeData.type)) {
|
||||
resolveNodeParameters(nodeData, nodeTypeDescription);
|
||||
resolveNodeWebhook(nodeData, nodeTypeDescription);
|
||||
}
|
||||
|
||||
return nodeData;
|
||||
}
|
||||
@ -1741,12 +1745,10 @@ export function useCanvasOperations() {
|
||||
workflowHelpers.initState(data);
|
||||
data.nodes.forEach((node) => {
|
||||
const nodeTypeDescription = requireNodeTypeDescription(node.type, node.typeVersion);
|
||||
const isUnknownNode =
|
||||
!nodeTypesStore.getNodeType(node.type, node.typeVersion) &&
|
||||
!nodeTypesStore.communityNodeType(node.type)?.nodeDescription;
|
||||
const isInstalledNode = nodeTypesStore.getIsNodeInstalled(node.type);
|
||||
nodeHelpers.matchCredentials(node);
|
||||
// skip this step because nodeTypeDescription is missing for unknown nodes
|
||||
if (!isUnknownNode) {
|
||||
if (isInstalledNode) {
|
||||
resolveNodeParameters(node, nodeTypeDescription);
|
||||
resolveNodeWebhook(node, nodeTypeDescription);
|
||||
}
|
||||
@ -1757,14 +1759,19 @@ export function useCanvasOperations() {
|
||||
|
||||
const initializeUnknownNodes = (nodes: INode[]) => {
|
||||
nodes.forEach((node) => {
|
||||
const nodeTypeDescription = requireNodeTypeDescription(node.type, node.typeVersion);
|
||||
// we need to fetch installed node, so remove preview token
|
||||
const nodeTypeDescription = requireNodeTypeDescription(
|
||||
removePreviewToken(node.type),
|
||||
node.typeVersion,
|
||||
);
|
||||
nodeHelpers.matchCredentials(node);
|
||||
resolveNodeParameters(node, nodeTypeDescription);
|
||||
resolveNodeWebhook(node, nodeTypeDescription);
|
||||
const nodeIndex = workflowsStore.workflow.nodes.findIndex((n) => {
|
||||
return n.name === node.name;
|
||||
});
|
||||
workflowState.updateNodeAtIndex(nodeIndex, node);
|
||||
// make sure that preview node type is always removed
|
||||
workflowState.updateNodeAtIndex(nodeIndex, { ...node, type: removePreviewToken(node.type) });
|
||||
});
|
||||
};
|
||||
|
||||
@ -1897,8 +1904,21 @@ export function useCanvasOperations() {
|
||||
|
||||
// Create a workflow with the new nodes and connections that we can use
|
||||
// the rename method
|
||||
const tempWorkflow: Workflow = workflowsStore.createWorkflowObject(createNodes, newConnections);
|
||||
const tempWorkflow: Workflow = workflowsStore.createWorkflowObject(
|
||||
createNodes,
|
||||
newConnections,
|
||||
true,
|
||||
);
|
||||
|
||||
// createWorkflowObject strips out unknown parameters, bring them back for not installed nodes
|
||||
for (const nodeName of Object.keys(tempWorkflow.nodes)) {
|
||||
const node = tempWorkflow.nodes[nodeName];
|
||||
const isInstalledNode = nodeTypesStore.getIsNodeInstalled(node.type);
|
||||
if (!isInstalledNode) {
|
||||
const originalParameters = createNodes.find((n) => n.name === nodeName)?.parameters;
|
||||
node.parameters = originalParameters ?? node.parameters;
|
||||
}
|
||||
}
|
||||
// Rename all the nodes of which the name changed
|
||||
for (oldName in nodeNameTable) {
|
||||
if (oldName === nodeNameTable[oldName]) {
|
||||
|
||||
@ -54,7 +54,7 @@ export const useNodeTypesStore = defineStore(STORES.NODE_TYPES, () => {
|
||||
|
||||
const communityNodeType = computed(() => {
|
||||
return (nodeTypeName: string) => {
|
||||
return vettedCommunityNodeTypes.value.get(nodeTypeName);
|
||||
return vettedCommunityNodeTypes.value.get(removePreviewToken(nodeTypeName));
|
||||
};
|
||||
});
|
||||
|
||||
@ -146,13 +146,13 @@ export const useNodeTypesStore = defineStore(STORES.NODE_TYPES, () => {
|
||||
if (!workflow.nodes[node.name]) {
|
||||
return false;
|
||||
}
|
||||
const nodeType = getNodeType.value(nodeTypeName);
|
||||
const nodeType =
|
||||
getNodeType.value(nodeTypeName) ?? communityNodeType.value(nodeTypeName)?.nodeDescription;
|
||||
if (!nodeType) {
|
||||
return false;
|
||||
}
|
||||
const outputs = NodeHelpers.getNodeOutputs(workflow, node, nodeType);
|
||||
const outputTypes = NodeHelpers.getConnectionTypes(outputs);
|
||||
|
||||
return outputTypes
|
||||
? outputTypes.filter((output) => output !== NodeConnectionTypes.Main).length > 0
|
||||
: false;
|
||||
@ -338,7 +338,6 @@ export const useNodeTypesStore = defineStore(STORES.NODE_TYPES, () => {
|
||||
|
||||
const getNodeTypes = async () => {
|
||||
const nodeTypes = await nodeTypesApi.getNodeTypes(rootStore.baseUrl);
|
||||
|
||||
if (nodeTypes.length) {
|
||||
setNodeTypes(nodeTypes);
|
||||
}
|
||||
@ -420,8 +419,10 @@ export const useNodeTypesStore = defineStore(STORES.NODE_TYPES, () => {
|
||||
|
||||
const getIsNodeInstalled = computed(() => {
|
||||
return (nodeTypeName: string) => {
|
||||
const cleanedNodeTypeName = removePreviewToken(nodeTypeName);
|
||||
return (
|
||||
!!getNodeType.value(nodeTypeName) || !!communityNodeType.value(nodeTypeName)?.isInstalled
|
||||
!!getNodeType.value(cleanedNodeTypeName) ||
|
||||
!!communityNodeType.value(cleanedNodeTypeName)?.isInstalled
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
@ -499,7 +499,10 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
|
||||
nodeTypes: {},
|
||||
init: async (): Promise<void> => {},
|
||||
getByNameAndVersion: (nodeType: string, version?: number): INodeType | undefined => {
|
||||
const nodeTypeDescription = nodeTypesStore.getNodeType(nodeType, version);
|
||||
const nodeTypeDescription =
|
||||
nodeTypesStore.getNodeType(nodeType, version) ??
|
||||
nodeTypesStore.communityNodeType(nodeType)?.nodeDescription ??
|
||||
null;
|
||||
if (nodeTypeDescription === null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@ -128,7 +128,10 @@ const onUninstall = async () => {
|
||||
});
|
||||
loading.value = true;
|
||||
await communityNodesStore.uninstallPackage(props.activePackageName);
|
||||
await useNodeTypesStore().getNodeTypes();
|
||||
await Promise.all([
|
||||
useNodeTypesStore().getNodeTypes(),
|
||||
useNodeTypesStore().fetchCommunityNodePreviews(),
|
||||
]);
|
||||
toast.showMessage({
|
||||
title: i18n.baseText('settings.communityNodes.messages.uninstall.success.title'),
|
||||
type: 'success',
|
||||
|
||||
@ -68,11 +68,15 @@ export function useInstallNode() {
|
||||
}
|
||||
|
||||
// refresh store information about installed nodes
|
||||
await nodeTypesStore.getNodeTypes();
|
||||
await credentialsStore.fetchCredentialTypes(true);
|
||||
await Promise.all([
|
||||
nodeTypesStore.getNodeTypes(),
|
||||
nodeTypesStore.fetchCommunityNodePreviews(),
|
||||
credentialsStore.fetchCredentialTypes(true),
|
||||
]);
|
||||
await nextTick();
|
||||
|
||||
// update parameters and webhooks for freshly installed nodes
|
||||
// rename types from preview version to the actual version
|
||||
const nodeType = props.nodeType;
|
||||
if (nodeType && workflowsStore.workflow.nodes?.length) {
|
||||
const nodesToUpdate = workflowsStore.workflow.nodes.filter(
|
||||
|
||||
@ -148,9 +148,15 @@ function onSelected(item: INodeCreateElement) {
|
||||
}
|
||||
|
||||
if (item.type === 'node') {
|
||||
const payload = nodeCreateElementToNodeTypeSelectedPayload(item);
|
||||
let nodeActions = getFilteredActions(item, actions);
|
||||
const notInstalledCommunityNode =
|
||||
isCommunityPackageName(item.key) && !useNodeTypesStore().getIsNodeInstalled(item.key);
|
||||
|
||||
if (shouldShowCommunityNodeDetails(isCommunityPackageName(item.key), activeViewStack.value)) {
|
||||
if (
|
||||
shouldShowCommunityNodeDetails(isCommunityPackageName(item.key), activeViewStack.value) ||
|
||||
notInstalledCommunityNode
|
||||
) {
|
||||
if (!nodeActions.length) {
|
||||
nodeActions = getFilteredActions(item, communityNodesAndActions.value.actions);
|
||||
}
|
||||
@ -166,8 +172,6 @@ function onSelected(item: INodeCreateElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = nodeCreateElementToNodeTypeSelectedPayload(item);
|
||||
|
||||
// If there is only one action, use it
|
||||
if (nodeActions.length === 1) {
|
||||
emit('nodeTypeSelected', [payload]);
|
||||
|
||||
@ -1,166 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { useInstallNode } from '@/features/settings/communityNodes/composables/useInstallNode';
|
||||
import { useNodeCreatorStore } from '@/features/shared/nodeCreator/nodeCreator.store';
|
||||
import { useUsersStore } from '@/features/settings/users/users.store';
|
||||
import { getNodeIconSource } from '@/app/utils/nodeIcon';
|
||||
import { N8nButton, N8nIcon, N8nText, N8nTooltip } from '@n8n/design-system';
|
||||
import { i18n } from '@n8n/i18n';
|
||||
import OfficialIcon from 'virtual:icons/mdi/verified';
|
||||
import { computed } from 'vue';
|
||||
import { useViewStacks } from '../../composables/useViewStacks';
|
||||
import { prepareCommunityNodeDetailsViewStack, removePreviewToken } from '../../nodeCreator.utils';
|
||||
import NodeIcon from '@/app/components/NodeIcon.vue';
|
||||
|
||||
const {
|
||||
activeViewStack,
|
||||
pushViewStack,
|
||||
popViewStack,
|
||||
getAllNodeCreateElements,
|
||||
updateCurrentViewStack,
|
||||
} = useViewStacks();
|
||||
|
||||
const { communityNodeDetails } = activeViewStack;
|
||||
|
||||
const nodeCreatorStore = useNodeCreatorStore();
|
||||
const { installNode, loading } = useInstallNode();
|
||||
|
||||
const isOwner = computed(() => useUsersStore().isInstanceOwner);
|
||||
|
||||
const updateViewStack = (key: string) => {
|
||||
const installedNodeKey = removePreviewToken(key);
|
||||
const installedNode = getAllNodeCreateElements().find((node) => node.key === installedNodeKey);
|
||||
|
||||
if (installedNode) {
|
||||
const nodeActions = nodeCreatorStore.actions?.[installedNode.key] || [];
|
||||
|
||||
popViewStack();
|
||||
|
||||
updateCurrentViewStack({ searchItems: nodeCreatorStore.mergedNodes });
|
||||
|
||||
const viewStack = prepareCommunityNodeDetailsViewStack(
|
||||
installedNode,
|
||||
getNodeIconSource(installedNode.properties),
|
||||
activeViewStack.rootView,
|
||||
nodeActions,
|
||||
);
|
||||
|
||||
pushViewStack(viewStack, {
|
||||
transitionDirection: 'none',
|
||||
});
|
||||
} else {
|
||||
const viewStack = { ...activeViewStack };
|
||||
viewStack.communityNodeDetails!.installed = true;
|
||||
|
||||
pushViewStack(activeViewStack, { resetStacks: true });
|
||||
}
|
||||
};
|
||||
|
||||
const updateStoresAndViewStack = (key: string) => {
|
||||
updateViewStack(key);
|
||||
nodeCreatorStore.removeNodeFromMergedNodes(key);
|
||||
};
|
||||
|
||||
const onInstall = async () => {
|
||||
if (isOwner.value && activeViewStack.communityNodeDetails && !communityNodeDetails?.installed) {
|
||||
const { key, packageName } = activeViewStack.communityNodeDetails;
|
||||
const result = await installNode({ type: 'verified', packageName, nodeType: key });
|
||||
if (result.success) {
|
||||
updateStoresAndViewStack(key);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="communityNodeDetails" :class="$style.container">
|
||||
<div :class="$style.header">
|
||||
<div :class="$style.title">
|
||||
<NodeIcon
|
||||
v-if="communityNodeDetails.nodeIcon"
|
||||
:class="$style.nodeIcon"
|
||||
:icon-source="communityNodeDetails.nodeIcon"
|
||||
:circle="false"
|
||||
:show-tooltip="false"
|
||||
/>
|
||||
<span>{{ communityNodeDetails.title }}</span>
|
||||
<N8nTooltip v-if="communityNodeDetails.official" placement="bottom" :show-after="500">
|
||||
<template #content>
|
||||
{{
|
||||
i18n.baseText('generic.officialNode.tooltip', {
|
||||
interpolate: {
|
||||
author: communityNodeDetails.companyName ?? communityNodeDetails.title,
|
||||
},
|
||||
})
|
||||
}}
|
||||
</template>
|
||||
<OfficialIcon :class="$style.officialIcon" />
|
||||
</N8nTooltip>
|
||||
</div>
|
||||
<div>
|
||||
<div v-if="communityNodeDetails.installed" :class="$style.installed">
|
||||
<N8nIcon v-if="!communityNodeDetails.official" :class="$style.installedIcon" icon="box" />
|
||||
<N8nText color="text-light" size="small" bold>
|
||||
{{ i18n.baseText('communityNodeDetails.installed') }}
|
||||
</N8nText>
|
||||
</div>
|
||||
|
||||
<N8nButton
|
||||
v-if="isOwner && !communityNodeDetails.installed"
|
||||
:loading="loading"
|
||||
:disabled="loading"
|
||||
:label="i18n.baseText('communityNodeDetails.install')"
|
||||
size="small"
|
||||
data-test-id="install-community-node-button"
|
||||
@click="onInstall"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" module>
|
||||
.container {
|
||||
width: 100%;
|
||||
padding: var(--spacing--sm);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-bottom: var(--spacing--xs);
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
gap: var(--spacing--2xs);
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--color--text);
|
||||
font-size: var(--font-size--xl);
|
||||
font-weight: var(--font-weight--bold);
|
||||
}
|
||||
.nodeIcon {
|
||||
--node--icon--size: 36px;
|
||||
margin-right: var(--spacing--sm);
|
||||
}
|
||||
|
||||
.installedIcon {
|
||||
margin-right: var(--spacing--3xs);
|
||||
color: var(--color--text);
|
||||
font-size: var(--font-size--2xs);
|
||||
}
|
||||
|
||||
.officialIcon {
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
margin-left: var(--spacing--4xs);
|
||||
color: var(--color--text);
|
||||
width: 14px;
|
||||
}
|
||||
|
||||
.installed {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: var(--spacing--xs);
|
||||
}
|
||||
</style>
|
||||
@ -42,7 +42,11 @@ import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
||||
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
|
||||
import { useExternalHooks } from '@/app/composables/useExternalHooks';
|
||||
|
||||
import { sortNodeCreateElements, transformNodeType } from '../nodeCreator.utils';
|
||||
import {
|
||||
removePreviewToken,
|
||||
sortNodeCreateElements,
|
||||
transformNodeType,
|
||||
} from '../nodeCreator.utils';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import { PUSH_NODES_OFFSET } from '@/app/utils/nodeViewUtils';
|
||||
import { useCanvasStore } from '@/app/stores/canvas.store';
|
||||
@ -215,7 +219,7 @@ export const useActions = () => {
|
||||
actionData: NodeCreateElement,
|
||||
): NodeTypeSelectedPayload {
|
||||
const result: NodeTypeSelectedPayload = {
|
||||
type: actionData.key,
|
||||
type: removePreviewToken(actionData.key),
|
||||
};
|
||||
|
||||
if (typeof actionData.resource === 'string' || typeof actionData.operation === 'string') {
|
||||
|
||||
@ -1042,6 +1042,10 @@ export function getNodeOutputs(
|
||||
): Array<NodeConnectionType | INodeOutputConfiguration> {
|
||||
let outputs: Array<NodeConnectionType | INodeOutputConfiguration> = [];
|
||||
|
||||
if (!nodeTypeData) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (Array.isArray(nodeTypeData.outputs)) {
|
||||
outputs = nodeTypeData.outputs;
|
||||
} else {
|
||||
@ -1567,6 +1571,9 @@ export function isTriggerNode(nodeTypeData: INodeTypeDescription) {
|
||||
}
|
||||
|
||||
export function isExecutable(workflow: Workflow, node: INode, nodeTypeData: INodeTypeDescription) {
|
||||
if (!nodeTypeData) {
|
||||
return false;
|
||||
}
|
||||
const outputs = getNodeOutputs(workflow, node, nodeTypeData);
|
||||
const outputNames = getConnectionTypes(outputs);
|
||||
return (
|
||||
|
||||
@ -26,6 +26,7 @@ import type {
|
||||
WorkflowExecuteMode,
|
||||
ProxyInput,
|
||||
INode,
|
||||
INodeType,
|
||||
} from './interfaces';
|
||||
import * as NodeHelpers from './node-helpers';
|
||||
import { createResultError, createResultOk } from './result';
|
||||
@ -165,7 +166,16 @@ export class WorkflowDataProxy {
|
||||
}
|
||||
|
||||
private buildAgentToolInfo(node: INode) {
|
||||
const nodeType = this.workflow.nodeTypes.getByNameAndVersion(node.type, node.typeVersion);
|
||||
const nodeType: INodeType | undefined = this.workflow.nodeTypes.getByNameAndVersion(
|
||||
node.type,
|
||||
node.typeVersion,
|
||||
);
|
||||
if (!nodeType) {
|
||||
return {
|
||||
name: node.name,
|
||||
type: node.type,
|
||||
};
|
||||
}
|
||||
const type = nodeType.description.displayName;
|
||||
const params = NodeHelpers.getNodeParameters(
|
||||
nodeType.description.properties,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user