Google Docs: Get Sheet Without Name or ID - google-apps-script

I'm writing some scripts for a client with the end goal of complete autonomy -- when complete, the spreadsheet will always work forever. Ideally, anyways.
Because I need information from other sheets, I have to access them in a way other than .getActiveSheet(). Because the client might re-name or re-order the sheets, I have to access the sheet in a way that works even after those changes. This rules out getSheetByName() and getSheets()[SHEET_NUMBER] (again, the client might re-name or re-order the sheets). However, it should be possible because of the "gid." Each sheet has a different gid and they do not change when you re-order or re-name the sheets (scroll to the end of the URL for each sheet to see what I mean).
All of the URL accesses only open the FIRST sheet. For instance,
SpreadsheetApp.openById(SHEET_ID).getDataRange().getValues()
returns the values of the first sheet, even if I include the "gid" part at the end. Same with openById and openFile.
So my question is, how do I access a sheet in a way that will work even after renaming the sheet or reordering the sheets within the spreadsheet?

There's no getSheetById method, but you can build your own using getSheetId(). Here:
function sheetsIdMap() {
var sheetsById = {};
SpreadsheetApp.getActive().getSheets().forEach(function(s){ sheetsById[s.getSheetId()] = s; });
//just checking that it worked
for( var id in sheetsById )
Logger.log(id+' - '+sheetsById[id].getName());
//usage example
var sId2 = sheetsById[2];
Logger.log('\n'+sId2.getName());
}
-- edit
Let's try a more straightforward function (although I don't like to do such a loop and don't store the data on a map for subsequent use o(1)).
function getSheetById(ssID, sheetID) {
var sheets = SpreadsheetApp.openById(ssID).getSheets();
for( var i in sheets )
if( sheets[i].getSheetId() == sheetID )
return sheets[i];
return null; //sheet id not found in spreadsheet, probably deleted?
}

Yes there is a sheet id. Its sheet.getSheetId. this id can be used from apps script and can also be transformed into a "real" gid for making a sheet url. Do (sheetId ^ 31578).toString(36) to get the gid.
I had to reverse-eng it to get it and I cant guarantee it will work forever.

Related

Script that adds new row to sheets in selected AND another Google Spreadsheet

I'm working on a Spreadsheet to keep track of team member's project hours. I've created a Spreadsheet per team member for them to fill out weekly, and a project overview Spreadsheet that takes in all data through IMPORTRANGE.
To be able to quickly add a new project I want a macro to insert a new row in the Project overview + the separate Spreadsheets per team member. However I can't figure out how to write the correct code for the separate team member Spreadsheets. What's going wrong here?
If possible I'd also like to make a macro to DELETE a row in the project overview + team member spreadsheets, and one to HIDE a row...
Project overview
Team member Kate
Team member David
My current code:
function InsertRow() {
var ss = SpreadsheetApp.getActive();
var allsheets = ss.getSheets();
var row = SpreadsheetApp.getActiveRange().getRow();
// Array holding the names of the sheets to exclude from the execution
// I only managed to make it work when I exclude the sheet that I actually want to affect instead of the other way around?
var exclude = (["PROJECTS"] ||
SpreadsheetApp.openById("1xjR3lx5_KAA9nqiD3YsjZnulQaMyWGPQqgYsjtzQ0xI").getSheets() ||
SpreadsheetApp.openById("1Q5gtZlqf41of1Zwi8pvZbDx4NN5LcDh5SxfwasLUDMU").getSheets())
for(var s in allsheets){
var sheet = allsheets[s];
// Stop iteration execution if the condition is meet.
if(exclude.indexOf(sheet.getName())==-1) continue;
sheet.insertRowBefore(row);
}
}
As I see it you have a couple of options, which I'll be listing here as A, B, and C. Please note that you might need two different .GS files as you are linking to two sheets
A
Try code found on google app script documentation
I found the google apps script documentation for this command found here, so you might want to check that for this questions and others , but here is the exact code included
// The code below opens a spreadsheet using its ID and logs the name for it.
// Note that the spreadsheet is NOT physically opened on the client side.
// It is opened on the server only (for modification by the script).
var ss = SpreadsheetApp.openById("abc1234567");
Logger.log(ss.getName());
B
use open by url instead of open by id
Your issue might be that your current id isn't correct, I have no way of knowing, so here is some alternate code here (link to documentation here)
// The code below opens a spreadsheet using its id and logs the name for it.
// Note that the spreadsheet is NOT physically opened on the client side.
// It is opened on the server only (for modification by the script).
var ss = SpreadsheetApp.openByUrl(
'https://docs.google.com/spreadsheets/d/abc1234567/edit');
Logger.log(ss.getName());
C
Tie the google script to one sheet
This last option doesn't require any code, just an explanation. Instead of trying to link your script to two separate sheets, you might be able to automatically link it to a single google sheet and create two pages in the sheets file that you treat as two different sheets but are one thing. This might not be what you want, but I included it anyways. You link the sheet to the code automatically by:
1 opening your sheet
2 going to "tools"
3 clicking script editor
4 copy and paste your code (except for the "open by id" part)
5 success!
Your exclude variable doesn't contain what you think it does. You're using an "or" operator (||), which will take the first "truthy" value and skip the rest.
console.log((["PROJECTS"] || 'something else')); // ["PROJECTS"]
Moreover, you don't have a good way of telling which spreadsheet belongs to which team member. To solve that problem, you can create an object.
const teamSpreadsheetIds = {
'DAVID': 'ABC',
'KATE': '123',
};
console.log(teamSpreadsheetIds['DAVID']); // ABC
With the teamSpreadsheetIds object, you can now go about updating your team member sheets locally as well as their individual spreadsheets. The "PROJECTS" sheet is unique, so there's only one check for it.
function InsertRow() {
const ss = SpreadsheetApp.getActive();
const allSheets = ss.getSheets();
const row = SpreadsheetApp.getActiveRange().getRow();
const teamSpreadsheetIds = {
'DAVID': '1Q5gtZlqf41of1Zwi8pvZbDx4NN5LcDh5SxfwasLUDMU',
'KATE': '1xjR3lx5_KAA9nqiD3YsjZnulQaMyWGPQqgYsjtzQ0xI',
};
for (let sheet of allSheets) {
const sheetName = sheet.getName();
const memberSpreadsheetId = teamSpreadsheetIds[sheetName];
const isSkippable = memberSpreadsheetId === undefined && sheetName !== 'PROJECTS';
if (isSkippable) { continue };
// Insert a row in the local sheet
sheet.insertRowBefore(row);
// Get the member sheet and insert a row
if (memberSpreadsheetId) {
const memberSpreadsheet = SpreadsheetApp.openById(memberSpreadsheetId);
const memberSheet = memberSpreadsheet.getSheets()[0]; // Assumes the first sheet is the one to modify
memberSheet.insertRowBefore(row);
}
}
}

Google sheets appscript to copy tabs to new sheets

I have a google sheet with around 190 tabs on that i need to split into 190 different files
The files need to be named the same as the tab, the contents of the tab need to be copied as values but i also need to bring the formatting accross (just not the formulas).
I have looked around, and through a combination of previous questions and answers plus using the function list help have formed the following code. It actually works for the first few tabs but then throws up an error about being unable to delete the only sheet.
function copySheetsToSS() {
var ss = SpreadsheetApp.getActive();
for(var n in ss.getSheets()){
var sheet = ss.getSheets()[n];// look at every sheet in spreadsheet
var name = sheet.getName();//get name
if(name != 'master' && name != 'test'){ // exclude some names
var alreadyExist = DriveApp.getFilesByName(name);// check if already there
while(alreadyExist.hasNext()){
alreadyExist.next().setTrashed(true);// delete all files with this name
}
var copy = SpreadsheetApp.create(name);// create the copy
sheet.copyTo(copy);
copy.deleteSheet(copy.getSheets()[0]);// remove original "Sheet1"
copy.getSheets()[0].setName(name);// rename first sheet to same name as SS
var target_sheet = copy.getSheetByName(name);
var source_range = sheet.getRange("A1:M50");
var target_range = target_sheet.getRange("A1:M50");
var values = source_range.getValues();
target_range.setValues(values);
}
}
}
I am hoping someone can tell me what i have done wrong as I cannot figure it out at this point. I am also open to better solutions though please be aware I am very much a beginner on google appscript, nothing too complex please.
thankyou
In principle your script correctly adds a new sheet to the new spreadsheet before removing the preexisting one
However, mind that calls to service such as SpreadsheetApp are asynchronous.
And this becomes the more noticeable, the longer your script runs.
In your case it apparently leads to behavior that the only sheet is being deleted before the new sheet is being created.
To avoid this, you can force the execution to be synchronous by implementing calls to SpreadsheetApp.flush().
This will ensure that the old sheet won't be deleted before the new one gets inserted.
Sample:
copy.deleteSheet(copy.getSheets()[0]);// remove original "Sheet1"
SpreadsheetApp.flush();
copy.getSheets()[0].setName(name);
You might want to introduce call toflush()` also at other positions where it is important for the code to run synchronously.

Can't find a setIndex in SpreadsheetApp

I want to code some protection into a Google Sheets spreadsheet, that will keep the pages in the right order. I can't protect the entire sheet from modification as it's a group workbook used to organize.. well the organization :P
In google scripts I found the getIndex but i need a setIndex
here is the skeleton of the code, in case I'm not being clear
function resetSheetPos() {
var s = SpreadsheetApp.getActiveSheet();
if s.getSheetName() = "Instructions" {
//the function I can't find would look like this
//s.setIndex(0);
s.activate(); // in case we aren't still looking at the same sheet when it changed it's index
};
if s.getSheetName() = "Notes and Comments" {
//and would be incremented for each sheet
//s.setIndex(1);
s.activate(); //again only if needed.
};
// and so on for each sheet
};
You'll need you use methods on the parent Spreadsheet rather than the Sheets directly.
See moveActiveSheet(index), or possibly insertSheet(index)
https://developers.google.com/apps-script/reference/spreadsheet/spreadsheet#moveActiveSheet(Integer)

Use App Script to carry out one function, once, on many spreadsheets

I want to run a Google Script which normalises a cell range, on a number (30+) of Google Sheets. Or more precisely, I want a (less technical) user to be able to do this. I can't seem to find a reasonable workflow.
The options I can see are:
Copy/paste the script as a bound script to each of the spreadsheets
That's messy because then there are many copies of the script, which generally won't be run again, and there's a lot of overhead and clicking around to install the macro for each one.
Use a library
I could put the body of the code in a library, then make the copy/paste just a stub (like the accepted answer here ).
However, that's still just as bad for the UX, plus various reports that libraries are messy to deal with.
Make an add-on
The "right way" seems to be to create an add-on which the user can enable for each spreadsheet. However, add-ons still seem to be in "developer preview" mode, and the authorisation cycle is uncertain and potentially slow. Google also expects that The script has been tested with multiple active users. which would be hard - by the time I'd tested it this thoroughly, the job would basically be done. And how would I test it without publishing the add-on anyway?
Other options?
Is there some other way, perhaps using an unbound-script? It's not possible to run a single script once and have it iterate over all the spreadsheets as a bit of user input is required (which range within the spreadsheet etc).
Is there a way where the user could install an unbound script, run it, and it would ask which spreadsheet to run it on?
openByUrl() is really close, but it doesn't actually open the spreadsheet UI, so I wouldn't be able to use functions like getActiveRange() etc.
In case it's relevant, here's the script:
/*function onOpen() {
var ui = SpreadsheetApp.getUi();
ui.createMenu('Normalize')
.addItem('Normalize Crosstab', 'normalizeCrossTab')
.addToUi();
}*/
function onOpen() {
var ss = SpreadsheetApp.getActive();
var items = [
{name: 'Normalize Crosstab', functionName: 'normalizeCrosstab'},
];
ss.addMenu('Normalize', items);
}
/* Converts crosstab format to normalized form. Given columns abcDE, the user puts the cursor somewhere in column D.
The result is a new sheet, NormalizedResult, like this:
a b c Field Value
a1 b1 c1 D D1
a1 b1 c1 E E1
a2 b2 c2 D D2
a2 b2 c2 E E2
...
*/
function normalizeCrosstab() {
var sheet = SpreadsheetApp.getActiveSheet();
var rows = sheet.getDataRange();
var numRows = rows.getNumRows();
var values = rows.getValues();
var firstDataCol = SpreadsheetApp.getActiveRange().getColumn();
var dataCols = values[0].slice(firstDataCol-1);
if (Browser.msgBox("This will create a new sheet, NormalizedResult. Place your cursor is in the first data column.\\n\\n" +
"These will be your data columns: " + dataCols,Browser.Buttons.OK_CANCEL) == "cancel") {
return;
}
var resultssheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("NormalizedResult");
if (resultssheet != null) {
SpreadsheetApp.getActive().deleteSheet(resultssheet);
}
var header = values[0].slice(0, firstDataCol - 1);
var newRows = [];
header.push("Field");
header.push("Value");
newRows.push(header);
for (var i = 1; i <= numRows - 1; i++) {
var row = values[i];
for (var datacol = 0; datacol < dataCols.length; datacol ++) {
newRow = row.slice(0, firstDataCol - 1); // copy repeating portion of each row
newRow.push(values[0][firstDataCol - 1 + datacol]); // field name
newRow.push(values[i][firstDataCol - 1 + datacol]); // field value
newRows.push(newRow);
}
}
var newSheet = SpreadsheetApp.getActiveSpreadsheet().insertSheet("NormalizedResult");
var r = newSheet.getRange(1,1,newRows.length, header.length);
r.setValues(newRows);
};
The first question is: "Who is the owner of all these sheets?" If you are the owner of all these sheets, then you have permission to access them remotely. If you don't own them, then the owner would need to share, and give editing permissions to whatever code is trying to modify their file.
If you own all the spreadsheets, you could create a Stand Alone App do all the processing from a central point. Then you can just email the link of the Stand Alone App to everyone, or have each user enter a link in their spreadsheet to the Stand Alone App. As you mentioned, for that option you won't be able to use methods like, getActiveSheet().
No matter what option you use, you'll need to either have people add something to their spreadsheet, or create some new, centralized interface. The best option for you may come down to ownership and setting permissions.
I'm guessing that if the users of the spreadsheets are the owners, and don't want to give you permission, they'll need to use one of your first three options. And I'd start with the library first.
If you can easily get the file ID's of the spreadsheets, you could create an object that matches the user to the FileID.
var objUserToFileID = {"user1":"abc34ciu89384u", "user2":"FileID_Two", "user3":"FileID_Three"};
Then have a way for the user to choose their name from the list, (Drop Down List) then run the code. That's for the Stand Alone App. Of course, then you'd need to figure out what happens if the user chooses the fileID for someone else's spreadsheet. Then you'd need to have a way to determine who the user of the App is.
You can retrieve the sheets that the user provided the URLs, exhibit them in simple HTML, one sheet below the other, and append a button column, which would call normalizeCrosstab() for that ROW. This is a publishedHTML solution, anybody could use without login.
If there's a defined number of sheets you could also generate them in HTML with a button next to the name, and it would generate the TABLE HTML.
Or use the library, I doubt there's anything you need and couldn't do, that answer is pretty old (12').
I'm using librarys and having no trouble with them, really handy for everything, all sheets must have these 3 functions to work as if the script was in the sheet themself:
function onOpen() {
library.onInitialize();
}
function onEdit(celEd) {
library.onMakeEdit(celEd);
}
function libraryFuncs( funcName, args ){ // Needed for sideBars to use library functions
if(args)
args = args.split("\!|"); // Predefined separation of args
else
args = [];
return library[ funcao ]().apply(this, args);
}

Google Sheets how to prevent sheet page from being renamed?

I know I can set an entire page to be protected, but is there a way I can just make the name of the page protected?
I have a Apps Script that depends on the sheet name so I don't want anyone to be able to rename it, but others still need to be able to have edit access to everything else on the sheet.
Thanks!
I don't think that's possible.
But in your script, use the sheet-id instead, which is unique and unchangeable, unless one deletes and recreates the sheet. In which case you could fallback to its name, or just understandably fail. Something like this:
function getSheetById(id) {
var sheets = SpreadsheetApp.getActive().getSheets();
for( var i in sheets )
if( sheets[i].getSheetId() == id )
return sheets[i];
throw new Error('Sheet with id "'+id+'" not found.');
}
Then parametrize and use this getSheetById function instead of using their names. You can see a sheet id by selecting it and looking at your browser's location bar. It's the last '#gid=' part. You can confirm it in code using:
function logId() {
Logger.log(SpreadsheetApp.getActive().getSheetByName('Sheet1').getSheetId());
//click View > Logs
}