feat(Google Sheets Node): Overhaul of node
This commit is contained in:
committed by
GitHub
parent
6eee155ecb
commit
d96d6f11db
@@ -0,0 +1,199 @@
|
||||
import { INodeProperties } from 'n8n-workflow';
|
||||
import * as append from './append.operation';
|
||||
import * as appendOrUpdate from './appendOrUpdate.operation';
|
||||
import * as clear from './clear.operation';
|
||||
import * as create from './create.operation';
|
||||
import * as del from './delete.operation';
|
||||
import * as read from './read.operation';
|
||||
import * as remove from './remove.operation';
|
||||
import * as update from './update.operation';
|
||||
|
||||
export { append, appendOrUpdate, clear, create, del as delete, read, remove, update };
|
||||
|
||||
export const descriptions: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['sheet'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Append',
|
||||
value: 'append',
|
||||
description: 'Append data to a sheet',
|
||||
action: 'Append data to a sheet',
|
||||
},
|
||||
{
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-option-name-wrong-for-upsert
|
||||
name: 'Append or Update',
|
||||
value: 'appendOrUpdate',
|
||||
description: 'Append a new row or update the current one if it already exists (upsert)',
|
||||
action: 'Append or update a sheet',
|
||||
},
|
||||
{
|
||||
name: 'Clear',
|
||||
value: 'clear',
|
||||
description: 'Clear data from a sheet',
|
||||
action: 'Clear a sheet',
|
||||
},
|
||||
{
|
||||
name: 'Create',
|
||||
value: 'create',
|
||||
description: 'Create a new sheet',
|
||||
action: 'Create a sheet',
|
||||
},
|
||||
{
|
||||
name: 'Delete',
|
||||
value: 'delete',
|
||||
description: 'Delete columns and rows from a sheet',
|
||||
action: 'Delete a sheet',
|
||||
},
|
||||
{
|
||||
name: 'Read Rows',
|
||||
value: 'read',
|
||||
description: 'Read all rows in a sheet',
|
||||
action: 'Read all rows',
|
||||
},
|
||||
{
|
||||
name: 'Remove',
|
||||
value: 'remove',
|
||||
description: 'Remove a sheet',
|
||||
action: 'Remove a sheet',
|
||||
},
|
||||
{
|
||||
name: 'Update',
|
||||
value: 'update',
|
||||
description: 'Update rows in a sheet',
|
||||
action: 'Update a sheet',
|
||||
},
|
||||
],
|
||||
default: 'read',
|
||||
},
|
||||
{
|
||||
displayName: 'Document',
|
||||
name: 'documentId',
|
||||
type: 'resourceLocator',
|
||||
default: { mode: 'list', value: '' },
|
||||
required: true,
|
||||
modes: [
|
||||
{
|
||||
displayName: 'From List',
|
||||
name: 'list',
|
||||
type: 'list',
|
||||
typeOptions: {
|
||||
searchListMethod: 'spreadSheetsSearch',
|
||||
searchable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'By URL',
|
||||
name: 'url',
|
||||
type: 'string',
|
||||
extractValue: {
|
||||
type: 'regex',
|
||||
regex:
|
||||
'https:\\/\\/(?:drive|docs)\\.google\\.com\\/\\w+\\/d\\/([0-9a-zA-Z\\-_]+)(?:\\/.*|)',
|
||||
},
|
||||
validation: [
|
||||
{
|
||||
type: 'regex',
|
||||
properties: {
|
||||
regex:
|
||||
'https:\\/\\/(?:drive|docs)\\.google.com\\/\\w+\\/d\\/([0-9a-zA-Z\\-_]+)(?:\\/.*|)',
|
||||
errorMessage: 'Not a valid Google Drive File URL',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'By ID',
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
validation: [
|
||||
{
|
||||
type: 'regex',
|
||||
properties: {
|
||||
regex: '[a-zA-Z0-9\\-_]{2,}',
|
||||
errorMessage: 'Not a valid Google Drive File ID',
|
||||
},
|
||||
},
|
||||
],
|
||||
url: '=https://docs.google.com/spreadsheets/d/{{$value}}/edit',
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['sheet'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Sheet',
|
||||
name: 'sheetName',
|
||||
type: 'resourceLocator',
|
||||
default: { mode: 'list', value: '' },
|
||||
// default: '', //empty string set to progresivly reveal fields
|
||||
required: true,
|
||||
modes: [
|
||||
{
|
||||
displayName: 'From List',
|
||||
name: 'list',
|
||||
type: 'list',
|
||||
typeOptions: {
|
||||
searchListMethod: 'sheetsSearch',
|
||||
searchable: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'By URL',
|
||||
name: 'url',
|
||||
type: 'string',
|
||||
extractValue: {
|
||||
type: 'regex',
|
||||
regex: `https:\\/\\/docs\\.google\\.com\/spreadsheets\\/d\\/[0-9a-zA-Z\\-_]+\\/edit\\#gid=([0-9]+)`,
|
||||
},
|
||||
validation: [
|
||||
{
|
||||
type: 'regex',
|
||||
properties: {
|
||||
regex: `https:\\/\\/docs\\.google\\.com\/spreadsheets\\/d\\/[0-9a-zA-Z\\-_]+\\/edit\\#gid=([0-9]+)`,
|
||||
errorMessage: 'Not a valid Sheet URL',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'By ID',
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
validation: [
|
||||
{
|
||||
type: 'regex',
|
||||
properties: {
|
||||
regex: '[0-9]{2,}',
|
||||
errorMessage: 'Not a valid Sheet ID',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['sheet'],
|
||||
operation: ['append', 'appendOrUpdate', 'clear', 'delete', 'read', 'remove', 'update'],
|
||||
},
|
||||
},
|
||||
},
|
||||
...append.description,
|
||||
...clear.description,
|
||||
...create.description,
|
||||
...del.description,
|
||||
...read.description,
|
||||
...update.description,
|
||||
...appendOrUpdate.description,
|
||||
];
|
||||
@@ -0,0 +1,188 @@
|
||||
import { IExecuteFunctions } from 'n8n-core';
|
||||
import { SheetProperties, ValueInputOption } from '../../helpers/GoogleSheets.types';
|
||||
import { IDataObject, INodeExecutionData } from 'n8n-workflow';
|
||||
import { GoogleSheet } from '../../helpers/GoogleSheet';
|
||||
import { autoMapInputData, mapFields, untilSheetSelected } from '../../helpers/GoogleSheets.utils';
|
||||
import { cellFormat, handlingExtraData } from './commonDescription';
|
||||
|
||||
export const description: SheetProperties = [
|
||||
{
|
||||
displayName: 'Data Mode',
|
||||
name: 'dataMode',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Auto-Map Input Data to Columns',
|
||||
value: 'autoMapInputData',
|
||||
description: 'Use when node input properties match destination column names',
|
||||
},
|
||||
{
|
||||
name: 'Map Each Column Below',
|
||||
value: 'defineBelow',
|
||||
description: 'Set the value for each destination column',
|
||||
},
|
||||
{
|
||||
name: 'Nothing',
|
||||
value: 'nothing',
|
||||
description: 'Do not send anything',
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['sheet'],
|
||||
operation: ['append'],
|
||||
},
|
||||
hide: {
|
||||
...untilSheetSelected,
|
||||
},
|
||||
},
|
||||
default: 'defineBelow',
|
||||
description: 'Whether to insert the input data this node receives in the new row',
|
||||
},
|
||||
{
|
||||
displayName:
|
||||
"In this mode, make sure the incoming data is named the same as the columns in your Sheet. (Use a 'set' node before this node to change it if required.)",
|
||||
name: 'autoMapNotice',
|
||||
type: 'notice',
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: ['append'],
|
||||
dataMode: ['autoMapInputData'],
|
||||
},
|
||||
hide: {
|
||||
...untilSheetSelected,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Fields to Send',
|
||||
name: 'fieldsUi',
|
||||
placeholder: 'Add Field',
|
||||
type: 'fixedCollection',
|
||||
typeOptions: {
|
||||
multipleValueButtonText: 'Add Field to Send',
|
||||
multipleValues: true,
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['sheet'],
|
||||
operation: ['append'],
|
||||
dataMode: ['defineBelow'],
|
||||
},
|
||||
hide: {
|
||||
...untilSheetSelected,
|
||||
},
|
||||
},
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Field',
|
||||
name: 'fieldValues',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Field Name or ID',
|
||||
name: 'fieldId',
|
||||
type: 'options',
|
||||
description:
|
||||
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>',
|
||||
typeOptions: {
|
||||
loadOptionsDependsOn: ['sheetName.value'],
|
||||
loadOptionsMethod: 'getSheetHeaderRowAndSkipEmpty',
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Field Value',
|
||||
name: 'fieldValue',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Option',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['sheet'],
|
||||
operation: ['append'],
|
||||
},
|
||||
hide: {
|
||||
...untilSheetSelected,
|
||||
},
|
||||
},
|
||||
options: [
|
||||
...cellFormat,
|
||||
{
|
||||
displayName: 'Data Location on Sheet',
|
||||
name: 'locationDefine',
|
||||
type: 'fixedCollection',
|
||||
placeholder: 'Select Range',
|
||||
default: { values: {} },
|
||||
options: [
|
||||
{
|
||||
displayName: 'Values',
|
||||
name: 'values',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Header Row',
|
||||
name: 'headerRow',
|
||||
type: 'number',
|
||||
typeOptions: {
|
||||
minValue: 1,
|
||||
},
|
||||
default: 1,
|
||||
description:
|
||||
'Index of the row which contains the keys. Starts at 1. The incoming node data is matched to the keys for assignment. The matching is case sensitive.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
...handlingExtraData,
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export async function execute(
|
||||
this: IExecuteFunctions,
|
||||
sheet: GoogleSheet,
|
||||
sheetName: string,
|
||||
): Promise<INodeExecutionData[]> {
|
||||
const items = this.getInputData();
|
||||
const dataMode = this.getNodeParameter('dataMode', 0) as string;
|
||||
|
||||
if (!items.length || dataMode === 'nothing') return [];
|
||||
|
||||
const options = this.getNodeParameter('options', 0, {}) as IDataObject;
|
||||
const locationDefine = ((options.locationDefine as IDataObject) || {}).values as IDataObject;
|
||||
|
||||
let headerRow = 1;
|
||||
if (locationDefine && locationDefine.headerRow) {
|
||||
headerRow = locationDefine.headerRow as number;
|
||||
}
|
||||
|
||||
let setData: IDataObject[] = [];
|
||||
|
||||
if (dataMode === 'autoMapInputData') {
|
||||
setData = await autoMapInputData.call(this, sheetName, sheet, items, options);
|
||||
} else {
|
||||
setData = mapFields.call(this, items.length);
|
||||
}
|
||||
|
||||
await sheet.appendSheetData(
|
||||
setData,
|
||||
sheetName,
|
||||
headerRow,
|
||||
(options.cellFormat as ValueInputOption) || 'RAW',
|
||||
false,
|
||||
);
|
||||
|
||||
return items;
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
import { IExecuteFunctions } from 'n8n-core';
|
||||
import { ISheetUpdateData, SheetProperties } from '../../helpers/GoogleSheets.types';
|
||||
import { IDataObject, INodeExecutionData, NodeOperationError } from 'n8n-workflow';
|
||||
import { GoogleSheet } from '../../helpers/GoogleSheet';
|
||||
import { ValueInputOption, ValueRenderOption } from '../../helpers/GoogleSheets.types';
|
||||
import { untilSheetSelected } from '../../helpers/GoogleSheets.utils';
|
||||
import { cellFormat, handlingExtraData, locationDefine } from './commonDescription';
|
||||
|
||||
export const description: SheetProperties = [
|
||||
{
|
||||
displayName: 'Data Mode',
|
||||
name: 'dataMode',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Auto-Map Input Data to Columns',
|
||||
value: 'autoMapInputData',
|
||||
description: 'Use when node input properties match destination column names',
|
||||
},
|
||||
{
|
||||
name: 'Map Each Column Below',
|
||||
value: 'defineBelow',
|
||||
description: 'Set the value for each destination column',
|
||||
},
|
||||
{
|
||||
name: 'Nothing',
|
||||
value: 'nothing',
|
||||
description: 'Do not send anything',
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['sheet'],
|
||||
operation: ['appendOrUpdate'],
|
||||
},
|
||||
hide: {
|
||||
...untilSheetSelected,
|
||||
},
|
||||
},
|
||||
default: 'defineBelow',
|
||||
description: 'Whether to insert the input data this node receives in the new row',
|
||||
},
|
||||
{
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased, n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
|
||||
displayName: 'Column to match on',
|
||||
name: 'columnToMatchOn',
|
||||
type: 'options',
|
||||
description:
|
||||
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>',
|
||||
typeOptions: {
|
||||
loadOptionsDependsOn: ['sheetName.value'],
|
||||
loadOptionsMethod: 'getSheetHeaderRowAndSkipEmpty',
|
||||
},
|
||||
default: '',
|
||||
hint: "Used to find the correct row to update. Doesn't get changed.",
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['sheet'],
|
||||
operation: ['appendOrUpdate'],
|
||||
},
|
||||
hide: {
|
||||
...untilSheetSelected,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Value of Column to Match On',
|
||||
name: 'valueToMatchOn',
|
||||
type: 'string',
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['sheet'],
|
||||
operation: ['appendOrUpdate'],
|
||||
dataMode: ['defineBelow'],
|
||||
},
|
||||
hide: {
|
||||
...untilSheetSelected,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Values to Send',
|
||||
name: 'fieldsUi',
|
||||
placeholder: 'Add Field',
|
||||
type: 'fixedCollection',
|
||||
typeOptions: {
|
||||
multipleValues: true,
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['sheet'],
|
||||
operation: ['appendOrUpdate'],
|
||||
dataMode: ['defineBelow'],
|
||||
},
|
||||
hide: {
|
||||
...untilSheetSelected,
|
||||
},
|
||||
},
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Field',
|
||||
name: 'values',
|
||||
values: [
|
||||
{
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
|
||||
displayName: 'Column',
|
||||
name: 'column',
|
||||
type: 'options',
|
||||
description:
|
||||
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>',
|
||||
typeOptions: {
|
||||
loadOptionsDependsOn: ['sheetName.value', 'columnToMatchOn'],
|
||||
loadOptionsMethod: 'getSheetHeaderRowAndAddColumn',
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Column Name',
|
||||
name: 'columnName',
|
||||
type: 'string',
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
column: ['newColumn'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Value',
|
||||
name: 'fieldValue',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Option',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['sheet'],
|
||||
operation: ['appendOrUpdate'],
|
||||
},
|
||||
hide: {
|
||||
...untilSheetSelected,
|
||||
},
|
||||
},
|
||||
options: [...cellFormat, ...locationDefine, ...handlingExtraData],
|
||||
},
|
||||
];
|
||||
|
||||
export async function execute(
|
||||
this: IExecuteFunctions,
|
||||
sheet: GoogleSheet,
|
||||
sheetName: string,
|
||||
): Promise<INodeExecutionData[]> {
|
||||
const items = this.getInputData();
|
||||
const valueInputMode = this.getNodeParameter('options.cellFormat', 0, 'RAW') as ValueInputOption;
|
||||
const range = `${sheetName}!A:Z`;
|
||||
|
||||
const options = this.getNodeParameter('options', 0, {}) as IDataObject;
|
||||
|
||||
const valueRenderMode = (options.valueRenderMode || 'UNFORMATTED_VALUE') as ValueRenderOption;
|
||||
|
||||
const locationDefine = ((options.locationDefine as IDataObject) || {}).values as IDataObject;
|
||||
|
||||
let headerRow = 0;
|
||||
let firstDataRow = 1;
|
||||
|
||||
if (locationDefine) {
|
||||
if (locationDefine.headerRow) {
|
||||
headerRow = parseInt(locationDefine.headerRow as string, 10) - 1;
|
||||
}
|
||||
if (locationDefine.firstDataRow) {
|
||||
firstDataRow = parseInt(locationDefine.firstDataRow as string, 10) - 1;
|
||||
}
|
||||
}
|
||||
|
||||
let columnNames: string[] = [];
|
||||
|
||||
const sheetData = await sheet.getData(sheetName, 'FORMATTED_VALUE');
|
||||
|
||||
if (sheetData === undefined || sheetData[headerRow] === undefined) {
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
`Could not retrieve the column names from row ${headerRow + 1}`,
|
||||
);
|
||||
}
|
||||
|
||||
columnNames = sheetData[headerRow];
|
||||
const newColumns = new Set<string>();
|
||||
|
||||
const columnToMatchOn = this.getNodeParameter('columnToMatchOn', 0) as string;
|
||||
const keyIndex = columnNames.indexOf(columnToMatchOn);
|
||||
|
||||
const columnValues = await sheet.getColumnValues(
|
||||
range,
|
||||
keyIndex,
|
||||
firstDataRow,
|
||||
valueRenderMode,
|
||||
sheetData,
|
||||
);
|
||||
|
||||
const updateData: ISheetUpdateData[] = [];
|
||||
const appendData: IDataObject[] = [];
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const dataMode = this.getNodeParameter('dataMode', i) as
|
||||
| 'defineBelow'
|
||||
| 'autoMapInputData'
|
||||
| 'nothing';
|
||||
|
||||
if (dataMode === 'nothing') continue;
|
||||
|
||||
const data: IDataObject[] = [];
|
||||
|
||||
if (dataMode === 'autoMapInputData') {
|
||||
const handlingExtraData = (options.handlingExtraData as string) || 'insertInNewColumn';
|
||||
if (handlingExtraData === 'ignoreIt') {
|
||||
data.push(items[i].json);
|
||||
}
|
||||
if (handlingExtraData === 'error') {
|
||||
Object.keys(items[i].json).forEach((key) => {
|
||||
if (columnNames.includes(key) === false) {
|
||||
throw new NodeOperationError(this.getNode(), `Unexpected fields in node input`, {
|
||||
itemIndex: i,
|
||||
description: `The input field '${key}' doesn't match any column in the Sheet. You can ignore this by changing the 'Handling extra data' field, which you can find under 'Options'.`,
|
||||
});
|
||||
}
|
||||
});
|
||||
data.push(items[i].json);
|
||||
}
|
||||
if (handlingExtraData === 'insertInNewColumn') {
|
||||
Object.keys(items[i].json).forEach((key) => {
|
||||
if (columnNames.includes(key) === false) {
|
||||
newColumns.add(key);
|
||||
}
|
||||
});
|
||||
data.push(items[i].json);
|
||||
}
|
||||
} else {
|
||||
const valueToMatchOn = this.getNodeParameter('valueToMatchOn', i) as string;
|
||||
|
||||
const fields = (this.getNodeParameter('fieldsUi.values', i, {}) as IDataObject[]).reduce(
|
||||
(acc, entry) => {
|
||||
if (entry.column === 'newColumn') {
|
||||
const columnName = entry.columnName as string;
|
||||
|
||||
if (columnNames.includes(columnName) === false) {
|
||||
newColumns.add(columnName);
|
||||
}
|
||||
|
||||
acc[columnName] = entry.fieldValue as string;
|
||||
} else {
|
||||
acc[entry.column as string] = entry.fieldValue as string;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{} as IDataObject,
|
||||
);
|
||||
|
||||
fields[columnToMatchOn] = valueToMatchOn;
|
||||
|
||||
data.push(fields);
|
||||
}
|
||||
|
||||
if (newColumns.size) {
|
||||
await sheet.updateRows(
|
||||
sheetName,
|
||||
[columnNames.concat([...newColumns])],
|
||||
(options.cellFormat as ValueInputOption) || 'RAW',
|
||||
headerRow + 1,
|
||||
);
|
||||
}
|
||||
|
||||
const preparedData = await sheet.prepareDataForUpdateOrUpsert(
|
||||
data,
|
||||
columnToMatchOn,
|
||||
range,
|
||||
headerRow,
|
||||
firstDataRow,
|
||||
valueRenderMode,
|
||||
true,
|
||||
[columnNames.concat([...newColumns])],
|
||||
columnValues,
|
||||
);
|
||||
|
||||
updateData.push(...preparedData.updateData);
|
||||
appendData.push(...preparedData.appendData);
|
||||
}
|
||||
|
||||
if (updateData.length) {
|
||||
await sheet.batchUpdate(updateData, valueInputMode);
|
||||
}
|
||||
if (appendData.length) {
|
||||
const lastRow = sheetData.length + 1;
|
||||
await sheet.appendSheetData(
|
||||
appendData,
|
||||
range,
|
||||
headerRow + 1,
|
||||
valueInputMode,
|
||||
false,
|
||||
[columnNames.concat([...newColumns])],
|
||||
lastRow,
|
||||
);
|
||||
}
|
||||
return items;
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
import { IExecuteFunctions } from 'n8n-core';
|
||||
import { INodeExecutionData } from 'n8n-workflow';
|
||||
import { SheetProperties } from '../../helpers/GoogleSheets.types';
|
||||
import { GoogleSheet } from '../../helpers/GoogleSheet';
|
||||
import {
|
||||
getColumnName,
|
||||
getColumnNumber,
|
||||
untilSheetSelected,
|
||||
} from '../../helpers/GoogleSheets.utils';
|
||||
|
||||
export const description: SheetProperties = [
|
||||
{
|
||||
displayName: 'Clear',
|
||||
name: 'clear',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Whole Sheet',
|
||||
value: 'wholeSheet',
|
||||
},
|
||||
{
|
||||
name: 'Specific Rows',
|
||||
value: 'specificRows',
|
||||
},
|
||||
{
|
||||
name: 'Specific Columns',
|
||||
value: 'specificColumns',
|
||||
},
|
||||
{
|
||||
name: 'Specific Range',
|
||||
value: 'specificRange',
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['sheet'],
|
||||
operation: ['clear'],
|
||||
},
|
||||
hide: {
|
||||
...untilSheetSelected,
|
||||
},
|
||||
},
|
||||
default: 'wholeSheet',
|
||||
description: 'What to clear',
|
||||
},
|
||||
{
|
||||
displayName: 'Keep First Row',
|
||||
name: 'keepFirstRow',
|
||||
type: 'boolean',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['sheet'],
|
||||
operation: ['clear'],
|
||||
clear: ['wholeSheet'],
|
||||
},
|
||||
hide: {
|
||||
...untilSheetSelected,
|
||||
},
|
||||
},
|
||||
default: false,
|
||||
},
|
||||
{
|
||||
displayName: 'Start Row Number',
|
||||
name: 'startIndex',
|
||||
type: 'number',
|
||||
typeOptions: {
|
||||
minValue: 1,
|
||||
},
|
||||
default: 1,
|
||||
description: 'The row number to delete from, The first row is 1',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['sheet'],
|
||||
operation: ['clear'],
|
||||
clear: ['specificRows'],
|
||||
},
|
||||
hide: {
|
||||
...untilSheetSelected,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Number of Rows to Delete',
|
||||
name: 'rowsToDelete',
|
||||
type: 'number',
|
||||
typeOptions: {
|
||||
minValue: 1,
|
||||
},
|
||||
default: 1,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['sheet'],
|
||||
operation: ['clear'],
|
||||
clear: ['specificRows'],
|
||||
},
|
||||
hide: {
|
||||
...untilSheetSelected,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
displayName: 'Start Column',
|
||||
name: 'startIndex',
|
||||
type: 'string',
|
||||
default: 'A',
|
||||
description: 'The column to delete',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['sheet'],
|
||||
operation: ['clear'],
|
||||
clear: ['specificColumns'],
|
||||
},
|
||||
hide: {
|
||||
...untilSheetSelected,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// Could this be better as "end column"?
|
||||
displayName: 'Number of Columns to Delete',
|
||||
name: 'columnsToDelete',
|
||||
type: 'number',
|
||||
typeOptions: {
|
||||
minValue: 1,
|
||||
},
|
||||
default: 1,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['sheet'],
|
||||
operation: ['clear'],
|
||||
clear: ['specificColumns'],
|
||||
},
|
||||
hide: {
|
||||
...untilSheetSelected,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Range',
|
||||
name: 'range',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['sheet'],
|
||||
operation: ['clear'],
|
||||
clear: ['specificRange'],
|
||||
},
|
||||
hide: {
|
||||
...untilSheetSelected,
|
||||
},
|
||||
},
|
||||
default: 'A:F',
|
||||
required: true,
|
||||
description:
|
||||
'The table range to read from or to append data to. See the Google <a href="https://developers.google.com/sheets/api/guides/values#writing">documentation</a> for the details. If it contains multiple sheets it can also be added like this: "MySheet!A:F"',
|
||||
},
|
||||
];
|
||||
|
||||
export async function execute(
|
||||
this: IExecuteFunctions,
|
||||
sheet: GoogleSheet,
|
||||
sheetName: string,
|
||||
): Promise<INodeExecutionData[]> {
|
||||
const items = this.getInputData();
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const clearType = this.getNodeParameter('clear', i) as string;
|
||||
const keepFirstRow = this.getNodeParameter('keepFirstRow', i, false) as boolean;
|
||||
let range = '';
|
||||
|
||||
if (clearType === 'specificRows') {
|
||||
const startIndex = this.getNodeParameter('startIndex', i) as number;
|
||||
const rowsToDelete = this.getNodeParameter('rowsToDelete', i) as number;
|
||||
const endIndex = rowsToDelete === 1 ? startIndex : startIndex + rowsToDelete - 1;
|
||||
|
||||
range = `${sheetName}!${startIndex}:${endIndex}`;
|
||||
}
|
||||
|
||||
if (clearType === 'specificColumns') {
|
||||
const startIndex = this.getNodeParameter('startIndex', i) as string;
|
||||
const columnsToDelete = this.getNodeParameter('columnsToDelete', i) as number;
|
||||
const columnNumber = getColumnNumber(startIndex);
|
||||
const endIndex = columnsToDelete === 1 ? columnNumber : columnNumber + columnsToDelete - 1;
|
||||
|
||||
range = `${sheetName}!${startIndex}:${getColumnName(endIndex)}`;
|
||||
}
|
||||
|
||||
if (clearType === 'specificRange') {
|
||||
const rangeField = this.getNodeParameter('range', i) as string;
|
||||
const region = rangeField.includes('!') ? rangeField.split('!')[1] || '' : rangeField;
|
||||
|
||||
range = `${sheetName}!${region}`;
|
||||
}
|
||||
|
||||
if (clearType === 'wholeSheet') {
|
||||
range = sheetName;
|
||||
}
|
||||
|
||||
if (keepFirstRow) {
|
||||
const firstRow = await sheet.getData(`${range}!1:1`, 'FORMATTED_VALUE');
|
||||
await sheet.clearData(range);
|
||||
await sheet.updateRows(range, firstRow as string[][], 'RAW', 1);
|
||||
} else {
|
||||
await sheet.clearData(range);
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
import { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
export const dataLocationOnSheet: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Data Location on Sheet',
|
||||
name: 'dataLocationOnSheet',
|
||||
type: 'fixedCollection',
|
||||
placeholder: 'Select Range',
|
||||
default: { values: { rangeDefinition: 'detectAutomatically' } },
|
||||
options: [
|
||||
{
|
||||
displayName: 'Values',
|
||||
name: 'values',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Range Definition',
|
||||
name: 'rangeDefinition',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Detect Automatically',
|
||||
value: 'detectAutomatically',
|
||||
description: 'Automatically detect the data range',
|
||||
},
|
||||
{
|
||||
name: 'Specify Range (A1 Notation)',
|
||||
value: 'specifyRangeA1',
|
||||
description: 'Manually specify the data range',
|
||||
},
|
||||
{
|
||||
name: 'Specify Range (Rows)',
|
||||
value: 'specifyRange',
|
||||
description: 'Manually specify the data range',
|
||||
},
|
||||
],
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Read Rows Until',
|
||||
name: 'readRowsUntil',
|
||||
type: 'options',
|
||||
default: 'lastRowInSheet',
|
||||
options: [
|
||||
{
|
||||
name: 'First Empty Row',
|
||||
value: 'firstEmptyRow',
|
||||
},
|
||||
{
|
||||
name: 'Last Row In Sheet',
|
||||
value: 'lastRowInSheet',
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
rangeDefinition: ['detectAutomatically'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Header Row',
|
||||
name: 'headerRow',
|
||||
type: 'number',
|
||||
typeOptions: {
|
||||
minValue: 1,
|
||||
},
|
||||
default: 1,
|
||||
description:
|
||||
'Index of the row which contains the keys. Starts at 1. The incoming node data is matched to the keys for assignment. The matching is case sensitive.',
|
||||
hint: 'From start of range. First row is row 1',
|
||||
displayOptions: {
|
||||
show: {
|
||||
rangeDefinition: ['specifyRange'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'First Data Row',
|
||||
name: 'firstDataRow',
|
||||
type: 'number',
|
||||
typeOptions: {
|
||||
minValue: 1,
|
||||
},
|
||||
default: 2,
|
||||
description:
|
||||
'Index of the first row which contains the actual data and not the keys. Starts with 1.',
|
||||
hint: 'From start of range. First row is row 1',
|
||||
displayOptions: {
|
||||
show: {
|
||||
rangeDefinition: ['specifyRange'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Range',
|
||||
name: 'range',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'A:Z',
|
||||
description:
|
||||
'The table range to read from or to append data to. See the Google <a href="https://developers.google.com/sheets/api/guides/values#writing">documentation</a> for the details.',
|
||||
hint: 'You can specify both the rows and the columns, e.g. C4:E7',
|
||||
displayOptions: {
|
||||
show: {
|
||||
rangeDefinition: ['specifyRangeA1'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const locationDefine: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Data Location on Sheet',
|
||||
name: 'locationDefine',
|
||||
type: 'fixedCollection',
|
||||
placeholder: 'Select Range',
|
||||
default: { values: {} },
|
||||
options: [
|
||||
{
|
||||
displayName: 'Values',
|
||||
name: 'values',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Header Row',
|
||||
name: 'headerRow',
|
||||
type: 'number',
|
||||
typeOptions: {
|
||||
minValue: 1,
|
||||
},
|
||||
default: 1,
|
||||
description:
|
||||
'Index of the row which contains the keys. Starts at 1. The incoming node data is matched to the keys for assignment. The matching is case sensitive.',
|
||||
hint: 'From start of range. First row is row 1',
|
||||
},
|
||||
{
|
||||
displayName: 'First Data Row',
|
||||
name: 'firstDataRow',
|
||||
type: 'number',
|
||||
typeOptions: {
|
||||
minValue: 1,
|
||||
},
|
||||
default: 2,
|
||||
description:
|
||||
'Index of the first row which contains the actual data and not the keys. Starts with 1.',
|
||||
hint: 'From start of range. First row is row 1',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const outputFormatting: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Output Formatting',
|
||||
name: 'outputFormatting',
|
||||
type: 'fixedCollection',
|
||||
placeholder: 'Add Formatting',
|
||||
default: { values: { general: 'UNFORMATTED_VALUE', date: 'FORMATTED_STRING' } },
|
||||
options: [
|
||||
{
|
||||
displayName: 'Values',
|
||||
name: 'values',
|
||||
values: [
|
||||
{
|
||||
displayName: 'General Formatting',
|
||||
name: 'general',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
|
||||
name: 'Values (unformatted)',
|
||||
value: 'UNFORMATTED_VALUE',
|
||||
description:
|
||||
'Numbers stay as numbers, but any currency signs or special formatting is lost',
|
||||
},
|
||||
{
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
|
||||
name: 'Values (formatted)',
|
||||
value: 'FORMATTED_VALUE',
|
||||
description:
|
||||
'Numbers are turned to text, and displayed as in Google Sheets (e.g. with commas or currency signs)',
|
||||
},
|
||||
{
|
||||
name: 'Formulas',
|
||||
value: 'FORMULA',
|
||||
},
|
||||
],
|
||||
default: '',
|
||||
description: 'Determines how values should be rendered in the output',
|
||||
},
|
||||
{
|
||||
displayName: 'Date Formatting',
|
||||
name: 'date',
|
||||
type: 'options',
|
||||
default: '',
|
||||
options: [
|
||||
{
|
||||
name: 'Formatted Text',
|
||||
value: 'FORMATTED_STRING',
|
||||
description: "As displayed in Google Sheets, e.g. '01/01/2022'",
|
||||
},
|
||||
{
|
||||
name: 'Serial Number',
|
||||
value: 'SERIAL_NUMBER',
|
||||
description: 'A number representing the number of days since Dec 30, 1899',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const cellFormat: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Cell Format',
|
||||
name: 'cellFormat',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
|
||||
name: 'Let n8n format',
|
||||
value: 'RAW',
|
||||
description: 'Cells have the same types as the input data',
|
||||
},
|
||||
{
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
|
||||
name: 'Let Google Sheets format',
|
||||
value: 'USER_ENTERED',
|
||||
description: 'Cells are styled as if you typed the values into Google Sheets directly',
|
||||
},
|
||||
],
|
||||
default: 'RAW',
|
||||
description: 'Determines how data should be interpreted',
|
||||
},
|
||||
];
|
||||
|
||||
export const handlingExtraData: INodeProperties[] = [
|
||||
{
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
|
||||
displayName: 'Handling extra fields in input',
|
||||
name: 'handlingExtraData',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Insert in New Column(s)',
|
||||
value: 'insertInNewColumn',
|
||||
description: 'Create a new column for extra data',
|
||||
},
|
||||
{
|
||||
name: 'Ignore Them',
|
||||
value: 'ignoreIt',
|
||||
description: 'Ignore extra data',
|
||||
},
|
||||
{
|
||||
name: 'Error',
|
||||
value: 'error',
|
||||
description: 'Throw an error',
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/dataMode': ['autoMapInputData'],
|
||||
},
|
||||
},
|
||||
default: 'insertInNewColumn',
|
||||
description: "What do to with fields that don't match any columns in the Google Sheet",
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,127 @@
|
||||
import { IExecuteFunctions } from 'n8n-core';
|
||||
import { IDataObject, INodeExecutionData } from 'n8n-workflow';
|
||||
import { SheetProperties } from '../../helpers/GoogleSheets.types';
|
||||
import { apiRequest } from '../../transport';
|
||||
import { GoogleSheet } from '../../helpers/GoogleSheet';
|
||||
import { getExistingSheetNames, hexToRgb } from '../../helpers/GoogleSheets.utils';
|
||||
|
||||
export const description: SheetProperties = [
|
||||
{
|
||||
displayName: 'Title',
|
||||
name: 'title',
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: 'n8n-sheet',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['sheet'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
description: 'The name of the sheet',
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Option',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['sheet'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Hidden',
|
||||
name: 'hidden',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: "Whether the sheet is hidden in the UI, false if it's visible",
|
||||
},
|
||||
{
|
||||
displayName: 'Right To Left',
|
||||
name: 'rightToLeft',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Whether the sheet is an RTL sheet instead of an LTR sheet',
|
||||
},
|
||||
{
|
||||
displayName: 'Sheet ID',
|
||||
name: 'sheetId',
|
||||
type: 'number',
|
||||
default: 0,
|
||||
description:
|
||||
'The ID of the sheet. Must be non-negative. This field cannot be changed once set.',
|
||||
},
|
||||
{
|
||||
displayName: 'Sheet Index',
|
||||
name: 'index',
|
||||
type: 'number',
|
||||
default: 0,
|
||||
description: 'The index of the sheet within the spreadsheet',
|
||||
},
|
||||
{
|
||||
displayName: 'Tab Color',
|
||||
name: 'tabColor',
|
||||
type: 'color',
|
||||
default: '0aa55c',
|
||||
description: 'The color of the tab in the UI',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export async function execute(
|
||||
this: IExecuteFunctions,
|
||||
sheet: GoogleSheet,
|
||||
sheetName: string,
|
||||
): Promise<INodeExecutionData[]> {
|
||||
let responseData;
|
||||
const returnData: IDataObject[] = [];
|
||||
const items = this.getInputData();
|
||||
|
||||
const existingSheetNames = await getExistingSheetNames(sheet);
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const sheetTitle = this.getNodeParameter('title', i, {}) as string;
|
||||
|
||||
if (existingSheetNames.includes(sheetTitle)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const options = this.getNodeParameter('options', i, {}) as IDataObject;
|
||||
const properties = { ...options };
|
||||
properties.title = sheetTitle;
|
||||
|
||||
if (options.tabColor) {
|
||||
const { red, green, blue } = hexToRgb(options.tabColor as string)!;
|
||||
properties.tabColor = { red: red / 255, green: green / 255, blue: blue / 255 };
|
||||
}
|
||||
|
||||
const requests = [
|
||||
{
|
||||
addSheet: {
|
||||
properties,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
responseData = await apiRequest.call(
|
||||
this,
|
||||
'POST',
|
||||
`/v4/spreadsheets/${sheetName}:batchUpdate`,
|
||||
{ requests },
|
||||
);
|
||||
|
||||
// simplify response
|
||||
Object.assign(responseData, responseData.replies[0].addSheet.properties);
|
||||
delete responseData.replies;
|
||||
|
||||
existingSheetNames.push(sheetTitle);
|
||||
|
||||
returnData.push(responseData);
|
||||
}
|
||||
return this.helpers.returnJsonArray(returnData);
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
import { IExecuteFunctions } from 'n8n-core';
|
||||
import { IDataObject, INodeExecutionData } from 'n8n-workflow';
|
||||
import { SheetProperties } from '../../helpers/GoogleSheets.types';
|
||||
import { GoogleSheet } from '../../helpers/GoogleSheet';
|
||||
import { getColumnNumber, untilSheetSelected } from '../../helpers/GoogleSheets.utils';
|
||||
|
||||
export const description: SheetProperties = [
|
||||
{
|
||||
displayName: 'To Delete',
|
||||
name: 'toDelete',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Rows',
|
||||
value: 'rows',
|
||||
description: 'Rows to delete',
|
||||
},
|
||||
{
|
||||
name: 'Columns',
|
||||
value: 'columns',
|
||||
description: 'Columns to delete',
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['sheet'],
|
||||
operation: ['delete'],
|
||||
},
|
||||
hide: {
|
||||
...untilSheetSelected,
|
||||
},
|
||||
},
|
||||
default: 'rows',
|
||||
description: 'What to delete',
|
||||
},
|
||||
{
|
||||
displayName: 'Start Row Number',
|
||||
name: 'startIndex',
|
||||
type: 'number',
|
||||
typeOptions: {
|
||||
minValue: 1,
|
||||
},
|
||||
default: 2,
|
||||
description: 'The row number to delete from, The first row is 2',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['sheet'],
|
||||
operation: ['delete'],
|
||||
toDelete: ['rows'],
|
||||
},
|
||||
hide: {
|
||||
...untilSheetSelected,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Number of Rows to Delete',
|
||||
name: 'numberToDelete',
|
||||
type: 'number',
|
||||
typeOptions: {
|
||||
minValue: 1,
|
||||
},
|
||||
default: 1,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['sheet'],
|
||||
operation: ['delete'],
|
||||
toDelete: ['rows'],
|
||||
},
|
||||
hide: {
|
||||
...untilSheetSelected,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Start Column',
|
||||
name: 'startIndex',
|
||||
type: 'string',
|
||||
default: 'A',
|
||||
description: 'The column to delete',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['sheet'],
|
||||
operation: ['delete'],
|
||||
toDelete: ['columns'],
|
||||
},
|
||||
hide: {
|
||||
...untilSheetSelected,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Number of Columns to Delete',
|
||||
name: 'numberToDelete',
|
||||
type: 'number',
|
||||
typeOptions: {
|
||||
minValue: 1,
|
||||
},
|
||||
default: 1,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['sheet'],
|
||||
operation: ['delete'],
|
||||
toDelete: ['columns'],
|
||||
},
|
||||
hide: {
|
||||
...untilSheetSelected,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export async function execute(
|
||||
this: IExecuteFunctions,
|
||||
sheet: GoogleSheet,
|
||||
sheetName: string,
|
||||
): Promise<INodeExecutionData[]> {
|
||||
const items = this.getInputData();
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const requests: IDataObject[] = [];
|
||||
let startIndex, endIndex, numberToDelete;
|
||||
const deleteType = this.getNodeParameter('toDelete', i) as string;
|
||||
|
||||
if (deleteType === 'rows') {
|
||||
startIndex = this.getNodeParameter('startIndex', i) as number;
|
||||
// We start from 1 now...
|
||||
startIndex--;
|
||||
numberToDelete = this.getNodeParameter('numberToDelete', i) as number;
|
||||
if (numberToDelete === 1) {
|
||||
endIndex = startIndex + 1;
|
||||
} else {
|
||||
endIndex = startIndex + numberToDelete;
|
||||
}
|
||||
requests.push({
|
||||
deleteDimension: {
|
||||
range: {
|
||||
sheetId: sheetName,
|
||||
dimension: 'ROWS',
|
||||
startIndex,
|
||||
endIndex,
|
||||
},
|
||||
},
|
||||
});
|
||||
} else if (deleteType === 'columns') {
|
||||
startIndex = this.getNodeParameter('startIndex', i) as string;
|
||||
numberToDelete = this.getNodeParameter('numberToDelete', i) as number;
|
||||
startIndex = getColumnNumber(startIndex) - 1;
|
||||
if (numberToDelete === 1) {
|
||||
endIndex = startIndex + 1;
|
||||
} else {
|
||||
endIndex = startIndex + numberToDelete;
|
||||
}
|
||||
requests.push({
|
||||
deleteDimension: {
|
||||
range: {
|
||||
sheetId: sheetName,
|
||||
dimension: 'COLUMNS',
|
||||
startIndex,
|
||||
endIndex,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
await sheet.spreadsheetBatchUpdate(requests);
|
||||
}
|
||||
|
||||
return this.helpers.returnJsonArray({ success: true });
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
import { IExecuteFunctions } from 'n8n-core';
|
||||
import { IDataObject, INodeExecutionData } from 'n8n-workflow';
|
||||
import { GoogleSheet } from '../../helpers/GoogleSheet';
|
||||
import {
|
||||
getRangeString,
|
||||
prepareSheetData,
|
||||
untilSheetSelected,
|
||||
} from '../../helpers/GoogleSheets.utils';
|
||||
import { ILookupValues, SheetProperties } from '../../helpers/GoogleSheets.types';
|
||||
import { dataLocationOnSheet, outputFormatting } from './commonDescription';
|
||||
import {
|
||||
RangeDetectionOptions,
|
||||
SheetRangeData,
|
||||
ValueRenderOption,
|
||||
} from '../../helpers/GoogleSheets.types';
|
||||
|
||||
export const description: SheetProperties = [
|
||||
{
|
||||
displayName: 'Filters',
|
||||
name: 'filtersUI',
|
||||
placeholder: 'Add Filter',
|
||||
type: 'fixedCollection',
|
||||
typeOptions: {
|
||||
multipleValueButtonText: 'Add Filter',
|
||||
multipleValues: true,
|
||||
},
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Filter',
|
||||
name: 'values',
|
||||
values: [
|
||||
{
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
|
||||
displayName: 'Column',
|
||||
name: 'lookupColumn',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsDependsOn: ['sheetName.value'],
|
||||
loadOptionsMethod: 'getSheetHeaderRowWithGeneratedColumnNames',
|
||||
},
|
||||
default: '',
|
||||
description:
|
||||
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>',
|
||||
},
|
||||
{
|
||||
displayName: 'Value',
|
||||
name: 'lookupValue',
|
||||
type: 'string',
|
||||
default: '',
|
||||
hint: 'The column must have this value to be matched',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['sheet'],
|
||||
operation: ['read'],
|
||||
},
|
||||
hide: {
|
||||
...untilSheetSelected,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Option',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['sheet'],
|
||||
operation: ['read'],
|
||||
},
|
||||
hide: {
|
||||
...untilSheetSelected,
|
||||
},
|
||||
},
|
||||
options: [
|
||||
...dataLocationOnSheet,
|
||||
...outputFormatting,
|
||||
{
|
||||
displayName: 'When Filter Has Multiple Matches',
|
||||
name: 'returnAllMatches',
|
||||
type: 'options',
|
||||
default: 'returnFirstMatch',
|
||||
options: [
|
||||
{
|
||||
name: 'Return First Match',
|
||||
value: 'returnFirstMatch',
|
||||
description: 'Return only the first match',
|
||||
},
|
||||
{
|
||||
name: 'Return All Matches',
|
||||
value: 'returnAllMatches',
|
||||
description: 'Return all values that match',
|
||||
},
|
||||
],
|
||||
description:
|
||||
'By default only the first result gets returned, Set to "Return All Matches" to get multiple matches',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export async function execute(
|
||||
this: IExecuteFunctions,
|
||||
sheet: GoogleSheet,
|
||||
sheetName: string,
|
||||
): Promise<INodeExecutionData[]> {
|
||||
const options = this.getNodeParameter('options', 0, {}) as IDataObject;
|
||||
const outputFormatting =
|
||||
(((options.outputFormatting as IDataObject) || {}).values as IDataObject) || {};
|
||||
|
||||
const dataLocationOnSheetOptions =
|
||||
(((options.dataLocationOnSheet as IDataObject) || {}).values as RangeDetectionOptions) || {};
|
||||
|
||||
if (dataLocationOnSheetOptions.rangeDefinition === undefined) {
|
||||
dataLocationOnSheetOptions.rangeDefinition = 'detectAutomatically';
|
||||
}
|
||||
|
||||
const range = getRangeString(sheetName, dataLocationOnSheetOptions);
|
||||
|
||||
const valueRenderMode = (outputFormatting.general || 'UNFORMATTED_VALUE') as ValueRenderOption;
|
||||
const dateTimeRenderOption = (outputFormatting.date || 'FORMATTED_STRING') as string;
|
||||
|
||||
const sheetData = (await sheet.getData(
|
||||
range,
|
||||
valueRenderMode,
|
||||
dateTimeRenderOption,
|
||||
)) as SheetRangeData;
|
||||
|
||||
if (sheetData === undefined || sheetData.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const { data, headerRow, firstDataRow } = prepareSheetData(sheetData, dataLocationOnSheetOptions);
|
||||
|
||||
let returnData = [];
|
||||
|
||||
const lookupValues = this.getNodeParameter('filtersUI.values', 0, []) as ILookupValues[];
|
||||
|
||||
if (lookupValues.length) {
|
||||
const returnAllMatches = options.returnAllMatches === 'returnAllMatches' ? true : false;
|
||||
|
||||
const items = this.getInputData();
|
||||
for (let i = 1; i < items.length; i++) {
|
||||
const itemLookupValues = this.getNodeParameter('filtersUI.values', i, []) as ILookupValues[];
|
||||
if (itemLookupValues.length) {
|
||||
lookupValues.push(...itemLookupValues);
|
||||
}
|
||||
}
|
||||
|
||||
returnData = await sheet.lookupValues(
|
||||
data as string[][],
|
||||
headerRow,
|
||||
firstDataRow,
|
||||
lookupValues,
|
||||
returnAllMatches,
|
||||
);
|
||||
} else {
|
||||
returnData = sheet.structureArrayDataByColumn(data as string[][], headerRow, firstDataRow);
|
||||
}
|
||||
|
||||
return this.helpers.returnJsonArray(returnData);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { IExecuteFunctions } from 'n8n-core';
|
||||
import { IDataObject, INodeExecutionData } from 'n8n-workflow';
|
||||
import { apiRequest } from '../../transport';
|
||||
import { GoogleSheet } from '../../helpers/GoogleSheet';
|
||||
|
||||
export async function execute(
|
||||
this: IExecuteFunctions,
|
||||
sheet: GoogleSheet,
|
||||
sheetName: string,
|
||||
): Promise<INodeExecutionData[]> {
|
||||
const returnData: IDataObject[] = [];
|
||||
const items = this.getInputData();
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const [spreadsheetId, sheetWithinDocument] = sheetName.split('||');
|
||||
const requests = [
|
||||
{
|
||||
deleteSheet: {
|
||||
sheetId: sheetWithinDocument,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
let responseData;
|
||||
|
||||
responseData = await apiRequest.call(
|
||||
this,
|
||||
'POST',
|
||||
`/v4/spreadsheets/${spreadsheetId}:batchUpdate`,
|
||||
{ requests },
|
||||
);
|
||||
delete responseData.replies;
|
||||
returnData.push(responseData);
|
||||
}
|
||||
|
||||
return this.helpers.returnJsonArray(returnData);
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
import { IExecuteFunctions } from 'n8n-core';
|
||||
import { ISheetUpdateData, SheetProperties } from '../../helpers/GoogleSheets.types';
|
||||
import { IDataObject, INodeExecutionData, NodeOperationError } from 'n8n-workflow';
|
||||
import { GoogleSheet } from '../../helpers/GoogleSheet';
|
||||
import { ValueInputOption, ValueRenderOption } from '../../helpers/GoogleSheets.types';
|
||||
import { untilSheetSelected } from '../../helpers/GoogleSheets.utils';
|
||||
import { cellFormat, handlingExtraData, locationDefine } from './commonDescription';
|
||||
|
||||
export const description: SheetProperties = [
|
||||
{
|
||||
displayName: 'Data Mode',
|
||||
name: 'dataMode',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Auto-Map Input Data to Columns',
|
||||
value: 'autoMapInputData',
|
||||
description: 'Use when node input properties match destination column names',
|
||||
},
|
||||
{
|
||||
name: 'Map Each Column Below',
|
||||
value: 'defineBelow',
|
||||
description: 'Set the value for each destination column',
|
||||
},
|
||||
{
|
||||
name: 'Nothing',
|
||||
value: 'nothing',
|
||||
description: 'Do not send anything',
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['sheet'],
|
||||
operation: ['update'],
|
||||
},
|
||||
hide: {
|
||||
...untilSheetSelected,
|
||||
},
|
||||
},
|
||||
default: 'defineBelow',
|
||||
description: 'Whether to insert the input data this node receives in the new row',
|
||||
},
|
||||
{
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased, n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
|
||||
displayName: 'Column to match on',
|
||||
name: 'columnToMatchOn',
|
||||
type: 'options',
|
||||
description:
|
||||
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>',
|
||||
typeOptions: {
|
||||
loadOptionsDependsOn: ['sheetName.value'],
|
||||
loadOptionsMethod: 'getSheetHeaderRowAndSkipEmpty',
|
||||
},
|
||||
default: '',
|
||||
hint: "Used to find the correct row to update. Doesn't get changed.",
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['sheet'],
|
||||
operation: ['update'],
|
||||
},
|
||||
hide: {
|
||||
...untilSheetSelected,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Value of Column to Match On',
|
||||
name: 'valueToMatchOn',
|
||||
type: 'string',
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['sheet'],
|
||||
operation: ['update'],
|
||||
dataMode: ['defineBelow'],
|
||||
},
|
||||
hide: {
|
||||
...untilSheetSelected,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Values to Send',
|
||||
name: 'fieldsUi',
|
||||
placeholder: 'Add Field',
|
||||
type: 'fixedCollection',
|
||||
typeOptions: {
|
||||
multipleValues: true,
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['sheet'],
|
||||
operation: ['update'],
|
||||
dataMode: ['defineBelow'],
|
||||
},
|
||||
hide: {
|
||||
...untilSheetSelected,
|
||||
},
|
||||
},
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Field',
|
||||
name: 'values',
|
||||
values: [
|
||||
{
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
|
||||
displayName: 'Column',
|
||||
name: 'column',
|
||||
type: 'options',
|
||||
description:
|
||||
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>',
|
||||
typeOptions: {
|
||||
loadOptionsDependsOn: ['sheetName.value', 'columnToMatchOn'],
|
||||
loadOptionsMethod: 'getSheetHeaderRowAndAddColumn',
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Column Name',
|
||||
name: 'columnName',
|
||||
type: 'string',
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
column: ['newColumn'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Value',
|
||||
name: 'fieldValue',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Option',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['sheet'],
|
||||
operation: ['update'],
|
||||
},
|
||||
hide: {
|
||||
...untilSheetSelected,
|
||||
},
|
||||
},
|
||||
options: [...cellFormat, ...locationDefine, ...handlingExtraData],
|
||||
},
|
||||
];
|
||||
|
||||
export async function execute(
|
||||
this: IExecuteFunctions,
|
||||
sheet: GoogleSheet,
|
||||
sheetName: string,
|
||||
): Promise<INodeExecutionData[]> {
|
||||
const items = this.getInputData();
|
||||
const valueInputMode = this.getNodeParameter('options.cellFormat', 0, 'RAW') as ValueInputOption;
|
||||
const range = `${sheetName}!A:Z`;
|
||||
|
||||
const options = this.getNodeParameter('options', 0, {}) as IDataObject;
|
||||
|
||||
const valueRenderMode = (options.valueRenderMode || 'UNFORMATTED_VALUE') as ValueRenderOption;
|
||||
|
||||
const locationDefine = ((options.locationDefine as IDataObject) || {}).values as IDataObject;
|
||||
|
||||
let headerRow = 0;
|
||||
let firstDataRow = 1;
|
||||
|
||||
if (locationDefine) {
|
||||
if (locationDefine.headerRow) {
|
||||
headerRow = parseInt(locationDefine.headerRow as string, 10) - 1;
|
||||
}
|
||||
if (locationDefine.firstDataRow) {
|
||||
firstDataRow = parseInt(locationDefine.firstDataRow as string, 10) - 1;
|
||||
}
|
||||
}
|
||||
|
||||
let columnNames: string[] = [];
|
||||
|
||||
const sheetData = await sheet.getData(sheetName, 'FORMATTED_VALUE');
|
||||
|
||||
if (sheetData === undefined || sheetData[headerRow] === undefined) {
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
`Could not retrieve the column names from row ${headerRow + 1}`,
|
||||
);
|
||||
}
|
||||
|
||||
columnNames = sheetData[headerRow];
|
||||
const newColumns = new Set<string>();
|
||||
|
||||
const columnToMatchOn = this.getNodeParameter('columnToMatchOn', 0) as string;
|
||||
const keyIndex = columnNames.indexOf(columnToMatchOn);
|
||||
|
||||
const columnValues = await sheet.getColumnValues(
|
||||
range,
|
||||
keyIndex,
|
||||
firstDataRow,
|
||||
valueRenderMode,
|
||||
sheetData,
|
||||
);
|
||||
|
||||
const updateData: ISheetUpdateData[] = [];
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const dataMode = this.getNodeParameter('dataMode', i) as
|
||||
| 'defineBelow'
|
||||
| 'autoMapInputData'
|
||||
| 'nothing';
|
||||
|
||||
if (dataMode === 'nothing') continue;
|
||||
|
||||
const data: IDataObject[] = [];
|
||||
|
||||
if (dataMode === 'autoMapInputData') {
|
||||
const handlingExtraData = (options.handlingExtraData as string) || 'insertInNewColumn';
|
||||
if (handlingExtraData === 'ignoreIt') {
|
||||
data.push(items[i].json);
|
||||
}
|
||||
if (handlingExtraData === 'error') {
|
||||
Object.keys(items[i].json).forEach((key) => {
|
||||
if (columnNames.includes(key) === false) {
|
||||
throw new NodeOperationError(this.getNode(), `Unexpected fields in node input`, {
|
||||
itemIndex: i,
|
||||
description: `The input field '${key}' doesn't match any column in the Sheet. You can ignore this by changing the 'Handling extra data' field, which you can find under 'Options'.`,
|
||||
});
|
||||
}
|
||||
});
|
||||
data.push(items[i].json);
|
||||
}
|
||||
if (handlingExtraData === 'insertInNewColumn') {
|
||||
Object.keys(items[i].json).forEach((key) => {
|
||||
if (columnNames.includes(key) === false) {
|
||||
newColumns.add(key);
|
||||
}
|
||||
});
|
||||
data.push(items[i].json);
|
||||
}
|
||||
} else {
|
||||
const valueToMatchOn = this.getNodeParameter('valueToMatchOn', i) as string;
|
||||
|
||||
const fields = (this.getNodeParameter('fieldsUi.values', i, {}) as IDataObject[]).reduce(
|
||||
(acc, entry) => {
|
||||
if (entry.column === 'newColumn') {
|
||||
const columnName = entry.columnName as string;
|
||||
|
||||
if (columnNames.includes(columnName) === false) {
|
||||
newColumns.add(columnName);
|
||||
}
|
||||
|
||||
acc[columnName] = entry.fieldValue as string;
|
||||
} else {
|
||||
acc[entry.column as string] = entry.fieldValue as string;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{} as IDataObject,
|
||||
);
|
||||
|
||||
fields[columnToMatchOn] = valueToMatchOn;
|
||||
|
||||
data.push(fields);
|
||||
}
|
||||
|
||||
if (newColumns.size) {
|
||||
await sheet.updateRows(
|
||||
sheetName,
|
||||
[columnNames.concat([...newColumns])],
|
||||
(options.cellFormat as ValueInputOption) || 'RAW',
|
||||
headerRow + 1,
|
||||
);
|
||||
}
|
||||
|
||||
const preparedData = await sheet.prepareDataForUpdateOrUpsert(
|
||||
data,
|
||||
columnToMatchOn,
|
||||
range,
|
||||
headerRow,
|
||||
firstDataRow,
|
||||
valueRenderMode,
|
||||
false,
|
||||
[columnNames.concat([...newColumns])],
|
||||
columnValues,
|
||||
);
|
||||
|
||||
updateData.push(...preparedData.updateData);
|
||||
}
|
||||
|
||||
if (updateData.length) {
|
||||
await sheet.batchUpdate(updateData, valueInputMode);
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
Reference in New Issue
Block a user