mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 17:46:45 +00:00
feat: Checkboxes and Radio Buttons field types (#17934)
Co-authored-by: Your Name <you@example.com> Co-authored-by: Roman Davydchuk <roman.davydchuk@n8n.io>
This commit is contained in:
@@ -295,9 +295,46 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
input[type='checkbox'] {
|
input[type='checkbox'] {
|
||||||
|
appearance: none;
|
||||||
width: var(--checkbox-size);
|
width: var(--checkbox-size);
|
||||||
height: var(--checkbox-size);
|
height: var(--checkbox-size);
|
||||||
|
border: 1px solid var(--color-input-border);
|
||||||
|
border-radius: 3px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.multiselect-checkbox:checked {
|
||||||
|
background: var(--color-focus-border);
|
||||||
|
border-color: var(--color-focus-border);
|
||||||
|
}
|
||||||
|
.multiselect-checkbox:checked::after {
|
||||||
|
content: '✔';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
color: white;
|
||||||
|
font-size: var(--font-size-label);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect[data-radio-select] .multiselect-checkbox {
|
||||||
|
border-radius: 50%;
|
||||||
|
width: var(--checkbox-size);
|
||||||
|
height: var(--checkbox-size);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect[data-radio-select] .multiselect-checkbox:checked {
|
||||||
|
background: white;
|
||||||
|
border-color: var(--color-focus-border);
|
||||||
|
border-width: 4px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
.multiselect[data-radio-select] .multiselect-checkbox:checked::after {
|
||||||
|
content: '';
|
||||||
}
|
}
|
||||||
/* required field ----------------------------- */
|
/* required field ----------------------------- */
|
||||||
.form-required {
|
.form-required {
|
||||||
@@ -454,7 +491,22 @@
|
|||||||
{{#if isMultiSelect}}
|
{{#if isMultiSelect}}
|
||||||
<div>
|
<div>
|
||||||
<label class='form-label {{inputRequired}}'>{{label}}</label>
|
<label class='form-label {{inputRequired}}'>{{label}}</label>
|
||||||
<div class='multiselect {{inputRequired}}' id='{{id}}'>
|
<div
|
||||||
|
class='multiselect {{inputRequired}}'
|
||||||
|
id='{{id}}'
|
||||||
|
{{#if radioSelect}}
|
||||||
|
data-radio-select='{{radioSelect}}'
|
||||||
|
{{/if}}
|
||||||
|
{{#if exactSelectedOptions}}
|
||||||
|
data-exact-select='{{exactSelectedOptions}}'
|
||||||
|
{{/if}}
|
||||||
|
{{#if minSelectedOptions}}
|
||||||
|
data-min-select='{{minSelectedOptions}}'
|
||||||
|
{{/if}}
|
||||||
|
{{#if maxSelectedOptions}}
|
||||||
|
data-max-select='{{maxSelectedOptions}}'
|
||||||
|
{{/if}}
|
||||||
|
>
|
||||||
{{#each multiSelectOptions}}
|
{{#each multiSelectOptions}}
|
||||||
<div class='multiselect-option'>
|
<div class='multiselect-option'>
|
||||||
<input type='checkbox' class='multiselect-checkbox' id='{{id}}' />
|
<input type='checkbox' class='multiselect-checkbox' id='{{id}}' />
|
||||||
@@ -620,6 +672,15 @@
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
|
function updateError(errorElement, action = 'add', message = '') {
|
||||||
|
if(action === 'add') {
|
||||||
|
errorElement.textContent = message;
|
||||||
|
errorElement.classList.add('error-show');
|
||||||
|
} else {
|
||||||
|
errorElement.classList.remove('error-show');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function validateInput(input, errorElement) {
|
function validateInput(input, errorElement) {
|
||||||
const value = input.value.trim();
|
const value = input.value.trim();
|
||||||
const type = input.type;
|
const type = input.type;
|
||||||
@@ -628,19 +689,17 @@
|
|||||||
return validateEmailInput(value, errorElement);
|
return validateEmailInput(value, errorElement);
|
||||||
} else if (type === 'number' && value !== '') {
|
} else if (type === 'number' && value !== '') {
|
||||||
if (isNaN(value)) {
|
if (isNaN(value)) {
|
||||||
errorElement.textContent = 'Enter only numbers in this field';
|
updateError(errorElement, 'add', 'Enter only numbers in this field');
|
||||||
errorElement.classList.add('error-show');
|
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
errorElement.classList.remove('error-show');
|
updateError(errorElement, 'remove');
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
} else if (value === '') {
|
} else if (value === '') {
|
||||||
errorElement.textContent = 'This field is required';
|
updateError(errorElement, 'add', 'This field is required');
|
||||||
errorElement.classList.add('error-show');
|
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
errorElement.classList.remove('error-show');
|
updateError(errorElement, 'remove');
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -650,12 +709,11 @@
|
|||||||
const isValidEmail = regex.test(value);
|
const isValidEmail = regex.test(value);
|
||||||
|
|
||||||
if (!isValidEmail) {
|
if (!isValidEmail) {
|
||||||
errorElement.textContent = 'Enter a valid email address in this field';
|
updateError(errorElement, 'add', 'Enter a valid email address in this field');
|
||||||
errorElement.classList.add('error-show');
|
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
errorElement.textContent = 'This field is required';
|
errorElement.textContent = 'This field is required';
|
||||||
errorElement.classList.remove('error-show');
|
updateError(errorElement, 'remove');
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -674,16 +732,41 @@
|
|||||||
return selectedValues;
|
return selectedValues;
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateMultiselect(input, errorElement) {
|
function getDataValues(input) {
|
||||||
const selectedValues = getSelectedValues(input);
|
const radio = input.dataset.radioSelect ? true : false;
|
||||||
|
const exact = input.dataset.exactSelect ? parseInt(input.dataset.exactSelect, 10) : null;
|
||||||
|
const maxSelect = input.dataset.maxSelect ? parseInt(input.dataset.maxSelect, 10) : null;
|
||||||
|
const minSelect = input.dataset.minSelect ? parseInt(input.dataset.minSelect, 10) : null;
|
||||||
|
return { radio, exact, maxSelect, minSelect };
|
||||||
|
}
|
||||||
|
|
||||||
if (!selectedValues.length) {
|
function validateMultiselect(input, errorElement) {
|
||||||
errorElement.classList.add('error-show');
|
const values = getSelectedValues(input);
|
||||||
|
const data = getDataValues(input);
|
||||||
|
const required = input.classList.contains('form-required');
|
||||||
|
|
||||||
|
if (required && !values.length) {
|
||||||
|
updateError(errorElement, 'add', 'This field is required');
|
||||||
return false;
|
return false;
|
||||||
} else {
|
|
||||||
errorElement.classList.remove('error-show');
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.exact && values.length !== data.exact) {
|
||||||
|
updateError(errorElement, 'add', `You must select ${data.exact} options`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.minSelect && values.length < data.minSelect) {
|
||||||
|
updateError(errorElement, 'add', `You must select at least ${data.minSelect} options`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(data.maxSelect && values.length > data.maxSelect) {
|
||||||
|
updateError(errorElement, 'add', `You can select a maximum of ${data.maxSelect} options`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateError(errorElement, 'remove');
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const form = document.querySelector('#n8n-form');
|
const form = document.querySelector('#n8n-form');
|
||||||
@@ -726,6 +809,32 @@
|
|||||||
|
|
||||||
const requiredInputs = document.querySelectorAll('.form-required:not(label)');
|
const requiredInputs = document.querySelectorAll('.form-required:not(label)');
|
||||||
const emailInputs = document.querySelectorAll("input[type=email]");
|
const emailInputs = document.querySelectorAll("input[type=email]");
|
||||||
|
const multiselectInputs = document.querySelectorAll('.multiselect');
|
||||||
|
|
||||||
|
multiselectInputs.forEach((input) => {
|
||||||
|
const data = getDataValues(input);
|
||||||
|
const errorElement = document.querySelector(`.error-${input.id}`);
|
||||||
|
|
||||||
|
if (data.radio) {
|
||||||
|
const checkboxes = input.querySelectorAll('.multiselect-checkbox');
|
||||||
|
checkboxes.forEach((checkbox) => {
|
||||||
|
checkbox.addEventListener('change', (event) => {
|
||||||
|
if (event.target.checked) {
|
||||||
|
checkboxes.forEach((cb) => {
|
||||||
|
if (cb !== event.target) {
|
||||||
|
cb.checked = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
updateError(errorElement, 'remove');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
input.addEventListener('click', () => {
|
||||||
|
validateMultiselect(input, errorElement);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
requiredInputs.forEach((input) => {
|
requiredInputs.forEach((input) => {
|
||||||
const errorSelector = `.error-${input.id}`;
|
const errorSelector = `.error-${input.id}`;
|
||||||
@@ -740,7 +849,7 @@
|
|||||||
validateInput(input, error);
|
validateInput(input, error);
|
||||||
});
|
});
|
||||||
input.addEventListener('input', () => {
|
input.addEventListener('input', () => {
|
||||||
error.classList.remove('error-show');
|
updateError(error, 'remove');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -823,7 +932,7 @@
|
|||||||
valid.push(validateEmailInput(value, error));
|
valid.push(validateEmailInput(value, error));
|
||||||
});
|
});
|
||||||
|
|
||||||
requiredInputs.forEach((input) => {
|
[...requiredInputs, ...multiselectInputs].forEach((input) => {
|
||||||
const errorSelector = `.error-${input.id}`;
|
const errorSelector = `.error-${input.id}`;
|
||||||
const error = document.querySelector(errorSelector);
|
const error = document.querySelector(errorSelector);
|
||||||
|
|
||||||
|
|||||||
@@ -268,7 +268,9 @@ export class Form extends Node {
|
|||||||
name: 'form',
|
name: 'form',
|
||||||
icon: 'file:form.svg',
|
icon: 'file:form.svg',
|
||||||
group: ['input'],
|
group: ['input'],
|
||||||
version: 1,
|
// since trigger and node are sharing descriptions and logic we need to sync the versions
|
||||||
|
// and keep them aligned in both nodes
|
||||||
|
version: [1, 2.3],
|
||||||
description: 'Generate webforms in n8n and pass their responses to the workflow',
|
description: 'Generate webforms in n8n and pass their responses to the workflow',
|
||||||
defaults: {
|
defaults: {
|
||||||
name: 'Form',
|
name: 'Form',
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export class FormTrigger extends VersionedNodeType {
|
|||||||
icon: 'file:form.svg',
|
icon: 'file:form.svg',
|
||||||
group: ['trigger'],
|
group: ['trigger'],
|
||||||
description: 'Generate webforms in n8n and pass their responses to the workflow',
|
description: 'Generate webforms in n8n and pass their responses to the workflow',
|
||||||
defaultVersion: 2.2,
|
defaultVersion: 2.3,
|
||||||
};
|
};
|
||||||
|
|
||||||
const nodeVersions: IVersionedNodeType['nodeVersions'] = {
|
const nodeVersions: IVersionedNodeType['nodeVersions'] = {
|
||||||
@@ -20,6 +20,7 @@ export class FormTrigger extends VersionedNodeType {
|
|||||||
2: new FormTriggerV2(baseDescription),
|
2: new FormTriggerV2(baseDescription),
|
||||||
2.1: new FormTriggerV2(baseDescription),
|
2.1: new FormTriggerV2(baseDescription),
|
||||||
2.2: new FormTriggerV2(baseDescription),
|
2.2: new FormTriggerV2(baseDescription),
|
||||||
|
2.3: new FormTriggerV2(baseDescription),
|
||||||
};
|
};
|
||||||
|
|
||||||
super(nodeVersions, baseDescription);
|
super(nodeVersions, baseDescription);
|
||||||
|
|||||||
@@ -77,6 +77,10 @@ export const formFields: INodeProperties = {
|
|||||||
default: 'text',
|
default: 'text',
|
||||||
description: 'The type of field to add to the form',
|
description: 'The type of field to add to the form',
|
||||||
options: [
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Checkboxes',
|
||||||
|
value: 'checkbox',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'Custom HTML',
|
name: 'Custom HTML',
|
||||||
value: 'html',
|
value: 'html',
|
||||||
@@ -86,7 +90,7 @@ export const formFields: INodeProperties = {
|
|||||||
value: 'date',
|
value: 'date',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Dropdown List',
|
name: 'Dropdown',
|
||||||
value: 'dropdown',
|
value: 'dropdown',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -109,6 +113,10 @@ export const formFields: INodeProperties = {
|
|||||||
name: 'Password',
|
name: 'Password',
|
||||||
value: 'password',
|
value: 'password',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Radio Buttons',
|
||||||
|
value: 'radio',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'Text',
|
name: 'Text',
|
||||||
value: 'text',
|
value: 'text',
|
||||||
@@ -141,7 +149,7 @@ export const formFields: INodeProperties = {
|
|||||||
default: '',
|
default: '',
|
||||||
displayOptions: {
|
displayOptions: {
|
||||||
hide: {
|
hide: {
|
||||||
fieldType: ['dropdown', 'date', 'file', 'html', 'hiddenField'],
|
fieldType: ['dropdown', 'date', 'file', 'html', 'hiddenField', 'radio', 'checkbox'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -171,6 +179,7 @@ export const formFields: INodeProperties = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
displayName: 'Field Options',
|
displayName: 'Field Options',
|
||||||
name: 'fieldOptions',
|
name: 'fieldOptions',
|
||||||
@@ -203,6 +212,82 @@ export const formFields: INodeProperties = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Checkboxes',
|
||||||
|
name: 'fieldOptions',
|
||||||
|
placeholder: 'Add Checkbox',
|
||||||
|
type: 'fixedCollection',
|
||||||
|
default: { values: [{ option: '' }] },
|
||||||
|
required: true,
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
fieldType: ['checkbox'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
typeOptions: {
|
||||||
|
multipleValues: true,
|
||||||
|
sortable: true,
|
||||||
|
},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
displayName: 'Values',
|
||||||
|
name: 'values',
|
||||||
|
values: [
|
||||||
|
{
|
||||||
|
displayName: 'Checkbox Label',
|
||||||
|
name: 'option',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Radio Buttons',
|
||||||
|
name: 'fieldOptions',
|
||||||
|
placeholder: 'Add Radio Button',
|
||||||
|
type: 'fixedCollection',
|
||||||
|
default: { values: [{ option: '' }] },
|
||||||
|
required: true,
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
fieldType: ['radio'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
typeOptions: {
|
||||||
|
multipleValues: true,
|
||||||
|
sortable: true,
|
||||||
|
},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
displayName: 'Values',
|
||||||
|
name: 'values',
|
||||||
|
values: [
|
||||||
|
{
|
||||||
|
displayName: 'Radio Button Label',
|
||||||
|
name: 'option',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName:
|
||||||
|
'Multiple Choice is a legacy option, please use Checkboxes or Radio Buttons field type instead',
|
||||||
|
name: 'multiselectLegacyNotice',
|
||||||
|
type: 'notice',
|
||||||
|
default: '',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
multiselect: [true],
|
||||||
|
fieldType: ['dropdown'],
|
||||||
|
'@version': [{ _cnd: { lt: 2.3 } }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Multiple Choice',
|
displayName: 'Multiple Choice',
|
||||||
name: 'multiselect',
|
name: 'multiselect',
|
||||||
@@ -213,6 +298,80 @@ export const formFields: INodeProperties = {
|
|||||||
displayOptions: {
|
displayOptions: {
|
||||||
show: {
|
show: {
|
||||||
fieldType: ['dropdown'],
|
fieldType: ['dropdown'],
|
||||||
|
'@version': [{ _cnd: { lt: 2.3 } }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Limit Selection',
|
||||||
|
name: 'limitSelection',
|
||||||
|
type: 'options',
|
||||||
|
default: 'unlimited',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Exact Number',
|
||||||
|
value: 'exact',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Range',
|
||||||
|
value: 'range',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Unlimited',
|
||||||
|
value: 'unlimited',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
fieldType: ['checkbox'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Number of Selections',
|
||||||
|
name: 'numberOfSelections',
|
||||||
|
type: 'number',
|
||||||
|
default: 1,
|
||||||
|
typeOptions: {
|
||||||
|
numberPrecision: 0,
|
||||||
|
minValue: 1,
|
||||||
|
},
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
fieldType: ['checkbox'],
|
||||||
|
limitSelection: ['exact'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Minimum Selections',
|
||||||
|
name: 'minSelections',
|
||||||
|
type: 'number',
|
||||||
|
default: 0,
|
||||||
|
typeOptions: {
|
||||||
|
numberPrecision: 0,
|
||||||
|
minValue: 0,
|
||||||
|
},
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
fieldType: ['checkbox'],
|
||||||
|
limitSelection: ['range'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Maximum Selections',
|
||||||
|
name: 'maxSelections',
|
||||||
|
type: 'number',
|
||||||
|
default: 1,
|
||||||
|
typeOptions: {
|
||||||
|
numberPrecision: 0,
|
||||||
|
minValue: 1,
|
||||||
|
},
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
fieldType: ['checkbox'],
|
||||||
|
limitSelection: ['range'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,20 +1,37 @@
|
|||||||
export type FormTriggerInput = {
|
import type { GenericValue } from 'n8n-workflow';
|
||||||
isSelect?: boolean;
|
|
||||||
isMultiSelect?: boolean;
|
export type FormField = {
|
||||||
isTextarea?: boolean;
|
|
||||||
isFileInput?: boolean;
|
|
||||||
isInput?: boolean;
|
|
||||||
label: string;
|
|
||||||
defaultValue?: string;
|
|
||||||
id: string;
|
id: string;
|
||||||
errorId: string;
|
errorId: string;
|
||||||
type?: 'text' | 'number' | 'date';
|
label: string;
|
||||||
|
placeholder?: string;
|
||||||
inputRequired: 'form-required' | '';
|
inputRequired: 'form-required' | '';
|
||||||
|
type?: 'text' | 'number' | 'date' | 'email';
|
||||||
|
defaultValue: GenericValue;
|
||||||
|
|
||||||
|
isInput?: boolean;
|
||||||
|
isTextarea?: boolean;
|
||||||
|
|
||||||
|
isSelect?: boolean;
|
||||||
selectOptions?: string[];
|
selectOptions?: string[];
|
||||||
|
|
||||||
|
isMultiSelect?: boolean;
|
||||||
|
radioSelect?: 'radio';
|
||||||
|
exactSelectedOptions?: number;
|
||||||
|
minSelectedOptions?: number;
|
||||||
|
maxSelectedOptions?: number;
|
||||||
multiSelectOptions?: Array<{ id: string; label: string }>;
|
multiSelectOptions?: Array<{ id: string; label: string }>;
|
||||||
|
|
||||||
|
isFileInput?: boolean;
|
||||||
acceptFileTypes?: string;
|
acceptFileTypes?: string;
|
||||||
multipleFiles?: 'multiple' | '';
|
multipleFiles?: 'multiple' | '';
|
||||||
placeholder?: string;
|
|
||||||
|
isHtml?: boolean;
|
||||||
|
html?: string;
|
||||||
|
|
||||||
|
isHidden?: boolean;
|
||||||
|
hiddenName?: string;
|
||||||
|
hiddenValue?: GenericValue;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FormTriggerData = {
|
export type FormTriggerData = {
|
||||||
@@ -26,7 +43,7 @@ export type FormTriggerData = {
|
|||||||
formSubmittedText?: string;
|
formSubmittedText?: string;
|
||||||
redirectUrl?: string;
|
redirectUrl?: string;
|
||||||
n8nWebsiteLink: string;
|
n8nWebsiteLink: string;
|
||||||
formFields: FormTriggerInput[];
|
formFields: FormField[];
|
||||||
useResponseData?: boolean;
|
useResponseData?: boolean;
|
||||||
appendAttribution?: boolean;
|
appendAttribution?: boolean;
|
||||||
buttonLabel?: string;
|
buttonLabel?: string;
|
||||||
|
|||||||
@@ -608,6 +608,440 @@ describe('FormTrigger, prepareFormData', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('FormTrigger, prepareFormData - Checkbox and Radio Fields', () => {
|
||||||
|
it('should correctly handle checkbox fields', () => {
|
||||||
|
const formFields: FormFieldsParameter = [
|
||||||
|
{
|
||||||
|
fieldLabel: 'Hobbies',
|
||||||
|
fieldType: 'checkbox',
|
||||||
|
requiredField: false,
|
||||||
|
fieldOptions: {
|
||||||
|
values: [{ option: 'Reading' }, { option: 'Gaming' }, { option: 'Sports' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const query = { Hobbies: 'Reading,Gaming' };
|
||||||
|
|
||||||
|
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].isMultiSelect).toBe(true);
|
||||||
|
expect(result.formFields[0].multiSelectOptions).toEqual([
|
||||||
|
{ id: 'option0_field-0', label: 'Reading' },
|
||||||
|
{ id: 'option1_field-0', label: 'Gaming' },
|
||||||
|
{ id: 'option2_field-0', label: 'Sports' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly handle radio fields', () => {
|
||||||
|
const formFields: FormFieldsParameter = [
|
||||||
|
{
|
||||||
|
fieldLabel: 'Preferred Contact Method',
|
||||||
|
fieldType: 'radio',
|
||||||
|
requiredField: true,
|
||||||
|
fieldOptions: {
|
||||||
|
values: [{ option: 'Email' }, { option: 'Phone' }, { option: 'Text Message' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const query = { 'Preferred Contact Method': 'Email' };
|
||||||
|
|
||||||
|
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].radioSelect).toBe('radio');
|
||||||
|
expect(result.formFields[0].multiSelectOptions).toEqual([
|
||||||
|
{ id: 'option0_field-0', label: 'Email' },
|
||||||
|
{ id: 'option1_field-0', label: 'Phone' },
|
||||||
|
{ id: 'option2_field-0', label: 'Text Message' },
|
||||||
|
]);
|
||||||
|
expect(result.formFields[0].defaultValue).toBe('Email');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle checkbox fields with no default selection', () => {
|
||||||
|
const formFields: FormFieldsParameter = [
|
||||||
|
{
|
||||||
|
fieldLabel: 'Newsletter Subscriptions',
|
||||||
|
fieldType: 'checkbox',
|
||||||
|
requiredField: false,
|
||||||
|
fieldOptions: {
|
||||||
|
values: [{ option: 'Tech News' }, { option: 'Product Updates' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const query = {};
|
||||||
|
|
||||||
|
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].isMultiSelect).toBe(true);
|
||||||
|
expect(result.formFields[0].defaultValue).toBe('');
|
||||||
|
expect(result.formFields[0].multiSelectOptions).toEqual([
|
||||||
|
{ id: 'option0_field-0', label: 'Tech News' },
|
||||||
|
{ id: 'option1_field-0', label: 'Product Updates' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle radio fields with no default selection', () => {
|
||||||
|
const formFields: FormFieldsParameter = [
|
||||||
|
{
|
||||||
|
fieldLabel: 'Experience Level',
|
||||||
|
fieldType: 'radio',
|
||||||
|
requiredField: false,
|
||||||
|
fieldOptions: {
|
||||||
|
values: [{ option: 'Beginner' }, { option: 'Intermediate' }, { option: 'Advanced' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const query = {};
|
||||||
|
|
||||||
|
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].radioSelect).toBe('radio');
|
||||||
|
expect(result.formFields[0].defaultValue).toBe('');
|
||||||
|
expect(result.formFields[0].multiSelectOptions).toEqual([
|
||||||
|
{ id: 'option0_field-0', label: 'Beginner' },
|
||||||
|
{ id: 'option1_field-0', label: 'Intermediate' },
|
||||||
|
{ id: 'option2_field-0', label: 'Advanced' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle mixed form with checkbox, radio, and other field types', () => {
|
||||||
|
const formFields: FormFieldsParameter = [
|
||||||
|
{
|
||||||
|
fieldLabel: 'Name',
|
||||||
|
fieldType: 'text',
|
||||||
|
requiredField: true,
|
||||||
|
placeholder: 'Enter your name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldLabel: 'Skills',
|
||||||
|
fieldType: 'checkbox',
|
||||||
|
requiredField: false,
|
||||||
|
fieldOptions: {
|
||||||
|
values: [{ option: 'JavaScript' }, { option: 'Python' }, { option: 'Java' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldLabel: 'Employment Status',
|
||||||
|
fieldType: 'radio',
|
||||||
|
requiredField: true,
|
||||||
|
fieldOptions: {
|
||||||
|
values: [{ option: 'Full-time' }, { option: 'Part-time' }, { option: 'Freelancer' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const query = {
|
||||||
|
Name: 'John Doe',
|
||||||
|
Skills: 'JavaScript,Python',
|
||||||
|
'Employment Status': 'Full-time',
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = prepareFormData({
|
||||||
|
formTitle: 'Developer Survey',
|
||||||
|
formDescription: 'Tell us about yourself',
|
||||||
|
formSubmittedText: 'Thank you for participating',
|
||||||
|
redirectUrl: 'example.com/thanks',
|
||||||
|
formFields,
|
||||||
|
testRun: false,
|
||||||
|
query,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.formFields[0]).toEqual({
|
||||||
|
id: 'field-0',
|
||||||
|
errorId: 'error-field-0',
|
||||||
|
label: 'Name',
|
||||||
|
inputRequired: 'form-required',
|
||||||
|
defaultValue: 'John Doe',
|
||||||
|
placeholder: 'Enter your name',
|
||||||
|
isInput: true,
|
||||||
|
type: 'text',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.formFields[1].isMultiSelect).toBe(true);
|
||||||
|
expect(result.formFields[1].multiSelectOptions).toEqual([
|
||||||
|
{ id: 'option0_field-1', label: 'JavaScript' },
|
||||||
|
{ id: 'option1_field-1', label: 'Python' },
|
||||||
|
{ id: 'option2_field-1', label: 'Java' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(result.formFields[2].radioSelect).toBe('radio');
|
||||||
|
expect(result.formFields[2].defaultValue).toBe('Full-time');
|
||||||
|
expect(result.formFields[2].multiSelectOptions).toEqual([
|
||||||
|
{ id: 'option0_field-2', label: 'Full-time' },
|
||||||
|
{ id: 'option1_field-2', label: 'Part-time' },
|
||||||
|
{ id: 'option2_field-2', label: 'Freelancer' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle checkbox fields with unique IDs when multiple checkbox fields exist', () => {
|
||||||
|
const formFields: FormFieldsParameter = [
|
||||||
|
{
|
||||||
|
fieldLabel: 'Programming Languages',
|
||||||
|
fieldType: 'checkbox',
|
||||||
|
requiredField: false,
|
||||||
|
fieldOptions: {
|
||||||
|
values: [{ option: 'JavaScript' }, { option: 'Python' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldLabel: 'Frameworks',
|
||||||
|
fieldType: 'checkbox',
|
||||||
|
requiredField: false,
|
||||||
|
fieldOptions: {
|
||||||
|
values: [{ option: 'React' }, { option: 'Vue' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const query = {
|
||||||
|
'Programming Languages': 'JavaScript',
|
||||||
|
Frameworks: 'React,Vue',
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = prepareFormData({
|
||||||
|
formTitle: 'Tech Survey',
|
||||||
|
formDescription: 'Your tech preferences',
|
||||||
|
formSubmittedText: 'Thanks!',
|
||||||
|
redirectUrl: 'example.com',
|
||||||
|
formFields,
|
||||||
|
testRun: false,
|
||||||
|
query,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.formFields[0].multiSelectOptions).toEqual([
|
||||||
|
{ id: 'option0_field-0', label: 'JavaScript' },
|
||||||
|
{ id: 'option1_field-0', label: 'Python' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(result.formFields[1].multiSelectOptions).toEqual([
|
||||||
|
{ id: 'option0_field-1', label: 'React' },
|
||||||
|
{ id: 'option1_field-1', label: 'Vue' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle radio fields with unique IDs when multiple radio fields exist', () => {
|
||||||
|
const formFields: FormFieldsParameter = [
|
||||||
|
{
|
||||||
|
fieldLabel: 'Experience Level',
|
||||||
|
fieldType: 'radio',
|
||||||
|
requiredField: true,
|
||||||
|
fieldOptions: {
|
||||||
|
values: [{ option: 'Junior' }, { option: 'Senior' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldLabel: 'Work Preference',
|
||||||
|
fieldType: 'radio',
|
||||||
|
requiredField: true,
|
||||||
|
fieldOptions: {
|
||||||
|
values: [{ option: 'Remote' }, { option: 'Office' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const query = {
|
||||||
|
'Experience Level': 'Senior',
|
||||||
|
'Work Preference': 'Remote',
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = prepareFormData({
|
||||||
|
formTitle: 'Job Survey',
|
||||||
|
formDescription: 'Your work preferences',
|
||||||
|
formSubmittedText: 'Thanks!',
|
||||||
|
redirectUrl: 'example.com',
|
||||||
|
formFields,
|
||||||
|
testRun: false,
|
||||||
|
query,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.formFields[0].multiSelectOptions).toEqual([
|
||||||
|
{ id: 'option0_field-0', label: 'Junior' },
|
||||||
|
{ id: 'option1_field-0', label: 'Senior' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(result.formFields[1].multiSelectOptions).toEqual([
|
||||||
|
{ id: 'option0_field-1', label: 'Remote' },
|
||||||
|
{ id: 'option1_field-1', label: 'Office' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('addFormResponseDataToReturnItem - Checkbox and Radio Fields', () => {
|
||||||
|
it('should process checkbox field data correctly', () => {
|
||||||
|
const returnItem: INodeExecutionData = { json: {} };
|
||||||
|
const formFields: FormFieldsParameter = [
|
||||||
|
{
|
||||||
|
fieldLabel: 'Hobbies',
|
||||||
|
fieldType: 'checkbox',
|
||||||
|
requiredField: false,
|
||||||
|
fieldOptions: {
|
||||||
|
values: [{ option: 'Reading' }, { option: 'Gaming' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const bodyData: IDataObject = {
|
||||||
|
'field-0': '["Reading", "Gaming"]',
|
||||||
|
};
|
||||||
|
|
||||||
|
addFormResponseDataToReturnItem(returnItem, formFields, bodyData);
|
||||||
|
|
||||||
|
expect(returnItem.json.Hobbies).toEqual(['Reading', 'Gaming']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should process radio field data correctly', () => {
|
||||||
|
const returnItem: INodeExecutionData = { json: {} };
|
||||||
|
const formFields: FormFieldsParameter = [
|
||||||
|
{
|
||||||
|
fieldLabel: 'Preferred Contact',
|
||||||
|
fieldType: 'radio',
|
||||||
|
requiredField: true,
|
||||||
|
fieldOptions: {
|
||||||
|
values: [{ option: 'Email' }, { option: 'Phone' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const bodyData: IDataObject = {
|
||||||
|
'field-0': '["Email"]',
|
||||||
|
};
|
||||||
|
|
||||||
|
addFormResponseDataToReturnItem(returnItem, formFields, bodyData);
|
||||||
|
|
||||||
|
expect(returnItem.json['Preferred Contact']).toBe('Email');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle radio field with array value by taking first element', () => {
|
||||||
|
const returnItem: INodeExecutionData = { json: {} };
|
||||||
|
const formFields: FormFieldsParameter = [
|
||||||
|
{
|
||||||
|
fieldLabel: 'Priority Level',
|
||||||
|
fieldType: 'radio',
|
||||||
|
requiredField: true,
|
||||||
|
fieldOptions: {
|
||||||
|
values: [{ option: 'High' }, { option: 'Medium' }, { option: 'Low' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const bodyData: IDataObject = {
|
||||||
|
'field-0': '["High", "Medium"]',
|
||||||
|
};
|
||||||
|
|
||||||
|
addFormResponseDataToReturnItem(returnItem, formFields, bodyData);
|
||||||
|
|
||||||
|
expect(returnItem.json['Priority Level']).toBe('High');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle checkbox field with null value', () => {
|
||||||
|
const returnItem: INodeExecutionData = { json: {} };
|
||||||
|
const formFields: FormFieldsParameter = [
|
||||||
|
{
|
||||||
|
fieldLabel: 'Optional Features',
|
||||||
|
fieldType: 'checkbox',
|
||||||
|
requiredField: false,
|
||||||
|
fieldOptions: {
|
||||||
|
values: [{ option: 'Feature A' }, { option: 'Feature B' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const bodyData: IDataObject = {};
|
||||||
|
|
||||||
|
addFormResponseDataToReturnItem(returnItem, formFields, bodyData);
|
||||||
|
|
||||||
|
expect(returnItem.json['Optional Features']).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle radio field with null value', () => {
|
||||||
|
const returnItem: INodeExecutionData = { json: {} };
|
||||||
|
const formFields: FormFieldsParameter = [
|
||||||
|
{
|
||||||
|
fieldLabel: 'Rating',
|
||||||
|
fieldType: 'radio',
|
||||||
|
requiredField: false,
|
||||||
|
fieldOptions: {
|
||||||
|
values: [{ option: '1 Star' }, { option: '2 Stars' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const bodyData: IDataObject = {};
|
||||||
|
|
||||||
|
addFormResponseDataToReturnItem(returnItem, formFields, bodyData);
|
||||||
|
|
||||||
|
expect(returnItem.json.Rating).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should process mixed form data with checkbox, radio, and other fields', () => {
|
||||||
|
const returnItem: INodeExecutionData = { json: {} };
|
||||||
|
const formFields: FormFieldsParameter = [
|
||||||
|
{
|
||||||
|
fieldLabel: 'Name',
|
||||||
|
fieldType: 'text',
|
||||||
|
requiredField: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldLabel: 'Skills',
|
||||||
|
fieldType: 'checkbox',
|
||||||
|
requiredField: false,
|
||||||
|
fieldOptions: {
|
||||||
|
values: [{ option: 'JavaScript' }, { option: 'Python' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldLabel: 'Experience',
|
||||||
|
fieldType: 'radio',
|
||||||
|
requiredField: true,
|
||||||
|
fieldOptions: {
|
||||||
|
values: [{ option: 'Junior' }, { option: 'Senior' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const bodyData: IDataObject = {
|
||||||
|
'field-0': 'John Doe',
|
||||||
|
'field-1': '["JavaScript", "Python"]',
|
||||||
|
'field-2': '["Senior"]',
|
||||||
|
};
|
||||||
|
|
||||||
|
addFormResponseDataToReturnItem(returnItem, formFields, bodyData);
|
||||||
|
|
||||||
|
expect(returnItem.json.Name).toBe('John Doe');
|
||||||
|
expect(returnItem.json.Skills).toEqual(['JavaScript', 'Python']);
|
||||||
|
expect(returnItem.json.Experience).toBe('Senior');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
jest.mock('luxon', () => ({
|
jest.mock('luxon', () => ({
|
||||||
DateTime: {
|
DateTime: {
|
||||||
fromFormat: jest.fn().mockReturnValue({
|
fromFormat: jest.fn().mockReturnValue({
|
||||||
@@ -1125,6 +1559,22 @@ describe('addFormResponseDataToReturnItem', () => {
|
|||||||
expect(returnItem.json['Text Field']).toBe('hello world');
|
expect(returnItem.json['Text Field']).toBe('hello world');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should parse radio field from JSON', () => {
|
||||||
|
const formFields: FormFieldsParameter = [{ fieldLabel: 'Radio Field', fieldType: 'radio' }];
|
||||||
|
const bodyData: IDataObject = { 'field-0': '["option1"]' };
|
||||||
|
|
||||||
|
addFormResponseDataToReturnItem(returnItem, formFields, bodyData);
|
||||||
|
expect(returnItem.json['Radio Field']).toEqual('option1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse checkboxes fields from JSON', () => {
|
||||||
|
const formFields: FormFieldsParameter = [{ fieldLabel: 'Checkboxes', fieldType: 'checkbox' }];
|
||||||
|
const bodyData: IDataObject = { 'field-0': '["option1", "option2"]' };
|
||||||
|
|
||||||
|
addFormResponseDataToReturnItem(returnItem, formFields, bodyData);
|
||||||
|
expect(returnItem.json['Checkboxes']).toEqual(['option1', 'option2']);
|
||||||
|
});
|
||||||
|
|
||||||
it('should parse multiselect fields from JSON', () => {
|
it('should parse multiselect fields from JSON', () => {
|
||||||
const formFields: FormFieldsParameter = [
|
const formFields: FormFieldsParameter = [
|
||||||
{ fieldLabel: 'Multi Field', fieldType: 'text', multiselect: true },
|
{ fieldLabel: 'Multi Field', fieldType: 'text', multiselect: true },
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import { getResolvables } from '../../../utils/utilities';
|
|||||||
import { WebhookAuthorizationError } from '../../Webhook/error';
|
import { WebhookAuthorizationError } from '../../Webhook/error';
|
||||||
import { validateWebhookAuthentication } from '../../Webhook/utils';
|
import { validateWebhookAuthentication } from '../../Webhook/utils';
|
||||||
import { FORM_TRIGGER_AUTHENTICATION_PROPERTY } from '../interfaces';
|
import { FORM_TRIGGER_AUTHENTICATION_PROPERTY } from '../interfaces';
|
||||||
import type { FormTriggerData, FormTriggerInput } from '../interfaces';
|
import type { FormTriggerData, FormField } from '../interfaces';
|
||||||
|
|
||||||
export function sanitizeHtml(text: string) {
|
export function sanitizeHtml(text: string) {
|
||||||
return sanitize(text, {
|
return sanitize(text, {
|
||||||
@@ -187,7 +187,7 @@ export function prepareFormData({
|
|||||||
for (const [index, field] of formFields.entries()) {
|
for (const [index, field] of formFields.entries()) {
|
||||||
const { fieldType, requiredField, multiselect, placeholder } = field;
|
const { fieldType, requiredField, multiselect, placeholder } = field;
|
||||||
|
|
||||||
const input: IDataObject = {
|
const input: FormField = {
|
||||||
id: `field-${index}`,
|
id: `field-${index}`,
|
||||||
errorId: `error-field-${index}`,
|
errorId: `error-field-${index}`,
|
||||||
label: field.fieldLabel,
|
label: field.fieldLabel,
|
||||||
@@ -196,13 +196,22 @@ export function prepareFormData({
|
|||||||
placeholder,
|
placeholder,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (multiselect) {
|
if (multiselect || (fieldType && ['radio', 'checkbox'].includes(fieldType))) {
|
||||||
input.isMultiSelect = true;
|
input.isMultiSelect = true;
|
||||||
input.multiSelectOptions =
|
input.multiSelectOptions =
|
||||||
field.fieldOptions?.values.map((e, i) => ({
|
field.fieldOptions?.values.map((e, i) => ({
|
||||||
id: `option${i}_${input.id}`,
|
id: `option${i}_${input.id}`,
|
||||||
label: e.option,
|
label: e.option,
|
||||||
})) ?? [];
|
})) ?? [];
|
||||||
|
|
||||||
|
if (fieldType === 'radio') {
|
||||||
|
input.radioSelect = 'radio';
|
||||||
|
} else if (field.limitSelection === 'exact') {
|
||||||
|
input.exactSelectedOptions = field.numberOfSelections;
|
||||||
|
} else if (field.limitSelection === 'range') {
|
||||||
|
input.minSelectedOptions = field.minSelections;
|
||||||
|
input.maxSelectedOptions = field.maxSelections;
|
||||||
|
}
|
||||||
} else if (fieldType === 'file') {
|
} else if (fieldType === 'file') {
|
||||||
input.isFileInput = true;
|
input.isFileInput = true;
|
||||||
input.acceptFileTypes = field.acceptFileTypes;
|
input.acceptFileTypes = field.acceptFileTypes;
|
||||||
@@ -226,7 +235,7 @@ export function prepareFormData({
|
|||||||
input.type = fieldType as 'text' | 'number' | 'date' | 'email';
|
input.type = fieldType as 'text' | 'number' | 'date' | 'email';
|
||||||
}
|
}
|
||||||
|
|
||||||
formData.formFields.push(input as FormTriggerInput);
|
formData.formFields.push(input);
|
||||||
}
|
}
|
||||||
|
|
||||||
return formData;
|
return formData;
|
||||||
@@ -305,8 +314,15 @@ export function addFormResponseDataToReturnItem(
|
|||||||
if (field.fieldType === 'text') {
|
if (field.fieldType === 'text') {
|
||||||
value = String(value).trim();
|
value = String(value).trim();
|
||||||
}
|
}
|
||||||
if (field.multiselect && typeof value === 'string') {
|
if (
|
||||||
|
(field.multiselect || field.fieldType === 'checkbox' || field.fieldType === 'radio') &&
|
||||||
|
typeof value === 'string'
|
||||||
|
) {
|
||||||
value = jsonParse(value);
|
value = jsonParse(value);
|
||||||
|
|
||||||
|
if (field.fieldType === 'radio' && Array.isArray(value)) {
|
||||||
|
value = value[0];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (field.fieldType === 'date' && value && field.formatDate !== '') {
|
if (field.fieldType === 'date' && value && field.formatDate !== '') {
|
||||||
value = DateTime.fromFormat(String(value), 'yyyy-mm-dd').toFormat(field.formatDate as string);
|
value = DateTime.fromFormat(String(value), 'yyyy-mm-dd').toFormat(field.formatDate as string);
|
||||||
|
|||||||
@@ -36,7 +36,9 @@ const descriptionV2: INodeTypeDescription = {
|
|||||||
name: 'formTrigger',
|
name: 'formTrigger',
|
||||||
icon: 'file:form.svg',
|
icon: 'file:form.svg',
|
||||||
group: ['trigger'],
|
group: ['trigger'],
|
||||||
version: [2, 2.1, 2.2],
|
// since trigger and node are sharing descriptions and logic we need to sync the versions
|
||||||
|
// and keep them aligned in both nodes
|
||||||
|
version: [2, 2.1, 2.2, 2.3],
|
||||||
description: 'Generate webforms in n8n and pass their responses to the workflow',
|
description: 'Generate webforms in n8n and pass their responses to the workflow',
|
||||||
defaults: {
|
defaults: {
|
||||||
name: 'On form submission',
|
name: 'On form submission',
|
||||||
|
|||||||
@@ -2857,6 +2857,10 @@ export type FormFieldsParameter = Array<{
|
|||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
fieldName?: string;
|
fieldName?: string;
|
||||||
fieldValue?: string;
|
fieldValue?: string;
|
||||||
|
limitSelection?: 'exact' | 'range' | 'unlimited';
|
||||||
|
numberOfSelections?: number;
|
||||||
|
minSelections?: number;
|
||||||
|
maxSelections?: number;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export type FieldTypeMap = {
|
export type FieldTypeMap = {
|
||||||
|
|||||||
Reference in New Issue
Block a user