Unprotect specific ranges via method call in Google Apps Script Sheets [duplicate] - google-apps-script

I was able to allow other users to add a new SKU to a sheet without unprotecting it (Original post). Now I am trying to do the inverse, to allow users to delete an SKU without unprotecting the sheet.
I started with the following, which works as expected:
function deleteEachRow(){
const ss = SpreadsheetApp.getActive();
var SHEET = ss.getSheetByName("Ordering");
var RANGE = SHEET.getDataRange();
const ui = SpreadsheetApp.getUi();
const response = ui.prompt('WARNING: \r\n \r\n Ensure the following sheets DO NOT contain data before proceeding: \r\n \r\n Accessory INV \r\n Apparel INV \r\n Pending TOs \r\n \r\n Enter New SKU:', ui.ButtonSet.OK_CANCEL);
if (response.getSelectedButton() === ui.Button.OK) {
const text = response.getResponseText();
var rangeVals = RANGE.getValues();
//Reverse the 'for' loop.
for(var i = rangeVals.length-1; i >= 0; i--){
if(rangeVals[i][0] === text){
SHEET.deleteRow(i+1);
};
};
};
};
I tried to Frankenstein the above code into the answer I was provided. Now the script runs without error but fails to delete the entered SKU as expected. This is the script I am running:
function deleteEachRow1(){
const ss = SpreadsheetApp.getActive();
var SHEET = ss.getSheetByName("Ordering");
var RANGE = SHEET.getDataRange();
const ui = SpreadsheetApp.getUi();
const response = ui.prompt('WARNING: \r\n \r\n Ensure the following sheets DO NOT contain data before proceeding: \r\n \r\n Accessory INV \r\n Apparel INV \r\n Pending TOs \r\n \r\n Delete Which SKU?:', ui.ButtonSet.OK_CANCEL);
if (response.getSelectedButton() === ui.Button.OK) {
const text = response.getResponseText();
const webAppsUrl = "WEB APP URL"; // Pleas set your Web Apps URL.
const url = webAppsUrl + "?text=" + text;
const res = UrlFetchApp.fetch(url, {muteHttpExceptions: true});
// ui.alert(res.getContentText()); // You can see the response value using this line.
}
}
function doGet(e) {
const text = e.parameter.text;
const sheet = SpreadsheetApp.getActive().getSheetByName('Ordering');
var rangeVals = RANGE.getValues();
//Reverse the 'for' loop.
for(var i = rangeVals.length-1; i >= 0; i--){
if(rangeVals[i][0] === text){
SHEET.deleteRow(i+1);
};
};
myFunction();
return ContentService.createTextOutput(text);
}
// This script is from https://tanaikech.github.io/2017/07/31/converting-a1notation-to-gridrange-for-google-sheets-api/
function a1notation2gridrange1(a1notation) {
var data = a1notation.match(/(^.+)!(.+):(.+$)/);
var ss = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(data[1]);
var range = ss.getRange(data[2] + ":" + data[3]);
var gridRange = {
sheetId: ss.getSheetId(),
startRowIndex: range.getRow() - 1,
endRowIndex: range.getRow() - 1 + range.getNumRows(),
startColumnIndex: range.getColumn() - 1,
endColumnIndex: range.getColumn() - 1 + range.getNumColumns(),
};
if (!data[2].match(/[0-9]/)) delete gridRange.startRowIndex;
if (!data[3].match(/[0-9]/)) delete gridRange.endRowIndex;
return gridRange;
}
// Please run this function.
function myFunction() {
const email = "MY EMAIL"; // <--- Please set your email address.
// Please set your sheet names and unprotected ranges you want to use.
const obj = [
{ sheetName: "Ordering", unprotectedRanges: ["O5:P", "C2:E2"] },
{ sheetName: "Accessory INV", unprotectedRanges: ["E5:H"] },
{ sheetName: "Apparel INV", unprotectedRanges: ["E5:F"] },
{sheetName: "Pending TOs", unprotectedRanges: ["E6:H"] },
{sheetName: "INV REF", unprotectedRanges: ["C6:C"] },
];
// 1. Retrieve sheet IDs and protected range IDs.
const spreadsheetId = SpreadsheetApp.getActiveSpreadsheet().getId();
const sheets = Sheets.Spreadsheets.get(spreadsheetId, { ranges: obj.map(({ sheetName }) => sheetName), fields: "sheets(protectedRanges(protectedRangeId),properties(sheetId))" }).sheets;
const { protectedRangeIds, sheetIds } = sheets.reduce((o, { protectedRanges, properties: { sheetId } }) => {
if (protectedRanges && protectedRanges.length > 0) o.protectedRangeIds.push(protectedRanges.map(({ protectedRangeId }) => protectedRangeId));
o.sheetIds.push(sheetId);
return o;
}, { protectedRangeIds: [], sheetIds: [] });
// 2. Convert A1Notation to Gridrange.
const gridranges = obj.map(({ sheetName, unprotectedRanges }, i) => unprotectedRanges.map(f => a1notation2gridrange1(`${sheetName}!${f}`)));
// 3. Create request body.
const deleteProptectedRanges = protectedRangeIds.flatMap(e => e.map(id => ({ deleteProtectedRange: { protectedRangeId: id } })));
const protects = sheetIds.map((sheetId, i) => ({ addProtectedRange: { protectedRange: { editors: {users: [email]}, range: { sheetId }, unprotectedRanges: gridranges[i] } } }));
// 4. Request to Sheets API with the created request body.
Sheets.Spreadsheets.batchUpdate({ requests: [...deleteProptectedRanges, ...protects] }, spreadsheetId);
}

Probably the easiest way to do this would be to avoid using a button and using a checkbox with a installable edit trigger, which also has a great side effect of mobile support.
Proposed solution:
Using a checkbox
Hook it to a installable edit trigger, which runs as the user who installed the trigger. Therefore, if the owner installs the trigger, no matter who edits the sheet, the trigger runs as the owner, giving access to privileged resources including protected ranges.
The installable version runs with the authorization of the user who created the trigger, even if another user with edit access opens the spreadsheet.
Notes:
Advantage:
Code simplicity and maintainabilty. No need for webapp or any complicated setup.
Disadvantage: Security (with possible workaround)
If the code is bound to the sheet, editors of the sheet get direct access to the script of the sheet. So, any editor with malicious intentions would be able to modify the code. If the function with installable trigger has gmail permissions, any editor would be able to log all the emails of the owner. So,special attention needs to be paid to permissions requested. Note that, this is already the case with your web app setup. Any editor maybe able to modify doGet to access protected data. If the webapp is in a separate standalone script, this isn't a issue. You may also be able to fix this issue by setting the trigger at a predetermined version instead of Head version. See this answer for more information.

Related

How to POST / PUT webhook from Google Sheets when new rows are added

Disclaimer : I'm a newbie and doesn't know much. I understand webhooks a little bit, but doesn't know how to code.
Now that is out of the way, what I'm trying to do is, send data from a CRM to google sheets, do some calculations and send it back. I was able to figure out the first step of sending from CRM and doing the calculations.
Here is what I want to do..
I'd like to be able to send the updated data from googlesheet back to the CRM as a webhook when a new row is added to the google sheets.
So I have created a google sheet, I was able to add a trigger to run myFunction on Edit. My current myFunction code is at the end.
**Ideally it should send "id" and "updated_value" fetched from google sheets to the CRM via webhook (PUT method) as it will be updating a contact record.
Can anyone please share the code to add here or guide me how to write one? Thank you in advance**
This is my code so far and I'm currently finding resources to learn to add more lines to it
Update: One problem I'm facing now is that, I'm getting the 1000th row as the last row as "I've used array function in one column. So I'm not actually getting the non-empty last row.
function myFunction() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName("Sheet1");
var lastRow = sheet.getLastRow();
var lastColumn = sheet.getLastColumn();
var range = sheet.getRange(lastRow, 1, 1, lastColumn);
var values = range.getValues();
var data = {
"id": values[0][1],
"f2130": values[0][3]
};
var options = {
'method': 'PUT',
'url': 'https://api.ontraport.com/1/Contacts',
'headers': {
'Content-Type': 'application/json',
'Api-Appid': 'xxxxxxxxxxxxxxx6',
'Api-Key': 'xxxxxxxxxxxxxxxH'
},
body: JSON.stringify({
"f2130": f2130,
"id": id
})
};
UrlFetchApp.fetch("https://api.ontraport.com/1/Contacts", options);
}
I finally able to figure it out by myself. Here is the code
Also you can read the steps here
https://damartech.com/how-to-send-and-receive-data-from-ontraport-to-google-sheet-and-back-without-zapier/
function opUpdater() {
// Replace `spreadsheetId` and `sheetName` with the ID and name of your Google Sheets document
const spreadsheetId = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
const sheetName = "xxxxxx";
// Get the sheet by ID and name
const sheet = SpreadsheetApp.openById(spreadsheetId).getSheetByName(sheetName);
// Get the number of rows in the sheet
const numRows = sheet.getLastRow();
// Initialize a variable to store the index of the last non-empty row
let lastNonEmptyRowIndex = 0;
// Loop through each row in the sheet
for (let i = 1; i <= numRows; i++) {
// Get the values in the current row
const rowValues = sheet.getRange(i, 1, 1, sheet.getLastColumn()).getValues()[0];
// Check if the row is empty
if (rowValues.every(cell => cell === "")) {
// If the row is not empty, store its index as the last non-empty row
lastNonEmptyRowIndex = i-1;
break;
}
}
// If a non-empty row was found, get its values
if (lastNonEmptyRowIndex > 0) {
const lastNonEmptyRowValues = sheet.getRange(lastNonEmptyRowIndex, 1, 1, sheet.getLastColumn()).getValues()[0];
// Define the variables that will be used in the request
// You can find the values from google sheet column number-1
var contact_id = parseInt(lastNonEmptyRowValues[1]);
var new_date = lastNonEmptyRowValues[3];
}
// Define the URL for the request
let url = "https://api.ontraport.com/1/Contacts";
// Define the options for the request
var options = {
"method": "put",
"headers": {
"Content-Type": "application/json",
"Accept": "application/json",
"Api-Appid": "xxxxxxxxxxxxxxxx",
"Api-Key": "xxxxxxxxxxxxxxxx"
},
"payload": JSON.stringify({
"id": contact_id,
"f2130": new_date
}),
"muteHttpExceptions": true
};
try {
const response = UrlFetchApp.fetch(url, options);
const responseBody = response.getContentText();
const data = JSON.parse(responseBody);
Logger.log(data.data.attrs.id);
Logger.log(data.data.attrs.f2130);
} catch (error) {
Logger.log(error.message);
Logger.log(error.response);
}
}

Google Apps script large data set transfer [duplicate]

I have a dataset which contains images in col C loaded via formula =IMAGE("") and the need is to refresh the data and have these formulas load the images at destination.
I tried the Spreadsheet API, but handling the data the way it's needed it still far to me - knowledge wise.
I try with the script below, but the column C shows as blank at destination:
function getOrdersData() {
const srcFile = SpreadsheetApp.openById('XXXXXXXXXXX');
const srcSht = srcFile.getSheetByName('Orders');
let srcData = srcSht.getRange(1, 1, srcSht.getLastRow(),
srcSht.getLastColumn()).getValues();
const orderHeaders = srcData[4]; //Colunm headers are actually in row 05
const imgCol = orderHeaders.indexOf('Image');//Whish is where the formulas loading the imgs are
const imgFormulas = srcSht.getRange(1, imgCol + 1, srcSht.getLastRow(), 1).getFormulas();
srcData.forEach(function (row) {
row.splice(imgCol, 1, imgFormulas);
});
const dstFile = SpreadsheetApp.openById('XXXXXXXXXXXXXXX');
const dstSht = dstFile.getSheetByName('Orders v2');
const dstShtLr = dstSht.getLastRow();
if (dstShtLr > 0) {
dstSht.getRange(1, 1, dstShtLr, dstSht.getLastColumn()).clearContent();
}
dstSht.getRange(1, 1, srcData.length, srcData[0].length).setValues(srcData);
}
What can I try next?
In your script, imgFormulas is a 2-dimensional array. In this case, by srcData.forEach(,,,), srcData is not 2 dimensional array. I thought that this might be the reason for your issue. When your script is modified, how about the following modification?
From:
srcData.forEach(function (row) {
row.splice(imgCol, 1, imgFormulas);
});
To:
srcData.forEach(function (row, i) {
if (i > 4) row.splice(imgCol, 1, imgFormulas[i][0]);
});
if (i > 4) was used for considering Colunm headers are actually in row 05.
Note:
In your situation, when Sheets API is used, the sample script is as follows. In this case, please enable Sheets API at Advanced Google services. When the number of cells are large, this might be useful.
function sample() {
const srcSSId = '###'; // Please set source Spreadsheet ID.
const dstSSId = '###'; // Please set destination Spreadsheet ID.
const srcSheetName = 'Orders';
const dstSheetName = 'Orders v2';
const srcValues = Sheets.Spreadsheets.Values.get(srcSSId, srcSheetName).values;
const srcFormulas = Sheets.Spreadsheets.Values.get(srcSSId, srcSheetName, { valueRenderOption: "FORMULA" }).values;
const data = [{ range: dstSheetName, values: srcValues }, { range: dstSheetName, values: srcFormulas }];
Sheets.Spreadsheets.Values.batchUpdate({ valueInputOption: "USER_ENTERED", data }, dstSSId);
}
References:
Method: spreadsheets.values.get
Method: spreadsheets.values.batchUpdate

Google Apps Script limitation to Extract data from web page using Cheerio Library

Continuing my previous question, #Tanaike proposed a solution to extract performance score from the following page:
This is the code snippet to get around it:
function CheckPageSpeed(url) {
const apiKey = "###"; // Please set your API key.
const apiEndpoint = `https://www.googleapis.com/pagespeedonline/v5/runPagespeed?key=${apiKey}&url=${encodeURIComponent(url)}&category=performance`;
const strategy = ["mobile"];
const res = UrlFetchApp.fetchAll(strategy.map(e => ({ url: `${apiEndpoint}&strategy=${e}`, muteHttpExceptions: true })));
const values = res.reduce((o, r, i) => {
if (r.getResponseCode() == 200) {
const obj = JSON.parse(r.getContentText());
o[strategy[i]] = obj.lighthouseResult.categories.performance.score * 100;
} else {
o[strategy[i]] = null;
}
return o;
}, {});
return values.mobile;
}
As I am using it in Google sheets as custom formula, sometimes it takes so much time that the sheet throws the following error:
Is there any way that we can counter this error so that it starts calculating the score again instead of throwing an error? Thank you.
Issue and workaround:
From your showing image, your error of Exceed maximum execution time and your updated script, in this case, it is considered that the execution time of the script is over 30 seconds. (In the current stage, the maximum execution time of the custom function is 30 seconds. Ref) In this case, when the error of Exceed maximum execution time occurs, unfortunately, this cannot be used as the trigger. And also, in the current stage, UrlFetchApp cannot be stopped over time. And, for example, even when all URLs are retrieved and each value is retrieved from the API, I'm not sure whether the processing time is over 6 minutes. I'm worried about this.
From the above situation, how about manually rerunning only the custom functions which occur the error?
Sample script:
Before you use this script, please enable Sheets API at Advanced Google services. How about executing this function by a button on Spreadsheet and/or the custom menu?
function reCalculation() {
const sheetName = "Sheet1"; // Please set sheet name.
const formula = "=CheckPageSpeed"; // Please set the function name of your custom function.
const dummy = "=sample";
const ss = SpreadsheetApp.getActiveSpreadsheet();
const ssId = ss.getId();
const sheet = ss.getSheetByName(sheetName);
const sheetId = sheet.getSheetId();
const values = sheet.getRange("B1:B" + sheet.getLastRow()).getDisplayValues();
const requests = values.reduce((ar, [a], i) => {
if (a == "#ERROR!") {
ar.push({ findReplace: { range: { sheetId, startRowIndex: i, endRowIndex: i + 1, startColumnIndex: 1, endColumnIndex: 2 }, find: `^${formula}`, replacement: dummy, includeFormulas: true, searchByRegex: true } }); // Modified
}
return ar;
}, []);
if (requests.length == 0) return;
Sheets.Spreadsheets.batchUpdate({ requests }, ssId);
SpreadsheetApp.flush();
requests.forEach(r => {
r.findReplace.find = dummy;
r.findReplace.replacement = formula;
r.findReplace.searchByRegex = false;
});
Sheets.Spreadsheets.batchUpdate({ requests }, ssId);
}
When this script is run, only the cells of #ERROR! in the column "B" are recalculated.
Note:
I thought that in this case, this function might be able to be executed by the time-driven trigger. But, in that case, it might affect the quotas (maximum execution time is 90 minutes/day) of the time-driven trigger. So, in this answer, I proposed to run this function using manual operation.
References:
Method: spreadsheets.batchUpdate
FindReplaceRequest
Added:
For example, in your situation, how about directly requesting the API endpoint using fetchAll method? The sample script is as follows. In this case, the URLs are retrieved from the column "A" and the values are retrieved and put to the column "C" in your sample Spreadsheet.
Sample script:
Please set your API key. And, please run this script with the script editor. By this, the values are retrieved using the API.
function reCalculation2() {
const apiKey = "###"; // Please set your API key.
const sheetName = "Sheet1"; // Please set sheet name.
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName(sheetName);
const values = sheet.getRange("A2:A" + sheet.getLastRow()).getValues();
const requests = values.map(([url]) => {
const apiEndpoint = `https://www.googleapis.com/pagespeedonline/v5/runPagespeed?key=${apiKey}&url=${encodeURIComponent(url)}&category=performance&strategy=mobile`;
return { url: apiEndpoint, muteHttpExceptions: true };
});
const res = UrlFetchApp.fetchAll(requests);
const v = res.map(r => {
if (r.getResponseCode() == 200) {
const obj = JSON.parse(r.getContentText());
return [obj.lighthouseResult.categories.performance.score * 100];
}
return [null];
});
sheet.getRange(2, 3, v.length).setValues(v);
}
In this case, fetchAll method is used. By this, I thought that the error of Exceeded maximum execution might be able to be avoided.

How to apply filter to protected sheet on Google Sheets? [duplicate]

I was able to allow other users to add a new SKU to a sheet without unprotecting it (Original post). Now I am trying to do the inverse, to allow users to delete an SKU without unprotecting the sheet.
I started with the following, which works as expected:
function deleteEachRow(){
const ss = SpreadsheetApp.getActive();
var SHEET = ss.getSheetByName("Ordering");
var RANGE = SHEET.getDataRange();
const ui = SpreadsheetApp.getUi();
const response = ui.prompt('WARNING: \r\n \r\n Ensure the following sheets DO NOT contain data before proceeding: \r\n \r\n Accessory INV \r\n Apparel INV \r\n Pending TOs \r\n \r\n Enter New SKU:', ui.ButtonSet.OK_CANCEL);
if (response.getSelectedButton() === ui.Button.OK) {
const text = response.getResponseText();
var rangeVals = RANGE.getValues();
//Reverse the 'for' loop.
for(var i = rangeVals.length-1; i >= 0; i--){
if(rangeVals[i][0] === text){
SHEET.deleteRow(i+1);
};
};
};
};
I tried to Frankenstein the above code into the answer I was provided. Now the script runs without error but fails to delete the entered SKU as expected. This is the script I am running:
function deleteEachRow1(){
const ss = SpreadsheetApp.getActive();
var SHEET = ss.getSheetByName("Ordering");
var RANGE = SHEET.getDataRange();
const ui = SpreadsheetApp.getUi();
const response = ui.prompt('WARNING: \r\n \r\n Ensure the following sheets DO NOT contain data before proceeding: \r\n \r\n Accessory INV \r\n Apparel INV \r\n Pending TOs \r\n \r\n Delete Which SKU?:', ui.ButtonSet.OK_CANCEL);
if (response.getSelectedButton() === ui.Button.OK) {
const text = response.getResponseText();
const webAppsUrl = "WEB APP URL"; // Pleas set your Web Apps URL.
const url = webAppsUrl + "?text=" + text;
const res = UrlFetchApp.fetch(url, {muteHttpExceptions: true});
// ui.alert(res.getContentText()); // You can see the response value using this line.
}
}
function doGet(e) {
const text = e.parameter.text;
const sheet = SpreadsheetApp.getActive().getSheetByName('Ordering');
var rangeVals = RANGE.getValues();
//Reverse the 'for' loop.
for(var i = rangeVals.length-1; i >= 0; i--){
if(rangeVals[i][0] === text){
SHEET.deleteRow(i+1);
};
};
myFunction();
return ContentService.createTextOutput(text);
}
// This script is from https://tanaikech.github.io/2017/07/31/converting-a1notation-to-gridrange-for-google-sheets-api/
function a1notation2gridrange1(a1notation) {
var data = a1notation.match(/(^.+)!(.+):(.+$)/);
var ss = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(data[1]);
var range = ss.getRange(data[2] + ":" + data[3]);
var gridRange = {
sheetId: ss.getSheetId(),
startRowIndex: range.getRow() - 1,
endRowIndex: range.getRow() - 1 + range.getNumRows(),
startColumnIndex: range.getColumn() - 1,
endColumnIndex: range.getColumn() - 1 + range.getNumColumns(),
};
if (!data[2].match(/[0-9]/)) delete gridRange.startRowIndex;
if (!data[3].match(/[0-9]/)) delete gridRange.endRowIndex;
return gridRange;
}
// Please run this function.
function myFunction() {
const email = "MY EMAIL"; // <--- Please set your email address.
// Please set your sheet names and unprotected ranges you want to use.
const obj = [
{ sheetName: "Ordering", unprotectedRanges: ["O5:P", "C2:E2"] },
{ sheetName: "Accessory INV", unprotectedRanges: ["E5:H"] },
{ sheetName: "Apparel INV", unprotectedRanges: ["E5:F"] },
{sheetName: "Pending TOs", unprotectedRanges: ["E6:H"] },
{sheetName: "INV REF", unprotectedRanges: ["C6:C"] },
];
// 1. Retrieve sheet IDs and protected range IDs.
const spreadsheetId = SpreadsheetApp.getActiveSpreadsheet().getId();
const sheets = Sheets.Spreadsheets.get(spreadsheetId, { ranges: obj.map(({ sheetName }) => sheetName), fields: "sheets(protectedRanges(protectedRangeId),properties(sheetId))" }).sheets;
const { protectedRangeIds, sheetIds } = sheets.reduce((o, { protectedRanges, properties: { sheetId } }) => {
if (protectedRanges && protectedRanges.length > 0) o.protectedRangeIds.push(protectedRanges.map(({ protectedRangeId }) => protectedRangeId));
o.sheetIds.push(sheetId);
return o;
}, { protectedRangeIds: [], sheetIds: [] });
// 2. Convert A1Notation to Gridrange.
const gridranges = obj.map(({ sheetName, unprotectedRanges }, i) => unprotectedRanges.map(f => a1notation2gridrange1(`${sheetName}!${f}`)));
// 3. Create request body.
const deleteProptectedRanges = protectedRangeIds.flatMap(e => e.map(id => ({ deleteProtectedRange: { protectedRangeId: id } })));
const protects = sheetIds.map((sheetId, i) => ({ addProtectedRange: { protectedRange: { editors: {users: [email]}, range: { sheetId }, unprotectedRanges: gridranges[i] } } }));
// 4. Request to Sheets API with the created request body.
Sheets.Spreadsheets.batchUpdate({ requests: [...deleteProptectedRanges, ...protects] }, spreadsheetId);
}
Probably the easiest way to do this would be to avoid using a button and using a checkbox with a installable edit trigger, which also has a great side effect of mobile support.
Proposed solution:
Using a checkbox
Hook it to a installable edit trigger, which runs as the user who installed the trigger. Therefore, if the owner installs the trigger, no matter who edits the sheet, the trigger runs as the owner, giving access to privileged resources including protected ranges.
The installable version runs with the authorization of the user who created the trigger, even if another user with edit access opens the spreadsheet.
Notes:
Advantage:
Code simplicity and maintainabilty. No need for webapp or any complicated setup.
Disadvantage: Security (with possible workaround)
If the code is bound to the sheet, editors of the sheet get direct access to the script of the sheet. So, any editor with malicious intentions would be able to modify the code. If the function with installable trigger has gmail permissions, any editor would be able to log all the emails of the owner. So,special attention needs to be paid to permissions requested. Note that, this is already the case with your web app setup. Any editor maybe able to modify doGet to access protected data. If the webapp is in a separate standalone script, this isn't a issue. You may also be able to fix this issue by setting the trigger at a predetermined version instead of Head version. See this answer for more information.

How to use conditional formatting in Google sheets api v4

Good day. Please tell me how I can convert this script to use Google sheets api v4
and reduce the cost of the request. Understand correctly that I need to dig to the side:
https://developers.google.com/sheets/api/samples/conditional-formatting?hl=en#add_a_conditional_formatting_rule_to_a_set_of_ranges
?
Sample code below
while (folders.hasNext()) {
var folder = folders.next().getId();
var sheet1 = SpreadsheetApp.openById(folder);
var sheet = sheet1.getActiveSheet();
var r1 = sheet.getRange('Q4:Q');var r2 = sheet.getRange('S4:S');
var rule = SpreadsheetApp.newConditionalFormatRule()
.setGradientMaxpoint("#06ff00")
.setGradientMidpointWithValue("#ffef00", SpreadsheetApp.InterpolationType.PERCENTILE, "50")
.setGradientMinpoint("#ff0000")
.setRanges([r1,r2,r3,r4,r5,r6,r7,r8,r9,r10,
r11,r12,r13,r14,r15,r16,r17,r18,r19,r20,
r21,r22,r23,r24,r25,r26,r27,r28,r29,r30,
r31,r32,r33,r34,r35,r36,r37,r38,r39,r40,
r41,r42,r43,r44,r45,r46,r47,r48,r49,r50,
r51,r52,r53,r54,r55,r56,r57,r58,r59,r60,
r61,r62,r63,r64,r65])
.build()
var rules = sheet.getConditionalFormatRules();
rules.push(rule);
sheet.setConditionalFormatRules(rules);
}
I will be grateful for any help
Answer
I understand that you want to use Sheet API v4 instead of Spreadsheet Service to reduce the cost of the request. I don't know how much the cost will be reduced using that way, but I will explain to you how to do it.
How to apply a Conditional Format Rule in Sheets API v4
Use the method batchUpdate. It takes a request body where you can define the Conditional Format Rule and the spreadsheetId. You can easily construct the request body using the section Try this API, it helps you to put and define all the parameters that you need.
Define the request body with a AddConditionalFormatRuleRequest object. It has two fields, the rule that describes the conditional format and the index that defines where the rule should be inserted.
Define the rule field with a ConditionalFormatRule object. It takes two fields, the ranges and the gradientRule or the boolearnRule (you can only choose one).
Define the range with a GridRange object.
Define the gradientRule with its three fields: minpoint, midpoint and maxpoint. Each of these is defined by an InterpolationPoint object.
Finally your code will look similar to the following:
function main(){
// start here
var folders = // your definition
const gridRangeList = createGridRange() // create the GridRange object
while (folders.hasNext()) {
var spreadsheetId = folders.next().getId();
applyConditionalFormating(spreadsheetId, gridRangeList) // apply the conditional format
}
}
function createGridRange(){
const ranges = ["Q4:Q", "S4:S"]
const temp = SpreadsheetApp.create("temp")
const rangeList = temp.getSheets()[0].getRangeList(ranges).getRanges()
const gridRangeList = rangeList.map(r => ({startRowIndex: r.getRow() - 1, startColumnIndex: r.getColumn() - 1, endColumnIndex: r.getColumn() + r.getNumColumns() - 1}))
DriveApp.getFileById(temp.getId()).setTrashed(true) // move the file to the trash
return gridRangeList
}
function applyConditionalFormating(spreadsheetId, gridRangeList){
const request = {
"requests": [
{
"addConditionalFormatRule": {
"rule": {
"gradientRule": {
"maxpoint": {
"type": "MAX",
"color": {red:6/255,green:255/255,blue:0}
},
"midpoint": {
"type": "PERCENTILE",
"value": "50",
"color": {red:255/255,green:239/255,blue:0}
},
"minpoint": {
"type": "MIN",
"color":{red:255/255,green:0,blue:0}
}
},
"ranges": [gridRangeList]
},
"index": 0
}
}
]
}
Sheets.Spreadsheets.batchUpdate(request,spreadsheetId)
}
Reference
Sheet API v4
Spreadsheet Service
Conditional Format Rule
Method: spreadsheets.batchUpdate
AddConditionalFormatRuleRequest
ConditionalFormatRule
GridRange
gradientRule
InterpolationPoint
I believe your goal as follows.
You want to reduce the process cost of your script.
Modification points:
When I saw your script, it seems that a conditional format rule with multiple ranges is added to a sheet in a Google Spreadsheet by one call. In this case, even when this script is converted to Sheets API instead of Spreadsheet service, the process cost might not be the large change. So please test the following modified script.
As the modification point, I would like to propose as follows.
In your script, the ranges are declared in the loop. When this is converted to Sheets API, the ranges can be created at outside of the loop.
When the file list is retrieved using Drive API, the process cost will be reduced a little.
When above points are reflected to your script, it becomes as follows.
Modified script:
Before you use this script, please enable Sheets API and Drive API at Advanced Google services. And, please set the variables of topFolderId and ranges. ranges is from your script. When you want to more ranges, please add them to the array.
function myFunction() {
var topFolderId = "###"; // Please set the top folder ID of the folder including the Spreadsheet.
// Retrieve file list using Drive API v3.
const headers = {authorization: `Bearer ${ScriptApp.getOAuthToken()}`};
const q = `'${topFolderId}' in parents and mimeType='${MimeType.GOOGLE_SHEETS}' and trashed=false`;
const url = `https://www.googleapis.com/drive/v3/files?pageSize=1000&q=${q}&fields=${encodeURIComponent("nextPageToken,files(id)")}`;
let pageToken = "";
let files = [];
do {
const res = UrlFetchApp.fetch(url + "&pageToken=" + pageToken, {headers: headers, muteHttpExceptions: true});
if (res.getResponseCode() != 200) throw new Error(res.getContentText());
const obj = JSON.parse(res.getContentText());
files = files.concat(obj.files);
pageToken = obj.nextPageToken || "";
} while(pageToken);
// Create range list.
const ranges = ["Q4:Q", "S4:S"]; // Please set the ranges as A1Notation. These ranges are used for addConditionalFormatRule.
const temp = SpreadsheetApp.create("temp");
const rangeList = temp.getSheets()[0].getRangeList(ranges).getRanges();
const gridRangeList = rangeList.map(r => ({startRowIndex: r.getRow() - 1, startColumnIndex: r.getColumn() - 1, endColumnIndex: r.getColumn() + r.getNumColumns() - 1}));
DriveApp.getFileById(temp.getId()).setTrashed(true);
// Request Sheets API for a sheet in each Spreadsheet.
files.forEach(({id}) => {
const sheet1 = SpreadsheetApp.openById(id);
const gr = gridRangeList.map(({startRowIndex, startColumnIndex, endColumnIndex}) => ({sheetId: sheet1.getSheetId(), startRowIndex, startColumnIndex, endColumnIndex}))
const requests = {addConditionalFormatRule:{rule:{gradientRule:{maxpoint:{color:{red:6/255,green:255/255,blue:0},type:"MAX"},midpoint:{color:{red:255/255,green:239/255,blue:0},type:"PERCENTILE",value:"50"},minpoint:{color:{red:255/255,green:0,blue:0},type:"MIN"}},ranges:[gr]},index:0}};
Sheets.Spreadsheets.batchUpdate({requests: requests}, id);
});
}
References:
Method: spreadsheets.batchUpdate
AddConditionalFormatRuleRequest