feat(editor): Node Creator AI nodes improvements (#9484)

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>
This commit is contained in:
oleg 2024-05-30 16:53:33 +02:00 committed by GitHub
parent e68a3fd6ce
commit be4f54de15
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
62 changed files with 661 additions and 204 deletions

View file

@ -35,7 +35,7 @@ export const INSTANCE_MEMBERS = [
];
export const MANUAL_TRIGGER_NODE_NAME = 'Manual Trigger';
export const MANUAL_TRIGGER_NODE_DISPLAY_NAME = 'When clicking "Test workflow"';
export const MANUAL_TRIGGER_NODE_DISPLAY_NAME = 'When clicking Test workflow';
export const MANUAL_CHAT_TRIGGER_NODE_NAME = 'Chat Trigger';
export const SCHEDULE_TRIGGER_NODE_NAME = 'Schedule Trigger';
export const CODE_NODE_NAME = 'Code';

View file

@ -510,7 +510,7 @@ describe('Execution', () => {
cy.wait('@workflowRun').then((interception) => {
expect(interception.request.body).to.have.property('runData').that.is.an('object');
const expectedKeys = ['When clicking "Test workflow"', 'fetch 5 random users'];
const expectedKeys = ['When clicking Test workflow', 'fetch 5 random users'];
expect(Object.keys(interception.request.body.runData)).to.have.lengthOf(expectedKeys.length);
expect(interception.request.body.runData).to.include.all.keys(expectedKeys);

View file

@ -35,7 +35,7 @@ describe('Node Creator', () => {
nodeCreatorFeature.actions.openNodeCreator();
nodeCreatorFeature.getters.searchBar().find('input').type('manual');
nodeCreatorFeature.getters.creatorItem().should('have.length', 2);
nodeCreatorFeature.getters.creatorItem().should('have.length', 1);
nodeCreatorFeature.getters.searchBar().find('input').clear().type('manual123');
nodeCreatorFeature.getters.creatorItem().should('have.length', 0);
nodeCreatorFeature.getters
@ -159,7 +159,7 @@ describe('Node Creator', () => {
it('should have "Triggers" section collapsed when opening actions view from Regular root view', () => {
nodeCreatorFeature.actions.openNodeCreator();
nodeCreatorFeature.getters.getCreatorItem('Manually').click();
nodeCreatorFeature.getters.getCreatorItem('Trigger manually').click();
nodeCreatorFeature.actions.openNodeCreator();
nodeCreatorFeature.getters.searchBar().find('input').clear().type('n8n');
@ -308,7 +308,7 @@ describe('Node Creator', () => {
nodeCreatorFeature.getters.getCategoryItem('Actions').click();
nodeCreatorFeature.getters.getCreatorItem('Create a credential').click();
NDVModal.actions.close();
WorkflowPage.actions.deleteNode('When clicking "Test workflow"');
WorkflowPage.actions.deleteNode('When clicking Test workflow');
WorkflowPage.getters.canvasNodePlusEndpointByName('n8n').click();
nodeCreatorFeature.getters.searchBar().find('input').clear().type('n8n');
nodeCreatorFeature.getters.getCreatorItem('n8n').click();

View file

@ -38,7 +38,7 @@ describe('Editors', () => {
});
ndv.actions.close();
workflowPage.actions.openNode('When clicking "Test workflow"');
workflowPage.actions.openNode('When clicking Test workflow');
ndv.actions.setPinnedData([{ table: 'test_table' }]);
ndv.actions.close();

View file

@ -652,7 +652,7 @@ describe('NDV', () => {
ndv.getters.backToCanvas().click();
workflowPage.actions.executeWorkflow();
// Manual tigger node should show success indicator
workflowPage.actions.openNode('When clicking "Test workflow"');
workflowPage.actions.openNode('When clicking Test workflow');
ndv.getters.nodeRunSuccessIndicator().should('exist');
// Code node should show error
ndv.getters.backToCanvas().click();

View file

@ -4,7 +4,7 @@
{
"parameters": {},
"id": "d0eda550-2526-42a1-aa19-dee411c8acf9",
"name": "When clicking \"Test workflow\"",
"name": "When clicking Test workflow",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
@ -91,7 +91,7 @@
],
"pinData": {},
"connections": {
"When clicking \"Test workflow\"": {
"When clicking Test workflow": {
"main": [
[
{

View file

@ -4,7 +4,7 @@
{
"parameters": {},
"id": "369fe424-dd3b-4399-9de3-50bd4ce1f75b",
"name": "When clicking \"Test workflow\"",
"name": "When clicking Test workflow",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
@ -570,7 +570,7 @@
],
"pinData": {},
"connections": {
"When clicking \"Test workflow\"": {
"When clicking Test workflow": {
"main": [
[
{

View file

@ -4,7 +4,7 @@
{
"parameters": {},
"id": "5ae8991f-08a2-4b27-b61c-85e3b8a83693",
"name": "When clicking \"Test workflow\"",
"name": "When clicking Test workflow",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
@ -78,14 +78,14 @@
}
}
],
"When clicking \"Test workflow\"": [
"When clicking Test workflow": [
{
"json": {}
}
]
},
"connections": {
"When clicking \"Test workflow\"": {
"When clicking Test workflow": {
"main": [
[
{

View file

@ -4,7 +4,7 @@
{
"parameters": {},
"id": "b3f0815d-b733-413f-ab3f-74e48277bd3a",
"name": "When clicking \"Test workflow\"",
"name": "When clicking Test workflow",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
@ -160,7 +160,7 @@
]
},
"connections": {
"When clicking \"Test workflow\"": {
"When clicking Test workflow": {
"main": [
[
{

View file

@ -4,7 +4,7 @@
{
"parameters": {},
"id": "46770685-44d1-4aad-9107-1d790cf26b50",
"name": "When clicking \"Test workflow\"",
"name": "When clicking Test workflow",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
@ -74,7 +74,7 @@
}
],
"pinData": {
"When clicking \"Test workflow\"": [
"When clicking Test workflow": [
{
"json": {
"id": "654cfa05fa51480dcb543b1a",
@ -599,7 +599,7 @@
]
},
"connections": {
"When clicking \"Test workflow\"": {
"When clicking Test workflow": {
"main": [
[
{

View file

@ -42,7 +42,7 @@
{
"parameters": {},
"id": "551313bb-1e01-4133-9956-e6f09968f2ce",
"name": "When clicking \"Test workflow\"",
"name": "When clicking Test workflow",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
@ -92,7 +92,7 @@
}
],
"connections": {
"When clicking \"Test workflow\"": {
"When clicking Test workflow": {
"main": [
[
{
@ -191,7 +191,7 @@
{
"parameters": {},
"id": "551313bb-1e01-4133-9956-e6f09968f2ce",
"name": "When clicking \"Test workflow\"",
"name": "When clicking Test workflow",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
@ -241,7 +241,7 @@
}
],
"connections": {
"When clicking \"Test workflow\"": {
"When clicking Test workflow": {
"main": [
[
{
@ -374,7 +374,7 @@
{
"parameters": {},
"id": "551313bb-1e01-4133-9956-e6f09968f2ce",
"name": "When clicking \"Test workflow\"",
"name": "When clicking Test workflow",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
@ -424,7 +424,7 @@
}
],
"connections": {
"When clicking \"Test workflow\"": {
"When clicking Test workflow": {
"main": [
[
{
@ -524,7 +524,7 @@
{
"parameters": {},
"id": "551313bb-1e01-4133-9956-e6f09968f2ce",
"name": "When clicking \"Test workflow\"",
"name": "When clicking Test workflow",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
@ -574,7 +574,7 @@
}
],
"connections": {
"When clicking \"Test workflow\"": {
"When clicking Test workflow": {
"main": [
[
{

View file

@ -4,7 +4,7 @@
{
"parameters": {},
"id": "f26332f3-c61a-4843-94bd-64a73ad161ff",
"name": "When clicking \"Test workflow\"",
"name": "When clicking Test workflow",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
@ -105,7 +105,7 @@
],
"pinData": {},
"connections": {
"When clicking \"Test workflow\"": {
"When clicking Test workflow": {
"main": [
[
{

View file

@ -19,7 +19,7 @@
{
"parameters": {},
"id": "449ab540-d9d7-480d-b131-05e9989a69cd",
"name": "When clicking \"Test workflow\"",
"name": "When clicking Test workflow",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
@ -42,7 +42,7 @@
}
],
"connections": {
"When clicking \"Test workflow\"": {
"When clicking Test workflow": {
"main": [
[
{

View file

@ -40,7 +40,7 @@
{
"parameters": {},
"id": "ef63cdc5-50bc-4525-9873-7e7f7589a60e",
"name": "When clicking \"Test workflow\"",
"name": "When clicking Test workflow",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
@ -199,7 +199,7 @@
]
]
},
"When clicking \"Test workflow\"": {
"When clicking Test workflow": {
"main": [
[
{

View file

@ -4,7 +4,7 @@
{
"parameters": {},
"id": "f332a7d1-31b4-4e78-b31e-9e8db945bf3f",
"name": "When clicking \"Test workflow\"",
"name": "When clicking Test workflow",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
@ -99,7 +99,7 @@
],
"pinData": {},
"connections": {
"When clicking \"Test workflow\"": {
"When clicking Test workflow": {
"main": [
[
{

View file

@ -30,7 +30,7 @@
{
"parameters": {},
"id": "4f4c6527-d565-448a-96bd-8f5414caf8cc",
"name": "When clicking \"Test workflow\"",
"name": "When clicking Test workflow",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
@ -136,7 +136,7 @@
]
]
},
"When clicking \"Test workflow\"": {
"When clicking Test workflow": {
"main": [
[
{

View file

@ -3,7 +3,7 @@
"nodes": [
{
"id": "2acca986-10a6-451e-b20a-86e95b50e627",
"name": "When clicking \"Test workflow\"",
"name": "When clicking Test workflow",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [460, 460]

View file

@ -7,7 +7,7 @@
{
"parameters": {},
"id": "09e4325e-ede1-40cf-a1ba-58612bbc7f1b",
"name": "When clicking \"Test workflow\"",
"name": "When clicking Test workflow",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
@ -77,7 +77,7 @@
}
],
"connections": {
"When clicking \"Test workflow\"": {
"When clicking Test workflow": {
"main": [
[
{

View file

@ -47,7 +47,7 @@
{
"parameters": {},
"id": "58512a93-dabf-4584-817f-27c608c1bdd5",
"name": "When clicking \"Test workflow\"",
"name": "When clicking Test workflow",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
@ -69,7 +69,7 @@
]
]
},
"When clicking \"Test workflow\"": {
"When clicking Test workflow": {
"main": [
[
{

View file

@ -47,7 +47,7 @@
{
"parameters": {},
"id": "3dc7cf26-ff25-4437-b9fd-0e8b127ebec9",
"name": "When clicking \"Test workflow\"",
"name": "When clicking Test workflow",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
@ -552,7 +552,7 @@
]
]
},
"When clicking \"Test workflow\"": {
"When clicking Test workflow": {
"main": [
[
{

View file

@ -4,7 +4,7 @@
{
"parameters": {},
"id": "0a60e507-7f34-41c0-a0f9-697d852033b6",
"name": "When clicking \"Test workflow\"",
"name": "When clicking Test workflow",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
@ -93,7 +93,7 @@
]
},
"connections": {
"When clicking \"Test workflow\"": {
"When clicking Test workflow": {
"main": [
[
{

View file

@ -6,7 +6,7 @@
{
"parameters": {},
"id": "8108d313-8b03-4aa4-963d-cd1c0fe8f85c",
"name": "When clicking \"Test workflow\"",
"name": "When clicking Test workflow",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
@ -37,7 +37,7 @@
}
],
"connections": {
"When clicking \"Test workflow\"": {
"When clicking Test workflow": {
"main": [
[
{

View file

@ -6,7 +6,7 @@
{
"parameters": {},
"id": "bcb6abdf-d34b-4ea7-a8ed-58155b708c43",
"name": "When clicking \"Test workflow\"",
"name": "When clicking Test workflow",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
@ -90,7 +90,7 @@
}
],
"connections": {
"When clicking \"Test workflow\"": {
"When clicking Test workflow": {
"main": [
[
{

View file

@ -4,7 +4,7 @@
{
"parameters": {},
"id": "25ff0c17-7064-4e14-aec6-45c71d63201b",
"name": "When clicking \"Test workflow\"",
"name": "When clicking Test workflow",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
@ -107,4 +107,4 @@
},
"id": "L3tgfoW660UOSuX6",
"tags": []
}
}

View file

@ -27,7 +27,7 @@
{
"parameters": {},
"id": "acdd1bdc-c642-4ea6-ad67-f4201b640cfa",
"name": "When clicking \"Test workflow\"",
"name": "When clicking Test workflow",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
@ -37,7 +37,7 @@
}
],
"connections": {
"When clicking \"Test workflow\"": {
"When clicking Test workflow": {
"main": [
[
{

View file

@ -6,7 +6,7 @@
{
"parameters": {},
"id": "40720511-19b6-4421-bdb0-3fb6efef4bc5",
"name": "When clicking \"Test workflow\"",
"name": "When clicking Test workflow",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
@ -64,7 +64,7 @@
}
],
"connections": {
"When clicking \"Test workflow\"": {
"When clicking Test workflow": {
"main": [
[
{

View file

@ -247,7 +247,7 @@ export class Agent implements INodeType {
alias: ['LangChain'],
categories: ['AI'],
subcategories: {
AI: ['Agents'],
AI: ['Agents', 'Root Nodes'],
},
resources: {
primaryDocumentation: [

View file

@ -31,7 +31,7 @@ export class OpenAiAssistant implements INodeType {
alias: ['LangChain'],
categories: ['AI'],
subcategories: {
AI: ['Agents'],
AI: ['Agents', 'Root Nodes'],
},
resources: {
primaryDocumentation: [

View file

@ -257,7 +257,7 @@ export class ChainLlm implements INodeType {
alias: ['LangChain'],
categories: ['AI'],
subcategories: {
AI: ['Chains'],
AI: ['Chains', 'Root Nodes'],
},
resources: {
primaryDocumentation: [

View file

@ -30,7 +30,7 @@ export class ChainRetrievalQa implements INodeType {
alias: ['LangChain'],
categories: ['AI'],
subcategories: {
AI: ['Chains'],
AI: ['Chains', 'Root Nodes'],
},
resources: {
primaryDocumentation: [

View file

@ -16,7 +16,7 @@ export class ChainSummarization extends VersionedNodeType {
alias: ['LangChain'],
categories: ['AI'],
subcategories: {
AI: ['Chains'],
AI: ['Chains', 'Root Nodes'],
},
resources: {
primaryDocumentation: [

View file

@ -77,7 +77,7 @@ export class MemoryManager implements INodeType {
codex: {
categories: ['AI'],
subcategories: {
AI: ['Miscellaneous'],
AI: ['Miscellaneous', 'Root Nodes'],
},
resources: {
primaryDocumentation: [

View file

@ -146,6 +146,79 @@ export class OutputParserStructured implements INodeType {
}
}`,
},
{
displayName: 'Schema Type',
name: 'schemaType',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Generate From JSON Example',
value: 'fromJson',
description: 'Generate a schema from an example JSON object',
},
{
name: 'Define Below',
value: 'manual',
description: 'Define the JSON schema manually',
},
],
default: 'fromJson',
description: 'How to specify the schema for the function',
displayOptions: {
show: {
'@version': [{ _cnd: { gte: 1.2 } }],
},
},
},
{
displayName: 'JSON Example',
name: 'jsonSchemaExample',
type: 'json',
default: `{
"state": "California",
"cities": ["Los Angeles", "San Francisco", "San Diego"]
}`,
noDataExpression: true,
typeOptions: {
rows: 10,
},
displayOptions: {
show: {
schemaType: ['fromJson'],
},
},
description: 'Example JSON object to use to generate the schema',
},
{
displayName: 'Input Schema',
name: 'inputSchema',
type: 'json',
default: `{
"type": "object",
"properties": {
"state": {
"type": "string"
},
"cities": {
"type": "array",
"items": {
"type": "string"
}
}
}
}`,
noDataExpression: true,
typeOptions: {
rows: 10,
},
displayOptions: {
show: {
schemaType: ['manual'],
},
},
description: 'Schema to use for the function',
},
{
displayName: 'JSON Schema',
name: 'jsonSchema',

View file

@ -34,9 +34,6 @@ export class ChatTrigger implements INodeType {
},
],
},
subcategories: {
'Core Nodes': ['Other Trigger Nodes'],
},
},
supportsCORS: true,
maxNodes: 1,

View file

@ -79,7 +79,7 @@ export const versionDescription: INodeTypeDescription = {
alias: ['LangChain', 'ChatGPT', 'DallE'],
categories: ['AI'],
subcategories: {
AI: ['Agents', 'Miscellaneous'],
AI: ['Agents', 'Miscellaneous', 'Root Nodes'],
},
resources: {
primaryDocumentation: [

View file

@ -1,5 +1,6 @@
<script setup lang="ts">
import { useI18n } from '../../composables/useI18n';
import type { NodeCreatorTag } from '../../types/node-creator-node';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
import N8nTooltip from '../N8nTooltip';
import { ElTag } from 'element-plus';
@ -9,7 +10,7 @@ export interface Props {
isAi?: boolean;
isTrigger?: boolean;
description?: string;
tag?: string;
tag?: NodeCreatorTag;
title: string;
showActionArrow?: boolean;
}
@ -37,8 +38,8 @@ const { t } = useI18n();
<div>
<div :class="$style.details">
<span :class="$style.name" data-test-id="node-creator-item-name" v-text="title" />
<ElTag v-if="tag" :class="$style.tag" size="small" round type="success">
{{ tag }}
<ElTag v-if="tag" :class="$style.tag" size="small" round :type="tag.type ?? 'success'">
{{ tag.text }}
</ElTag>
<FontAwesomeIcon
v-if="isTrigger"
@ -87,8 +88,16 @@ const { t } = useI18n();
.creatorNode:hover .panelIcon {
color: var(--action-arrow-color-hover, var(--color-text-light));
}
.tag {
:root .tag {
margin-left: var(--spacing-2xs);
line-height: var(--font-size-3xs);
font-size: var(--font-size-3xs);
padding: 0.1875rem var(--spacing-3xs) var(--spacing-4xs) var(--spacing-3xs);
height: auto;
span {
font-size: var(--font-size-2xs) !important;
}
}
.panelIcon {
flex-grow: 1;

View file

@ -8,3 +8,4 @@ export * from './menu';
export * from './select';
export * from './user';
export * from './keyboardshortcut';
export * from './node-creator-node';

View file

@ -0,0 +1,6 @@
import type { ElTag } from 'element-plus';
export type NodeCreatorTag = {
text: string;
type?: (typeof ElTag)['type'];
};

View file

@ -9,7 +9,7 @@ import type {
VIEWS,
ROLE,
} from '@/constants';
import type { IMenuItem } from 'n8n-design-system';
import type { IMenuItem, NodeCreatorTag } from 'n8n-design-system';
import {
type GenericValue,
type IConnections,
@ -932,7 +932,9 @@ export type SimplifiedNodeType = Pick<
| 'codex'
| 'defaults'
| 'outputs'
>;
> & {
tag?: string;
};
export interface SubcategoryItemProps {
description?: string;
iconType?: string;
@ -951,11 +953,21 @@ export interface ViewItemProps {
title: string;
description: string;
icon: string;
tag?: string;
tag?: NodeCreatorTag;
borderless?: boolean;
}
export interface LabelItemProps {
key: string;
}
export interface LinkItemProps {
url: string;
key: string;
newTab?: boolean;
title: string;
description: string;
icon: string;
tag?: NodeCreatorTag;
}
export interface ActionTypeDescription extends SimplifiedNodeType {
displayOptions?: IDisplayOptions;
values?: IDataObject;
@ -1010,6 +1022,11 @@ export interface LabelCreateElement extends CreateElementBase {
properties: LabelItemProps;
}
export interface LinkCreateElement extends CreateElementBase {
type: 'link';
properties: LinkItemProps;
}
export interface ActionCreateElement extends CreateElementBase {
type: 'action';
subcategory: string;
@ -1023,7 +1040,8 @@ export type INodeCreateElement =
| SectionCreateElement
| ViewCreateElement
| LabelCreateElement
| ActionCreateElement;
| ActionCreateElement
| LinkCreateElement;
export interface SubcategorizedNodeTypes {
[subcategory: string]: INodeCreateElement[];

View file

@ -0,0 +1,32 @@
<template>
<n8n-node-creator-node
:class="$style.creatorLink"
:title="link.title"
:is-trigger="false"
:description="link.description"
:tag="link.tag"
:show-action-arrow="true"
>
<template #icon>
<n8n-node-icon type="icon" :name="link.icon" :circle="false" :show-tooltip="false" />
</template>
</n8n-node-creator-node>
</template>
<script setup lang="ts">
import type { LinkItemProps } from '@/Interface';
export interface Props {
link: LinkItemProps;
}
defineProps<Props>();
</script>
<style lang="scss" module>
.creatorLink {
--action-arrow-color: var(--color-text-light);
margin-left: var(--spacing-s);
margin-right: var(--spacing-xs);
}
</style>

View file

@ -8,6 +8,7 @@
:show-action-arrow="showActionArrow"
:is-trigger="isTrigger"
:data-test-id="dataTestId"
:tag="nodeType.tag"
@dragstart="onDragStart"
@dragend="onDragEnd"
>

View file

@ -139,6 +139,13 @@ function onSelected(item: INodeCreateElement) {
searchItems: mergedNodes,
});
}
if (item.type === 'link') {
window.open(item.properties.url, '_blank');
telemetry.trackNodesPanel('nodeCreateList.onLinkSelected', {
link: item.properties.url,
});
}
}
function subcategoriesMapper(item: INodeCreateElement) {
@ -195,13 +202,13 @@ function onKeySelect(activeItemId: string) {
registerKeyHook('MainViewArrowRight', {
keyboardKeys: ['ArrowRight', 'Enter'],
condition: (type) => ['subcategory', 'node', 'view'].includes(type),
condition: (type) => ['subcategory', 'node', 'link', 'view'].includes(type),
handler: onKeySelect,
});
registerKeyHook('MainViewArrowLeft', {
keyboardKeys: ['ArrowLeft'],
condition: (type) => ['subcategory', 'node', 'view'].includes(type),
condition: (type) => ['subcategory', 'node', 'link', 'view'].includes(type),
handler: arrowLeft,
});
</script>

View file

@ -8,6 +8,7 @@ import SubcategoryItem from '../ItemTypes/SubcategoryItem.vue';
import LabelItem from '../ItemTypes/LabelItem.vue';
import ActionItem from '../ItemTypes/ActionItem.vue';
import ViewItem from '../ItemTypes/ViewItem.vue';
import LinkItem from '../ItemTypes/LinkItem.vue';
import CategorizedItemsRenderer from './CategorizedItemsRenderer.vue';
export interface Props {
@ -147,6 +148,8 @@ watch(
[$style.active]: activeItemId === item.uuid,
[$style.iteratorItem]: true,
[$style[item.type]]: true,
// Borderless is only applied to views
[$style.borderless]: item.type === 'view' && item.properties.borderless === true,
}"
data-test-id="item-iterator-item"
:data-keyboard-nav-type="item.type !== 'label' ? item.type : undefined"
@ -175,6 +178,12 @@ watch(
:view="item.properties"
:class="$style.viewItem"
/>
<LinkItem
v-else-if="item.type === 'link'"
:link="item.properties"
:class="$style.linkItem"
/>
</div>
</div>
<n8n-loading v-else :loading="true" :rows="1" variant="p" :class="$style.itemSkeleton" />
@ -223,12 +232,14 @@ watch(
display: none;
}
}
.view {
position: relative;
&:last-child {
margin-top: var(--spacing-s);
padding-top: var(--spacing-xs);
&:after {
content: '';
position: absolute;
@ -241,4 +252,34 @@ watch(
}
}
}
.link {
position: relative;
&:last-child {
margin-bottom: var(--spacing-s);
padding-bottom: var(--spacing-xs);
&:after {
content: '';
position: absolute;
left: var(--spacing-s);
right: var(--spacing-s);
top: 0;
margin: auto;
bottom: 0;
border-bottom: 1px solid var(--color-foreground-base);
}
}
}
.borderless {
&:last-child {
margin-top: 0;
padding-top: 0;
&:after {
content: none;
}
}
}
</style>

View file

@ -76,7 +76,7 @@ describe('NodesListPanel', () => {
await fireEvent.click(container.querySelector('.backButton')!);
await nextTick();
expect(screen.queryAllByTestId('item-iterator-item')).toHaveLength(7);
expect(screen.queryAllByTestId('item-iterator-item')).toHaveLength(8);
});
it('should render regular nodes', async () => {
@ -136,7 +136,7 @@ describe('NodesListPanel', () => {
await nextTick();
expect(screen.getByText('What happens next?')).toBeInTheDocument();
expect(screen.queryAllByTestId('item-iterator-item')).toHaveLength(6);
expect(screen.queryAllByTestId('item-iterator-item')).toHaveLength(5);
screen.getByText('Action in an app').click();
await nextTick();

View file

@ -1,19 +1,29 @@
import type { INodeCreateElement, NodeFilterType, SimplifiedNodeType } from '@/Interface';
import type {
INodeCreateElement,
NodeCreateElement,
NodeFilterType,
SimplifiedNodeType,
} from '@/Interface';
import {
AI_CATEGORY_ROOT_NODES,
AI_CODE_NODE_TYPE,
AI_NODE_CREATOR_VIEW,
AI_OTHERS_NODE_CREATOR_VIEW,
AI_SUBCATEGORY,
DEFAULT_SUBCATEGORY,
TRIGGER_NODE_CREATOR_VIEW,
} from '@/constants';
import { defineStore } from 'pinia';
import { v4 as uuid } from 'uuid';
import { computed, nextTick, ref } from 'vue';
import difference from 'lodash-es/difference';
import { useNodeCreatorStore } from '@/stores/nodeCreator.store';
import {
flattenCreateElements,
groupItemsInSections,
isAINode,
searchNodes,
sortNodeCreateElements,
subcategorizeItems,
@ -27,6 +37,7 @@ import { useKeyboardNavigation } from './useKeyboardNavigation';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import type { INodeInputFilter, NodeConnectionType } from 'n8n-workflow';
import { useCanvasStore } from '@/stores/canvas.store';
interface ViewStack {
uuid?: string;
@ -60,11 +71,12 @@ interface ViewStack {
export const useViewStacks = defineStore('nodeCreatorViewStacks', () => {
const nodeCreatorStore = useNodeCreatorStore();
const { getActiveItemIndex } = useKeyboardNavigation();
const i18n = useI18n();
const viewStacks = ref<ViewStack[]>([]);
const activeStackItems = computed<INodeCreateElement[]>(() => {
const stack = viewStacks.value[viewStacks.value.length - 1];
const stack = getLastActiveStack();
if (!stack?.baselineItems) {
return stack.items ? extendItemsWithUUID(stack.items) : [];
@ -76,13 +88,24 @@ export const useViewStacks = defineStore('nodeCreatorViewStacks', () => {
? searchBaseItems.value
: flattenCreateElements(stack.baselineItems ?? []);
return extendItemsWithUUID(searchNodes(stack.search || '', searchBase));
const canvasHasAINodes = useCanvasStore().aiNodes.length > 0;
const filteredNodes =
isAiRootView(stack) || canvasHasAINodes ? searchBase : filterOutAiNodes(searchBase);
const searchResults = extendItemsWithUUID(searchNodes(stack.search || '', filteredNodes));
const groupedNodes = groupIfAiNodes(searchResults, false) ?? searchResults;
// Set the active index to the second item if there's a section
// as the first item is collapsable
stack.activeIndex = groupedNodes.some((node) => node.type === 'section') ? 1 : 0;
return groupedNodes;
}
return extendItemsWithUUID(stack.baselineItems);
return extendItemsWithUUID(groupIfAiNodes(stack.baselineItems, true));
});
const activeViewStack = computed<ViewStack>(() => {
const stack = viewStacks.value[viewStacks.value.length - 1];
const stack = getLastActiveStack();
if (!stack) return {};
const flatBaselineItems = flattenCreateElements(stack.baselineItems ?? []);
@ -99,34 +122,148 @@ export const useViewStacks = defineStore('nodeCreatorViewStacks', () => {
);
const searchBaseItems = computed<INodeCreateElement[]>(() => {
const stack = viewStacks.value[viewStacks.value.length - 1];
const stack = getLastActiveStack();
if (!stack?.searchItems) return [];
return stack.searchItems.map((item) => transformNodeType(item, stack.subcategory));
});
function getLastActiveStack() {
return viewStacks.value[viewStacks.value.length - 1];
}
// Generate a delta between the global search results(all nodes) and the stack search results
const globalSearchItemsDiff = computed<INodeCreateElement[]>(() => {
const stack = viewStacks.value[viewStacks.value.length - 1];
const stack = getLastActiveStack();
if (!stack?.search) return [];
const allNodes = nodeCreatorStore.mergedNodes.map((item) => transformNodeType(item));
const globalSearchResult = extendItemsWithUUID(searchNodes(stack.search || '', allNodes));
// Apply filtering for AI nodes if the current view is not the AI root view
const filteredNodes = isAiRootView(stack) ? allNodes : filterOutAiNodes(allNodes);
return globalSearchResult.filter((item) => {
return !activeStackItems.value.find((activeItem) => activeItem.key === item.key);
let globalSearchResult: INodeCreateElement[] = extendItemsWithUUID(
searchNodes(stack.search || '', filteredNodes),
);
if (isAiRootView(stack)) {
globalSearchResult = groupIfAiNodes(globalSearchResult);
}
const filteredItems = globalSearchResult.filter((item) => {
return !activeStackItems.value.find((activeItem) => {
if (activeItem.type === 'section') {
const matchingSectionItem = activeItem.children.some(
(sectionItem) => sectionItem.key === item.key,
);
return matchingSectionItem;
}
return activeItem.key === item.key;
});
});
// Filter out empty sections if all of their children are filtered out
const filteredSections = filteredItems.filter((item) => {
if (item.type === 'section') {
const hasVisibleChildren = item.children.some((child) =>
activeStackItems.value.some((filteredItem) => filteredItem.key === child.key),
);
return hasVisibleChildren;
}
return true;
});
return filteredSections;
});
const itemsBySubcategory = computed(() => subcategorizeItems(nodeCreatorStore.mergedNodes));
function isAiRootView(stack: ViewStack) {
return stack.rootView === AI_NODE_CREATOR_VIEW;
}
function groupIfAiNodes(items: INodeCreateElement[], sortAlphabetically = true) {
const aiNodes = items.filter((node): node is NodeCreateElement => isAINode(node));
if (aiNodes.length > 0) {
const sectionsMap = new Map<string, NodeViewItemSection>();
aiNodes.forEach((node) => {
const section = node.properties.codex?.subcategories?.[AI_SUBCATEGORY]?.[0];
if (section) {
const currentItems = sectionsMap.get(section)?.items ?? [];
const isSubnodesSection =
!node.properties.codex?.subcategories?.[AI_SUBCATEGORY].includes(
AI_CATEGORY_ROOT_NODES,
);
sectionsMap.set(section, {
key: section,
title: isSubnodesSection
? `${section} (${i18n.baseText('nodeCreator.subnodes')})`
: section,
items: [...currentItems, node.key],
});
}
});
const nonAiNodes = difference(items, aiNodes);
const nonAiTriggerNodes = nonAiNodes.filter(
(item) => item.type === 'node' && useNodeTypesStore().isTriggerNode(item.properties.name),
);
const nonAiRegularNodes = difference(nonAiNodes, nonAiTriggerNodes);
if (nonAiNodes.length > 0) {
let sectionKey = '';
if (nonAiRegularNodes.length && nonAiTriggerNodes.length) {
sectionKey = i18n.baseText('nodeCreator.actionsCategory.regularAndTriggers');
} else {
sectionKey = nonAiRegularNodes.length
? i18n.baseText('nodeCreator.actionsCategory.regularNodes')
: i18n.baseText('nodeCreator.actionsCategory.triggerNodes');
}
const nodesKeys = nonAiNodes.map((node) => node.key);
sectionsMap.set(sectionKey, {
key: sectionKey,
title: sectionKey,
items: [...nodesKeys],
});
}
// Convert sectionsMap to array of sections
const sections = Array.from(sectionsMap.values());
return groupItemsInSections(items, sections, sortAlphabetically);
}
return items;
}
function filterOutAiNodes(items: INodeCreateElement[]) {
const filteredSearchBase = items.filter((item) => {
if (item.type === 'node') {
const isAICategory = item.properties.codex?.categories?.includes(AI_SUBCATEGORY) === true;
if (!isAICategory) return true;
const isRootNodeSubcategory =
item.properties.codex?.subcategories?.[AI_SUBCATEGORY]?.includes(AI_CATEGORY_ROOT_NODES);
return isRootNodeSubcategory;
}
return true;
});
return filteredSearchBase;
}
async function gotoCompatibleConnectionView(
connectionType: NodeConnectionType,
isOutput?: boolean,
filter?: INodeInputFilter,
) {
const i18n = useI18n();
let nodesByConnectionType: { [key: string]: string[] };
let relatedAIView: { properties: NodeViewItem['properties'] } | undefined;
@ -185,7 +322,7 @@ export const useViewStacks = defineStore('nodeCreatorViewStacks', () => {
}
function setStackBaselineItems() {
const stack = viewStacks.value[viewStacks.value.length - 1];
const stack = getLastActiveStack();
if (!stack || !activeViewStack.value.uuid) return;
let stackItems = stack?.items ?? [];
@ -258,7 +395,7 @@ export const useViewStacks = defineStore('nodeCreatorViewStacks', () => {
}
function updateCurrentViewStack(stack: Partial<ViewStack>) {
const currentStack = viewStacks.value[viewStacks.value.length - 1];
const currentStack = getLastActiveStack();
const matchedIndex = viewStacks.value.findIndex((s) => s.uuid === currentStack.uuid);
if (!currentStack) return;

View file

@ -6,12 +6,18 @@ import type {
INodeCreateElement,
SectionCreateElement,
} from '@/Interface';
import { AI_SUBCATEGORY, CORE_NODES_CATEGORY, DEFAULT_SUBCATEGORY } from '@/constants';
import {
AI_CATEGORY_AGENTS,
AI_SUBCATEGORY,
CORE_NODES_CATEGORY,
DEFAULT_SUBCATEGORY,
} from '@/constants';
import { v4 as uuidv4 } from 'uuid';
import { sublimeSearch } from '@/utils/sortUtils';
import { i18n } from '@/plugins/i18n';
import type { NodeViewItemSection } from './viewsData';
import { i18n } from '@/plugins/i18n';
import { sortBy } from 'lodash-es';
export function transformNodeType(
node: SimplifiedNodeType,
@ -70,6 +76,7 @@ export function sortNodeCreateElements(nodes: INodeCreateElement[]) {
export function searchNodes(searchFilter: string, items: INodeCreateElement[]) {
// In order to support the old search we need to remove the 'trigger' part
const trimmedFilter = searchFilter.toLowerCase().replace('trigger', '').trimEnd();
const result = (
sublimeSearch<INodeCreateElement>(trimmedFilter, items, [
{ key: 'properties.displayName', weight: 1.3 },
@ -83,38 +90,72 @@ export function searchNodes(searchFilter: string, items: INodeCreateElement[]) {
export function flattenCreateElements(items: INodeCreateElement[]): INodeCreateElement[] {
return items.map((item) => (item.type === 'section' ? item.children : item)).flat();
}
export function isAINode(node: INodeCreateElement) {
const isNode = node.type === 'node';
if (!isNode) return false;
if (node.properties.codex?.categories?.includes(AI_SUBCATEGORY)) {
const isAgentSubcategory =
node.properties.codex?.subcategories?.[AI_SUBCATEGORY]?.includes(AI_CATEGORY_AGENTS);
return !isAgentSubcategory;
}
return false;
}
export function groupItemsInSections(
items: INodeCreateElement[],
sections: string[] | NodeViewItemSection[],
sortAlphabetically = true,
): INodeCreateElement[] {
const filteredSections = sections.filter(
(section): section is NodeViewItemSection => typeof section === 'object',
);
const itemsBySection = items.reduce((acc: Record<string, INodeCreateElement[]>, item) => {
const section = filteredSections.find((s) => s.items.includes(item.key));
const key = section?.key ?? 'other';
acc[key] = [...(acc[key] ?? []), item];
return acc;
}, {});
const itemsBySection = (items2: INodeCreateElement[]) =>
items2.reduce((acc: Record<string, INodeCreateElement[]>, item) => {
const section = filteredSections.find((s) => s.items.includes(item.key));
const result: SectionCreateElement[] = filteredSections
.map(
const key = section?.key ?? 'other';
if (key) {
acc[key] = [...(acc[key] ?? []), item];
}
return acc;
}, {});
const mapNewSections = (
newSections: NodeViewItemSection[],
children: Record<string, INodeCreateElement[]>,
) =>
newSections.map(
(section): SectionCreateElement => ({
type: 'section',
key: section.key,
title: section.title,
children: sortNodeCreateElements(itemsBySection[section.key] ?? []),
children: sortAlphabetically
? sortNodeCreateElements(children[section.key] ?? [])
: children[section.key] ?? [],
}),
)
);
const nonAINodes = items.filter((item) => !isAINode(item));
const AINodes = items.filter((item) => isAINode(item));
const nonAINodesBySection = itemsBySection(nonAINodes);
const nonAINodesSections = mapNewSections(filteredSections, nonAINodesBySection);
const AINodesBySection = itemsBySection(AINodes);
const AINodesSections = mapNewSections(sortBy(filteredSections, ['title']), AINodesBySection);
const result = [...nonAINodesSections, ...AINodesSections]
.concat({
type: 'section',
key: 'other',
title: i18n.baseText('nodeCreator.sectionNames.other'),
children: sortNodeCreateElements(itemsBySection.other ?? []),
children: sortNodeCreateElements(nonAINodesBySection.other ?? []),
})
.filter((section) => section.children.length > 0);
.filter((section) => section.type !== 'section' || section.children.length > 0);
if (result.length <= 1) {
return items;

View file

@ -5,10 +5,10 @@ import {
EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE,
FORM_TRIGGER_NODE_TYPE,
MANUAL_TRIGGER_NODE_TYPE,
MANUAL_CHAT_TRIGGER_NODE_TYPE,
SCHEDULE_TRIGGER_NODE_TYPE,
REGULAR_NODE_CREATOR_VIEW,
TRANSFORM_DATA_SUBCATEGORY,
FILES_SUBCATEGORY,
FLOWS_CONTROL_SUBCATEGORY,
TRIGGER_NODE_CREATOR_VIEW,
EMAIL_IMAP_NODE_TYPE,
@ -52,6 +52,8 @@ import {
EMAIL_SEND_NODE_TYPE,
EDIT_IMAGE_NODE_TYPE,
COMPRESSION_NODE_TYPE,
AI_CODE_TOOL_LANGCHAIN_NODE_TYPE,
AI_WORKFLOW_TOOL_LANGCHAIN_NODE_TYPE,
} from '@/constants';
import { useI18n } from '@/composables/useI18n';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
@ -76,13 +78,17 @@ export interface NodeViewItem {
iconProps?: {
color?: string;
};
url?: string;
connectionType?: NodeConnectionType;
panelClass?: string;
group?: string[];
sections?: NodeViewItemSection[];
description?: string;
displayName?: string;
tag?: string;
tag?: {
type: string;
text: string;
};
forceIncludeNodes?: string[];
iconData?: {
type: string;
@ -141,12 +147,24 @@ export function AIView(_nodes: SimplifiedNodeType[]): NodeView {
value: AI_NODE_CREATOR_VIEW,
title: i18n.baseText('nodeCreator.aiPanel.aiNodes'),
subtitle: i18n.baseText('nodeCreator.aiPanel.selectAiNode'),
info: i18n.baseText('nodeCreator.aiPanel.infoBox', {
interpolate: { link: templatesStore.getWebsiteCategoryURL('ai') },
}),
items: [
...chainNodes,
{
key: 'ai_templates_root',
type: 'link',
properties: {
title: i18n.baseText('nodeCreator.aiPanel.linkItem.title'),
icon: 'box-open',
description: i18n.baseText('nodeCreator.aiPanel.linkItem.description'),
name: 'ai_templates_root',
url: templatesStore.getWebsiteCategoryURL(undefined, 'AdvancedAI'),
tag: {
type: 'info',
text: i18n.baseText('nodeCreator.triggerHelperPanel.manualTriggerTag'),
},
},
},
...agentNodes,
...chainNodes,
{
key: AI_OTHERS_NODE_CREATOR_VIEW,
type: 'view',
@ -159,6 +177,7 @@ export function AIView(_nodes: SimplifiedNodeType[]): NodeView {
],
};
}
export function AINodesView(_nodes: SimplifiedNodeType[]): NodeView {
const i18n = useI18n();
@ -232,12 +251,20 @@ export function AINodesView(_nodes: SimplifiedNodeType[]): NodeView {
},
},
{
key: AI_CATEGORY_TOOLS,
type: 'subcategory',
key: AI_CATEGORY_TOOLS,
category: CORE_NODES_CATEGORY,
properties: {
title: AI_CATEGORY_TOOLS,
icon: 'tools',
...getAISubcategoryProperties(NodeConnectionType.AiTool),
sections: [
{
key: 'popular',
title: i18n.baseText('nodeCreator.sectionNames.popular'),
items: [AI_WORKFLOW_TOOL_LANGCHAIN_NODE_TYPE, AI_CODE_TOOL_LANGCHAIN_NODE_TYPE],
},
],
},
},
{
@ -278,6 +305,18 @@ export function TriggerView() {
title: i18n.baseText('nodeCreator.triggerHelperPanel.selectATrigger'),
subtitle: i18n.baseText('nodeCreator.triggerHelperPanel.selectATriggerDescription'),
items: [
{
key: MANUAL_TRIGGER_NODE_TYPE,
type: 'node',
category: [CORE_NODES_CATEGORY],
properties: {
group: [],
name: MANUAL_TRIGGER_NODE_TYPE,
displayName: i18n.baseText('nodeCreator.triggerHelperPanel.manualTriggerDisplayName'),
description: i18n.baseText('nodeCreator.triggerHelperPanel.manualTriggerDescription'),
icon: 'fa:mouse-pointer',
},
},
{
key: DEFAULT_SUBCATEGORY,
type: 'subcategory',
@ -331,18 +370,6 @@ export function TriggerView() {
},
},
},
{
key: MANUAL_TRIGGER_NODE_TYPE,
type: 'node',
category: [CORE_NODES_CATEGORY],
properties: {
group: [],
name: MANUAL_TRIGGER_NODE_TYPE,
displayName: i18n.baseText('nodeCreator.triggerHelperPanel.manualTriggerDisplayName'),
description: i18n.baseText('nodeCreator.triggerHelperPanel.manualTriggerDescription'),
icon: 'fa:mouse-pointer',
},
},
{
key: EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE,
type: 'node',
@ -355,6 +382,18 @@ export function TriggerView() {
icon: 'fa:sign-out-alt',
},
},
{
key: MANUAL_CHAT_TRIGGER_NODE_TYPE,
type: 'node',
category: [CORE_NODES_CATEGORY],
properties: {
group: [],
name: MANUAL_CHAT_TRIGGER_NODE_TYPE,
displayName: i18n.baseText('nodeCreator.triggerHelperPanel.manualChatTriggerDisplayName'),
description: i18n.baseText('nodeCreator.triggerHelperPanel.manualChatTriggerDescription'),
icon: 'fa:comments',
},
},
{
type: 'subcategory',
key: OTHER_TRIGGER_NODES_SUBCATEGORY,
@ -447,22 +486,6 @@ export function RegularView(nodes: SimplifiedNodeType[]) {
],
},
},
{
type: 'subcategory',
key: FILES_SUBCATEGORY,
category: CORE_NODES_CATEGORY,
properties: {
title: FILES_SUBCATEGORY,
icon: 'file-alt',
sections: [
{
key: 'popular',
title: i18n.baseText('nodeCreator.sectionNames.popular'),
items: [CONVERT_TO_FILE_NODE_TYPE, EXTRACT_FROM_FILE_NODE_TYPE],
},
],
},
},
{
type: 'subcategory',
key: HELPERS_SUBCATEGORY,
@ -491,9 +514,13 @@ export function RegularView(nodes: SimplifiedNodeType[]) {
title: i18n.baseText('nodeCreator.aiPanel.langchainAiNodes'),
icon: 'robot',
description: i18n.baseText('nodeCreator.aiPanel.nodesForAi'),
tag: i18n.baseText('nodeCreator.aiPanel.newTag'),
tag: {
type: 'success',
text: i18n.baseText('nodeCreator.aiPanel.newTag'),
},
borderless: true,
},
});
} as NodeViewItem);
view.items.push({
key: TRIGGER_NODE_CREATOR_VIEW,

View file

@ -264,8 +264,10 @@ export const AI_CATEGORY_RETRIEVERS = 'Retrievers';
export const AI_CATEGORY_EMBEDDING = 'Embeddings';
export const AI_CATEGORY_DOCUMENT_LOADERS = 'Document Loaders';
export const AI_CATEGORY_TEXT_SPLITTERS = 'Text Splitters';
export const AI_CATEGORY_ROOT_NODES = 'Root Nodes';
export const AI_UNCATEGORIZED_CATEGORY = 'Miscellaneous';
export const AI_CODE_TOOL_LANGCHAIN_NODE_TYPE = '@n8n/n8n-nodes-langchain.toolCode';
export const AI_WORKFLOW_TOOL_LANGCHAIN_NODE_TYPE = '@n8n/n8n-nodes-langchain.toolWorkflow';
export const REQUEST_NODE_FORM_URL = 'https://n8n-community.typeform.com/to/K1fBVTZ3';
// Node Connection Types
@ -674,10 +676,17 @@ export const AI_ASSISTANT_EXPERIMENT = {
variant: 'variant',
};
export const CANVAS_AUTO_ADD_MANUAL_TRIGGER_EXPERIMENT = {
name: '20_canvas_auto_add_manual_trigger',
control: 'control',
variant: 'variant',
};
export const EXPERIMENTS_TO_TRACK = [
ASK_AI_EXPERIMENT.name,
TEMPLATE_CREDENTIAL_SETUP_EXPERIMENT,
AI_ASSISTANT_EXPERIMENT.name,
CANVAS_AUTO_ADD_MANUAL_TRIGGER_EXPERIMENT.name,
];
export const MFA_AUTHENTICATION_REQUIRED_ERROR_CODE = 998;

View file

@ -928,9 +928,9 @@
"ndv.output.of": " of ",
"ndv.output.pageSize": "Page Size",
"ndv.output.run": "Run",
"ndv.output.runNodeHint": "Test this node to output data",
"ndv.output.runNodeHint": "Execute this node to view data",
"ndv.output.runNodeHintSubNode": "Output will appear here once the parent node is run",
"ndv.output.insertTestData": "insert test data",
"ndv.output.insertTestData": "set mock data",
"ndv.output.staleDataWarning.regular": "Node parameters have changed.<br>Test node again to refresh output.",
"ndv.output.staleDataWarning.pinData": "Node parameter changes will not affect pinned output data.",
"ndv.output.tooMuchData.message": "The node contains {size} MB of data. Displaying it may cause problems. <br /> If you do decide to display it, avoid the JSON view.",
@ -986,6 +986,9 @@
"nodeCreator.actionsCategory.onNewEvent": "On new {event} event",
"nodeCreator.actionsCategory.onEvent": "On {event}",
"nodeCreator.actionsCategory.triggers": "Triggers",
"nodeCreator.actionsCategory.triggerNodes": "Trigger Nodes",
"nodeCreator.actionsCategory.regularNodes": "Regular Nodes",
"nodeCreator.actionsCategory.regularAndTriggers": "Regular & Trigger Nodes",
"nodeCreator.actionsCategory.searchActions": "Search {node} Actions...",
"nodeCreator.actionsCategory.noMatchingActions": "No matching Actions. <i>Reset search</i>",
"nodeCreator.actionsCategory.noMatchingTriggers": "No matching Triggers. <i>Reset search</i>",
@ -996,6 +999,7 @@
"nodeCreator.actionsTooltip.actionsPerformStep": "Actions perform a step once your workflow has already started. <a target=\"_blank\" href=\"https://docs.n8n.io/integrations/builtin/\"> Learn more</a>",
"nodeCreator.actionsCallout.noTriggerItems": "No <strong>{nodeName}</strong> Triggers available. Users often combine the following Triggers with <strong>{nodeName}</strong> Actions.",
"nodeCreator.categoryNames.otherCategories": "Results in other categories",
"nodeCreator.subnodes": "sub-nodes",
"nodeCreator.noResults.dontWorryYouCanProbablyDoItWithThe": "Dont worry, you can probably do it with the",
"nodeCreator.noResults.httpRequest": "HTTP Request",
"nodeCreator.noResults.node": "node",
@ -1007,10 +1011,10 @@
"nodeCreator.searchBar.searchNodes": "Search nodes...",
"nodeCreator.subcategoryDescriptions.appTriggerNodes": "Runs the flow when something happens in an app like Telegram, Notion or Airtable",
"nodeCreator.subcategoryDescriptions.appRegularNodes": "Do something in an app or service like Google Sheets, Telegram or Notion",
"nodeCreator.subcategoryDescriptions.dataTransformation": "Manipulate data, run JavaScript code, etc.",
"nodeCreator.subcategoryDescriptions.dataTransformation": "Manipulate, filter or convert data",
"nodeCreator.subcategoryDescriptions.files": "CSV, XLS, XML, text, images, etc.",
"nodeCreator.subcategoryDescriptions.flow": "IF, Switch, Wait, Compare and Merge data, etc.",
"nodeCreator.subcategoryDescriptions.helpers": "Code, HTTP Requests (API Calls), Webhook, and other helpers",
"nodeCreator.subcategoryDescriptions.flow": "Branch, merge or loop the flow, etc.",
"nodeCreator.subcategoryDescriptions.helpers": "Run code, make HTTP requests, set webhooks, etc.",
"nodeCreator.subcategoryDescriptions.otherTriggerNodes": "Runs the flow on workflow errors, file changes, etc.",
"nodeCreator.subcategoryDescriptions.agents": "Autonomous entities that interact and make decisions.",
"nodeCreator.subcategoryDescriptions.chains": "Structured assemblies for specific tasks.",
@ -1054,11 +1058,14 @@
"nodeCreator.triggerHelperPanel.scheduleTriggerDisplayName": "On a schedule",
"nodeCreator.triggerHelperPanel.scheduleTriggerDescription": "Runs the flow every day, hour, or custom interval",
"nodeCreator.triggerHelperPanel.webhookTriggerDisplayName": "On webhook call",
"nodeCreator.triggerHelperPanel.webhookTriggerDescription": "Runs the flow when another app sends a webhook",
"nodeCreator.triggerHelperPanel.webhookTriggerDescription": "Runs the flow on receiving an HTTP request",
"nodeCreator.triggerHelperPanel.formTriggerDisplayName": "On form submission",
"nodeCreator.triggerHelperPanel.formTriggerDescription": "Runs the flow when an n8n generated webform is submitted",
"nodeCreator.triggerHelperPanel.manualTriggerDisplayName": "Manually",
"nodeCreator.triggerHelperPanel.manualTriggerDescription": "Runs the flow on clicking a button in n8n",
"nodeCreator.triggerHelperPanel.manualTriggerDisplayName": "Trigger manually",
"nodeCreator.triggerHelperPanel.manualTriggerDescription": "Runs the flow on clicking a button in n8n. Good for getting started quickly",
"nodeCreator.triggerHelperPanel.manualChatTriggerDisplayName": "On chat message",
"nodeCreator.triggerHelperPanel.manualChatTriggerDescription": "Runs the flow when a user sends a chat message. For use with AI nodes",
"nodeCreator.triggerHelperPanel.manualTriggerTag": "Recommended",
"nodeCreator.triggerHelperPanel.whatHappensNext": "What happens next?",
"nodeCreator.triggerHelperPanel.selectATrigger": "What triggers this workflow?",
"nodeCreator.triggerHelperPanel.selectATriggerDescription": "A trigger is a step that starts your workflow",
@ -1072,7 +1079,8 @@
"nodeCreator.aiPanel.newTag": "New",
"nodeCreator.aiPanel.langchainAiNodes": "Advanced AI",
"nodeCreator.aiPanel.title": "When should this workflow run?",
"nodeCreator.aiPanel.infoBox": "Check out our <a href=\"{link}\" target=\"_blank\">templates</a> for workflow examples and inspiration.",
"nodeCreator.aiPanel.linkItem.description": "See what's possible and get started 5x faster",
"nodeCreator.aiPanel.linkItem.title": "AI Templates",
"nodeCreator.aiPanel.scheduleTriggerDisplayName": "On a schedule",
"nodeCreator.aiPanel.scheduleTriggerDescription": "Runs the flow every day, hour, or custom interval",
"nodeCreator.aiPanel.webhookTriggerDisplayName": "On webhook call",

View file

@ -15,7 +15,7 @@ import {
scaleReset,
scaleSmaller,
} from '@/utils/canvasUtils';
import { START_NODE_TYPE } from '@/constants';
import { MANUAL_TRIGGER_NODE_TYPE, START_NODE_TYPE } from '@/constants';
import type {
BeforeStartEventParams,
BrowserJsPlumbInstance,
@ -61,6 +61,9 @@ export const useCanvasStore = defineStore('canvas', () => {
(node) => node.type === START_NODE_TYPE || nodeTypesStore.isTriggerNode(node.type),
),
);
const aiNodes = computed<INodeUi[]>(() =>
nodes.value.filter((node) => node.type.includes('langchain')),
);
const isDemo = ref<boolean>(false);
const nodeViewScale = ref<number>(1);
const canvasAddButtonPosition = ref<XYPosition>([1, 1]);
@ -91,6 +94,23 @@ export const useCanvasStore = defineStore('canvas', () => {
};
};
const getAutoAddManualTriggerNode = (): INodeUi | null => {
const manualTriggerNode = nodeTypesStore.getNodeType(MANUAL_TRIGGER_NODE_TYPE);
if (!manualTriggerNode) {
console.error('Could not find the manual trigger node');
return null;
}
return {
id: uuid(),
name: manualTriggerNode.defaults.name?.toString() ?? manualTriggerNode.displayName,
type: MANUAL_TRIGGER_NODE_TYPE,
parameters: {},
position: canvasAddButtonPosition.value,
typeVersion: 1,
};
};
const getNodesWithPlaceholderNode = (): INodeUi[] =>
triggerNodes.value.length > 0 ? nodes.value : [getPlaceholderTriggerNodeUI(), ...nodes.value];
@ -298,6 +318,7 @@ export const useCanvasStore = defineStore('canvas', () => {
newNodeInsertPosition,
jsPlumbInstance,
isLoading: loadingService.isLoading,
aiNodes,
startLoading: loadingService.startLoading,
setLoadingText: loadingService.setLoadingText,
stopLoading: loadingService.stopLoading,
@ -311,5 +332,6 @@ export const useCanvasStore = defineStore('canvas', () => {
zoomToFit,
wheelScroll,
initInstance,
getAutoAddManualTriggerNode,
};
});

View file

@ -121,7 +121,7 @@ export const useTemplatesStore = defineStore(STORES.TEMPLATES, {
* Constructs URLSearchParams object based on the default parameters for the template repository
* and provided additional parameters
*/
websiteTemplateRepositoryParameters() {
websiteTemplateRepositoryParameters(roleOverride?: string) {
const rootStore = useRootStore();
const userStore = useUsersStore();
const workflowsStore = useWorkflowsStore();
@ -133,6 +133,7 @@ export const useTemplatesStore = defineStore(STORES.TEMPLATES, {
};
const userRole: string | undefined =
userStore.currentUserCloudInfo?.role ?? userStore.currentUser?.personalizationAnswers?.role;
if (userRole) {
defaultParameters.utm_user_role = userRole;
}
@ -156,10 +157,15 @@ export const useTemplatesStore = defineStore(STORES.TEMPLATES, {
* Construct the URL for the template category page on the website for a given category id
*/
getWebsiteCategoryURL() {
return (id: string) => {
return `${TEMPLATES_URLS.BASE_WEBSITE_URL}/?${this.websiteTemplateRepositoryParameters({
categories: id,
}).toString()}`;
return (id?: string, roleOverride?: string) => {
const payload: Record<string, string> = {};
if (id) {
payload.categories = id;
}
if (roleOverride) {
payload.utm_user_role = roleOverride;
}
return `${TEMPLATES_URLS.BASE_WEBSITE_URL}/?${this.websiteTemplateRepositoryParameters(payload).toString()}`;
};
},
},

View file

@ -11,7 +11,7 @@ const MOCK_EXECUTION: Partial<IExecutionResponse> = {
startData: {},
resultData: {
runData: {
'When clicking "Test workflow"': [
'When clicking Test workflow': [
{
startTime: 1706027170005,
executionTime: 0,
@ -24,7 +24,7 @@ const MOCK_EXECUTION: Partial<IExecutionResponse> = {
{
startTime: 1706027170005,
executionTime: 1,
source: [{ previousNode: 'When clicking "Test workflow"' }],
source: [{ previousNode: 'When clicking Test workflow' }],
executionStatus: 'success',
data: {
main: [
@ -258,54 +258,54 @@ describe('pairedItemUtils', () => {
const actual = getPairedItemsMapping(MOCK_EXECUTION);
const expected = {
DebugHelper_r0_o0_i0: new Set([
'When clicking "Test workflow"_r0_o0_i0',
'When clicking Test workflow_r0_o0_i0',
'If_r0_o0_i0',
'Edit Fields_r1_o0_i0',
'Edit Fields1_r1_o0_i0',
]),
DebugHelper_r0_o0_i1: new Set([
'When clicking "Test workflow"_r0_o0_i0',
'When clicking Test workflow_r0_o0_i0',
'If_r0_o1_i0',
'Edit Fields_r0_o0_i0',
'Edit Fields1_r0_o0_i0',
]),
'Edit Fields1_r0_o0_i0': new Set([
'When clicking "Test workflow"_r0_o0_i0',
'When clicking Test workflow_r0_o0_i0',
'DebugHelper_r0_o0_i1',
'If_r0_o1_i0',
'Edit Fields_r0_o0_i0',
]),
'Edit Fields1_r1_o0_i0': new Set([
'When clicking "Test workflow"_r0_o0_i0',
'When clicking Test workflow_r0_o0_i0',
'DebugHelper_r0_o0_i0',
'If_r0_o0_i0',
'Edit Fields_r1_o0_i0',
]),
'Edit Fields_r0_o0_i0': new Set([
'When clicking "Test workflow"_r0_o0_i0',
'When clicking Test workflow_r0_o0_i0',
'DebugHelper_r0_o0_i1',
'If_r0_o1_i0',
'Edit Fields1_r0_o0_i0',
]),
'Edit Fields_r1_o0_i0': new Set([
'When clicking "Test workflow"_r0_o0_i0',
'When clicking Test workflow_r0_o0_i0',
'DebugHelper_r0_o0_i0',
'If_r0_o0_i0',
'Edit Fields1_r1_o0_i0',
]),
If_r0_o0_i0: new Set([
'When clicking "Test workflow"_r0_o0_i0',
'When clicking Test workflow_r0_o0_i0',
'DebugHelper_r0_o0_i0',
'Edit Fields_r1_o0_i0',
'Edit Fields1_r1_o0_i0',
]),
If_r0_o1_i0: new Set([
'When clicking "Test workflow"_r0_o0_i0',
'When clicking Test workflow_r0_o0_i0',
'DebugHelper_r0_o0_i1',
'Edit Fields_r0_o0_i0',
'Edit Fields1_r0_o0_i0',
]),
'When clicking "Test workflow"_r0_o0_i0': new Set([
'When clicking Test workflow_r0_o0_i0': new Set([
'DebugHelper_r0_o0_i0',
'DebugHelper_r0_o0_i1',
'If_r0_o0_i0',

View file

@ -250,6 +250,7 @@ import {
UPDATE_WEBHOOK_ID_NODE_TYPES,
TIME,
AI_ASSISTANT_LOCAL_STORAGE_KEY,
CANVAS_AUTO_ADD_MANUAL_TRIGGER_EXPERIMENT,
} from '@/constants';
import useGlobalLinkActions from '@/composables/useGlobalLinkActions';
@ -395,6 +396,7 @@ import type { ProjectSharingData } from '@/features/projects/projects.types';
import { useAIStore } from '@/stores/ai.store';
import { useStorage } from '@/composables/useStorage';
import { isJSPlumbEndpointElement } from '@/utils/typeGuards';
import { usePostHog } from '@/stores/posthog.store';
import { ProjectTypes } from '@/features/projects/projects.utils';
interface AddNodeOptions {
@ -944,6 +946,17 @@ export default defineComponent({
action: this.openSelectiveNodeCreator,
});
this.registerCustomAction({
key: 'showNodeCreator',
action: () => {
this.ndvStore.activeNodeName = null;
void this.$nextTick(() => {
this.showTriggerCreator(NODE_CREATOR_OPEN_SOURCES.TAB);
});
},
});
this.readOnlyEnvRouteCheck();
this.canvasStore.isDemo = this.isDemo;
},
@ -1177,12 +1190,6 @@ export default defineComponent({
? this.$locale.baseText('nodeView.addOrEnableTriggerNode')
: this.$locale.baseText('nodeView.addATriggerNodeFirst');
this.registerCustomAction({
key: 'showNodeCreator',
action: () =>
this.showTriggerCreator(NODE_CREATOR_OPEN_SOURCES.NO_TRIGGER_EXECUTION_TOOLTIP),
});
const notice = this.showMessage({
type: 'info',
title: this.$locale.baseText('nodeView.cantExecuteNoTrigger'),
@ -1257,9 +1264,15 @@ export default defineComponent({
},
showTriggerCreator(source: NodeCreatorOpenSource) {
if (this.createNodeActive) return;
this.ndvStore.activeNodeName = null;
this.nodeCreatorStore.setSelectedView(TRIGGER_NODE_CREATOR_VIEW);
this.nodeCreatorStore.setShowScrim(true);
this.onToggleNodeCreator({ source, createNodeActive: true });
this.onToggleNodeCreator({
source,
createNodeActive: true,
nodeCreatorView: TRIGGER_NODE_CREATOR_VIEW,
});
},
async openExecution(executionId: string) {
this.canvasStore.startLoading();
@ -3659,6 +3672,7 @@ export default defineComponent({
this.workflowsStore.workflow.scopes = scopes;
},
async newWorkflow(): Promise<void> {
const { getVariant } = usePostHog();
this.canvasStore.startLoading();
this.resetWorkspace();
this.workflowData = await this.workflowsStore.getNewWorkflowData(
@ -3670,15 +3684,24 @@ export default defineComponent({
this.uiStore.stateIsDirty = false;
this.canvasStore.setZoomLevel(1, [0, 0]);
await this.tryToAddWelcomeSticky();
this.canvasStore.zoomToFit();
this.uiStore.nodeViewInitialized = true;
this.historyStore.reset();
this.executionsStore.activeExecution = null;
this.makeNewWorkflowShareable();
this.canvasStore.stopLoading();
},
async tryToAddWelcomeSticky(): Promise<void> {
this.canvasStore.zoomToFit();
// Pre-populate the canvas with the manual trigger node if the experiment is enabled and the user is in the variant group
if (
getVariant(CANVAS_AUTO_ADD_MANUAL_TRIGGER_EXPERIMENT.name) ===
CANVAS_AUTO_ADD_MANUAL_TRIGGER_EXPERIMENT.variant
) {
const manualTriggerNode = this.canvasStore.getAutoAddManualTriggerNode();
if (manualTriggerNode) {
await this.addNodes([manualTriggerNode]);
this.uiStore.lastSelectedNode = manualTriggerNode.name;
}
}
},
async initView(): Promise<void> {
if (this.$route.params.action === 'workflowSave') {
@ -5375,4 +5398,3 @@ export default defineComponent({
);
}
</style>
, IRun, IPushDataExecutionFinished

View file

@ -4,7 +4,7 @@
{
"parameters": {},
"id": "59f5ae0f-52f7-4bc8-b325-29d2b0d810f8",
"name": "When clicking \"Test workflow\"",
"name": "When clicking Test workflow",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
@ -217,7 +217,7 @@
]
},
"connections": {
"When clicking \"Test workflow\"": {
"When clicking Test workflow": {
"main": [
[
{

View file

@ -7,7 +7,7 @@
{
"parameters": {},
"id": "fb826323-2e48-4f11-bb0e-e12de32e22ee",
"name": "When clicking \"Test workflow\"",
"name": "When clicking Test workflow",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [180, 160]
@ -26,7 +26,7 @@
}
],
"connections": {
"When clicking \"Test workflow\"": {
"When clicking Test workflow": {
"main": [
[
{

View file

@ -4,7 +4,7 @@
{
"parameters": {},
"id": "fcc3e9dc-90c9-4b26-9b44-e661e0ebf658",
"name": "When clicking \"Test workflow\"",
"name": "When clicking Test workflow",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
@ -305,7 +305,7 @@
]
},
"connections": {
"When clicking \"Test workflow\"": {
"When clicking Test workflow": {
"main": [
[
{
@ -409,4 +409,4 @@
},
"id": "H0sZEXDuE7VIP5vz",
"tags": []
}
}

View file

@ -16,7 +16,7 @@ export class ManualTrigger implements INodeType {
eventTriggerDescription: '',
maxNodes: 1,
defaults: {
name: 'When clicking "Test workflow"',
name: 'When clicking Test workflow',
color: '#909298',
},
@ -25,7 +25,7 @@ export class ManualTrigger implements INodeType {
properties: [
{
displayName:
'This node is where a manual workflow execution starts. To make one, go back to the canvas and click test workflow',
'This node is where the workflow execution starts (when you click the test button on the canvas).<br><br> <a data-action="showNodeCreator">Explore other ways to trigger your workflow</a> (e.g on a schedule, or a webhook)',
name: 'notice',
type: 'notice',
default: '',

View file

@ -4,7 +4,7 @@
{
"parameters": {},
"id": "94003e55-6c4e-492f-802a-49f4fb5b5f4b",
"name": "When clicking \"Test Workflow\"",
"name": "When clicking Test workflow",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
@ -485,7 +485,7 @@
]
},
"connections": {
"When clicking \"Test Workflow\"": {
"When clicking Test workflow": {
"main": [
[
{

View file

@ -4,7 +4,7 @@
{
"parameters": {},
"id": "1301e15e-7a64-44bf-bc4b-d60e7b8c629a",
"name": "When clicking \"Test workflow\"",
"name": "When clicking Test workflow",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
@ -273,7 +273,7 @@
]
},
"connections": {
"When clicking \"Test workflow\"": {
"When clicking Test workflow": {
"main": [
[
{

View file

@ -667,7 +667,7 @@ describe('generateNodesGraph', () => {
{
parameters: {},
id: 'fe69383c-e418-4f98-9c0e-924deafa7f93',
name: 'When clicking "Test workflow"',
name: 'When clicking Test workflow',
type: 'n8n-nodes-base.manualTrigger',
typeVersion: 1,
position: [540, 220],
@ -692,7 +692,7 @@ describe('generateNodesGraph', () => {
},
],
connections: {
'When clicking "Test workflow"': {
'When clicking Test workflow': {
main: [
[
{
@ -758,7 +758,7 @@ describe('generateNodesGraph', () => {
is_pinned: false,
},
nameIndices: {
'When clicking "Test workflow"': '0',
'When clicking Test workflow': '0',
Chain: '1',
Model: '2',
},

View file

@ -3,7 +3,7 @@
"startData": {},
"resultData": {
"runData": {
"When clicking \"Test workflow\"": [
"When clicking Test workflow": [
{
"startTime": 1707471743600,
"executionTime": 1,
@ -29,7 +29,7 @@
"executionTime": 1,
"source": [
{
"previousNode": "When clicking \"Test workflow\""
"previousNode": "When clicking Test workflow"
}
],
"executionStatus": "success",
@ -956,7 +956,7 @@
"source": {
"main": [
{
"previousNode": "When clicking \"Test workflow\""
"previousNode": "When clicking Test workflow"
}
]
}
@ -999,7 +999,7 @@
"source": {
"main": [
{
"previousNode": "When clicking \"Test workflow\""
"previousNode": "When clicking Test workflow"
}
]
}

View file

@ -4,7 +4,7 @@
{
"parameters": {},
"id": "b5122d27-4bb5-4100-a69b-03b1dcac76c7",
"name": "When clicking \"Test workflow\"",
"name": "When clicking Test workflow",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [740, 1680]
@ -561,7 +561,7 @@
]
},
"connections": {
"When clicking \"Test workflow\"": {
"When clicking Test workflow": {
"main": [
[
{