Skip to content

Commit 271a9dd

Browse files
authored
feat(Form Node): Split form name and label (#22304)
1 parent 44e2bc0 commit 271a9dd

File tree

7 files changed

+348
-8
lines changed

7 files changed

+348
-8
lines changed

packages/nodes-base/nodes/Form/Form.node.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,7 @@ export class Form extends Node {
269269
group: ['input'],
270270
// since trigger and node are sharing descriptions and logic we need to sync the versions
271271
// and keep them aligned in both nodes
272-
version: [1, 2.3],
272+
version: [1, 2.3, 2.4],
273273
description: 'Generate webforms in n8n and pass their responses to the workflow',
274274
defaults: {
275275
name: 'Form',

packages/nodes-base/nodes/Form/FormTrigger.node.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export class FormTrigger extends VersionedNodeType {
1212
icon: 'file:form.svg',
1313
group: ['trigger'],
1414
description: 'Generate webforms in n8n and pass their responses to the workflow',
15-
defaultVersion: 2.3,
15+
defaultVersion: 2.4,
1616
};
1717

1818
const nodeVersions: IVersionedNodeType['nodeVersions'] = {
@@ -21,6 +21,7 @@ export class FormTrigger extends VersionedNodeType {
2121
2.1: new FormTriggerV2(baseDescription),
2222
2.2: new FormTriggerV2(baseDescription),
2323
2.3: new FormTriggerV2(baseDescription),
24+
2.4: new FormTriggerV2(baseDescription),
2425
};
2526

2627
super(nodeVersions, baseDescription);

packages/nodes-base/nodes/Form/common.descriptions.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,23 @@ export const formFields: INodeProperties = {
5858
values: [
5959
{
6060
displayName: 'Field Name',
61+
name: 'fieldName',
62+
description:
63+
'The name of the field, used in input attributes and referenced by the workflow',
64+
required: true,
65+
type: 'string',
66+
default: '',
67+
displayOptions: {
68+
hide: {
69+
fieldType: ['html'],
70+
},
71+
show: {
72+
'@version': [{ _cnd: { gte: 2.4 } }],
73+
},
74+
},
75+
},
76+
{
77+
displayName: 'Label',
6178
name: 'fieldLabel',
6279
type: 'string',
6380
default: '',
@@ -68,6 +85,26 @@ export const formFields: INodeProperties = {
6885
hide: {
6986
fieldType: ['hiddenField', 'html'],
7087
},
88+
show: {
89+
'@version': [{ _cnd: { gte: 2.4 } }],
90+
},
91+
},
92+
},
93+
{
94+
displayName: 'Field Name',
95+
name: 'fieldLabel',
96+
type: 'string',
97+
default: '',
98+
placeholder: 'e.g. What is your name?',
99+
description: 'Label that appears above the input field',
100+
required: true,
101+
displayOptions: {
102+
hide: {
103+
fieldType: ['hiddenField', 'html'],
104+
},
105+
show: {
106+
'@version': [{ _cnd: { lt: 2.4 } }],
107+
},
71108
},
72109
},
73110
{
@@ -80,6 +117,7 @@ export const formFields: INodeProperties = {
80117
displayOptions: {
81118
show: {
82119
fieldType: ['hiddenField'],
120+
'@version': [{ _cnd: { lt: 2.4 } }],
83121
},
84122
},
85123
},

packages/nodes-base/nodes/Form/test/utils.test.ts

Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1155,6 +1155,82 @@ describe('FormTrigger, prepareFormData - Checkbox and Radio Fields', () => {
11551155
{ id: 'option1_field-1', label: 'Office' },
11561156
]);
11571157
});
1158+
1159+
describe('Version 2.4+ fieldName support', () => {
1160+
it('should use fieldName for query parameters in v2.4+', () => {
1161+
const formFields: FormFieldsParameter = [
1162+
{
1163+
fieldName: 'userName',
1164+
fieldLabel: 'User Name',
1165+
fieldType: 'text',
1166+
},
1167+
];
1168+
const query: IDataObject = { userName: 'John Doe' };
1169+
1170+
const result = prepareFormData({
1171+
formTitle: 'Test Form',
1172+
formDescription: 'Test Description',
1173+
formSubmittedText: undefined,
1174+
redirectUrl: undefined,
1175+
formFields,
1176+
testRun: true,
1177+
query,
1178+
nodeVersion: 2.4,
1179+
});
1180+
1181+
expect(result.formFields[0].defaultValue).toBe('John Doe');
1182+
expect(result.formFields[0].label).toBe('User Name'); // Label should still be fieldLabel for rendering
1183+
});
1184+
1185+
it('should use fieldLabel for query parameters in v2.3 and earlier', () => {
1186+
const formFields: FormFieldsParameter = [
1187+
{
1188+
fieldName: 'userName',
1189+
fieldLabel: 'User Name',
1190+
fieldType: 'text',
1191+
},
1192+
];
1193+
const query: IDataObject = { 'User Name': 'John Doe' };
1194+
1195+
const result = prepareFormData({
1196+
formTitle: 'Test Form',
1197+
formDescription: 'Test Description',
1198+
formSubmittedText: undefined,
1199+
redirectUrl: undefined,
1200+
formFields,
1201+
testRun: true,
1202+
query,
1203+
nodeVersion: 2.3,
1204+
});
1205+
1206+
expect(result.formFields[0].defaultValue).toBe('John Doe');
1207+
expect(result.formFields[0].label).toBe('User Name');
1208+
});
1209+
1210+
it('should fallback to fieldLabel if fieldName is missing in v2.4+', () => {
1211+
const formFields: FormFieldsParameter = [
1212+
{
1213+
fieldLabel: 'User Name',
1214+
fieldType: 'text',
1215+
},
1216+
];
1217+
const query: IDataObject = { 'User Name': 'John Doe' };
1218+
1219+
const result = prepareFormData({
1220+
formTitle: 'Test Form',
1221+
formDescription: 'Test Description',
1222+
formSubmittedText: undefined,
1223+
redirectUrl: undefined,
1224+
formFields,
1225+
testRun: true,
1226+
query,
1227+
nodeVersion: 2.4,
1228+
});
1229+
1230+
expect(result.formFields[0].defaultValue).toBe('John Doe');
1231+
expect(result.formFields[0].label).toBe('User Name');
1232+
});
1233+
});
11581234
});
11591235

11601236
describe('addFormResponseDataToReturnItem - Checkbox and Radio Fields', () => {
@@ -1494,6 +1570,126 @@ describe('prepareFormReturnItem', () => {
14941570
expect(result.json.formQueryParameters).toBeUndefined();
14951571
});
14961572

1573+
describe('Version 2.4+ fieldName support', () => {
1574+
it('should use fieldName for binary property names in v2.4+', async () => {
1575+
const mockFile: Partial<MultiPartFormData.File> = {
1576+
filepath: '/tmp/uploaded-file',
1577+
originalFilename: 'test.txt',
1578+
mimetype: 'text/plain',
1579+
size: 1024,
1580+
newFilename: 'test.txt',
1581+
};
1582+
1583+
mockContext.getBodyData.mockReturnValue({
1584+
data: {},
1585+
files: { 'field-0': mockFile },
1586+
});
1587+
1588+
mockContext.getNode.mockReturnValue({
1589+
...formNode,
1590+
typeVersion: 2.4,
1591+
} as INode);
1592+
1593+
const formFields: FormFieldsParameter = [
1594+
{
1595+
fieldName: 'resume',
1596+
fieldLabel: 'Resume Upload',
1597+
fieldType: 'file',
1598+
},
1599+
];
1600+
1601+
const result = await prepareFormReturnItem(mockContext, formFields, 'test');
1602+
1603+
expect(result.binary).toBeDefined();
1604+
expect(result.binary!.resume).toBeDefined();
1605+
expect(result.binary!['Resume_Upload']).toBeUndefined();
1606+
});
1607+
1608+
it('should use fieldLabel for binary property names in v2.3 and earlier', async () => {
1609+
const mockFile: Partial<MultiPartFormData.File> = {
1610+
filepath: '/tmp/uploaded-file',
1611+
originalFilename: 'test.txt',
1612+
mimetype: 'text/plain',
1613+
size: 1024,
1614+
newFilename: 'test.txt',
1615+
};
1616+
1617+
mockContext.getBodyData.mockReturnValue({
1618+
data: {},
1619+
files: { 'field-0': mockFile },
1620+
});
1621+
1622+
mockContext.getNode.mockReturnValue({
1623+
...formNode,
1624+
typeVersion: 2.3,
1625+
} as INode);
1626+
1627+
const formFields: FormFieldsParameter = [
1628+
{
1629+
fieldName: 'resume',
1630+
fieldLabel: 'Resume Upload',
1631+
fieldType: 'file',
1632+
},
1633+
];
1634+
1635+
const result = await prepareFormReturnItem(mockContext, formFields, 'test');
1636+
1637+
expect(result.binary).toBeDefined();
1638+
expect(result.binary!['Resume_Upload']).toBeDefined();
1639+
expect(result.binary!.resume).toBeUndefined();
1640+
});
1641+
1642+
it('should use fieldName for output data keys in v2.4+', async () => {
1643+
mockContext.getBodyData.mockReturnValue({
1644+
data: { 'field-0': 'John Doe' },
1645+
files: {},
1646+
});
1647+
1648+
mockContext.getNode.mockReturnValue({
1649+
...formNode,
1650+
typeVersion: 2.4,
1651+
} as INode);
1652+
1653+
const formFields: FormFieldsParameter = [
1654+
{
1655+
fieldName: 'userName',
1656+
fieldLabel: 'User Name',
1657+
fieldType: 'text',
1658+
},
1659+
];
1660+
1661+
const result = await prepareFormReturnItem(mockContext, formFields, 'test');
1662+
1663+
expect(result.json.userName).toBe('John Doe');
1664+
expect(result.json['User Name']).toBeUndefined();
1665+
});
1666+
1667+
it('should use fieldLabel for output data keys in v2.3 and earlier', async () => {
1668+
mockContext.getBodyData.mockReturnValue({
1669+
data: { 'field-0': 'John Doe' },
1670+
files: {},
1671+
});
1672+
1673+
mockContext.getNode.mockReturnValue({
1674+
...formNode,
1675+
typeVersion: 2.3,
1676+
} as INode);
1677+
1678+
const formFields: FormFieldsParameter = [
1679+
{
1680+
fieldName: 'userName',
1681+
fieldLabel: 'User Name',
1682+
fieldType: 'text',
1683+
},
1684+
];
1685+
1686+
const result = await prepareFormReturnItem(mockContext, formFields, 'test');
1687+
1688+
expect(result.json['User Name']).toBe('John Doe');
1689+
expect(result.json.userName).toBeUndefined();
1690+
});
1691+
});
1692+
14971693
it('should return html if field name is set', async () => {
14981694
mockContext.getBodyData.mockReturnValue({
14991695
data: { 'field-0': '<div>hi</div>', 'field-1': '<h1><haha/hi>' },
@@ -1867,6 +2063,88 @@ describe('addFormResponseDataToReturnItem', () => {
18672063
addFormResponseDataToReturnItem(returnItem, formFields, bodyData);
18682064
expect(returnItem.json['File Field']).toEqual(['file1.pdf']);
18692065
});
2066+
2067+
describe('Version 2.4+ fieldName support', () => {
2068+
it('should use fieldName for output data keys in v2.4+', () => {
2069+
const formFields: FormFieldsParameter = [
2070+
{
2071+
fieldName: 'userName',
2072+
fieldLabel: 'User Name',
2073+
fieldType: 'text',
2074+
},
2075+
];
2076+
const bodyData: IDataObject = { 'field-0': 'John Doe' };
2077+
2078+
addFormResponseDataToReturnItem(returnItem, formFields, bodyData, 2.4);
2079+
2080+
expect(returnItem.json.userName).toBe('John Doe');
2081+
expect(returnItem.json['User Name']).toBeUndefined();
2082+
});
2083+
2084+
it('should use fieldLabel for output data keys in v2.3 and earlier', () => {
2085+
const formFields: FormFieldsParameter = [
2086+
{
2087+
fieldName: 'userName',
2088+
fieldLabel: 'User Name',
2089+
fieldType: 'text',
2090+
},
2091+
];
2092+
const bodyData: IDataObject = { 'field-0': 'John Doe' };
2093+
2094+
addFormResponseDataToReturnItem(returnItem, formFields, bodyData, 2.3);
2095+
2096+
expect(returnItem.json['User Name']).toBe('John Doe');
2097+
expect(returnItem.json.userName).toBeUndefined();
2098+
});
2099+
2100+
it('should fallback to fieldLabel if fieldName is missing in v2.4+', () => {
2101+
const formFields: FormFieldsParameter = [
2102+
{
2103+
fieldLabel: 'User Name',
2104+
fieldType: 'text',
2105+
},
2106+
];
2107+
const bodyData: IDataObject = { 'field-0': 'John Doe' };
2108+
2109+
addFormResponseDataToReturnItem(returnItem, formFields, bodyData, 2.4);
2110+
2111+
expect(returnItem.json['User Name']).toBe('John Doe');
2112+
});
2113+
2114+
it('should handle multiple fields with fieldName in v2.4+', () => {
2115+
const formFields: FormFieldsParameter = [
2116+
{
2117+
fieldName: 'firstName',
2118+
fieldLabel: 'First Name',
2119+
fieldType: 'text',
2120+
},
2121+
{
2122+
fieldName: 'lastName',
2123+
fieldLabel: 'Last Name',
2124+
fieldType: 'text',
2125+
},
2126+
{
2127+
fieldName: 'email',
2128+
fieldLabel: 'Email Address',
2129+
fieldType: 'email',
2130+
},
2131+
];
2132+
const bodyData: IDataObject = {
2133+
'field-0': 'John',
2134+
'field-1': 'Doe',
2135+
'field-2': '[email protected]',
2136+
};
2137+
2138+
addFormResponseDataToReturnItem(returnItem, formFields, bodyData, 2.4);
2139+
2140+
expect(returnItem.json.firstName).toBe('John');
2141+
expect(returnItem.json.lastName).toBe('Doe');
2142+
expect(returnItem.json.email).toBe('[email protected]');
2143+
expect(returnItem.json['First Name']).toBeUndefined();
2144+
expect(returnItem.json['Last Name']).toBeUndefined();
2145+
expect(returnItem.json['Email Address']).toBeUndefined();
2146+
});
2147+
});
18702148
});
18712149

18722150
describe('FormTrigger, prepareFormData - Default Value', () => {

0 commit comments

Comments
 (0)