fix(Google Sheets Node): Tweaks (#7357)

Github issue / Community forum post (link here to close automatically):
This commit is contained in:
Michael Kret 2023-10-17 18:41:30 +03:00 committed by GitHub
parent a2d2e3dda7
commit d8531a53b9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 414 additions and 304 deletions

View file

@ -11,7 +11,7 @@ export class GoogleSheets extends VersionedNodeType {
name: 'googleSheets', name: 'googleSheets',
icon: 'file:googleSheets.svg', icon: 'file:googleSheets.svg',
group: ['input', 'output'], group: ['input', 'output'],
defaultVersion: 4, defaultVersion: 4.1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Read, update and write data to Google Sheets', description: 'Read, update and write data to Google Sheets',
}; };
@ -21,6 +21,7 @@ export class GoogleSheets extends VersionedNodeType {
2: new GoogleSheetsV1(baseDescription), 2: new GoogleSheetsV1(baseDescription),
3: new GoogleSheetsV2(baseDescription), 3: new GoogleSheetsV2(baseDescription),
4: new GoogleSheetsV2(baseDescription), 4: new GoogleSheetsV2(baseDescription),
4.1: new GoogleSheetsV2(baseDescription),
}; };
super(nodeVersions, baseDescription); super(nodeVersions, baseDescription);

View file

@ -1,7 +1,12 @@
import type { IExecuteFunctions, IDataObject, INodeExecutionData } from 'n8n-workflow'; import type { IExecuteFunctions, IDataObject, INodeExecutionData } from 'n8n-workflow';
import type { SheetProperties, ValueInputOption } from '../../helpers/GoogleSheets.types'; import type { SheetProperties, ValueInputOption } from '../../helpers/GoogleSheets.types';
import type { GoogleSheet } from '../../helpers/GoogleSheet'; import type { GoogleSheet } from '../../helpers/GoogleSheet';
import { autoMapInputData, mapFields, untilSheetSelected } from '../../helpers/GoogleSheets.utils'; import {
autoMapInputData,
cellFormatDefault,
mapFields,
untilSheetSelected,
} from '../../helpers/GoogleSheets.utils';
import { cellFormat, handlingExtraData } from './commonDescription'; import { cellFormat, handlingExtraData } from './commonDescription';
export const description: SheetProperties = [ export const description: SheetProperties = [
@ -131,7 +136,7 @@ export const description: SheetProperties = [
show: { show: {
resource: ['sheet'], resource: ['sheet'],
operation: ['append'], operation: ['append'],
'@version': [4], '@version': [4, 4.1],
}, },
hide: { hide: {
...untilSheetSelected, ...untilSheetSelected,
@ -154,7 +159,7 @@ export const description: SheetProperties = [
}, },
}, },
options: [ options: [
...cellFormat, cellFormat,
{ {
displayName: 'Data Location on Sheet', displayName: 'Data Location on Sheet',
name: 'locationDefine', name: 'locationDefine',
@ -181,7 +186,11 @@ export const description: SheetProperties = [
}, },
], ],
}, },
...handlingExtraData, handlingExtraData,
{
...handlingExtraData,
displayOptions: { show: { '/columns.mappingMode': ['autoMapInputData'] } },
},
], ],
}, },
]; ];
@ -227,7 +236,7 @@ export async function execute(
setData, setData,
sheetName, sheetName,
headerRow, headerRow,
(options.cellFormat as ValueInputOption) || 'RAW', (options.cellFormat as ValueInputOption) || cellFormatDefault(nodeVersion),
false, false,
); );

View file

@ -7,7 +7,7 @@ import type {
} from '../../helpers/GoogleSheets.types'; } from '../../helpers/GoogleSheets.types';
import { NodeOperationError } from 'n8n-workflow'; import { NodeOperationError } from 'n8n-workflow';
import type { GoogleSheet } from '../../helpers/GoogleSheet'; import type { GoogleSheet } from '../../helpers/GoogleSheet';
import { untilSheetSelected } from '../../helpers/GoogleSheets.utils'; import { cellFormatDefault, untilSheetSelected } from '../../helpers/GoogleSheets.utils';
import { cellFormat, handlingExtraData, locationDefine } from './commonDescription'; import { cellFormat, handlingExtraData, locationDefine } from './commonDescription';
export const description: SheetProperties = [ export const description: SheetProperties = [
@ -172,7 +172,7 @@ export const description: SheetProperties = [
show: { show: {
resource: ['sheet'], resource: ['sheet'],
operation: ['appendOrUpdate'], operation: ['appendOrUpdate'],
'@version': [4], '@version': [4, 4.1],
}, },
hide: { hide: {
...untilSheetSelected, ...untilSheetSelected,
@ -194,7 +194,15 @@ export const description: SheetProperties = [
...untilSheetSelected, ...untilSheetSelected,
}, },
}, },
options: [...cellFormat, ...locationDefine, ...handlingExtraData], options: [
cellFormat,
locationDefine,
handlingExtraData,
{
...handlingExtraData,
displayOptions: { show: { '/columns.mappingMode': ['autoMapInputData'] } },
},
],
}, },
]; ];
@ -205,9 +213,16 @@ export async function execute(
sheetId: string, sheetId: string,
): Promise<INodeExecutionData[]> { ): Promise<INodeExecutionData[]> {
const items = this.getInputData(); const items = this.getInputData();
const valueInputMode = this.getNodeParameter('options.cellFormat', 0, 'RAW') as ValueInputOption; const nodeVersion = this.getNode().typeVersion;
const range = `${sheetName}!A:Z`; const range = `${sheetName}!A:Z`;
const valueInputMode = this.getNodeParameter(
'options.cellFormat',
0,
cellFormatDefault(nodeVersion),
) as ValueInputOption;
const options = this.getNodeParameter('options', 0, {}); const options = this.getNodeParameter('options', 0, {});
const valueRenderMode = (options.valueRenderMode || 'UNFORMATTED_VALUE') as ValueRenderOption; const valueRenderMode = (options.valueRenderMode || 'UNFORMATTED_VALUE') as ValueRenderOption;
@ -238,7 +253,7 @@ export async function execute(
} }
columnNames = sheetData[headerRow]; columnNames = sheetData[headerRow];
const nodeVersion = this.getNode().typeVersion;
const newColumns = new Set<string>(); const newColumns = new Set<string>();
const columnsToMatchOn: string[] = const columnsToMatchOn: string[] =
@ -346,7 +361,7 @@ export async function execute(
await sheet.updateRows( await sheet.updateRows(
sheetName, sheetName,
[columnNames.concat([...newColumns])], [columnNames.concat([...newColumns])],
(options.cellFormat as ValueInputOption) || 'RAW', (options.cellFormat as ValueInputOption) || cellFormatDefault(nodeVersion),
headerRow + 1, headerRow + 1,
); );
} }

View file

@ -1,270 +1,260 @@
import type { INodeProperties } from 'n8n-workflow'; import type { INodeProperties } from 'n8n-workflow';
export const dataLocationOnSheet: INodeProperties[] = [ export const dataLocationOnSheet: INodeProperties = {
{ displayName: 'Data Location on Sheet',
displayName: 'Data Location on Sheet', name: 'dataLocationOnSheet',
name: 'dataLocationOnSheet', type: 'fixedCollection',
type: 'fixedCollection', placeholder: 'Select Range',
placeholder: 'Select Range', default: { values: { rangeDefinition: 'detectAutomatically' } },
default: { values: { rangeDefinition: 'detectAutomatically' } }, options: [
options: [ {
{ displayName: 'Values',
displayName: 'Values', name: 'values',
name: 'values', values: [
values: [ {
{ displayName: 'Range Definition',
displayName: 'Range Definition', name: 'rangeDefinition',
name: 'rangeDefinition', type: 'options',
type: 'options', options: [
options: [ {
{ name: 'Detect Automatically',
name: 'Detect Automatically', value: 'detectAutomatically',
value: 'detectAutomatically', description: 'Automatically detect the data range',
description: 'Automatically detect the data range', },
}, {
{ name: 'Specify Range (A1 Notation)',
name: 'Specify Range (A1 Notation)', value: 'specifyRangeA1',
value: 'specifyRangeA1', description: 'Manually specify the data range',
description: 'Manually specify the data range', },
}, {
{ name: 'Specify Range (Rows)',
name: 'Specify Range (Rows)', value: 'specifyRange',
value: 'specifyRange', description: 'Manually specify the data range',
description: 'Manually specify the data range', },
}, ],
], default: '',
default: '', },
}, {
{ displayName: 'Read Rows Until',
displayName: 'Read Rows Until', name: 'readRowsUntil',
name: 'readRowsUntil', type: 'options',
type: 'options', default: 'lastRowInSheet',
default: 'lastRowInSheet', options: [
options: [ {
{ name: 'First Empty Row',
name: 'First Empty Row', value: 'firstEmptyRow',
value: 'firstEmptyRow', },
}, {
{ name: 'Last Row In Sheet',
name: 'Last Row In Sheet', value: 'lastRowInSheet',
value: 'lastRowInSheet', },
}, ],
], displayOptions: {
displayOptions: { show: {
show: { rangeDefinition: ['detectAutomatically'],
rangeDefinition: ['detectAutomatically'],
},
}, },
}, },
{ },
displayName: 'Header Row', {
name: 'headerRow', displayName: 'Header Row',
type: 'number', name: 'headerRow',
typeOptions: { type: 'number',
minValue: 1, typeOptions: {
}, minValue: 1,
default: 1, },
description: "Index is relative to the set 'Range', first row index is 1", default: 1,
hint: 'Index of the row which contains the column names', description: "Index is relative to the set 'Range', first row index is 1",
displayOptions: { hint: 'Index of the row which contains the column names',
show: { displayOptions: {
rangeDefinition: ['specifyRange'], show: {
}, rangeDefinition: ['specifyRange'],
}, },
}, },
{ },
displayName: 'First Data Row', {
name: 'firstDataRow', displayName: 'First Data Row',
type: 'number', name: 'firstDataRow',
typeOptions: { type: 'number',
minValue: 1, typeOptions: {
}, minValue: 1,
default: 2, },
description: "Index is relative to the set 'Range', first row index is 1", default: 2,
hint: 'Index of first row which contains the actual data', description: "Index is relative to the set 'Range', first row index is 1",
displayOptions: { hint: 'Index of first row which contains the actual data',
show: { displayOptions: {
rangeDefinition: ['specifyRange'], show: {
}, rangeDefinition: ['specifyRange'],
}, },
}, },
{ },
displayName: 'Range', {
name: 'range', displayName: 'Range',
type: 'string', name: 'range',
default: '', type: 'string',
placeholder: 'A:Z', default: '',
description: placeholder: 'A:Z',
'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.', description:
hint: 'You can specify both the rows and the columns, e.g. C4:E7', '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.',
displayOptions: { hint: 'You can specify both the rows and the columns, e.g. C4:E7',
show: { displayOptions: {
rangeDefinition: ['specifyRangeA1'], show: {
}, rangeDefinition: ['specifyRangeA1'],
}, },
}, },
], },
}, ],
], },
}, ],
]; };
export const locationDefine: INodeProperties[] = [ export const locationDefine: INodeProperties = {
{ displayName: 'Data Location on Sheet',
displayName: 'Data Location on Sheet', name: 'locationDefine',
name: 'locationDefine', type: 'fixedCollection',
type: 'fixedCollection', placeholder: 'Select Range',
placeholder: 'Select Range', default: { values: {} },
default: { values: {} }, options: [
options: [ {
{ displayName: 'Values',
displayName: 'Values', name: 'values',
name: 'values', values: [
values: [ {
{ displayName: 'Header Row',
displayName: 'Header Row', name: 'headerRow',
name: 'headerRow', type: 'number',
type: 'number', typeOptions: {
typeOptions: { minValue: 1,
minValue: 1, },
}, default: 1,
default: 1, description: "Index is relative to the set 'Range', first row index is 1",
description: "Index is relative to the set 'Range', first row index is 1", hint: 'Index of the row which contains the column names',
hint: 'Index of the row which contains the column names', },
}, {
{ displayName: 'First Data Row',
displayName: 'First Data Row', name: 'firstDataRow',
name: 'firstDataRow', type: 'number',
type: 'number', typeOptions: {
typeOptions: { minValue: 1,
minValue: 1, },
}, default: 2,
default: 2, description: "Index is relative to the set 'Range', first row index is 1",
description: "Index is relative to the set 'Range', first row index is 1", hint: 'Index of first row which contains the actual data',
hint: 'Index of first row which contains the actual data', },
}, ],
], },
}, ],
], };
},
]; export const outputFormatting: INodeProperties = {
displayName: 'Output Formatting',
export const outputFormatting: INodeProperties[] = [ name: 'outputFormatting',
{ type: 'fixedCollection',
displayName: 'Output Formatting', placeholder: 'Add Formatting',
name: 'outputFormatting', default: { values: { general: 'UNFORMATTED_VALUE', date: 'FORMATTED_STRING' } },
type: 'fixedCollection', options: [
placeholder: 'Add Formatting', {
default: { values: { general: 'UNFORMATTED_VALUE', date: 'FORMATTED_STRING' } }, displayName: 'Values',
options: [ name: 'values',
{ values: [
displayName: 'Values', {
name: 'values', displayName: 'General Formatting',
values: [ name: 'general',
{ type: 'options',
displayName: 'General Formatting', options: [
name: 'general', {
type: 'options', // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
options: [ name: 'Values (unformatted)',
{ value: 'UNFORMATTED_VALUE',
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased description:
name: 'Values (unformatted)', 'Numbers stay as numbers, but any currency signs or special formatting is lost',
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',
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased description:
name: 'Values (formatted)', 'Numbers are turned to text, and displayed as in Google Sheets (e.g. with commas or currency signs)',
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',
{ },
name: 'Formulas', ],
value: 'FORMULA', default: '',
}, description: 'Determines how values should be rendered in the output',
], },
default: '', {
description: 'Determines how values should be rendered in the output', displayName: 'Date Formatting',
}, name: 'date',
{ type: 'options',
displayName: 'Date Formatting', default: '',
name: 'date', options: [
type: 'options', {
default: '', name: 'Formatted Text',
options: [ value: 'FORMATTED_STRING',
{ description: "As displayed in Google Sheets, e.g. '01/01/2022'",
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',
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',
export const cellFormat: INodeProperties[] = [ options: [
{ {
displayName: 'Cell Format', // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
name: 'cellFormat', name: 'Let Google Sheets format',
type: 'options', value: 'USER_ENTERED',
options: [ description: 'Cells are styled as if you typed the values into Google Sheets directly',
{ },
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased {
name: 'Let n8n format', // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
value: 'RAW', name: 'Let n8n format',
description: 'Cells have the same types as the input data', 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', default: 'USER_ENTERED',
value: 'USER_ENTERED', description: 'Determines how data should be interpreted',
description: 'Cells are styled as if you typed the values into Google Sheets directly', };
},
], export const handlingExtraData: INodeProperties = {
default: 'RAW', // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
description: 'Determines how data should be interpreted', displayName: 'Handling extra fields in input',
}, name: 'handlingExtraData',
]; type: 'options',
options: [
export const handlingExtraData: INodeProperties[] = [ {
{ name: 'Insert in New Column(s)',
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased value: 'insertInNewColumn',
displayName: 'Handling extra fields in input', description: 'Create a new column for extra data',
name: 'handlingExtraData', },
type: 'options', {
options: [ name: 'Ignore Them',
{ value: 'ignoreIt',
name: 'Insert in New Column(s)', description: 'Ignore extra data',
value: 'insertInNewColumn', },
description: 'Create a new column for extra data', {
}, name: 'Error',
{ value: 'error',
name: 'Ignore Them', description: 'Throw an error',
value: 'ignoreIt', },
description: 'Ignore extra data', ],
}, displayOptions: {
{ show: {
name: 'Error', '/dataMode': ['autoMapInputData'],
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",
}, },
]; default: 'insertInNewColumn',
description: "What do to with fields that don't match any columns in the Google Sheet",
};

View file

@ -80,8 +80,8 @@ export const description: SheetProperties = [
}, },
}, },
options: [ options: [
...dataLocationOnSheet, dataLocationOnSheet,
...outputFormatting, outputFormatting,
{ {
displayName: 'When Filter Has Multiple Matches', displayName: 'When Filter Has Multiple Matches',
name: 'returnAllMatches', name: 'returnAllMatches',

View file

@ -7,7 +7,7 @@ import type {
} from '../../helpers/GoogleSheets.types'; } from '../../helpers/GoogleSheets.types';
import { NodeOperationError } from 'n8n-workflow'; import { NodeOperationError } from 'n8n-workflow';
import type { GoogleSheet } from '../../helpers/GoogleSheet'; import type { GoogleSheet } from '../../helpers/GoogleSheet';
import { untilSheetSelected } from '../../helpers/GoogleSheets.utils'; import { cellFormatDefault, untilSheetSelected } from '../../helpers/GoogleSheets.utils';
import { cellFormat, handlingExtraData, locationDefine } from './commonDescription'; import { cellFormat, handlingExtraData, locationDefine } from './commonDescription';
export const description: SheetProperties = [ export const description: SheetProperties = [
@ -172,7 +172,7 @@ export const description: SheetProperties = [
show: { show: {
resource: ['sheet'], resource: ['sheet'],
operation: ['update'], operation: ['update'],
'@version': [4], '@version': [4, 4.1],
}, },
hide: { hide: {
...untilSheetSelected, ...untilSheetSelected,
@ -194,7 +194,15 @@ export const description: SheetProperties = [
...untilSheetSelected, ...untilSheetSelected,
}, },
}, },
options: [...cellFormat, ...locationDefine, ...handlingExtraData], options: [
cellFormat,
locationDefine,
handlingExtraData,
{
...handlingExtraData,
displayOptions: { show: { '/columns.mappingMode': ['autoMapInputData'] } },
},
],
}, },
]; ];
@ -204,17 +212,22 @@ export async function execute(
sheetName: string, sheetName: string,
): Promise<INodeExecutionData[]> { ): Promise<INodeExecutionData[]> {
const items = this.getInputData(); const items = this.getInputData();
const valueInputMode = this.getNodeParameter('options.cellFormat', 0, 'RAW') as ValueInputOption; const nodeVersion = this.getNode().typeVersion;
const range = `${sheetName}!A:Z`; const range = `${sheetName}!A:Z`;
const valueInputMode = this.getNodeParameter(
'options.cellFormat',
0,
cellFormatDefault(nodeVersion),
) as ValueInputOption;
const options = this.getNodeParameter('options', 0, {}); const options = this.getNodeParameter('options', 0, {});
const valueRenderMode = (options.valueRenderMode || 'UNFORMATTED_VALUE') as ValueRenderOption; const valueRenderMode = (options.valueRenderMode || 'UNFORMATTED_VALUE') as ValueRenderOption;
const locationDefineOptions = (options.locationDefine as IDataObject)?.values as IDataObject; const locationDefineOptions = (options.locationDefine as IDataObject)?.values as IDataObject;
const nodeVersion = this.getNode().typeVersion;
let headerRow = 0; let headerRow = 0;
let firstDataRow = 1; let firstDataRow = 1;
@ -254,6 +267,7 @@ export async function execute(
// TODO: Add support for multiple columns to match on in the next overhaul // TODO: Add support for multiple columns to match on in the next overhaul
const keyIndex = columnNames.indexOf(columnsToMatchOn[0]); const keyIndex = columnNames.indexOf(columnsToMatchOn[0]);
//not used when updating row
const columnValues = await sheet.getColumnValues( const columnValues = await sheet.getColumnValues(
range, range,
keyIndex, keyIndex,
@ -276,7 +290,7 @@ export async function execute(
if (handlingExtraDataOption === 'ignoreIt') { if (handlingExtraDataOption === 'ignoreIt') {
data.push(items[i].json); data.push(items[i].json);
} }
if (handlingExtraDataOption === 'error') { if (handlingExtraDataOption === 'error' && columnsToMatchOn[0] !== 'row_number') {
Object.keys(items[i].json).forEach((key) => { Object.keys(items[i].json).forEach((key) => {
if (!columnNames.includes(key)) { if (!columnNames.includes(key)) {
throw new NodeOperationError(this.getNode(), 'Unexpected fields in node input', { throw new NodeOperationError(this.getNode(), 'Unexpected fields in node input', {
@ -287,7 +301,7 @@ export async function execute(
}); });
data.push(items[i].json); data.push(items[i].json);
} }
if (handlingExtraDataOption === 'insertInNewColumn') { if (handlingExtraDataOption === 'insertInNewColumn' && columnsToMatchOn[0] !== 'row_number') {
Object.keys(items[i].json).forEach((key) => { Object.keys(items[i].json).forEach((key) => {
if (!columnNames.includes(key)) { if (!columnNames.includes(key)) {
newColumns.add(key); newColumns.add(key);
@ -350,22 +364,29 @@ export async function execute(
await sheet.updateRows( await sheet.updateRows(
sheetName, sheetName,
[columnNames.concat([...newColumns])], [columnNames.concat([...newColumns])],
(options.cellFormat as ValueInputOption) || 'RAW', (options.cellFormat as ValueInputOption) || cellFormatDefault(nodeVersion),
headerRow + 1, headerRow + 1,
); );
} }
const preparedData = await sheet.prepareDataForUpdateOrUpsert( let preparedData;
data, if (columnsToMatchOn[0] === 'row_number') {
columnsToMatchOn[0], preparedData = sheet.prepareDataForUpdatingByRowNumber(data, range, [
range, columnNames.concat([...newColumns]),
headerRow, ]);
firstDataRow, } else {
valueRenderMode, preparedData = await sheet.prepareDataForUpdateOrUpsert(
false, data,
[columnNames.concat([...newColumns])], columnsToMatchOn[0],
columnValues, range,
); headerRow,
firstDataRow,
valueRenderMode,
false,
[columnNames.concat([...newColumns])],
columnValues,
);
}
updateData.push(...preparedData.updateData); updateData.push(...preparedData.updateData);
} }

View file

@ -9,7 +9,7 @@ export const versionDescription: INodeTypeDescription = {
name: 'googleSheets', name: 'googleSheets',
icon: 'file:googleSheets.svg', icon: 'file:googleSheets.svg',
group: ['input', 'output'], group: ['input', 'output'],
version: [3, 4], version: [3, 4, 4.1],
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Read, update and write data to Google Sheets', description: 'Read, update and write data to Google Sheets',
defaults: { defaults: {

View file

@ -5,9 +5,9 @@ import type {
IPollFunctions, IPollFunctions,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow'; import { NodeOperationError } from 'n8n-workflow';
import { apiRequest } from '../transport';
import { utils as xlsxUtils } from 'xlsx'; import { utils as xlsxUtils } from 'xlsx';
import get from 'lodash/get'; import get from 'lodash/get';
import { apiRequest } from '../transport';
import type { import type {
ILookupValues, ILookupValues,
ISheetUpdateData, ISheetUpdateData,
@ -553,6 +553,53 @@ export class GoogleSheet {
return { updateData, appendData }; return { updateData, appendData };
} }
/**
* Updates data in a sheet
*
* @param {IDataObject[]} inputData Data to update Sheet with
* @param {string} range The range to look for data
* @param {number} dataStartRowIndex Index of the first row which contains data
* @param {string[][]} columnNamesList The column names to use
* @returns {Promise<string[][]>}
* @memberof GoogleSheet
*/
prepareDataForUpdatingByRowNumber(
inputData: IDataObject[],
range: string,
columnNamesList: string[][],
) {
const decodedRange = this.getDecodedSheetRange(range);
const columnNames = columnNamesList[0];
const updateData: ISheetUpdateData[] = [];
for (const item of inputData) {
const updateRowIndex = item.row_number as number;
for (const name of columnNames) {
if (name === 'row_number') continue;
if (item[name] === undefined || item[name] === null) continue;
const columnToUpdate = this.getColumnWithOffset(
decodedRange.start?.column || 'A',
columnNames.indexOf(name),
);
let updateValue = item[name] as string;
if (typeof updateValue === 'object') {
try {
updateValue = JSON.stringify(updateValue);
} catch (error) {}
}
updateData.push({
range: `${decodedRange.name}!${columnToUpdate}${updateRowIndex}`,
values: [[updateValue]],
});
}
}
return { updateData };
}
/** /**
* Looks for a specific value in a column and if it gets found it returns the whole row * Looks for a specific value in a column and if it gets found it returns the whole row
* *

View file

@ -315,3 +315,10 @@ export function sortLoadOptions(data: INodePropertyOptions[] | INodeListSearchIt
return returnData; return returnData;
} }
export function cellFormatDefault(nodeVersion: number) {
if (nodeVersion < 4.1) {
return 'RAW';
}
return 'USER_ENTERED';
}

View file

@ -1,6 +1,11 @@
import type { IDataObject, ILoadOptionsFunctions, ResourceMapperFields } from 'n8n-workflow'; import type {
IDataObject,
ILoadOptionsFunctions,
ResourceMapperField,
ResourceMapperFields,
} from 'n8n-workflow';
import { GoogleSheet } from '../helpers/GoogleSheet'; import { GoogleSheet } from '../helpers/GoogleSheet';
import type { ResourceLocator } from '../helpers/GoogleSheets.types'; import { ROW_NUMBER, type ResourceLocator } from '../helpers/GoogleSheets.types';
import { getSpreadsheetId } from '../helpers/GoogleSheets.utils'; import { getSpreadsheetId } from '../helpers/GoogleSheets.utils';
export async function getMappingColumns( export async function getMappingColumns(
@ -22,17 +27,32 @@ export async function getMappingColumns(
const sheetData = await sheet.getData(`${sheetName}!1:1`, 'FORMATTED_VALUE'); const sheetData = await sheet.getData(`${sheetName}!1:1`, 'FORMATTED_VALUE');
const columns = sheet.testFilter(sheetData || [], 0, 0).filter((col) => col !== ''); const columns = sheet.testFilter(sheetData || [], 0, 0).filter((col) => col !== '');
const columnData: ResourceMapperFields = {
fields: columns.map((col) => ({ const fields: ResourceMapperField[] = columns.map((col) => ({
id: col, id: col,
displayName: col, displayName: col,
required: false,
defaultMatch: col === 'id',
display: true,
type: 'string',
canBeUsedToMatch: true,
}));
const operation = this.getNodeParameter('operation', 0) as string;
if (operation === 'update') {
fields.push({
id: ROW_NUMBER,
displayName: ROW_NUMBER,
required: false, required: false,
defaultMatch: col === 'id', defaultMatch: false,
display: true, display: true,
type: 'string', type: 'string',
canBeUsedToMatch: true, canBeUsedToMatch: true,
})), readOnly: true,
}; removed: true,
});
}
return columnData; return { fields };
} }