feat: Data transformation nodes and actions in Nodes Panel (#7760)
- Split Items List node into separate nodes per action - Review node descriptions - New icons - New sections in subcategories --------- Co-authored-by: Giulio Andreini <andreini@netseven.it> Co-authored-by: Deborah <deborah@starfallprojects.co.uk> Co-authored-by: Michael Kret <michael.k@radency.com>
This commit is contained in:
19
packages/nodes-base/nodes/Transform/Sort/Sort.node.json
Normal file
19
packages/nodes-base/nodes/Transform/Sort/Sort.node.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"node": "n8n-nodes-base.sort",
|
||||
"nodeVersion": "1.0",
|
||||
"codexVersion": "1.0",
|
||||
"details": "",
|
||||
"categories": ["Core Nodes"],
|
||||
"resources": {
|
||||
"primaryDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.sort/"
|
||||
}
|
||||
],
|
||||
"generic": []
|
||||
},
|
||||
"alias": ["Sort", "Order", "Transform", "Array", "List", "Item"],
|
||||
"subcategories": {
|
||||
"Core Nodes": ["Data Transformation"]
|
||||
}
|
||||
}
|
||||
285
packages/nodes-base/nodes/Transform/Sort/Sort.node.ts
Normal file
285
packages/nodes-base/nodes/Transform/Sort/Sort.node.ts
Normal file
@@ -0,0 +1,285 @@
|
||||
import get from 'lodash/get';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import lt from 'lodash/lt';
|
||||
import {
|
||||
NodeOperationError,
|
||||
type IDataObject,
|
||||
type IExecuteFunctions,
|
||||
type INodeExecutionData,
|
||||
type INodeType,
|
||||
type INodeTypeDescription,
|
||||
} from 'n8n-workflow';
|
||||
import { shuffleArray, sortByCode } from './utils';
|
||||
|
||||
export class Sort implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Sort',
|
||||
name: 'sort',
|
||||
icon: 'file:sort.svg',
|
||||
group: ['transform'],
|
||||
subtitle: '',
|
||||
version: 1,
|
||||
description: 'Change items order',
|
||||
defaults: {
|
||||
name: 'Sort',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Type',
|
||||
name: 'type',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Simple',
|
||||
value: 'simple',
|
||||
},
|
||||
{
|
||||
name: 'Random',
|
||||
value: 'random',
|
||||
},
|
||||
{
|
||||
name: 'Code',
|
||||
value: 'code',
|
||||
},
|
||||
],
|
||||
default: 'simple',
|
||||
description: 'The fields of the input items to compare to see if they are the same',
|
||||
},
|
||||
{
|
||||
displayName: 'Fields To Sort By',
|
||||
name: 'sortFieldsUi',
|
||||
type: 'fixedCollection',
|
||||
typeOptions: {
|
||||
multipleValues: true,
|
||||
},
|
||||
placeholder: 'Add Field To Sort By',
|
||||
options: [
|
||||
{
|
||||
displayName: '',
|
||||
name: 'sortField',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Field Name',
|
||||
name: 'fieldName',
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: '',
|
||||
description: 'The field to sort by',
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-placeholder-miscased-id
|
||||
placeholder: 'e.g. id',
|
||||
hint: ' Enter the field name as text',
|
||||
requiresDataPath: 'single',
|
||||
},
|
||||
{
|
||||
displayName: 'Order',
|
||||
name: 'order',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Ascending',
|
||||
value: 'ascending',
|
||||
},
|
||||
{
|
||||
name: 'Descending',
|
||||
value: 'descending',
|
||||
},
|
||||
],
|
||||
default: 'ascending',
|
||||
description: 'The order to sort by',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
default: {},
|
||||
description: 'The fields of the input items to compare to see if they are the same',
|
||||
displayOptions: {
|
||||
show: {
|
||||
type: ['simple'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Code',
|
||||
name: 'code',
|
||||
type: 'string',
|
||||
typeOptions: {
|
||||
alwaysOpenEditWindow: true,
|
||||
editor: 'code',
|
||||
rows: 10,
|
||||
},
|
||||
default: `// The two items to compare are in the variables a and b
|
||||
// Access the fields in a.json and b.json
|
||||
// Return -1 if a should go before b
|
||||
// Return 1 if b should go before a
|
||||
// Return 0 if there's no difference
|
||||
|
||||
fieldName = 'myField';
|
||||
|
||||
if (a.json[fieldName] < b.json[fieldName]) {
|
||||
return -1;
|
||||
}
|
||||
if (a.json[fieldName] > b.json[fieldName]) {
|
||||
return 1;
|
||||
}
|
||||
return 0;`,
|
||||
description: 'Javascript code to determine the order of any two items',
|
||||
displayOptions: {
|
||||
show: {
|
||||
type: ['code'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Field',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
type: ['simple'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Disable Dot Notation',
|
||||
name: 'disableDotNotation',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description:
|
||||
'Whether to disallow referencing child fields using `parent.child` in the field name',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const items = this.getInputData();
|
||||
let returnData = [...items];
|
||||
const type = this.getNodeParameter('type', 0) as string;
|
||||
const disableDotNotation = this.getNodeParameter(
|
||||
'options.disableDotNotation',
|
||||
0,
|
||||
false,
|
||||
) as boolean;
|
||||
|
||||
if (type === 'random') {
|
||||
shuffleArray(returnData);
|
||||
return [returnData];
|
||||
}
|
||||
|
||||
if (type === 'simple') {
|
||||
const sortFieldsUi = this.getNodeParameter('sortFieldsUi', 0) as IDataObject;
|
||||
const sortFields = sortFieldsUi.sortField as Array<{
|
||||
fieldName: string;
|
||||
order: 'ascending' | 'descending';
|
||||
}>;
|
||||
|
||||
if (!sortFields?.length) {
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
'No sorting specified. Please add a field to sort by',
|
||||
);
|
||||
}
|
||||
|
||||
for (const { fieldName } of sortFields) {
|
||||
let found = false;
|
||||
for (const item of items) {
|
||||
if (!disableDotNotation) {
|
||||
if (get(item.json, fieldName) !== undefined) {
|
||||
found = true;
|
||||
}
|
||||
} else if (item.json.hasOwnProperty(fieldName)) {
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
if (!found && disableDotNotation && fieldName.includes('.')) {
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
`Couldn't find the field '${fieldName}' in the input data`,
|
||||
{
|
||||
description:
|
||||
"If you're trying to use a nested field, make sure you turn off 'disable dot notation' in the node options",
|
||||
},
|
||||
);
|
||||
} else if (!found) {
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
`Couldn't find the field '${fieldName}' in the input data`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const sortFieldsWithDirection = sortFields.map((field) => ({
|
||||
name: field.fieldName,
|
||||
dir: field.order === 'ascending' ? 1 : -1,
|
||||
}));
|
||||
|
||||
returnData.sort((a, b) => {
|
||||
let result = 0;
|
||||
for (const field of sortFieldsWithDirection) {
|
||||
let equal;
|
||||
if (!disableDotNotation) {
|
||||
const _a =
|
||||
typeof get(a.json, field.name) === 'string'
|
||||
? (get(a.json, field.name) as string).toLowerCase()
|
||||
: get(a.json, field.name);
|
||||
const _b =
|
||||
typeof get(b.json, field.name) === 'string'
|
||||
? (get(b.json, field.name) as string).toLowerCase()
|
||||
: get(b.json, field.name);
|
||||
equal = isEqual(_a, _b);
|
||||
} else {
|
||||
const _a =
|
||||
typeof a.json[field.name] === 'string'
|
||||
? (a.json[field.name] as string).toLowerCase()
|
||||
: a.json[field.name];
|
||||
const _b =
|
||||
typeof b.json[field.name] === 'string'
|
||||
? (b.json[field.name] as string).toLowerCase()
|
||||
: b.json[field.name];
|
||||
equal = isEqual(_a, _b);
|
||||
}
|
||||
|
||||
if (!equal) {
|
||||
let lessThan;
|
||||
if (!disableDotNotation) {
|
||||
const _a =
|
||||
typeof get(a.json, field.name) === 'string'
|
||||
? (get(a.json, field.name) as string).toLowerCase()
|
||||
: get(a.json, field.name);
|
||||
const _b =
|
||||
typeof get(b.json, field.name) === 'string'
|
||||
? (get(b.json, field.name) as string).toLowerCase()
|
||||
: get(b.json, field.name);
|
||||
lessThan = lt(_a, _b);
|
||||
} else {
|
||||
const _a =
|
||||
typeof a.json[field.name] === 'string'
|
||||
? (a.json[field.name] as string).toLowerCase()
|
||||
: a.json[field.name];
|
||||
const _b =
|
||||
typeof b.json[field.name] === 'string'
|
||||
? (b.json[field.name] as string).toLowerCase()
|
||||
: b.json[field.name];
|
||||
lessThan = lt(_a, _b);
|
||||
}
|
||||
if (lessThan) {
|
||||
result = -1 * field.dir;
|
||||
} else {
|
||||
result = 1 * field.dir;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
});
|
||||
} else {
|
||||
returnData = sortByCode.call(this, returnData);
|
||||
}
|
||||
return [returnData];
|
||||
}
|
||||
}
|
||||
6
packages/nodes-base/nodes/Transform/Sort/sort.svg
Normal file
6
packages/nodes-base/nodes/Transform/Sort/sort.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M130.5 422.5C123.873 422.5 118.5 417.127 118.5 410.5L118.5 59.5C118.5 52.8726 123.873 47.5 130.5 47.5L154.5 47.5C161.127 47.5 166.5 52.8726 166.5 59.5L166.5 410.5C166.5 417.127 161.127 422.5 154.5 422.5L130.5 422.5Z" fill="#8287EB"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M36.0768 333.482C45.4754 324.136 60.6713 324.178 70.0178 333.577L142.5 406.464L214.982 333.577C224.329 324.178 239.525 324.136 248.923 333.482C258.322 342.829 258.364 358.025 249.018 367.423L159.518 457.423C155.013 461.953 148.888 464.5 142.5 464.5C136.112 464.5 129.987 461.953 125.482 457.423L35.9822 367.423C26.6358 358.025 26.6781 342.829 36.0768 333.482Z" fill="#8287EB"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M381.5 89.5C388.127 89.5 393.5 94.8726 393.5 101.5L393.5 452.5C393.5 459.127 388.127 464.5 381.5 464.5L357.5 464.5C350.873 464.5 345.5 459.127 345.5 452.5L345.5 101.5C345.5 94.8726 350.873 89.5 357.5 89.5L381.5 89.5Z" fill="#8287EB"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M475.923 178.518C466.525 187.864 451.329 187.822 441.982 178.423L369.5 105.536L297.018 178.423C287.671 187.822 272.475 187.864 263.077 178.518C253.678 169.171 253.636 153.975 262.982 144.577L352.482 54.5768C356.987 50.0469 363.112 47.5 369.5 47.5C375.888 47.5 382.013 50.0469 386.518 54.5768L476.018 144.577C485.364 153.975 485.322 169.171 475.923 178.518Z" fill="#8287EB"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,5 @@
|
||||
import { testWorkflows, getWorkflowFilenames } from '@test/nodes/Helpers';
|
||||
|
||||
const workflows = getWorkflowFilenames(__dirname);
|
||||
|
||||
describe('Test Sort Node', () => testWorkflows(workflows));
|
||||
213
packages/nodes-base/nodes/Transform/Sort/test/workflow.sort.json
Normal file
213
packages/nodes-base/nodes/Transform/Sort/test/workflow.sort.json
Normal file
@@ -0,0 +1,213 @@
|
||||
{
|
||||
"name": "sort test",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "6c90bf81-0c0e-4c5f-9f0c-297f06d9668a",
|
||||
"name": "When clicking \"Execute Workflow\"",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [-440, 300]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "return [\n {id: 3, char: 'c'},\n {id: 4, char: 'd'},\n {id: 5, char: 'e'},\n {id: 1, char: 'a'},\n {id: 2, char: 'b'},\n];"
|
||||
},
|
||||
"id": "2e0011d5-c6a0-4a40-ab8c-9d011cde40d5",
|
||||
"name": "Code",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 1,
|
||||
"position": [-180, 300]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"sortFieldsUi": {
|
||||
"sortField": [
|
||||
{
|
||||
"fieldName": "char",
|
||||
"order": "descending"
|
||||
},
|
||||
{
|
||||
"fieldName": "id",
|
||||
"order": "descending"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "20031848-2374-45b2-98db-69d7b8d055ad",
|
||||
"name": "Item Lists1",
|
||||
"type": "n8n-nodes-base.sort",
|
||||
"typeVersion": 1,
|
||||
"position": [80, 300]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"sortFieldsUi": {
|
||||
"sortField": [
|
||||
{
|
||||
"fieldName": "id"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "93dd8c32-21e1-4762-a340-b3e8c6866811",
|
||||
"name": "Item Lists",
|
||||
"type": "n8n-nodes-base.sort",
|
||||
"typeVersion": 1,
|
||||
"position": [80, 120]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"type": "code",
|
||||
"code": "// The two items to compare are in the variables a and b\n// Access the fields in a.json and b.json\n// Return -1 if a should go before b\n// Return 1 if b should go before a\n// Return 0 if there's no difference\n\nfieldName = 'id';\n\nif (a.json[fieldName] < b.json[fieldName]) {\n\t\treturn -1;\n}\nif (a.json[fieldName] > b.json[fieldName]) {\n\t\treturn 1;\n}\nreturn 0;"
|
||||
},
|
||||
"id": "112c72e6-b5d9-4d6d-87fc-2621fbaa5bf7",
|
||||
"name": "Item Lists2",
|
||||
"type": "n8n-nodes-base.sort",
|
||||
"typeVersion": 1,
|
||||
"position": [80, 500]
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Item Lists": [
|
||||
{
|
||||
"json": {
|
||||
"id": 1,
|
||||
"char": "a"
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"id": 2,
|
||||
"char": "b"
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"id": 3,
|
||||
"char": "c"
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"id": 4,
|
||||
"char": "d"
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"id": 5,
|
||||
"char": "e"
|
||||
}
|
||||
}
|
||||
],
|
||||
"Item Lists1": [
|
||||
{
|
||||
"json": {
|
||||
"id": 5,
|
||||
"char": "e"
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"id": 4,
|
||||
"char": "d"
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"id": 3,
|
||||
"char": "c"
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"id": 2,
|
||||
"char": "b"
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"id": 1,
|
||||
"char": "a"
|
||||
}
|
||||
}
|
||||
],
|
||||
"Item Lists2": [
|
||||
{
|
||||
"json": {
|
||||
"id": 1,
|
||||
"char": "a"
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"id": 2,
|
||||
"char": "b"
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"id": 3,
|
||||
"char": "c"
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"id": 4,
|
||||
"char": "d"
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"id": 5,
|
||||
"char": "e"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking \"Execute Workflow\"": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Code",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Code": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Item Lists1",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Item Lists",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Item Lists2",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {},
|
||||
"versionId": "6f896427-a3be-44bc-898f-c1a6f58fa1e1",
|
||||
"id": "105",
|
||||
"meta": {
|
||||
"instanceId": "36203ea1ce3cef713fa25999bd9874ae26b9e4c2c3a90a365f2882a154d031d0"
|
||||
},
|
||||
"tags": []
|
||||
}
|
||||
31
packages/nodes-base/nodes/Transform/Sort/utils.ts
Normal file
31
packages/nodes-base/nodes/Transform/Sort/utils.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { NodeVM } from '@n8n/vm2';
|
||||
import { type IExecuteFunctions, type INodeExecutionData, NodeOperationError } from 'n8n-workflow';
|
||||
|
||||
export const shuffleArray = (array: any[]) => {
|
||||
for (let i = array.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[array[i], array[j]] = [array[j], array[i]];
|
||||
}
|
||||
};
|
||||
|
||||
const returnRegExp = /\breturn\b/g;
|
||||
export function sortByCode(
|
||||
this: IExecuteFunctions,
|
||||
items: INodeExecutionData[],
|
||||
): INodeExecutionData[] {
|
||||
const code = this.getNodeParameter('code', 0) as string;
|
||||
if (!returnRegExp.test(code)) {
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
"Sort code doesn't return. Please add a 'return' statement to your code",
|
||||
);
|
||||
}
|
||||
|
||||
const mode = this.getMode();
|
||||
const vm = new NodeVM({
|
||||
console: mode === 'manual' ? 'redirect' : 'inherit',
|
||||
sandbox: { items },
|
||||
});
|
||||
|
||||
return vm.run(`module.exports = items.sort((a, b) => { ${code} })`);
|
||||
}
|
||||
Reference in New Issue
Block a user