Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/cli/src/abstract-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { OnShutdown } from '@n8n/decorators';
import { Container, Service } from '@n8n/di';
import compression from 'compression';
import express from 'express';
import { engine as expressHandlebars } from 'express-handlebars';
import { readFile } from 'fs/promises';
import type { Server } from 'http';
import isbot from 'isbot';
Expand All @@ -16,6 +15,7 @@ import { ServiceUnavailableError } from '@/errors/response-errors/service-unavai
import { ExternalHooks } from '@/external-hooks';
import { rawBodyReader, bodyParser, corsMiddleware } from '@/middlewares';
import { send, sendErrorResponse } from '@/response-helper';
import { createHandlebarsEngine } from '@/utils/handlebars.util';
import { LiveWebhooks } from '@/webhooks/live-webhooks';
import { TestWebhooks } from '@/webhooks/test-webhooks';
import { WaitingForms } from '@/webhooks/waiting-forms';
Expand Down Expand Up @@ -68,7 +68,7 @@ export abstract class AbstractServer {
this.app = express();
this.app.disable('x-powered-by');
this.app.set('query parser', 'extended');
this.app.engine('handlebars', expressHandlebars({ defaultLayout: false }));
this.app.engine('handlebars', createHandlebarsEngine());
this.app.set('view engine', 'handlebars');
this.app.set('views', TEMPLATES_DIR);

Expand Down
29 changes: 29 additions & 0 deletions packages/cli/src/utils/handlebars.util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { engine as expressHandlebars } from 'express-handlebars';

/**
* Creates a configured Handlebars engine for express with custom helpers
*/
export function createHandlebarsEngine() {
return expressHandlebars({
defaultLayout: false,
helpers: {
eq: (a: unknown, b: unknown) => a === b,
includes: (arr: unknown, val: unknown) => {
if (Array.isArray(arr)) {
return arr.includes(val);
}
if (typeof arr === 'string' && typeof val === 'string') {
// Handle single string match or comma-separated strings
if (arr === val) {
return true;
}
return arr
.split(',')
.map((s) => s.trim())
.includes(val);
}
return false;
},
},
});
}
26 changes: 23 additions & 3 deletions packages/cli/templates/form-trigger.handlebars
Original file line number Diff line number Diff line change
Expand Up @@ -509,7 +509,19 @@
>
{{#each multiSelectOptions}}
<div class='multiselect-option'>
<input type='checkbox' class='multiselect-checkbox' id='{{id}}' value='{{label}}' />
{{#if ../radioSelect}}
{{#if (eq ../defaultValue label)}}
<input type='checkbox' class='multiselect-checkbox' id='{{id}}' value='{{label}}' checked />
{{else}}
<input type='checkbox' class='multiselect-checkbox' id='{{id}}' value='{{label}}' />
{{/if}}
{{else}}
{{#if (includes ../defaultValue label)}}
<input type='checkbox' class='multiselect-checkbox' id='{{id}}' value='{{label}}' checked />
{{else}}
<input type='checkbox' class='multiselect-checkbox' id='{{id}}' value='{{label}}' />
{{/if}}
{{/if}}
<label for='{{id}}'>{{label}}</label>
</div>
{{/each}}
Expand All @@ -525,9 +537,17 @@
<label class='form-label {{inputRequired}}' for='{{id}}'>{{label}}</label>
<div class='select-input'>
<select id='{{id}}' name='{{id}}' class='{{inputRequired}}'>
<option value='' disabled selected>Select an option ...</option>
{{#if defaultValue}}
<option value='' disabled>Select an option ...</option>
{{else}}
<option value='' disabled selected>Select an option ...</option>
{{/if}}
{{#each selectOptions}}
<option value='{{this}}'>{{this}}</option>
{{#if (eq ../defaultValue this)}}
<option value='{{this}}' selected>{{this}}</option>
{{else}}
<option value='{{this}}'>{{this}}</option>
{{/if}}
{{/each}}
</select>
</div>
Expand Down
12 changes: 12 additions & 0 deletions packages/nodes-base/nodes/Form/common.descriptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,18 @@ export const formFields: INodeProperties = {
},
},
},
{
displayName: 'Default Value',
name: 'defaultValue',
description: 'Default value that will be pre-filled in the form field',
type: 'string',
default: '',
displayOptions: {
hide: {
fieldType: ['dropdown', 'date', 'file', 'html', 'hiddenField', 'radio', 'checkbox'],
},
},
Comment on lines +176 to +180
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I applied the same rules as for Placeholder form field.

},
{
displayName: 'Field Value',
name: 'fieldValue',
Expand Down
81 changes: 81 additions & 0 deletions packages/nodes-base/nodes/Form/test/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1868,3 +1868,84 @@ describe('addFormResponseDataToReturnItem', () => {
expect(returnItem.json['File Field']).toEqual(['file1.pdf']);
});
});

describe('FormTrigger, prepareFormData - Default Value', () => {
it('should use defaultValue when no query parameter is provided', () => {
const formFields: FormFieldsParameter = [
{
fieldLabel: 'Name',
fieldType: 'text',
requiredField: true,
placeholder: 'Enter your name',
defaultValue: 'John Doe',
},
{
fieldLabel: 'Email',
fieldType: 'email',
requiredField: true,
placeholder: 'Enter your email',
defaultValue: '[email protected]',
},
];

const result = prepareFormData({
formTitle: 'Test Form',
formDescription: 'This is a test form',
formSubmittedText: 'Thank you',
redirectUrl: 'example.com',
formFields,
testRun: false,
query: {},
});

expect(result.formFields[0].defaultValue).toBe('John Doe');
expect(result.formFields[1].defaultValue).toBe('[email protected]');
});

it('should prioritize query parameter over defaultValue', () => {
const formFields: FormFieldsParameter = [
{
fieldLabel: 'Name',
fieldType: 'text',
requiredField: true,
defaultValue: 'Default Name',
},
];

const query = { Name: 'Query Name' };

const result = prepareFormData({
formTitle: 'Test Form',
formDescription: 'This is a test form',
formSubmittedText: 'Thank you',
redirectUrl: 'example.com',
formFields,
testRun: false,
query,
});

expect(result.formFields[0].defaultValue).toBe('Query Name');
});

it('should use empty string when neither defaultValue nor query parameter is provided', () => {
const formFields: FormFieldsParameter = [
{
fieldLabel: 'Name',
fieldType: 'text',
requiredField: true,
},
];

const result = prepareFormData({
formTitle: 'Test Form',
formDescription: 'This is a test form',
formSubmittedText: 'Thank you',
redirectUrl: 'example.com',
formFields,
testRun: false,
query: {},
});

expect(result.formFields[0].defaultValue).toBe('');
});
});
4 changes: 2 additions & 2 deletions packages/nodes-base/nodes/Form/utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,14 +197,14 @@ export function prepareFormData({
}

for (const [index, field] of formFields.entries()) {
const { fieldType, requiredField, multiselect, placeholder } = field;
const { fieldType, requiredField, multiselect, placeholder, defaultValue } = field;

const input: FormField = {
id: `field-${index}`,
errorId: `error-field-${index}`,
label: field.fieldLabel,
inputRequired: requiredField ? 'form-required' : '',
defaultValue: query[field.fieldLabel] ?? '',
defaultValue: query[field.fieldLabel] ?? defaultValue ?? '',
placeholder,
};

Expand Down
1 change: 1 addition & 0 deletions packages/workflow/src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3010,6 +3010,7 @@ export type FormFieldsParameter = Array<{
formatDate?: string;
html?: string;
placeholder?: string;
defaultValue?: string;
fieldName?: string;
fieldValue?: string;
limitSelection?: 'exact' | 'range' | 'unlimited';
Expand Down
1 change: 1 addition & 0 deletions packages/workflow/src/type-validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ const ALLOWED_FORM_FIELDS_KEYS = [
'fieldLabel',
'fieldType',
'placeholder',
'defaultValue',
'fieldOptions',
'multiselect',
'multipleFiles',
Expand Down
Loading