feat: Add assignment component with drag and drop to Set node (#8283)

Co-authored-by: Giulio Andreini <andreini@netseven.it>
This commit is contained in:
Elias Meire
2024-02-06 18:34:34 +01:00
committed by GitHub
parent c04f92f7fd
commit 2799de491b
53 changed files with 3296 additions and 1060 deletions

View File

@@ -21,7 +21,7 @@ const versionDescription: INodeTypeDescription = {
name: 'set',
icon: 'fa:pen',
group: ['input'],
version: [3, 3.1, 3.2],
version: [3, 3.1, 3.2, 3.3],
description: 'Modify, add, or remove item fields',
subtitle: '={{$parameter["mode"]}}',
defaults: {
@@ -44,7 +44,7 @@ const versionDescription: INodeTypeDescription = {
action: 'Edit item fields one by one',
},
{
name: 'JSON Output',
name: 'JSON',
value: 'raw',
description: 'Customize item output with JSON',
action: 'Customize item output with JSON',
@@ -96,6 +96,11 @@ const versionDescription: INodeTypeDescription = {
type: 'options',
description: 'How to select the fields you want to include in your output items',
default: 'all',
displayOptions: {
show: {
'@version': [3, 3.1, 3.2],
},
},
options: [
{
name: 'All Input Fields',
@@ -119,6 +124,49 @@ const versionDescription: INodeTypeDescription = {
},
],
},
{
displayName: 'Include Other Input Fields',
name: 'includeOtherFields',
type: 'boolean',
default: false,
description:
"Whether to pass to the output all the input fields (along with the fields set in 'Fields to Set')",
displayOptions: {
hide: {
'@version': [3, 3.1, 3.2],
},
},
},
{
displayName: 'Input Fields to Include',
name: 'include',
type: 'options',
description: 'How to select the fields you want to include in your output items',
default: 'all',
displayOptions: {
hide: {
'@version': [3, 3.1, 3.2],
'/includeOtherFields': [false],
},
},
options: [
{
name: 'All',
value: INCLUDE.ALL,
description: 'Also include all unchanged fields from the input',
},
{
name: 'Selected',
value: INCLUDE.SELECTED,
description: 'Also include the fields listed in the parameter “Fields to Include”',
},
{
name: 'All Except',
value: INCLUDE.EXCEPT,
description: 'Exclude the fields listed in the parameter “Fields to Exclude”',
},
],
},
{
displayName: 'Fields to Include',
name: 'includeFields',
@@ -232,11 +280,16 @@ export class SetV2 implements INodeType {
}
for (let i = 0; i < items.length; i++) {
const include = this.getNodeParameter('include', i) as IncludeMods;
const includeOtherFields = this.getNodeParameter('includeOtherFields', i, false) as boolean;
const include = this.getNodeParameter('include', i, 'all') as IncludeMods;
const options = this.getNodeParameter('options', i, {});
const node = this.getNode();
options.include = include;
if (node.typeVersion >= 3.3) {
options.include = includeOtherFields ? include : 'none';
} else {
options.include = include;
}
const newItem = await setNode[mode].execute.call(
this,

View File

@@ -17,6 +17,12 @@ export type SetField = {
objectValue?: string | IDataObject;
};
export type AssignmentSetField = {
name: string;
value: unknown;
type: string;
};
export const INCLUDE = {
ALL: 'all',
NONE: 'none',

View File

@@ -4,21 +4,22 @@ import type {
IExecuteFunctions,
INode,
INodeExecutionData,
ValidationResult,
} from 'n8n-workflow';
import {
deepCopy,
ApplicationError,
NodeOperationError,
deepCopy,
jsonParse,
validateFieldType,
ApplicationError,
} from 'n8n-workflow';
import set from 'lodash/set';
import get from 'lodash/get';
import set from 'lodash/set';
import unset from 'lodash/unset';
import { getResolvables, sanitazeDataPathKey } from '../../../../utils/utilities';
import type { SetNodeOptions, SetField } from './interfaces';
import type { SetNodeOptions } from './interfaces';
import { INCLUDE } from './interfaces';
const configureFieldHelper = (dotNotation?: boolean) => {
@@ -163,46 +164,43 @@ export const parseJsonParameter = (
};
export const validateEntry = (
entry: SetField,
name: string,
type: FieldType,
value: unknown,
node: INode,
itemIndex: number,
ignoreErrors = false,
nodeVersion?: number,
) => {
let entryValue = entry[entry.type];
const name = entry.name;
if (nodeVersion && nodeVersion >= 3.2 && (entryValue === undefined || entryValue === null)) {
if (nodeVersion && nodeVersion >= 3.2 && (value === undefined || value === null)) {
return { name, value: null };
}
const entryType = entry.type.replace('Value', '') as FieldType;
const description = `To fix the error try to change the type for the field "${name}" or activate the option “Ignore Type Conversion Errors” to apply a less strict type validation`;
if (entryType === 'string') {
if (nodeVersion && nodeVersion > 3 && (entryValue === undefined || entryValue === null)) {
if (type === 'string') {
if (nodeVersion && nodeVersion > 3 && (value === undefined || value === null)) {
if (ignoreErrors) {
return { name, value: null };
} else {
throw new NodeOperationError(
node,
`'${name}' expects a ${entryType} but we got '${String(entryValue)}' [item ${itemIndex}]`,
`'${name}' expects a ${type} but we got '${String(value)}' [item ${itemIndex}]`,
{ description },
);
}
} else if (typeof entryValue === 'object') {
entryValue = JSON.stringify(entryValue);
} else if (typeof value === 'object') {
value = JSON.stringify(value);
} else {
entryValue = String(entryValue);
value = String(value);
}
}
const validationResult = validateFieldType(name, entryValue, entryType);
const validationResult = validateFieldType(name, value, type);
if (!validationResult.valid) {
if (ignoreErrors) {
validationResult.newValue = entry[entry.type];
validationResult.newValue = value as ValidationResult['newValue'];
} else {
const message = `${validationResult.errorMessage} [item ${itemIndex}]`;
throw new NodeOperationError(node, message, {
@@ -212,9 +210,10 @@ export const validateEntry = (
}
}
const value = validationResult.newValue === undefined ? null : validationResult.newValue;
return { name, value };
return {
name,
value: validationResult.newValue === undefined ? null : validationResult.newValue,
};
};
export function resolveRawData(this: IExecuteFunctions, rawData: string, i: number) {

View File

@@ -1,4 +1,6 @@
import type {
AssignmentCollectionValue,
FieldType,
IDataObject,
IExecuteFunctions,
INode,
@@ -23,6 +25,11 @@ const properties: INodeProperties[] = [
placeholder: 'Add Field',
type: 'fixedCollection',
description: 'Edit existing fields or add new ones to modify the output data',
displayOptions: {
show: {
'@version': [3, 3.1, 3.2],
},
},
typeOptions: {
multipleValues: true,
sortable: true,
@@ -156,6 +163,17 @@ const properties: INodeProperties[] = [
},
],
},
{
displayName: 'Fields to Set',
name: 'assignments',
type: 'assignmentCollection',
displayOptions: {
hide: {
'@version': [3, 3.1, 3.2],
},
},
default: {},
},
];
const displayOptions = {
@@ -175,35 +193,60 @@ export async function execute(
node: INode,
) {
try {
const fields = this.getNodeParameter('fields.values', i, []) as SetField[];
if (node.typeVersion < 3.3) {
const fields = this.getNodeParameter('fields.values', i, []) as SetField[];
const newData: IDataObject = {};
const newData: IDataObject = {};
for (const entry of fields) {
if (
entry.type === 'objectValue' &&
rawFieldsData[entry.name] !== undefined &&
entry.objectValue !== undefined &&
entry.objectValue !== null
) {
entry.objectValue = parseJsonParameter(
resolveRawData.call(this, rawFieldsData[entry.name] as string, i),
for (const entry of fields) {
if (
entry.type === 'objectValue' &&
rawFieldsData[entry.name] !== undefined &&
entry.objectValue !== undefined &&
entry.objectValue !== null
) {
entry.objectValue = parseJsonParameter(
resolveRawData.call(this, rawFieldsData[entry.name] as string, i),
node,
i,
entry.name,
);
}
const { name, value } = validateEntry(
entry.name,
entry.type.replace('Value', '') as FieldType,
entry[entry.type],
node,
i,
entry.name,
options.ignoreConversionErrors,
node.typeVersion,
);
newData[name] = value;
}
const { name, value } = validateEntry(
entry,
node,
i,
options.ignoreConversionErrors,
node.typeVersion,
);
newData[name] = value;
return composeReturnItem.call(this, i, item, newData, options);
}
const assignmentCollection = this.getNodeParameter(
'assignments',
i,
) as AssignmentCollectionValue;
const newData = Object.fromEntries(
(assignmentCollection?.assignments ?? []).map((assignment) => {
const { name, value } = validateEntry(
assignment.name,
assignment.type as FieldType,
assignment.value,
node,
i,
options.ignoreConversionErrors,
node.typeVersion,
);
return [name, value];
}),
);
return composeReturnItem.call(this, i, item, newData, options);
} catch (error) {
if (this.continueOnFail()) {

View File

@@ -13,7 +13,7 @@ import type { SetNodeOptions } from './helpers/interfaces';
const properties: INodeProperties[] = [
{
displayName: 'JSON Output',
displayName: 'JSON',
name: 'jsonOutput',
type: 'json',
typeOptions: {