feat(n8n Form Page Node): New node (#10390)

Co-authored-by: Giulio Andreini <g.andreini@gmail.com>
Co-authored-by: Shireen Missi <94372015+ShireenMissi@users.noreply.github.com>
This commit is contained in:
Michael Kret
2024-10-17 16:59:53 +03:00
committed by GitHub
parent 86a94b5523
commit 643d66c0ae
39 changed files with 2101 additions and 229 deletions

View File

@@ -38,6 +38,7 @@ export const FUNCTION_NODE_TYPE = 'n8n-nodes-base.function';
export const FUNCTION_ITEM_NODE_TYPE = 'n8n-nodes-base.functionItem';
export const MERGE_NODE_TYPE = 'n8n-nodes-base.merge';
export const AI_TRANSFORM_NODE_TYPE = 'n8n-nodes-base.aiTransform';
export const FORM_NODE_TYPE = 'n8n-nodes-base.form';
export const FORM_TRIGGER_NODE_TYPE = 'n8n-nodes-base.formTrigger';
export const CHAT_TRIGGER_NODE_TYPE = '@n8n/n8n-nodes-langchain.chatTrigger';
export const WAIT_NODE_TYPE = 'n8n-nodes-base.wait';
@@ -56,6 +57,8 @@ export const SCRIPTING_NODE_TYPES = [
AI_TRANSFORM_NODE_TYPE,
];
export const ADD_FORM_NOTICE = 'addFormPage';
/**
* Nodes whose parameter values may refer to other nodes without expressions.
* Their content may need to be updated when the referenced node is renamed.

View File

@@ -1111,6 +1111,7 @@ export interface IWebhookFunctions extends FunctionsBaseWithRequiredKeys<'getMod
options?: IGetNodeParameterOptions,
): NodeParameterValueType | object;
getNodeWebhookUrl: (name: string) => string | undefined;
evaluateExpression(expression: string, itemIndex?: number): NodeParameterValueType;
getParamsData(): object;
getQueryData(): object;
getRequestObject(): express.Request;
@@ -2026,7 +2027,7 @@ export interface IWebhookResponseData {
}
export type WebhookResponseData = 'allEntries' | 'firstEntryJson' | 'firstEntryBinary' | 'noData';
export type WebhookResponseMode = 'onReceived' | 'lastNode' | 'responseNode';
export type WebhookResponseMode = 'onReceived' | 'lastNode' | 'responseNode' | 'formPage';
export interface INodeTypes {
getByName(nodeType: string): INodeType | IVersionedNodeType;
@@ -2584,6 +2585,18 @@ export interface ResourceMapperField {
readOnly?: boolean;
}
export type FormFieldsParameter = Array<{
fieldLabel: string;
fieldType?: string;
requiredField?: boolean;
fieldOptions?: { values: Array<{ option: string }> };
multiselect?: boolean;
multipleFiles?: boolean;
acceptFileTypes?: string;
formatDate?: string;
placeholder?: string;
}>;
export type FieldTypeMap = {
// eslint-disable-next-line id-denylist
boolean: boolean;
@@ -2599,6 +2612,7 @@ export type FieldTypeMap = {
options: any;
url: string;
jwt: string;
'form-fields': FormFieldsParameter;
};
export type FieldType = keyof FieldTypeMap;

View File

@@ -2,7 +2,12 @@ import isObject from 'lodash/isObject';
import { DateTime } from 'luxon';
import { ApplicationError } from './errors';
import type { FieldType, INodePropertyOptions, ValidationResult } from './Interfaces';
import type {
FieldType,
FormFieldsParameter,
INodePropertyOptions,
ValidationResult,
} from './Interfaces';
import { jsonParse } from './utils';
export const tryToParseNumber = (value: unknown): number => {
@@ -148,6 +153,96 @@ export const tryToParseObject = (value: unknown): object => {
}
};
const ALLOWED_FORM_FIELDS_KEYS = [
'fieldLabel',
'fieldType',
'placeholder',
'fieldOptions',
'multiselect',
'multipleFiles',
'acceptFileTypes',
'formatDate',
'requiredField',
];
const ALLOWED_FIELD_TYPES = [
'date',
'dropdown',
'email',
'file',
'number',
'password',
'text',
'textarea',
];
export const tryToParseJsonToFormFields = (value: unknown): FormFieldsParameter => {
const fields: FormFieldsParameter = [];
try {
const rawFields = jsonParse<Array<{ [key: string]: unknown }>>(value as string, {
acceptJSObject: true,
});
for (const [index, field] of rawFields.entries()) {
for (const key of Object.keys(field)) {
if (!ALLOWED_FORM_FIELDS_KEYS.includes(key)) {
throw new ApplicationError(`Key '${key}' in field ${index} is not valid for form fields`);
}
if (
key !== 'fieldOptions' &&
!['string', 'number', 'boolean'].includes(typeof field[key])
) {
field[key] = String(field[key]);
} else if (typeof field[key] === 'string') {
field[key] = field[key].replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
if (key === 'fieldType' && !ALLOWED_FIELD_TYPES.includes(field[key] as string)) {
throw new ApplicationError(
`Field type '${field[key] as string}' in field ${index} is not valid for form fields`,
);
}
if (key === 'fieldOptions') {
if (Array.isArray(field[key])) {
field[key] = { values: field[key] };
}
if (
typeof field[key] !== 'object' ||
!(field[key] as { [key: string]: unknown }).values
) {
throw new ApplicationError(
`Field dropdown in field ${index} does has no 'values' property that contain an array of options`,
);
}
for (const [optionIndex, option] of (
(field[key] as { [key: string]: unknown }).values as Array<{
[key: string]: { option: string };
}>
).entries()) {
if (Object.keys(option).length !== 1 || typeof option.option !== 'string') {
throw new ApplicationError(
`Field dropdown in field ${index} has an invalid option ${optionIndex}`,
);
}
}
}
}
fields.push(field as FormFieldsParameter[number]);
}
} catch (error) {
if (error instanceof ApplicationError) throw error;
throw new ApplicationError('Value is not valid JSON');
}
return fields;
};
export const getValueDescription = <T>(value: T): string => {
if (typeof value === 'object') {
if (value === null) return "'null'";
@@ -325,6 +420,16 @@ export function validateFieldType(
};
}
}
case 'form-fields': {
try {
return { valid: true, newValue: tryToParseJsonToFormFields(value) };
} catch (e) {
return {
valid: false,
errorMessage: (e as Error).message,
};
}
}
default: {
return { valid: true, newValue: value };
}

View File

@@ -1365,6 +1365,7 @@ export class WorkflowDataProxy {
$thisRunIndex: this.runIndex,
$nodeVersion: that.workflow.getNode(that.activeNodeName)?.typeVersion,
$nodeId: that.workflow.getNode(that.activeNodeName)?.id,
$webhookId: that.workflow.getNode(that.activeNodeName)?.webhookId,
};
return new Proxy(base, {

View File

@@ -121,7 +121,7 @@ type JSONParseOptions<T> = { acceptJSObject?: boolean } & MutuallyExclusive<
*
* @param {string} jsonString - The JSON string to parse.
* @param {Object} [options] - Optional settings for parsing the JSON string. Either `fallbackValue` or `errorMessage` can be set, but not both.
* @param {boolean} [options.parseJSObject=false] - If true, attempts to recover from common JSON format errors by parsing the JSON string as a JavaScript Object.
* @param {boolean} [options.acceptJSObject=false] - If true, attempts to recover from common JSON format errors by parsing the JSON string as a JavaScript Object.
* @param {string} [options.errorMessage] - A custom error message to throw if the JSON string cannot be parsed.
* @param {*} [options.fallbackValue] - A fallback value to return if the JSON string cannot be parsed.
* @returns {Object} - The parsed object, or the fallback value if parsing fails and `fallbackValue` is set.