feat(editor): Change default node names depending on node operation and resource (#16188)

This commit is contained in:
Charlie Kolb
2025-06-12 13:57:26 +02:00
committed by GitHub
parent ecd9a1e53e
commit 4e94488622
26 changed files with 646 additions and 197 deletions

View File

@@ -9,7 +9,11 @@ import {
STICKY_NODE_TYPE,
} from '@/constants';
import { useUIStore } from '@/stores/ui.store';
import type { AddedNodesAndConnections, ToggleNodeCreatorOptions } from '@/Interface';
import type {
AddedNodesAndConnections,
NodeTypeSelectedPayload,
ToggleNodeCreatorOptions,
} from '@/Interface';
import { useActions } from './NodeCreator/composables/useActions';
import { useThrottleFn } from '@vueuse/core';
import KeyboardShortcutTooltip from '@/components/KeyboardShortcutTooltip.vue';
@@ -83,8 +87,8 @@ function closeNodeCreator(hasAddedNodes = false) {
}
}
function nodeTypeSelected(nodeTypes: string[]) {
emit('addNodes', getAddedNodesAndConnections(nodeTypes.map((type) => ({ type }))));
function nodeTypeSelected(value: NodeTypeSelectedPayload[]) {
emit('addNodes', getAddedNodesAndConnections(value));
closeNodeCreator(true);
}

View File

@@ -5,6 +5,7 @@ import type {
IUpdateInformation,
ActionCreateElement,
NodeCreateElement,
NodeTypeSelectedPayload,
} from '@/Interface';
import {
HTTP_REQUEST_NODE_TYPE,
@@ -23,7 +24,7 @@ import { useViewStacks } from '../composables/useViewStacks';
import ItemsRenderer from '../Renderers/ItemsRenderer.vue';
import CategorizedItemsRenderer from '../Renderers/CategorizedItemsRenderer.vue';
import { type IDataObject } from 'n8n-workflow';
import type { IDataObject } from 'n8n-workflow';
import { useTelemetry } from '@/composables/useTelemetry';
import { useI18n } from '@n8n/i18n';
import { useNodeCreatorStore } from '@/stores/nodeCreator.store';
@@ -34,7 +35,7 @@ import CommunityNodeInfo from '../Panel/CommunityNodeInfo.vue';
import CommunityNodeFooter from '../Panel/CommunityNodeFooter.vue';
const emit = defineEmits<{
nodeTypeSelected: [value: [actionKey: string, nodeName: string] | [nodeName: string]];
nodeTypeSelected: [value: NodeTypeSelectedPayload[]];
}>();
const telemetry = useTelemetry();
const i18n = useI18n();
@@ -45,6 +46,7 @@ const { registerKeyHook } = useKeyboardNavigation();
const {
setAddedNodeActionParameters,
getActionData,
actionDataToNodeTypeSelectedPayload,
getPlaceholderTriggerActions,
parseCategoryActions,
actionsCategoryLocales,
@@ -166,17 +168,18 @@ function onSelected(actionCreateElement: INodeCreateElement) {
if (isPlaceholderTriggerAction && isTriggerRootView.value) {
const actionNode = actions.value[0]?.key;
if (actionNode) emit('nodeTypeSelected', [actionData.key as string, actionNode]);
if (actionNode) emit('nodeTypeSelected', [{ type: actionData.key }, { type: actionNode }]);
} else if (
actionData?.key === OPEN_AI_NODE_TYPE &&
(actionData?.value as IDataObject)?.resource === 'assistant' &&
(actionData?.value as IDataObject)?.operation === 'message'
) {
emit('nodeTypeSelected', [OPEN_AI_NODE_MESSAGE_ASSISTANT_TYPE]);
emit('nodeTypeSelected', [{ type: OPEN_AI_NODE_MESSAGE_ASSISTANT_TYPE }]);
} else if (isNodePreviewKey(actionData?.key)) {
return;
} else {
emit('nodeTypeSelected', [actionData.key as string]);
const payload = actionDataToNodeTypeSelectedPayload(actionData);
emit('nodeTypeSelected', [payload]);
}
if (telemetry) setAddedNodeActionParameters(actionData, telemetry, rootView.value);
@@ -217,7 +220,7 @@ function addHttpNode() {
},
} as IUpdateInformation;
emit('nodeTypeSelected', [HTTP_REQUEST_NODE_TYPE]);
emit('nodeTypeSelected', [{ type: HTTP_REQUEST_NODE_TYPE }]);
if (telemetry) setAddedNodeActionParameters(updateData);
const app_identifier = actions.value[0]?.key;

View File

@@ -6,6 +6,7 @@ import type {
INodeCreateElement,
NodeCreateElement,
NodeFilterType,
NodeTypeSelectedPayload,
} from '@/Interface';
import {
TRIGGER_NODE_CREATOR_VIEW,
@@ -47,14 +48,14 @@ export interface Props {
}
const emit = defineEmits<{
nodeTypeSelected: [nodeTypes: string[]];
nodeTypeSelected: [value: NodeTypeSelectedPayload[]];
}>();
const i18n = useI18n();
const { mergedNodes, actions, onSubcategorySelected } = useNodeCreatorStore();
const { pushViewStack, popViewStack, isAiSubcategoryView } = useViewStacks();
const { setAddedNodeActionParameters } = useActions();
const { setAddedNodeActionParameters, nodeCreateElementToNodeTypeSelectedPayload } = useActions();
const { registerKeyHook } = useKeyboardNavigation();
@@ -97,10 +98,6 @@ function getHumanInTheLoopActions(nodeActions: ActionTypeDescription[]) {
return nodeActions.filter((action) => action.actionKey === SEND_AND_WAIT_OPERATION);
}
function selectNodeType(nodeTypes: string[]) {
emit('nodeTypeSelected', nodeTypes);
}
function onSelected(item: INodeCreateElement) {
if (item.type === 'subcategory') {
const subcategoryKey = camelCase(item.properties.title);
@@ -152,9 +149,11 @@ function onSelected(item: INodeCreateElement) {
return;
}
const payload = nodeCreateElementToNodeTypeSelectedPayload(item);
// If there is only one action, use it
if (nodeActions.length === 1) {
selectNodeType([item.key]);
emit('nodeTypeSelected', [payload]);
setAddedNodeActionParameters({
name: nodeActions[0].defaults.name ?? item.properties.displayName,
key: item.key,
@@ -165,7 +164,7 @@ function onSelected(item: INodeCreateElement) {
// Only show actions if there are more than one or if the view is not an AI subcategory
if (nodeActions.length === 0 || activeViewStack.value.hideActions) {
selectNodeType([item.key]);
emit('nodeTypeSelected', [payload]);
return;
}
@@ -299,8 +298,8 @@ registerKeyHook('MainViewArrowLeft', {
:root-view="activeViewStack.rootView"
show-icon
show-request
@add-webhook-node="selectNodeType([WEBHOOK_NODE_TYPE])"
@add-http-node="selectNodeType([HTTP_REQUEST_NODE_TYPE])"
@add-webhook-node="emit('nodeTypeSelected', [{ type: WEBHOOK_NODE_TYPE }])"
@add-http-node="emit('nodeTypeSelected', [{ type: HTTP_REQUEST_NODE_TYPE }])"
/>
</template>
</ItemsRenderer>

View File

@@ -15,10 +15,11 @@ import { DRAG_EVENT_DATA_KEY } from '@/constants';
import { useAssistantStore } from '@/stores/assistant.store';
import N8nIconButton from '@n8n/design-system/components/N8nIconButton/IconButton.vue';
import { useBuilderStore } from '@/stores/builder.store';
import type { NodeTypeSelectedPayload } from '@/Interface';
export interface Props {
active?: boolean;
onNodeTypeSelected?: (nodeType: string[]) => void;
onNodeTypeSelected?: (value: NodeTypeSelectedPayload[]) => void;
}
const props = defineProps<Props>();
@@ -26,7 +27,7 @@ const { resetViewStacks } = useViewStacks();
const { registerKeyHook } = useKeyboardNavigation();
const emit = defineEmits<{
closeNodeCreator: [];
nodeTypeSelected: [value: string[]];
nodeTypeSelected: [value: NodeTypeSelectedPayload[]];
}>();
const uiStore = useUIStore();
const assistantStore = useAssistantStore();

View File

@@ -13,6 +13,8 @@ import type {
INodeCreateElement,
IUpdateInformation,
LabelCreateElement,
NodeCreateElement,
NodeTypeSelectedPayload,
} from '@/Interface';
import {
AGENT_NODE_TYPE,
@@ -153,7 +155,13 @@ export const useActions = () => {
return filteredActions;
}
function getActionData(actionItem: ActionTypeDescription): IUpdateInformation {
type ActionData = {
name: string;
key: string;
value: INodeParameters;
};
function getActionData(actionItem: ActionTypeDescription): ActionData {
const displayOptions = actionItem.displayOptions;
const displayConditions = Object.keys(displayOptions?.show ?? {}).reduce(
@@ -171,6 +179,51 @@ export const useActions = () => {
};
}
function actionDataToNodeTypeSelectedPayload(actionData: ActionData): NodeTypeSelectedPayload {
const result: NodeTypeSelectedPayload = {
type: actionData.key,
};
if (
typeof actionData.value.resource === 'string' ||
typeof actionData.value.operation === 'string'
) {
result.parameters = {};
if (typeof actionData.value.resource === 'string') {
result.parameters.resource = actionData.value.resource;
}
if (typeof actionData.value.operation === 'string') {
result.parameters.operation = actionData.value.operation;
}
}
return result;
}
function nodeCreateElementToNodeTypeSelectedPayload(
actionData: NodeCreateElement,
): NodeTypeSelectedPayload {
const result: NodeTypeSelectedPayload = {
type: actionData.key,
};
if (typeof actionData.resource === 'string' || typeof actionData.operation === 'string') {
result.parameters = {};
if (typeof actionData.resource === 'string') {
result.parameters.resource = actionData.resource;
}
if (typeof actionData.operation === 'string') {
result.parameters.operation = actionData.operation;
}
}
return result;
}
/**
* Checks if added nodes contain trigger followed by another node
* In this case, we should connect the trigger with the following node
@@ -305,7 +358,7 @@ export const useActions = () => {
return { nodes, connections };
}
// Hook into addNode action to set the last node parameters & track the action selected
// Hook into addNode action to set the last node parameters, adjust default name and track the action selected
function setAddedNodeActionParameters(
action: IUpdateInformation,
telemetry?: Telemetry,
@@ -323,7 +376,6 @@ export const useActions = () => {
});
},
);
return storeWatcher;
}
@@ -344,6 +396,8 @@ export const useActions = () => {
return {
actionsCategoryLocales,
actionDataToNodeTypeSelectedPayload,
nodeCreateElementToNodeTypeSelectedPayload,
getPlaceholderTriggerActions,
parseCategoryActions,
getAddedNodesAndConnections,

View File

@@ -53,6 +53,7 @@ import { importCurlEventBus, ndvEventBus } from '@/event-bus';
import { ProjectTypes } from '@/types/projects.types';
import { updateDynamicConnections } from '@/utils/nodeSettingsUtils';
import FreeAiCreditsCallout from '@/components/FreeAiCreditsCallout.vue';
import { useCanvasOperations } from '@/composables/useCanvasOperations';
const props = withDefaults(
defineProps<{
@@ -96,6 +97,7 @@ const telemetry = useTelemetry();
const nodeHelpers = useNodeHelpers();
const externalHooks = useExternalHooks();
const i18n = useI18n();
const canvasOperations = useCanvasOperations();
const nodeValid = ref(true);
const openPanel = ref<'params' | 'settings'>('params');
@@ -579,6 +581,15 @@ const valueChanged = (parameterData: IUpdateInformation) => {
}
}
if (NodeHelpers.isDefaultNodeName(_node.name, nodeType, node.value?.parameters ?? {})) {
const newName = NodeHelpers.makeNodeName(nodeParameters ?? {}, nodeType);
// Account for unique-ified nodes with `<name><digit>`
if (!_node.name.startsWith(newName)) {
// We need a timeout here to support events reacting to the valueChange based on node names
setTimeout(async () => await canvasOperations.renameNode(_node.name, newName));
}
}
for (const key of Object.keys(nodeParameters as object)) {
if (nodeParameters && nodeParameters[key] !== null && nodeParameters[key] !== undefined) {
setValue(`parameters.${key}`, nodeParameters[key] as string);