Skip to content

Commit d924d82

Browse files
CadiacMiloradFilipovicnikhilkuria
authored
fix(Google Sheets Node): Make it possible to set cell values empty on updates (#17224)
Co-authored-by: Milorad FIlipović <[email protected]> Co-authored-by: Nikhil Kuriakose <[email protected]>
1 parent 22f505d commit d924d82

File tree

9 files changed

+375
-5
lines changed

9 files changed

+375
-5
lines changed

packages/frontend/editor-ui/src/components/ParameterInputList.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -607,6 +607,7 @@ const onCalloutDismiss = async (parameter: INodeProperties) => {
607607
:path="getPath(parameter.name)"
608608
:dependent-parameters-values="getDependentParametersValues(parameter)"
609609
:is-read-only="isReadOnly"
610+
:allow-empty-strings="parameter.typeOptions?.resourceMapper?.allowEmptyValues"
610611
input-size="small"
611612
label-size="small"
612613
@value-changed="valueChanged"

packages/frontend/editor-ui/src/components/ResourceMapper/ResourceMapper.vue

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ type Props = {
4040
teleported?: boolean;
4141
dependentParametersValues?: string | null;
4242
isReadOnly?: boolean;
43+
allowEmptyStrings?: boolean;
4344
};
4445
4546
const nodeTypesStore = useNodeTypesStore();
@@ -50,6 +51,7 @@ const props = withDefaults(defineProps<Props>(), {
5051
teleported: true,
5152
dependentParametersValues: null,
5253
isReadOnly: false,
54+
allowEmptyStrings: false,
5355
});
5456
5557
const { onDocumentVisible } = useDocumentVisibility();
@@ -436,8 +438,8 @@ function fieldValueChanged(updateInfo: IUpdateInformation): void {
436438
let newValue = null;
437439
if (
438440
updateInfo.value !== undefined &&
439-
updateInfo.value !== '' &&
440441
updateInfo.value !== null &&
442+
(props.allowEmptyStrings || updateInfo.value !== '') &&
441443
isResourceMapperValue(updateInfo.value)
442444
) {
443445
newValue = updateInfo.value;

packages/nodes-base/nodes/Google/Sheet/GoogleSheets.node.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export class GoogleSheets extends VersionedNodeType {
1111
name: 'googleSheets',
1212
icon: 'file:googleSheets.svg',
1313
group: ['input', 'output'],
14-
defaultVersion: 4.6,
14+
defaultVersion: 4.7,
1515
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
1616
description: 'Read, update and write data to Google Sheets',
1717
};
@@ -27,6 +27,7 @@ export class GoogleSheets extends VersionedNodeType {
2727
4.4: new GoogleSheetsV2(baseDescription),
2828
4.5: new GoogleSheetsV2(baseDescription),
2929
4.6: new GoogleSheetsV2(baseDescription),
30+
4.7: new GoogleSheetsV2(baseDescription),
3031
};
3132

3233
super(nodeVersions, baseDescription);

packages/nodes-base/nodes/Google/Sheet/test/v2/node/appendOrUpdate.test.ts

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,3 +96,151 @@ describe('Google Sheet - Append or Update', () => {
9696
});
9797
});
9898
});
99+
100+
describe('Google Sheet - Append or Update v4.6 vs v4.7 Behavior', () => {
101+
let mockExecuteFunctions: MockProxy<IExecuteFunctions>;
102+
let mockGoogleSheet: MockProxy<GoogleSheet>;
103+
104+
afterEach(() => {
105+
jest.resetAllMocks();
106+
});
107+
108+
it('v4.6: empty string in UI gets filtered out, field not sent to backend', async () => {
109+
mockExecuteFunctions = mock<IExecuteFunctions>();
110+
mockGoogleSheet = mock<GoogleSheet>();
111+
112+
mockExecuteFunctions.getNode
113+
.mockReturnValueOnce(mock<INode>({ typeVersion: 4.6 }))
114+
.mockReturnValueOnce(mock<INode>({ typeVersion: 4.6 }));
115+
116+
mockExecuteFunctions.getInputData.mockReturnValueOnce([
117+
{
118+
json: {},
119+
pairedItem: { item: 0, input: undefined },
120+
},
121+
]);
122+
123+
mockExecuteFunctions.getNodeParameter.mockImplementation((parameterName: string) => {
124+
const params: { [key: string]: any } = {
125+
'options.cellFormat': 'USER_ENTERED',
126+
options: {},
127+
'columns.mappingMode': 'defineBelow',
128+
'columns.schema': [],
129+
'columns.matchingColumns': ['id'],
130+
'columns.value': {
131+
id: 1,
132+
name: 'John',
133+
// email field is NOT present here because user typed '' in UI
134+
// and v4.6 frontend filtered it out (allowEmptyValues: false)
135+
},
136+
};
137+
return params[parameterName];
138+
});
139+
140+
mockGoogleSheet.getData.mockResolvedValueOnce([
141+
['id', 'name', 'email'],
142+
['1', 'Old Name', '[email protected]'],
143+
]);
144+
145+
mockGoogleSheet.getColumnValues.mockResolvedValueOnce(['1']);
146+
mockGoogleSheet.updateRows.mockResolvedValueOnce([]);
147+
148+
mockGoogleSheet.prepareDataForUpdateOrUpsert.mockResolvedValueOnce({
149+
updateData: [],
150+
appendData: [
151+
{
152+
id: 1,
153+
name: 'John',
154+
// email is not included, so it keeps old value
155+
},
156+
],
157+
});
158+
159+
mockGoogleSheet.appendEmptyRowsOrColumns.mockResolvedValueOnce([]);
160+
mockGoogleSheet.appendSheetData.mockResolvedValueOnce([]);
161+
162+
await execute.call(mockExecuteFunctions, mockGoogleSheet, 'Sheet1', '1234');
163+
164+
// v4.6: Only fields with non-empty values are sent to prepareDataForUpdateOrUpsert
165+
expect(mockGoogleSheet.prepareDataForUpdateOrUpsert).toHaveBeenCalledWith(
166+
expect.objectContaining({
167+
inputData: [
168+
{
169+
id: 1,
170+
name: 'John',
171+
// email is NOT in the inputData, so cell keeps old value
172+
},
173+
],
174+
}),
175+
);
176+
});
177+
178+
it('v4.7: empty string in UI is preserved and sent to backend to clear cell', async () => {
179+
mockExecuteFunctions = mock<IExecuteFunctions>();
180+
mockGoogleSheet = mock<GoogleSheet>();
181+
182+
mockExecuteFunctions.getNode
183+
.mockReturnValueOnce(mock<INode>({ typeVersion: 4.7 }))
184+
.mockReturnValueOnce(mock<INode>({ typeVersion: 4.7 }));
185+
186+
mockExecuteFunctions.getInputData.mockReturnValueOnce([
187+
{
188+
json: {},
189+
pairedItem: { item: 0, input: undefined },
190+
},
191+
]);
192+
193+
mockExecuteFunctions.getNodeParameter.mockImplementation((parameterName: string) => {
194+
const params: { [key: string]: any } = {
195+
'options.cellFormat': 'USER_ENTERED',
196+
options: {},
197+
'columns.mappingMode': 'defineBelow',
198+
'columns.schema': [],
199+
'columns.matchingColumns': ['id'],
200+
'columns.value': {
201+
id: 1,
202+
name: 'John',
203+
email: '', // Empty string is preserved in v4.7 (allowEmptyValues: true)
204+
},
205+
};
206+
return params[parameterName];
207+
});
208+
209+
mockGoogleSheet.getData.mockResolvedValueOnce([
210+
['id', 'name', 'email'],
211+
['1', 'Old Name', '[email protected]'],
212+
]);
213+
214+
mockGoogleSheet.getColumnValues.mockResolvedValueOnce(['1']);
215+
mockGoogleSheet.updateRows.mockResolvedValueOnce([]);
216+
217+
mockGoogleSheet.prepareDataForUpdateOrUpsert.mockResolvedValueOnce({
218+
updateData: [],
219+
appendData: [
220+
{
221+
id: 1,
222+
name: 'John',
223+
email: '', // Empty string will clear the cell
224+
},
225+
],
226+
});
227+
228+
mockGoogleSheet.appendEmptyRowsOrColumns.mockResolvedValueOnce([]);
229+
mockGoogleSheet.appendSheetData.mockResolvedValueOnce([]);
230+
231+
await execute.call(mockExecuteFunctions, mockGoogleSheet, 'Sheet1', '1234');
232+
233+
// v4.7: Empty strings are preserved and sent to prepareDataForUpdateOrUpsert
234+
expect(mockGoogleSheet.prepareDataForUpdateOrUpsert).toHaveBeenCalledWith(
235+
expect.objectContaining({
236+
inputData: [
237+
{
238+
id: 1,
239+
name: 'John',
240+
email: '', // Empty string is preserved and will clear the cell
241+
},
242+
],
243+
}),
244+
);
245+
});
246+
});

packages/nodes-base/nodes/Google/Sheet/test/v2/node/update.test.ts

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,3 +319,150 @@ describe('Google Sheet - Update 4.6', () => {
319319
);
320320
});
321321
});
322+
323+
describe('Google Sheet - Update v4.6 vs v4.7 Behavior', () => {
324+
let mockExecuteFunctions: MockProxy<IExecuteFunctions>;
325+
let mockGoogleSheet: MockProxy<GoogleSheet>;
326+
327+
afterEach(() => {
328+
jest.resetAllMocks();
329+
});
330+
331+
it('v4.6: empty string in UI gets filtered out, field not sent to backend', async () => {
332+
mockExecuteFunctions = mock<IExecuteFunctions>();
333+
mockGoogleSheet = mock<GoogleSheet>();
334+
mockExecuteFunctions.getNode.mockReturnValueOnce(mock<INode>({ typeVersion: 4.6 }));
335+
mockGoogleSheet.batchUpdate.mockResolvedValueOnce([]);
336+
337+
mockExecuteFunctions.getInputData.mockReturnValueOnce([
338+
{
339+
json: {},
340+
pairedItem: { item: 0, input: undefined },
341+
},
342+
]);
343+
344+
mockExecuteFunctions.getNodeParameter.mockImplementation((parameterName: string) => {
345+
const params: { [key: string]: string | object } = {
346+
options: {},
347+
'options.cellFormat': 'USER_ENTERED',
348+
'columns.matchingColumns': ['id'],
349+
'columns.mappingMode': 'defineBelow',
350+
'columns.value': {
351+
id: 1,
352+
name: 'John',
353+
// email field is NOT present here because user typed '' in UI
354+
// and v4.6 frontend filtered it out (allowEmptyStrings: false)
355+
},
356+
};
357+
return params[parameterName];
358+
});
359+
360+
mockGoogleSheet.getData.mockResolvedValueOnce([
361+
['id', 'name', 'email'],
362+
['1', 'Old Name', '[email protected]'],
363+
]);
364+
365+
mockGoogleSheet.getColumnValues.mockResolvedValueOnce(['1']);
366+
367+
mockGoogleSheet.prepareDataForUpdateOrUpsert.mockResolvedValueOnce({
368+
updateData: [
369+
{
370+
range: 'Sheet1!B2',
371+
values: [['John']],
372+
},
373+
// No update for email column - it keeps its old value
374+
],
375+
appendData: [],
376+
});
377+
378+
await execute.call(mockExecuteFunctions, mockGoogleSheet, 'Sheet1');
379+
380+
// v4.6: Only name field is updated, email is not included in the update
381+
expect(mockGoogleSheet.prepareDataForUpdateOrUpsert).toHaveBeenCalledWith({
382+
inputData: [
383+
{
384+
id: 1,
385+
name: 'John',
386+
// email is NOT in the inputData, so cell keeps old value
387+
},
388+
],
389+
indexKey: 'id',
390+
range: 'Sheet1!A:Z',
391+
keyRowIndex: 0,
392+
dataStartRowIndex: 1,
393+
valueRenderMode: 'UNFORMATTED_VALUE',
394+
columnNamesList: [['id', 'name', 'email']],
395+
columnValuesList: ['1'],
396+
});
397+
});
398+
399+
it('v4.7: empty string in UI is preserved and sent to backend to clear cell', async () => {
400+
mockExecuteFunctions = mock<IExecuteFunctions>();
401+
mockGoogleSheet = mock<GoogleSheet>();
402+
mockExecuteFunctions.getNode.mockReturnValueOnce(mock<INode>({ typeVersion: 4.7 }));
403+
mockGoogleSheet.batchUpdate.mockResolvedValueOnce([]);
404+
405+
mockExecuteFunctions.getInputData.mockReturnValueOnce([
406+
{
407+
json: {},
408+
pairedItem: { item: 0, input: undefined },
409+
},
410+
]);
411+
412+
mockExecuteFunctions.getNodeParameter.mockImplementation((parameterName: string) => {
413+
const params: { [key: string]: string | object } = {
414+
options: {},
415+
'options.cellFormat': 'USER_ENTERED',
416+
'columns.matchingColumns': ['id'],
417+
'columns.mappingMode': 'defineBelow',
418+
'columns.value': {
419+
id: 1,
420+
name: 'John',
421+
email: '', // Empty string is preserved in v4.7 (allowEmptyStrings: true)
422+
},
423+
};
424+
return params[parameterName];
425+
});
426+
427+
mockGoogleSheet.getData.mockResolvedValueOnce([
428+
['id', 'name', 'email'],
429+
['1', 'Old Name', '[email protected]'],
430+
]);
431+
432+
mockGoogleSheet.getColumnValues.mockResolvedValueOnce(['1']);
433+
434+
mockGoogleSheet.prepareDataForUpdateOrUpsert.mockResolvedValueOnce({
435+
updateData: [
436+
{
437+
range: 'Sheet1!B2',
438+
values: [['John']],
439+
},
440+
{
441+
range: 'Sheet1!C2',
442+
values: [['']],
443+
},
444+
],
445+
appendData: [],
446+
});
447+
448+
await execute.call(mockExecuteFunctions, mockGoogleSheet, 'Sheet1');
449+
450+
// v4.7: Both name and email fields are updated, email is cleared with empty string
451+
expect(mockGoogleSheet.prepareDataForUpdateOrUpsert).toHaveBeenCalledWith({
452+
inputData: [
453+
{
454+
id: 1,
455+
name: 'John',
456+
email: '', // Empty string is preserved and will clear the cell
457+
},
458+
],
459+
indexKey: 'id',
460+
range: 'Sheet1!A:Z',
461+
keyRowIndex: 0,
462+
dataStartRowIndex: 1,
463+
valueRenderMode: 'UNFORMATTED_VALUE',
464+
columnNamesList: [['id', 'name', 'email']],
465+
columnValuesList: ['1'],
466+
});
467+
});
468+
});

0 commit comments

Comments
 (0)