fix(editor): Support renaming node in HTML parameters (#16315)

This commit is contained in:
Charlie Kolb
2025-06-13 15:44:21 +02:00
committed by GitHub
parent aa273745ec
commit 88e3c90e71
6 changed files with 539 additions and 1 deletions

View File

@@ -573,6 +573,329 @@ const setNode: LoadedClass<INodeType> = {
},
};
const codeNode: LoadedClass<INodeType> = {
sourcePath: '',
type: {
description: {
displayName: 'Code',
name: 'code',
group: ['input'],
version: 1,
description: 'Code node',
defaults: {
name: 'Code',
color: '#0000FF',
},
inputs: [NodeConnectionTypes.Main],
outputs: [NodeConnectionTypes.Main],
properties: [
{
displayName: 'Code',
name: 'jsCode',
type: 'string',
default: '// placeholder',
},
],
},
},
};
const htmlNode: LoadedClass<INodeType> = {
sourcePath: '',
type: {
description: {
displayName: 'HTML',
name: 'html',
group: ['input'],
version: 1,
description: 'HTML node',
defaults: {
name: 'HTML',
color: '#0000FF',
},
inputs: [NodeConnectionTypes.Main],
outputs: [NodeConnectionTypes.Main],
properties: [
{
displayName: 'HTML',
name: 'html',
type: 'string',
default: '// placeholder',
},
],
},
},
};
const formNode: LoadedClass<INodeType> = {
sourcePath: '',
type: {
description: {
displayName: 'Form',
name: 'form',
group: ['input'],
version: 1,
description: 'Form node',
defaults: {
name: 'Form',
color: '#0000FF',
},
inputs: [NodeConnectionTypes.Main],
outputs: [NodeConnectionTypes.Main],
properties: [
{
displayName: 'Form Elements',
name: 'formFields',
placeholder: 'Add Form Element',
type: 'fixedCollection',
default: {},
typeOptions: {
multipleValues: true,
sortable: true,
},
options: [
{
displayName: 'Values',
name: 'values',
values: [
{
displayName: 'Field Name',
name: 'fieldLabel',
type: 'string',
default: '',
placeholder: 'e.g. What is your name?',
description: 'Label that appears above the input field',
required: true,
displayOptions: {
hide: {
fieldType: ['hiddenField', 'html'],
},
},
},
{
displayName: 'Element Type',
name: 'fieldType',
type: 'options',
default: 'text',
description: 'The type of field to add to the form',
options: [
{
name: 'Custom HTML',
value: 'html',
},
{
name: 'Date',
value: 'date',
},
{
name: 'Dropdown List',
value: 'dropdown',
},
{
name: 'Email',
value: 'email',
},
{
name: 'File',
value: 'file',
},
{
name: 'Hidden Field',
value: 'hiddenField',
},
{
name: 'Number',
value: 'number',
},
{
name: 'Password',
value: 'password',
},
{
name: 'Text',
value: 'text',
},
{
name: 'Textarea',
value: 'textarea',
},
],
required: true,
},
{
displayName: 'Element Name',
name: 'elementName',
type: 'string',
default: '',
placeholder: 'e.g. content-section',
description: 'Optional field. It can be used to include the html in the output.',
displayOptions: {
show: {
fieldType: ['html'],
},
},
},
{
displayName: 'Placeholder',
name: 'placeholder',
description: 'Sample text to display inside the field',
type: 'string',
default: '',
displayOptions: {
hide: {
fieldType: ['dropdown', 'date', 'file', 'html', 'hiddenField'],
},
},
},
{
displayName: 'Field Name',
name: 'fieldName',
description:
'The name of the field, used in input attributes and referenced by the workflow',
type: 'string',
default: '',
displayOptions: {
show: {
fieldType: ['hiddenField'],
},
},
},
{
displayName: 'Field Value',
name: 'fieldValue',
description:
'Input value can be set here or will be passed as a query parameter via Field Name if no value is set',
type: 'string',
default: '',
displayOptions: {
show: {
fieldType: ['hiddenField'],
},
},
},
{
displayName: 'Field Options',
name: 'fieldOptions',
placeholder: 'Add Field Option',
description: 'List of options that can be selected from the dropdown',
type: 'fixedCollection',
default: { values: [{ option: '' }] },
required: true,
displayOptions: {
show: {
fieldType: ['dropdown'],
},
},
typeOptions: {
multipleValues: true,
sortable: true,
},
options: [
{
displayName: 'Values',
name: 'values',
values: [
{
displayName: 'Option',
name: 'option',
type: 'string',
default: '',
},
],
},
],
},
{
displayName: 'Multiple Choice',
name: 'multiselect',
type: 'boolean',
default: false,
description:
'Whether to allow the user to select multiple options from the dropdown list',
displayOptions: {
show: {
fieldType: ['dropdown'],
},
},
},
{
displayName: 'HTML',
name: 'html',
typeOptions: {
editor: 'htmlEditor',
},
type: 'string',
noDataExpression: true,
default: '<!-- Your custom HTML here --->',
description: 'HTML elements to display on the form page',
hint: 'Does not accept <code>&lt;script&gt;</code>, <code>&lt;style&gt;</code> or <code>&lt;input&gt;</code> tags',
displayOptions: {
show: {
fieldType: ['html'],
},
},
},
{
displayName: 'Multiple Files',
name: 'multipleFiles',
type: 'boolean',
default: true,
description:
'Whether to allow the user to select multiple files from the file input or just one',
displayOptions: {
show: {
fieldType: ['file'],
},
},
},
{
displayName: 'Accepted File Types',
name: 'acceptFileTypes',
type: 'string',
default: '',
description: 'Comma-separated list of allowed file extensions',
hint: 'Leave empty to allow all file types',
placeholder: 'e.g. .jpg, .png',
displayOptions: {
show: {
fieldType: ['file'],
},
},
},
{
displayName:
"The displayed date is formatted based on the locale of the user's browser",
name: 'formatDate',
type: 'notice',
default: '',
displayOptions: {
show: {
fieldType: ['date'],
},
},
},
{
displayName: 'Required Field',
name: 'requiredField',
type: 'boolean',
default: false,
description:
'Whether to require the user to enter a value for this field before submitting the form',
displayOptions: {
hide: {
fieldType: ['html', 'hiddenField'],
},
},
},
],
},
],
},
],
},
},
};
const manualTriggerNode: LoadedClass<INodeType> = {
sourcePath: '',
type: {
@@ -1005,6 +1328,9 @@ export class NodeTypes implements INodeTypes {
nodeTypes: INodeTypeData = {
'n8n-nodes-base.stickyNote': stickyNode,
'n8n-nodes-base.set': setNode,
'n8n-nodes-base.code': codeNode,
'n8n-nodes-base.html': htmlNode,
'n8n-nodes-base.form': formNode,
'test.googleSheets': googleSheetsNode,
'test.set': setNode,
'n8n-nodes-base.executeWorkflow': executeWorkflowNode,

View File

@@ -0,0 +1,54 @@
import { mockFn } from 'jest-mock-extended';
import type { INode } from '@/index';
import { renameFormFields } from '@/node-parameters/rename-node-utils';
const makeNode = (formFieldValues: Array<Record<string, unknown>>) =>
({
parameters: {
formFields: {
values: formFieldValues,
},
},
}) as unknown as INode;
const mockMapping = mockFn();
describe('renameFormFields', () => {
beforeEach(() => {
mockMapping.mockReset();
});
it.each([
{ parameters: {} },
{ parameters: { otherField: null } },
{ parameters: { formFields: 'a' } },
{ parameters: { formFields: { values: 3 } } },
{ parameters: { formFields: { values: { newKey: true } } } },
{ parameters: { formFields: { values: [] } } },
{ parameters: { formFields: { values: [{ fieldType: 'json' }] } } },
{ parameters: { formFields: { values: [{ fieldType: 'html' }] } } },
] as unknown as INode[])('should not modify %s without formFields.values parameters', (node) => {
renameFormFields(node, mockMapping);
expect(mockMapping).not.toBeCalled();
});
it('should rename fields based on the provided mapping', () => {
const node = makeNode([{ fieldType: 'html', html: 'some text' }]);
renameFormFields(node, mockMapping);
expect(mockMapping).toBeCalledWith('some text');
});
it('should rename multiple fields', () => {
const node = makeNode([
{ fieldType: 'html', html: 'some text' },
{ fieldType: 'html', html: 'some text' },
{ fieldType: 'html', html: 'some text' },
{ fieldType: 'html', html: 'some text' },
{ fieldType: 'html', html: 'some text' },
]);
renameFormFields(node, mockMapping);
expect(mockMapping).toBeCalledTimes(5);
});
});

View File

@@ -22,6 +22,7 @@ import * as Helpers from './helpers';
interface StubNode {
name: string;
parameters: INodeParameters;
type?: string;
}
describe('Workflow', () => {
@@ -988,6 +989,109 @@ describe('Workflow', () => {
},
},
},
{
description: 'rename node with jsCode parameter',
input: {
currentName: 'Node1',
newName: 'Node1New',
nodes: [
{
name: 'Node1',
type: 'n8n-nodes-base.code',
parameters: {
jsCode: '$("Node1").params',
},
},
],
connections: {},
},
output: {
nodes: [
{
name: 'Node1New',
type: 'n8n-nodes-base.code',
parameters: {
jsCode: '$("Node1New").params',
},
},
],
connections: {},
},
},
{
description: 'rename node with html parameter',
input: {
currentName: 'Node1',
newName: 'Node1New',
nodes: [
{
name: 'Node1',
type: 'n8n-nodes-base.html',
parameters: {
html: '$("Node1").params',
},
},
],
connections: {},
},
output: {
nodes: [
{
name: 'Node1New',
type: 'n8n-nodes-base.html',
parameters: {
html: '$("Node1New").params',
},
},
],
connections: {},
},
},
{
description: 'rename form node with html parameter',
input: {
currentName: 'Node1',
newName: 'Node1New',
nodes: [
{
name: 'Node1',
type: 'n8n-nodes-base.form',
parameters: {
formFields: {
values: [
{
fieldType: 'html',
html: '$("Node1").params',
elementName: '$("Node1").params',
},
],
},
},
},
],
connections: {},
},
output: {
nodes: [
{
name: 'Node1New',
type: 'n8n-nodes-base.form',
parameters: {
formFields: {
values: [
{
fieldType: 'html',
html: '$("Node1New").params',
elementName: '$("Node1").params',
},
],
},
},
},
],
connections: {},
},
},
// This does just a basic test if "renameNodeInParameterValue" gets used. More complex
// tests with different formats and levels are in the separate tests for the function
// "renameNodeInParameterValue"
@@ -1042,7 +1146,7 @@ describe('Workflow', () => {
return {
name: stubData.name,
parameters: stubData.parameters,
type: 'test.set',
type: stubData.type ?? 'test.set',
typeVersion: 1,
id: 'uuid-1234',
position: [100, 100],