I'm pretty new to learning app script and looked over/tried to edit this script, but I'm not getting my desired result. I have a sheet titled "Menu" where I'm wanting a user to select from three different drop down options in Cell A2 (e.g. Blue, Yellow, Green). I then want to hide the different sheets based on the selection. So if a user selects "Blue" I want only the sheets that start with the word "Blue" to be visible + the "Menu" sheet and the rest to be hidden. Same for Yellow and Green. As a note there are 13 sheets for each color.
Any help with this is much appreciated.
This is an alternative implementation of #JSmith's answer, using the Sheets REST API to more efficiently hide & unhide a large number of sheets.
To use the Sheets REST API from Apps Script, you will first need to enable it, as it is an "advanced service."
The Sheets API approach enables you to work with the JavaScript representation of the data, rather than needing to interact with the Spreadsheet Service repeatedly (e.g. to check each sheet's name). Additionally, a batch API call is processed as one operation, so all visibility changes are reflected simultaneously, while the Spreadsheet Service's showSheet() and hideSheet() methods flush to the browser after each invocation.
var MENUSHEET = "Menu";
function onEdit(e) {
if (!e) return; // No running this from the Script Editor.
const edited = e.range,
sheet = edited.getSheet();
if (sheet.getName() === MENUSHEET && edited.getA1Notation() === "A2")
hideUnselected_(e.source, e.value);
}
function hideUnselected_(wb, choice) {
// Get all the sheets' gridids, titles, and hidden state:
const initial = Sheets.Spreadsheets.get(wb.getId(), {
fields: "sheets(properties(hidden,sheetId,title)),spreadsheetId"
});
// Prefixing the choice with `^` ensures "Red" will match "Reddish Balloons" but not "Sacred Texts"
const pattern = new RegExp("^" + choice, "i");
// Construct the batch request.
const rqs = [];
initial.sheets.forEach(function (s) {
// s is a simple object, not an object of type `Sheet` with class methods
// Create the basic request for this sheet, e.g. what to modify and which sheet we are referencing.
var rq = { fields: "hidden", properties: {sheetId: s.properties.sheetId} };
// The menu sheet and any sheet name that matches the pattern should be visible
if (s.properties.title === MENUSHEET || pattern.test(s.properties.title))
rq.properties.hidden = false;
else
rq.properties.hidden = true;
// Only send the request if it would do something.
if ((!!s.properties.hidden) !== (!!rq.properties.hidden))
rqs.push( { updateSheetProperties: rq } );
});
if (rqs.length) {
// Visibility changes will fail if they would hide the last visible sheet, even if a later request in the batch
// would make one visible. Thus, sort the requests such that unhiding comes first.
rqs.sort(function (a, b) { return a.updateSheetProperties.properties.hidden - b.updateSheetProperties.properties.hidden; });
Sheets.Spreadsheets.batchUpdate({requests: rqs}, initial.spreadsheetId);
}
}
There are a fair number of resources to be familiar with when working with Google's various REST APIs:
Google APIs Explorer (interactive request testing)
Google Sheets REST API Reference
Partial Responses (aka the "fields" parameter)
Determining method signatures
google-sheets-api
A little testing in a workbook with 54 sheets, in which I used the Sheets API to apply some changes and #JSmith's code to revert the changes, showed the API approach to be about 15x faster, as measured with console.time & console.timeEnd. API changes took from 0.4 to 1.1s (avg 1s), while the Spreadsheet Service method took between 15 and 42s (avg 20s).
try this code:
function onEdit(e)
{
//filter the range
if (e.range.getA1Notation() == "A2")
{
// get value of cell (yellow||green||...)
onlySheet(e.value)
}
}
function onlySheet(str)
{
var ss = SpreadsheetApp.getActiveSpreadsheet();
//get all sheets
var sheets = ss.getSheets();
for (var i = 0; i < sheets.length; i++)
{
//get the sheet name
var name = sheets[i].getName();
// check if the sheet is not the "Menu" sheet
if (name != "Menu")
{
// check if the name of the sheet contains the value of the cell, here str
//if it does then show sheet if it doesn't hide sheet
if (name.match(new RegExp(str, "gi")))
sheets[i].showSheet();
else
sheets[i].hideSheet();
}
}
}
Related
We're using Google Sheets for financial reconciliation internally and facing some mistakes in it.
There is a spreadsheet with all the data to which almost everyone in company has access for editing.
What I want to do is to lock certain cells for all users except few people when simple condition (for example, cell fill color is changed to red) is met.
So the function description looks like:
everyone has access to spreadsheet
cells in range (which one should be locked) are not locked
cells are not locked until condition is met
user enters value to cell/range
user applies condition (fill color, for example)
cell locks. access from all users except few ones is removed
users with access could edit/unlock
It would be much appreciated if someone could help with the exact function to apply.
Many thanks in advance!
The only thing I did found is the documentation which is close to my problem: https://developers.google.com/apps-script/reference/spreadsheet/range
https://developers.google.com/apps-script/reference/spreadsheet/protection
But I'm zero in Apps Script which is used by Google Sheets(
This gets you close but unfortunately I ran into the problem of onEdit events do not consider background color changes apparently... So ultimately this will only fire after a cell value changes.
If the cell is Red and the range is not yet protected, it will be protected as you being the only editor. If the background is not red it will either strip away the protection or not change.
/**
* Protects and unprotects ranges;
* #param {Object} e event object;
*/
function onEdit(e) {
//access cell formats;
var bgColor = e.range.getBackground();
var bold = e.range.getFontWeight();
//access edited range, value and sheet;
var rng = e.range;
var val = e.value;
var sh = rng.getSheet();
//access edited range row and column;
var row = rng.getRow();
var col = rng.getColumn();
//access protections;
var ps = sh.getProtections(SpreadsheetApp.ProtectionType.RANGE);
//filter out other cells protections;
ps = ps.filter(function(p){
var ptd = p.getRange();
if(row===ptd.getRow()&&col===ptd.getColumn()) {
return p;
}
})[0];
//SpreadsheetApp.getActive().toast(bgColor); //Uncomment to get a toast displaying background color of edited cell.
//if protection not set -> protect;
if(!ps) {
if (bgColor === '#ff0000' && bold === 'bold') {
SpreadsheetApp.getActive().toast("Cell Locked");
var protection = rng.protect(); //protect Range;
var users = protection.getEditors(); //get current editors;
var emails = [Session.getEffectiveUser(),'email1#email.com','email2#email.com']; //declare list of users (emails)
protection.addEditor(Session.getEffectiveUser());
protection.addEditors(emails);
protection.removeEditors(users); //remove other editors' access;
}}else {
if(!val || bgColor != '#ff0000' || bold != 'bold') { ps.remove(); } //if cell is empty -> remove protection;
}
}
Perhaps someone can improve on this and adjust for background events only. Also if working fast this doesn't keep up well.
I know this question has been asked before but the answers given are not valid for my case because it's slightly different.
I've created a formula that looks for sheets with a pattern in the name and then uses it's content to generate the output. For example
function sampleFormula(searchTerm) {
const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
const sheets = spreadsheet.getSheets()
.filter(function(sheet) {
// If sheet name starts with DATA_SHEET_...
return sheet.getSheetName().indexOf('DATA_SHEET_') === 0;
});
const result = [];
sheets.forEach(function(sheet) {
// We get all the rows in the sheet
const rows = sheet.getDataRange().getValues();
rows.forEach(function(row) => {
// If the row it's what we are looking for we pick the columns A and C
if (row[1] === searchTerm) {
result.push([ row[0], row[2] ])
}
});
});
// If we found values we return them, otherwise we return emptry string
return result.length ? result : '';
}
The thing is I need this formula to be re-calculated when a cell in a sheet with a name starting with DATA_SHEET_ changes.
I see most answers (and what I usually do) is to pass the range we want to watch as a parameter for the formula even if it's not used. But in this case it will not work because we don't know how many ranges are we watching and we don't even know the whole sheet name (it's injected by a web service using Google Spreadsheet API).
I was expecting Google Script to have something like range.watch(formula) or range.onChange(this) but I can't found anything like that.
I also tried to create a simple function that changes the value of cell B2 which every formula depends on but I need to restore it immediately so it's not considered a change (If I actually change it all formulas will break):
// This does NOT work
function forceUpdate() {
const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
const sheet = spreadsheet.getActiveSheet();
const range = sheet.getRange(1, 1);
const content = range.getValue();
range.setValue('POTATO');
range.setValue(content);
}
So I don't know what else can I do, I have like a hundred formulas on multiple sheets doing this and they are not updating when I modify the DATA_SHEET_... sheets.
To force that a custom function be recalculated we could use a "triggering argument" that it's only taks will be to trigger the custom function recalculation. This triggering argument could be a cell reference that will be updated by a simple edit trigger or we could use an edit installable trigger to update all the formulas.
Example of using a cell reference as triggering argument
=sampleFormula("searchTerm",Triggers!A1)
Example of using an edit installable trigger to update all the formulas
Let say that formulas has the following form and the cell that holds the formula is Test!A1 and Test!F5
=sampleFormula("searchTerm",0)
where 0 just will be ignored by sampleFormula but will make it to be recalculated.
Set a edit installable trigger to fire the following function
function forceRecalculation(){
updateFormula(['Test!A1','Test!F5']);
}
The function that will make the update could be something like the following:
function updateFormula(references){
var rL = SpreadsheetApp.getActive().getRangeList(references);
rL.getRanges().forEach(function(r){
var formula = r.getFormula();
var x = formula.match(/,(\d+)\)/)[1];
var y = parseInt(x)+1;
var newFormula = formula.replace(x,y.toString());
r.setFormula(newFormula);
});
}
As you can imagine the above example will be slower that using a cell reference as the triggering argument but in some scenarios could be convenient.
I know this topic has bee covered before sort of, but none of it really makes sense to me. Basically I wrote a function that will generate a PDF from some API data using user inputs. It looks vaguely like this and works when I run it in the script editor.
function myfunction(InputA,InputB,InputC,InputD,InputE) {
......
var sourceSpreadsheet = SpreadsheetApp.getActive();
var parents = DriveApp.getFileById(sourceSpreadsheet.getId()).getParents();
if (parents.hasNext()) {
var folder = parents.next();
}
var response = UrlFetchApp.fetch(url, options)
var blob = response.getAs('application/pdf').setName(InputA + InputB + InputC)
var newFile = folder.createFile(blob);
return newFile
}
The problem is Google permissions. The classic: "You don't have permission to access the drive".
I have tried publishing the script as a private sheets addon and enabled it on my spreadsheet. But that didn't really do anything. And I don't really know why because I authorized the app for all the required scopes when I approved the add on. I can see it in the extensions menu but I am still getting errors when I try to call the function.
The button method of enabling permissions doesn't work for me because I need to run the code several times based on parameters defined in the Sheet. I tried simple triggers since I want the code to run weekly anyways, but found the same problem.
Can someone give me the step by step of how I'm supposed to do this.
Please don't send links to the google documentation because I have read the related pages and still don't know what I'm doing wrong.
I recommend you use an installable onEdit trigger. I have this approach and see if it works for you.
Sample Data:
Assuming url is from Input D column.
Create a column of checkboxes that will trigger the installed trigger. In my case, ticking it will create the file and unticking it will remove the file created.
Ticking rows where an input (at least 1) is missing, will cancel the creation of the file and then untick the checkbox ticked.
Drive folder:
Script:
function createFileTrigger(e) {
var spreadsheet = e.source;
var sheet = spreadsheet.getActiveSheet();
var range = e.range;
var value = e.value;
var row = range.getRow();
var col = range.getColumn();
// proceed if edited cell is Sheet1!H2:H
if(sheet.getSheetName() == 'Sheet1' && col == 8 && row > 1) {
// if checkbox is ticked
if(value == 'TRUE') {
// inputs = [Input A, Input B, Input C, Input D, Input E]
var inputs = range.offset(0, -6, 1, 5).getValues().flat();
var parents = DriveApp.getFileById(spreadsheet.getId()).getParents();
var folder = parents.next();
// set some conditions here to pre-check the inputs
// e.g. if at least 1 input is blank, cancel file creation (return)
if(inputs.filter(String).length < 5) {
// untick the checkbox ticked
range.setValue('FALSE');
// skip creation of file
return;
}
// assuming url is from Input D (removed options as not needed for presentation)
var response = UrlFetchApp.fetch(inputs[3]);
var newFileName = `${inputs[0]} ${inputs[1]} ${inputs[2]}`;
// if file is existing (which should not happen but just in case)
if(folder.getFilesByName(newFileName).hasNext()) {
// do something else that is needed to be done to avoid duplication of file
// e.g. overwrite or skip creating file
console.log(newFileName + ' is already existing in the parent folder');
}
// if not existing
else {
// create the file
var blob = response.getAs('application/pdf').setName(newFileName)
// for presenation purposes, will write the id of the created file
range.offset(0, -1).setValue(folder.createFile(blob).getId());
}
}
// if checkbox is unticked
else {
// do something else that is needed to be done
// e.g. delete the file using the id returned (using Drive Advanced Services)
var fileIdRange = range.offset(0, -1);
Drive.Files.remove(fileIdRange.getValue());
// remove file id content on the cell
fileIdRange.clearContent();
}
}
}
Ticking checkbox (folder):
Ticking checkbox (sheet):
Note:
This can still be improved, but should already be enough for your case.
onEdit and onChanged does not seem to trigger when removing or giving permission to a user.
I have not seen any event related to that in the documentation.
If there is no such events, is there a way to programatically track permission changes?
Edit: I am sorry if the question was not clear but I meant sheet protection permissions for individual sheets inside a workbook instead of Drive sharing permission for the workbook.
If I understand you correctly, you want to keep track of changes in Sheet protections. As you said, there are no Apps Script triggers that can track this.
Workaround (time-based trigger and Properties Service):
As a workaround, I'd propose doing the following:
Create a time-based trigger that will fire a function periodically (every 1 minute, for example). You can do this either manually, or programmatically, by running this function once:
function createTrigger() {
ScriptApp.newTrigger("trackProtections")
.timeBased()
.everyMinutes(1)
.create();
}
This will fire the function trackProtections every minute. This function's purpose is to track changes to the sheet protections since last time it was fired (in this example, 1 minute ago).
In the triggered function, retrieve the current sheet protections, store them in script properties, and compare the previously store protections to the current ones (check editors, etc.). You could use JSON.stringify() and JSON.parse() to be able to store these protections to script properties (which only accept strings) and convert them back. It could be something along the following lines (check inline comments):
function trackProtections() {
var scriptProperties = PropertiesService.getScriptProperties(); // Get script properties (old protections)
var ss = SpreadsheetApp.getActive();
var sheets = ss.getSheets(); // Get all sheets in spreadsheet
sheets.forEach(function(sheet) { // Iterate through each sheet in the spreadsheet
var sheetName = sheet.getName();
// Get sheet current protection (null if it's unprotected):
var protection = sheet.getProtections(SpreadsheetApp.ProtectionType.SHEET)[0];
// Get previous editors of current sheet (stored in script properties):
var oldEditors;
if (scriptProperties.getProperty(sheetName) !== null) {
oldEditors = scriptProperties.getProperty(sheetName).split(',');
}
// Get current editors of current sheet:
var newEditors;
if (protection) {
newEditors = protection.getEditors().map(function(editor) {
return editor.getEmail();
});
}
if (oldEditors && !newEditors) { // Protection for the sheet was removed
scriptProperties.deleteProperty(sheetName); // Remove old property (protection doesn't exist anymore)
Logger.log('protection for ' + sheetName + ' was removed!');
} else if (!oldEditors && !newEditors) { // There were and there aren't any protections for the sheet
Logger.log('there are no protections');
} else if (!oldEditors && newEditors) { // Protection for the sheet was added
Logger.log('protection for ' + sheetName + ' was added!');
scriptProperties.setProperty(sheetName, newEditors.toString()); // Add script property with current editors
} else {
if (newEditors.sort().join(',') !== oldEditors.sort().join(',')) { // Check if old and current editors are the same
var addedEditors = newEditors.filter(function(editor) {
return oldEditors.indexOf(editor) === -1; // Return editors that are in current protection but not in old one (added)
});
var removedEditors = oldEditors.filter(function(editor) {
return newEditors.indexOf(editor) === -1; // Return editors that were in old protection but not in current one (removed)
});
Logger.log('protection for ' + sheetName + ' was modified!');
Logger.log('these editors were added: ' + JSON.stringify(addedEditors));
Logger.log('these editors were removed: ' + JSON.stringify(removedEditors));
scriptProperties.setProperty(sheetName, newEditors.toString()); // Add script property with current editors
}
}
});
}
This function iterates through all sheets in the spreadsheet, and looks for changes in the protection since last execution (whether the sheet became protected, or unprotected, or whether the editors changed).
In this sample, the protections are getting stored in script properties the following way: each property has the name of the sheet as the key and the emails of the different editors of the sheet as value (comma-separated). The script is using toString and split to store the array of editor emails in the property, because script properties only accept strings as value.
You could also keep track of other protection settings that change (description, etc.) which would make the script a bit more complex (and you'd probably have to use JSON.stringify and JSON.parse, but I hope this simplified example will be enough for you to understand the workflow.
The function should also check if there is a change between the current protections and the ones that were previously stored, and do whatever you had planned to do when sheet protections changed. Since I don't know what you wanted to do in this situation, I'm just using Logger.log to log the changes that happened to the protection since last execution.
Reference:
Class Protection
Class ClockTriggerBuilder
Class PropertiesService
I know this question has been asked before but the answers given are not valid for my case because it's slightly different.
I've created a formula that looks for sheets with a pattern in the name and then uses it's content to generate the output. For example
function sampleFormula(searchTerm) {
const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
const sheets = spreadsheet.getSheets()
.filter(function(sheet) {
// If sheet name starts with DATA_SHEET_...
return sheet.getSheetName().indexOf('DATA_SHEET_') === 0;
});
const result = [];
sheets.forEach(function(sheet) {
// We get all the rows in the sheet
const rows = sheet.getDataRange().getValues();
rows.forEach(function(row) => {
// If the row it's what we are looking for we pick the columns A and C
if (row[1] === searchTerm) {
result.push([ row[0], row[2] ])
}
});
});
// If we found values we return them, otherwise we return emptry string
return result.length ? result : '';
}
The thing is I need this formula to be re-calculated when a cell in a sheet with a name starting with DATA_SHEET_ changes.
I see most answers (and what I usually do) is to pass the range we want to watch as a parameter for the formula even if it's not used. But in this case it will not work because we don't know how many ranges are we watching and we don't even know the whole sheet name (it's injected by a web service using Google Spreadsheet API).
I was expecting Google Script to have something like range.watch(formula) or range.onChange(this) but I can't found anything like that.
I also tried to create a simple function that changes the value of cell B2 which every formula depends on but I need to restore it immediately so it's not considered a change (If I actually change it all formulas will break):
// This does NOT work
function forceUpdate() {
const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
const sheet = spreadsheet.getActiveSheet();
const range = sheet.getRange(1, 1);
const content = range.getValue();
range.setValue('POTATO');
range.setValue(content);
}
So I don't know what else can I do, I have like a hundred formulas on multiple sheets doing this and they are not updating when I modify the DATA_SHEET_... sheets.
To force that a custom function be recalculated we could use a "triggering argument" that it's only taks will be to trigger the custom function recalculation. This triggering argument could be a cell reference that will be updated by a simple edit trigger or we could use an edit installable trigger to update all the formulas.
Example of using a cell reference as triggering argument
=sampleFormula("searchTerm",Triggers!A1)
Example of using an edit installable trigger to update all the formulas
Let say that formulas has the following form and the cell that holds the formula is Test!A1 and Test!F5
=sampleFormula("searchTerm",0)
where 0 just will be ignored by sampleFormula but will make it to be recalculated.
Set a edit installable trigger to fire the following function
function forceRecalculation(){
updateFormula(['Test!A1','Test!F5']);
}
The function that will make the update could be something like the following:
function updateFormula(references){
var rL = SpreadsheetApp.getActive().getRangeList(references);
rL.getRanges().forEach(function(r){
var formula = r.getFormula();
var x = formula.match(/,(\d+)\)/)[1];
var y = parseInt(x)+1;
var newFormula = formula.replace(x,y.toString());
r.setFormula(newFormula);
});
}
As you can imagine the above example will be slower that using a cell reference as the triggering argument but in some scenarios could be convenient.