mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 09:36:44 +00:00
feat: Human in the loop section (#12883)
Co-authored-by: Dana <152518854+dana-gill@users.noreply.github.com> Co-authored-by: Jonathan Bennetts <jonathan.bennetts@gmail.com>
This commit is contained in:
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<string>(() => {
|
||||
if (isSendAndWaitCategory.value) {
|
||||
return '';
|
||||
}
|
||||
if (
|
||||
props.subcategory === DEFAULT_SUBCATEGORY &&
|
||||
!props.nodeType.name.startsWith(CREDENTIAL_ONLY_NODE_PREFIX)
|
||||
@@ -56,7 +60,8 @@ const description = computed<string>(() => {
|
||||
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',
|
||||
);
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { camelCase } from 'lodash-es';
|
||||
import { computed } from 'vue';
|
||||
import type { INodeCreateElement, NodeCreateElement, NodeFilterType } from '@/Interface';
|
||||
import type {
|
||||
ActionTypeDescription,
|
||||
INodeCreateElement,
|
||||
NodeCreateElement,
|
||||
NodeFilterType,
|
||||
} from '@/Interface';
|
||||
import {
|
||||
TRIGGER_NODE_CREATOR_VIEW,
|
||||
HTTP_REQUEST_NODE_TYPE,
|
||||
@@ -9,6 +14,7 @@ import {
|
||||
REGULAR_NODE_CREATOR_VIEW,
|
||||
AI_NODE_CREATOR_VIEW,
|
||||
AI_OTHERS_NODE_CREATOR_VIEW,
|
||||
HITL_SUBCATEGORY,
|
||||
} from '@/constants';
|
||||
|
||||
import type { BaseTextKey } from '@/plugins/i18n';
|
||||
@@ -26,7 +32,7 @@ import { useI18n } from '@/composables/useI18n';
|
||||
import { getNodeIcon, getNodeIconColor, getNodeIconUrl } from '@/utils/nodeTypesUtils';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { useActions } from '../composables/useActions';
|
||||
import type { INodeParameters } from 'n8n-workflow';
|
||||
import { SEND_AND_WAIT_OPERATION, type INodeParameters } from 'n8n-workflow';
|
||||
|
||||
export interface Props {
|
||||
rootView: 'trigger' | 'action';
|
||||
@@ -51,12 +57,19 @@ const globalSearchItemsDiff = computed(() => useViewStacks().globalSearchItemsDi
|
||||
|
||||
function getFilteredActions(node: NodeCreateElement) {
|
||||
const nodeActions = actions?.[node.key] || [];
|
||||
if (activeViewStack.value.subcategory === HITL_SUBCATEGORY) {
|
||||
return getHumanInTheLoopActions(nodeActions);
|
||||
}
|
||||
if (activeViewStack.value.actionsFilter) {
|
||||
return activeViewStack.value.actionsFilter(nodeActions);
|
||||
}
|
||||
return nodeActions;
|
||||
}
|
||||
|
||||
function getHumanInTheLoopActions(nodeActions: ActionTypeDescription[]) {
|
||||
return nodeActions.filter((action) => action.actionKey === SEND_AND_WAIT_OPERATION);
|
||||
}
|
||||
|
||||
function selectNodeType(nodeTypes: string[]) {
|
||||
emit('nodeTypeSelected', nodeTypes);
|
||||
}
|
||||
|
||||
@@ -136,7 +136,7 @@ describe('NodesListPanel', () => {
|
||||
|
||||
await nextTick();
|
||||
expect(screen.getByText('What happens next?')).toBeInTheDocument();
|
||||
expect(screen.queryAllByTestId('item-iterator-item')).toHaveLength(5);
|
||||
expect(screen.queryAllByTestId('item-iterator-item')).toHaveLength(6);
|
||||
|
||||
screen.getByText('Action in an app').click();
|
||||
await nextTick();
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
AI_TRANSFORM_NODE_TYPE,
|
||||
CORE_NODES_CATEGORY,
|
||||
DEFAULT_SUBCATEGORY,
|
||||
HUMAN_IN_THE_LOOP_CATEGORY,
|
||||
} from '@/constants';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
@@ -23,6 +24,7 @@ import { sortBy } from 'lodash-es';
|
||||
import * as changeCase from 'change-case';
|
||||
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
import { SEND_AND_WAIT_OPERATION } from 'n8n-workflow';
|
||||
|
||||
export function transformNodeType(
|
||||
node: SimplifiedNodeType,
|
||||
@@ -46,7 +48,11 @@ export function transformNodeType(
|
||||
}
|
||||
|
||||
export function subcategorizeItems(items: SimplifiedNodeType[]) {
|
||||
const WHITE_LISTED_SUBCATEGORIES = [CORE_NODES_CATEGORY, AI_SUBCATEGORY];
|
||||
const WHITE_LISTED_SUBCATEGORIES = [
|
||||
CORE_NODES_CATEGORY,
|
||||
AI_SUBCATEGORY,
|
||||
HUMAN_IN_THE_LOOP_CATEGORY,
|
||||
];
|
||||
return items.reduce((acc: SubcategorizedNodeTypes, item) => {
|
||||
// Only some subcategories are allowed
|
||||
let subcategories: string[] = [DEFAULT_SUBCATEGORY];
|
||||
@@ -174,13 +180,21 @@ export function groupItemsInSections(
|
||||
|
||||
return 0;
|
||||
});
|
||||
if (result.length <= 1) {
|
||||
if (!shouldRenderSectionSubtitle(result)) {
|
||||
return items;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
const shouldRenderSectionSubtitle = (sections: SectionCreateElement[]) => {
|
||||
if (!sections.length) return false;
|
||||
if (sections.length > 1) return true;
|
||||
if (sections[0].key === SEND_AND_WAIT_OPERATION) return true;
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export const formatTriggerActionName = (actionPropertyName: string) => {
|
||||
let name = actionPropertyName;
|
||||
if (actionPropertyName.includes('.')) {
|
||||
|
||||
@@ -49,12 +49,14 @@ import {
|
||||
SPLIT_IN_BATCHES_NODE_TYPE,
|
||||
HTTP_REQUEST_NODE_TYPE,
|
||||
HELPERS_SUBCATEGORY,
|
||||
HITL_SUBCATEGORY,
|
||||
RSS_READ_NODE_TYPE,
|
||||
EMAIL_SEND_NODE_TYPE,
|
||||
EDIT_IMAGE_NODE_TYPE,
|
||||
COMPRESSION_NODE_TYPE,
|
||||
AI_CODE_TOOL_LANGCHAIN_NODE_TYPE,
|
||||
AI_WORKFLOW_TOOL_LANGCHAIN_NODE_TYPE,
|
||||
HUMAN_IN_THE_LOOP_CATEGORY,
|
||||
} from '@/constants';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||
@@ -442,6 +444,12 @@ export function RegularView(nodes: SimplifiedNodeType[]) {
|
||||
AI_TRANSFORM_NODE_TYPE,
|
||||
];
|
||||
|
||||
const getSendAndWaitNodes = (nodes: SimplifiedNodeType[]) => {
|
||||
return (nodes ?? [])
|
||||
.filter((node) => node.codex?.categories?.includes(HUMAN_IN_THE_LOOP_CATEGORY))
|
||||
.map((node) => node.name);
|
||||
};
|
||||
|
||||
const view: NodeView = {
|
||||
value: REGULAR_NODE_CREATOR_VIEW,
|
||||
title: i18n.baseText('nodeCreator.triggerHelperPanel.whatHappensNext'),
|
||||
@@ -532,22 +540,39 @@ export function RegularView(nodes: SimplifiedNodeType[]) {
|
||||
],
|
||||
},
|
||||
},
|
||||
// To add node to this subcategory:
|
||||
// - add "HITL" to the "categories" property of the node's codex
|
||||
// - add "HITL": ["Human in the Loop"] to the "subcategories" property of the node's codex
|
||||
// node has to have the "sendAndWait" operation, if a new operation needs to be included here:
|
||||
// - update getHumanInTheLoopActions in packages/editor-ui/src/components/Node/NodeCreator/Modes/NodesMode.vue
|
||||
{
|
||||
type: 'subcategory',
|
||||
key: HITL_SUBCATEGORY,
|
||||
category: HUMAN_IN_THE_LOOP_CATEGORY,
|
||||
properties: {
|
||||
title: HITL_SUBCATEGORY,
|
||||
icon: 'user-check',
|
||||
sections: [
|
||||
{
|
||||
key: 'sendAndWait',
|
||||
title: i18n.baseText('nodeCreator.sectionNames.sendAndWait'),
|
||||
items: getSendAndWaitNodes(nodes),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const hasAINodes = (nodes ?? []).some((node) => node.codex?.categories?.includes(AI_SUBCATEGORY));
|
||||
if (hasAINodes)
|
||||
view.items.push({
|
||||
view.items.unshift({
|
||||
key: AI_NODE_CREATOR_VIEW,
|
||||
type: 'view',
|
||||
properties: {
|
||||
title: i18n.baseText('nodeCreator.aiPanel.langchainAiNodes'),
|
||||
icon: 'robot',
|
||||
description: i18n.baseText('nodeCreator.aiPanel.nodesForAi'),
|
||||
tag: {
|
||||
type: 'success',
|
||||
text: i18n.baseText('nodeCreator.aiPanel.newTag'),
|
||||
},
|
||||
borderless: true,
|
||||
},
|
||||
} as NodeViewItem);
|
||||
|
||||
@@ -262,6 +262,7 @@ export const NODE_CREATOR_OPEN_SOURCES: Record<
|
||||
'': '',
|
||||
};
|
||||
export const CORE_NODES_CATEGORY = 'Core Nodes';
|
||||
export const HUMAN_IN_THE_LOOP_CATEGORY = 'HITL';
|
||||
export const CUSTOM_NODES_CATEGORY = 'Custom Nodes';
|
||||
export const DEFAULT_SUBCATEGORY = '*';
|
||||
export const AI_OTHERS_NODE_CREATOR_VIEW = 'AI Other';
|
||||
@@ -274,6 +275,7 @@ export const FILES_SUBCATEGORY = 'Files';
|
||||
export const FLOWS_CONTROL_SUBCATEGORY = 'Flow';
|
||||
export const AI_SUBCATEGORY = 'AI';
|
||||
export const HELPERS_SUBCATEGORY = 'Helpers';
|
||||
export const HITL_SUBCATEGORY = 'Human in the Loop';
|
||||
export const AI_CATEGORY_AGENTS = 'Agents';
|
||||
export const AI_CATEGORY_CHAINS = 'Chains';
|
||||
export const AI_CATEGORY_LANGUAGE_MODELS = 'Language Models';
|
||||
|
||||
@@ -1115,6 +1115,7 @@
|
||||
"nodeCreator.subcategoryDescriptions.tools": "Utility components providing various functionalities.",
|
||||
"nodeCreator.subcategoryDescriptions.vectorStores": "Handles storage and retrieval of vector representations.",
|
||||
"nodeCreator.subcategoryDescriptions.miscellaneous": "Other AI related nodes.",
|
||||
"nodeCreator.subcategoryDescriptions.humanInTheLoop": "Wait for approval or human input before continuing",
|
||||
"nodeCreator.subcategoryInfos.languageModels": "Chat models are designed for interactive conversations and follow instructions well, while text completion models focus on generating continuations of a given text input",
|
||||
"nodeCreator.subcategoryInfos.memory": "Memory allows an AI model to remember and reference past interactions with it",
|
||||
"nodeCreator.subcategoryInfos.vectorStores": "Vector stores allow an AI model to reference relevant pieces of documents, useful for question answering and document search",
|
||||
@@ -1137,8 +1138,10 @@
|
||||
"nodeCreator.subcategoryNames.tools": "Tools",
|
||||
"nodeCreator.subcategoryNames.vectorStores": "Vector Stores",
|
||||
"nodeCreator.subcategoryNames.miscellaneous": "Miscellaneous",
|
||||
"nodeCreator.subcategoryNames.humanInTheLoop": "Human in the loop",
|
||||
"nodeCreator.sectionNames.popular": "Popular",
|
||||
"nodeCreator.sectionNames.other": "Other",
|
||||
"nodeCreator.sectionNames.sendAndWait": "Send and wait for response",
|
||||
"nodeCreator.sectionNames.transform.combine": "Combine items",
|
||||
"nodeCreator.sectionNames.transform.addOrRemove": "Add or remove items",
|
||||
"nodeCreator.sectionNames.transform.convert": "Convert data",
|
||||
|
||||
@@ -82,6 +82,7 @@ import {
|
||||
faHandScissors,
|
||||
faHandPointLeft,
|
||||
faHandshake,
|
||||
faUserCheck,
|
||||
faHashtag,
|
||||
faHdd,
|
||||
faHistory,
|
||||
@@ -258,6 +259,7 @@ export const FontAwesomePlugin: Plugin = {
|
||||
addIcon(faHandshake);
|
||||
addIcon(faHandPointLeft);
|
||||
addIcon(faHashtag);
|
||||
addIcon(faUserCheck);
|
||||
addIcon(faHdd);
|
||||
addIcon(faHistory);
|
||||
addIcon(faHome);
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
"node": "n8n-nodes-base.emailSend",
|
||||
"nodeVersion": "1.0",
|
||||
"codexVersion": "1.0",
|
||||
"categories": ["Communication", "Core Nodes"],
|
||||
"categories": ["Communication", "HITL", "Core Nodes"],
|
||||
"subcategories": {
|
||||
"HITL": ["Human in the Loop"]
|
||||
},
|
||||
"resources": {
|
||||
"credentialDocumentation": [
|
||||
{
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
"node": "n8n-nodes-base.googleChat",
|
||||
"nodeVersion": "1.0",
|
||||
"codexVersion": "1.0",
|
||||
"categories": ["Communication", "HILT"],
|
||||
"categories": ["Communication", "HITL"],
|
||||
"subcategories": {
|
||||
"HILT": ["Human in the Loop"]
|
||||
"HITL": ["Human in the Loop"]
|
||||
},
|
||||
"resources": {
|
||||
"credentialDocumentation": [
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
"node": "n8n-nodes-base.gmail",
|
||||
"nodeVersion": "1.0",
|
||||
"codexVersion": "1.0",
|
||||
"categories": ["Communication"],
|
||||
"categories": ["Communication", "HITL"],
|
||||
"subcategories": {
|
||||
"HITL": ["Human in the Loop"]
|
||||
},
|
||||
"resources": {
|
||||
"credentialDocumentation": [
|
||||
{
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
"node": "n8n-nodes-base.microsoftOutlook",
|
||||
"nodeVersion": "1.0",
|
||||
"codexVersion": "1.0",
|
||||
"categories": ["Communication"],
|
||||
"categories": ["Communication", "HITL"],
|
||||
"subcategories": {
|
||||
"HITL": ["Human in the Loop"]
|
||||
},
|
||||
"resources": {
|
||||
"credentialDocumentation": [
|
||||
{
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
"node": "n8n-nodes-base.slack",
|
||||
"nodeVersion": "1.0",
|
||||
"codexVersion": "1.0",
|
||||
"categories": ["Communication"],
|
||||
"categories": ["Communication", "HITL"],
|
||||
"subcategories": {
|
||||
"HITL": ["Human in the Loop"]
|
||||
},
|
||||
"alias": ["human", "form", "wait"],
|
||||
"resources": {
|
||||
"credentialDocumentation": [
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
"node": "n8n-nodes-base.telegram",
|
||||
"nodeVersion": "1.0",
|
||||
"codexVersion": "1.0",
|
||||
"categories": ["Communication"],
|
||||
"categories": ["Communication", "HITL"],
|
||||
"subcategories": {
|
||||
"HITL": ["Human in the Loop"]
|
||||
},
|
||||
"resources": {
|
||||
"credentialDocumentation": [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user