setActiveCell on all sheets within a spreadsheet. Google Apps Script - google-apps-script

I have a Google spreadsheet with multiple sheets. I would like the cursor to be set to a particular cell onOpen for all tabs. Each cell of the first row represents a week of the year, start to finish, for the whole year. I've written some code to have that particular column (week of the year) preselected on row 5 when opening.
Currently, only the last sheet has the cell selected that I want. This is because setActiveCell can only be used for one cell at a time I guess. I have also tried setActiveRange and setActiveSelection to no avail.
I found a question from years ago Here, but the solution does not work for me. Perhaps something has changed since then.
Here is my code:
function onOpen() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
Date.prototype.getWeek = function() {
var onejan = new Date(this.getFullYear(),0,1);
return Math.ceil((((this - onejan) / 86400000) + onejan.getDay()+1)/7);
}
var now = new Date();
var weekNum = now.getWeek();
var sheets = ss.getSheets();
for(var i = 0; i < sheets.length; i++) {
var newRange = sheets[i].getRange(5,weekNum + 1);
sheets[i].setActiveCell(newRange);
}
}
Within the link I provided above, I've tried variances of all of the given solutions, but my code was never exactly the same because I wasn't looking for the exact same things as the OP. For example, using Jacob Jan Tuistra's latest code solution, I came up with:
function setCursor() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
Date.prototype.getWeek = function() {
var onejan = new Date(this.getFullYear(),0,1);
return Math.ceil((((this - onejan) / 86400000) + onejan.getDay()+1)/7);
}
var now = new Date();
var weekNum = now.getWeek();
ss.getSheets().forEach(function (sheet, weekNum) {
Logger.log(weekNum);
sheet.setActiveRange(sheet.getRange(5,weekNum+1));
});
}
Which also only set the last sheet

Looks like you need to make a call to SpreadsheetApp.flush() when you loop over the sheets collection:
function onOpen() {
SpreadsheetApp.getActive().getSheets().forEach(function (s) {
s.setActiveSelection("B4");
SpreadsheetApp.flush(); // Force this update to happen
});
}
My guess is Apps Script's execution engine optimizes the writes to a single call, and in that single call only one active selection can be made. Calling flush forces 1 call per sheet to be updated.

Related

Applying routine refreshes to data with ImportXML & ImportHTML AND monitoring changes [duplicate]

I have a large sheet with around 30 importxml functions that obtain data from a website that updates usually twice a day.
I would like to run the importxml function on a timely basis (every 8 hours) for my Google Spreadsheet to save the data in another sheet. The saving already works, however the updating does not!
I read in Google Spreadsheet row update that it might run every 2 hours, however I do not believe that this is true, because since I added it to my sheet nothing has changed or updated, when the spreadsheet is NOT opened.
How can I "trigger" the importxml function in my Google Spreadsheet in an easy way, as I have a lot of importxml functions in it?
I made a couple of adjustments to Mogsdad's answer:
Fixed the releaseLock() call placement
Updates (or adds) a querystring parameter to the url in the import function (as opposed to storing, removing, waiting 5 seconds, and then restoring all relevant formulas)
Works on a specific sheet in your spreadsheet
Shows time of last update
...
function RefreshImports() {
var lock = LockService.getScriptLock();
if (!lock.tryLock(5000)) return; // Wait up to 5s for previous refresh to end.
var id = "[YOUR SPREADSHEET ID]";
var ss = SpreadsheetApp.openById(id);
var sheet = ss.getSheetByName("[SHEET NAME]");
var dataRange = sheet.getDataRange();
var formulas = dataRange.getFormulas();
var content = "";
var now = new Date();
var time = now.getTime();
var re = /.*[^a-z0-9]import(?:xml|data|feed|html|range)\(.*/gi;
var re2 = /((\?|&)(update=[0-9]*))/gi;
var re3 = /(",)/gi;
for (var row=0; row<formulas.length; row++) {
for (var col=0; col<formulas[0].length; col++) {
content = formulas[row][col];
if (content != "") {
var match = content.search(re);
if (match !== -1 ) {
// import function is used in this cell
var updatedContent = content.toString().replace(re2,"$2update=" + time);
if (updatedContent == content) {
// No querystring exists yet in url
updatedContent = content.toString().replace(re3,"?update=" + time + "$1");
}
// Update url in formula with querystring param
sheet.getRange(row+1, col+1).setFormula(updatedContent);
}
}
}
}
// Done refresh; release the lock.
lock.releaseLock();
// Show last updated time on sheet somewhere
sheet.getRange(7,2).setValue("Rates were last updated at " + now.toLocaleTimeString())
}
The Google Spreadsheet row update question and its answers refer to the "Old Sheets", which had different behaviour than the 2015 version of Google Sheets does. There is no automatic refresh of content with "New Sheets"; changes are only evaluated now in response to edits.
While Sheets no longer provides this capability natively, we can use a script to refresh the "import" formulas (IMPORTXML, IMPORTDATA, IMPORTHTML and IMPORTANGE).
Utility script
For periodic refresh of IMPORT formulas, set this function up as a time-driven trigger.
Caveats:
Import function Formula changes made to the spreadsheet by other scripts or users during the refresh period COULD BE OVERWRITTEN.
Overlapping refreshes might make your spreadsheet unstable. To mitigate that, the utility script uses a ScriptLock. This may conflict with other uses of that lock in your script.
/**
* Go through all sheets in a spreadsheet, identify and remove all spreadsheet
* import functions, then replace them a while later. This causes a "refresh"
* of the "import" functions. For periodic refresh of these formulas, set this
* function up as a time-based trigger.
*
* Caution: Formula changes made to the spreadsheet by other scripts or users
* during the refresh period COULD BE OVERWRITTEN.
*
* From: https://stackoverflow.com/a/33875957/1677912
*/
function RefreshImports() {
var lock = LockService.getScriptLock();
if (!lock.tryLock(5000)) return; // Wait up to 5s for previous refresh to end.
// At this point, we are holding the lock.
var id = "YOUR-SHEET-ID";
var ss = SpreadsheetApp.openById(id);
var sheets = ss.getSheets();
for (var sheetNum=0; sheetNum<sheets.length; sheetNum++) {
var sheet = sheets[sheetNum];
var dataRange = sheet.getDataRange();
var formulas = dataRange.getFormulas();
var tempFormulas = [];
for (var row=0; row<formulas.length; row++) {
for (col=0; col<formulas[0].length; col++) {
// Blank all formulas containing any "import" function
// See https://regex101.com/r/bE7fJ6/2
var re = /.*[^a-z0-9]import(?:xml|data|feed|html|range)\(.*/gi;
if (formulas[row][col].search(re) !== -1 ) {
tempFormulas.push({row:row+1,
col:col+1,
formula:formulas[row][col]});
sheet.getRange(row+1, col+1).setFormula("");
}
}
}
// After a pause, replace the import functions
Utilities.sleep(5000);
for (var i=0; i<tempFormulas.length; i++) {
var cell = tempFormulas[i];
sheet.getRange( cell.row, cell.col ).setFormula(cell.formula)
}
// Done refresh; release the lock.
lock.releaseLock();
}
}
To answer your question for an easy "trigger" to force the function to reload:
add an additional not used parameter to the url you are loading, while referencing a cell for the value of that parameter.
Once you alter the content of that cell, the function reloads.
example:
importxml("http://www.example.com/?noop=" & $A$1,"...")
unfortunately you cannot put a date calculating function into the referenced cell, that throws an error that this is not allowed.
You can also put each XML formula as a comment in the respective cells and record a macro to copy and paste it in the same cell. Later use the Scripts and then the Trigger functionality to schedule this macro.

Google Sheets: How to automatically insert a word into a cell on a new row detected?

I have searched high and low but I have been unable to find an answer (I am sure I am not explaining it right)
I have a Google Sheet that have multiple sheets (tabs) labeled TabA, TabB and TabC.
On this Google Sheet, I submit a slash command on Slack, which then auto-fills a row on one of the tabs using apps script.
What I am trying to do is simply insert a word called TabA into a specific cell each time a new row has been detected. And insert a word called TabB when a new row has been made on TabB sheet etc.
I am sure I just am typing my questions wrong which is why I am unable to find an answer.
I am not actually sure which part of the code posts to the sheet, I think it is this?
if(sheetName) {
sheetName = sheetName.charAt(0).toUpperCase() + sheetName.slice(1)
} else {
sheetName = "Current"
}
// Find sheet
var sheetFlag = false;
for (var r = 1; r < settings.length; r++) {
if (settings[r][1] == channelID) {
var sheetID = settings[r][2];
var targetChannelID = settings[r][4];
var title = settings[r][0];
sheetFlag = true;
break;
}
}
if (sheetFlag) {
var sheet = SpreadsheetApp.openById(sheetID).getSheetByName(sheetName);
if(!sheet) {
sheet = SpreadsheetApp.openById(sheetID).insertSheet(sheetName);
}
var lastRow = sheet.getLastRow();
var slackDetails = ["", "", text1, "","","","","",realName, new Date(),title,text2];
// paste the slack details to the sheet
sheet.getRange(lastRow + 1,1,1,slackDetails.length).setValues([slackDetails]);```
Thank you in advance
If I understood you correctly, you want to:
Keep track of new rows that are added to each sheet in your spreadsheet (TabA, TabB, TabC).
Write the name of the sheet in successive rows of column D of each sheet every time news rows are detected.
As you were told in the comments, Apps Script has no triggers to track changes made to the spreadsheet by a script. For example, onEdit trigger "runs automatically when a user changes the value of any cell in a spreadsheet".
Workaround (time-based trigger and script properties):
A possible workaround to this limitation is using a time-based trigger that will fire a function periodically. You can create this trigger manually, or programmatically, by running this function once:
function createTrigger() {
ScriptApp.newTrigger("trackRows")
.timeBased()
.everyMinutes(1)
.create();
}
This will fire the function trackRows every minute. This function's purpose is to track changes to each sheet rows since last time it was fired (in this example, 1 minute ago) and write the sheet name to a certain cell if the sheet has more rows with content than during last execution.
To accomplish this, you can use the Properties Service, which lets you store key-value pairs and use them in later executions.
The function trackRows could be something along the following lines:
function trackRows() {
var props = PropertiesService.getScriptProperties();
var ss = SpreadsheetApp.openById("your-spreadsheet-id"); // Please change accordingly
var sheets = ss.getSheets();
sheets.forEach(function(sheet) {
var sheetName = sheet.getName();
var currentRows = sheet.getLastRow();
var oldRows = props.getProperty(sheetName);
if (currentRows > oldRows) {
var firstRow = 2;
var column = 4;
var numRows = sheet.getLastRow() - firstRow + 1;
var rowIndex = sheet.getRange(firstRow, column, numRows).getValues().filter(function(value) {
return value[0] !== "";
}).length;
var cell = sheet.getRange(rowIndex + firstRow, column);
cell.setValue(sheetName);
}
props.setProperty(sheetName, currentRows);
});
}
This function does the following:
Retrieve the script properties that were stored in previous executions.
Get all the sheets in the spreadsheet.
Check the last row with content in each sheet (via Sheet.getLastRow()), and compare the value with the one previously stored in script properties.
If the current last row is higher than the one stored in properties, write the sheet name in the first empty row of column D of the corresponding (starting at D2).
Store the current last row in script properties.
Notes:
The script is adding the sheet name to the first empty row of column D once, if it detects that new rows were added. It's not taking into account how many rows were added since last execution, it only considers if rows were added. This could easily be changed though, if that's what you wanted.
If you want to start from fresh, it would be useful to delete all previously stored properties. To do that, you could run this function once:
function deleteProps() {
var props = PropertiesService.getScriptProperties();
props.deleteAllProperties();
}
Reference:
Class ClockTriggerBuilder
Class PropertiesService
Sheet.getLastRow()

How to increment simple formula by 1 in Google Apps Script?

Disclaimer: I'm a Google Apps Script newbie.
I'm trying to create a timesheet in Google Sheets that lets a user clock in & clock out to log hours on a given project. I've borrowed code from a YouTube video on the general structure of setting the whole thing up.
Here's what the blank time sheet looks like. It's pretty basic:
I've created a user button (off to the right) where the user presses "Start" and cell A2 will input a timestamp. Then the user can press an "End" button, and a second timestamp, this time in B2, will appear, along with a simple calculation in C2 that measures the delta in the two timestamps, thus giving a duration of time spent on a given task or project. Here's what it looks like:
When the user needs to press "Start" again, a new timestamp appears in cell A3, and so on so forth, along with a new delta calculation for each new row.
Problem: I'm unable to get the simple delta calculation in column C to increment down each new rows so that the setFormula function doesn't contain hardcoded references to cells A2 & B2. See below code for what I have so far:
function setValue(cellName, value) {
SpreadsheetApp.getActiveSpreadsheet().getRange(cellName).setValue(value);
}
function getValue(cellName) {
return SpreadsheetApp.getActiveSpreadsheet().getRange(cellName).getValue();
}
function getNextRow() {
return SpreadsheetApp.getActiveSpreadsheet().getLastRow() + 1;
}
function addStartRecord (a) {
var row = getNextRow();
setValue('A' + row, a);
}
function addEndRecord (b, c) {
var row = getNextRow()-1;
setValue('B' + row, b);
setValue('C' + row, c);
}
function punchIn() {
addSRecord(new Date());
}
function punchOut() {
addERecord(new Date(), '=B2-A2');
}
The problem is with the punchOut() function there at the bottom. Any idea on the best way to increment this delta calculator down each new row?
Note: I saw a pretty good answer to a similar question here, but the code is throwing an error in the script editor after the line containing data[i] = ['=A' + i+1.toString() + ' + 1 ' ]. Also, I don't want to set a definitive last row for the delta calculation (such as 20 in this example). I'd want the user to be able to record as many new start/end times for a project as they'd want.
Edit: Here's a link to the timesheet so you can test the code.
Try modifying your punchOut method like this:
function punchOut() {
var ss = SpreadsheetApp.getActiveSheet();
var row = ss.getLastRow();
addEndRecord(new Date(), '=B' + row + '-A' + row);
}
I tested it in the sheet and it worked well.
setFormula() - this enables you to describe the formula to be inserted into column C.
The following is two simple functions that handle "Punch in" and "Punch Out" (with its calculation).
function so5695101401in() {
// punchin
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getActiveSheet();
var lR = sheet.getLastRow();
// Logger.log("DEBUG: the last row is "+lR);
var punchinRange = sheet.getRange(lR+1, 1);
// Logger.log("DEBUG: the punchinRange = "+punchinRange.getA1Notation());
punchinRange.setValue(new Date());
}
function so5695101401out() {
// punchout
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getActiveSheet();
var lR = sheet.getLastRow();
//Logger.log("DEBUG: the last row is "+lR);
var punchoutRange = sheet.getRange(lR, 2);
// Logger.log("DEBUG: the punchoutRange = "+punchoutRange.getA1Notation());
punchoutRange.setValue(new Date());
var timeElapsed = sheet.getRange(lR, 3).setNumberFormat("hh:mm:ss");
timeElapsed.setFormula("=B2-A2");
}
setFormula
I use a workaround for this problem, via app script copy the cell with the formula to de new row or range!.
for you problem:
var formula1 = sheetDatos.getRange(lastRow, 3); //get the formula
var copyRange = sheetDatos.getRange(lastRow+1, 3);
formula1.copyTo(copyRange);
for me is more easy in this way, try to do in sheet to understand how this work.
you need a initial formula to go in this way ;)

Trying to create auto-calculation for hours of in and out script for google sheet

I created a script for my team in google sheets where they can click a button to clock in and another button to clock out.
However, I've been trying to create a script to auto-calculate the amount of hours they worked in that time frame and I'm not able to build it.
I inserted a drawing of a few blocks that says Bianca, In and Out. I assigned a script to each block for the script below.
This is the portion that is not working.
function onPunchOut (){
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getActiveSheet();
var lastRow = sheet.getLastRow();
var finish = sheet.getRange("E"+lastRow).getValue();
var hoursWorked = sheet.getRange("F"+lastRow);
}
if (finish === "Out") {
hoursWorked.setFormulaR1C1('=R[0]C[-2]-R[-1]C[-2]');
hoursWorked.setNumberFormat("H:mm");
}
Current code that works for clocking in and out:
function setValue(cellName, value) {
SpreadsheetApp.getActiveSpreadsheet().getRange(cellName).setValue(value);
}
function getValue(cellName){
return SpreadsheetApp.getActiveSpreadsheet().getRange(cellName).getValue();
}
function getNextRow() {
return SpreadsheetApp.getActiveSpreadsheet().getLastRow() + 1;
}
function setBianca() {
setValue('L1' , 'Bianca');
}
function addRecord(a, b, c){
var row = getNextRow();
setValue('A' + row, a);
setValue('D' + row, b);
setValue('E' + row, c);
}
function punchInBianca(){
addRecord(getValue('L1'), new Date(), 'In');
}
function punchOutBianca(){
addRecord(getValue('L1'), new Date(), 'Out');
}
Expected results:
Name Date/Time In/Out **Hours worked**
Bianca 4/25/2019 7:59:34 In **3.15**
Bianca 4/25/2019 11:15:20 Out **0.29**
Bianca 4/25/2019 11:44:44 In **4.55**
Actual results: get script errors trying to get hours worked. Says cannot find variable "finish"
Your problem is that (as you know) finish is not defined. This is because the variable is local inside of the function onPunchOut(), and your if statement can not directly gain access to the variable. Variables have two different kinds of scope; you can learn more about them here. To solve your problem, move the if statement inside of the closing bracket of onPunchOut().
That worked. Thanks!
function onPunchOut (){
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getActiveSheet();
var lastRow = sheet.getLastRow();
var finish = sheet.getRange("E"+lastRow).getValue();
var hoursWorked = sheet.getRange("F"+lastRow);
if (finish === "Out") {
hoursWorked.setFormulaR1C1('=R[0]C[-2]-R[-1]C[-2]');
hoursWorked.setNumberFormat("H:mm");
}
}

Functions work from menu or manually run - but not using Google Time Event

I have two functions -
-The first gets Emails - getEmails();
-The second deletes my Trash - deleteForever();
a) I created a custom menu to trigger the above functions separately & they both function.
b) After testing I added a Google Project Trigger using Timed Event
The getEmails Function stops working properly.
When Ran, the script has no errors & the results are:
Data insert into worksheet <- REASON FOR THIS POST-THIS NEVER HAPPENS
Emails move to trash getEmails(); <- SUCCESS-I physically See it
Emails delete w/ deleteForever(); <- SUCCESS-I physically See it
Environment Variations: I tested when spreadsheet & gmail was both open and closed in browser
FIX Attempt: I had used getActiveSheet() * Note it Commented it out *
And replaced with the actual spreadsheet ID thinking that would help my problem but same results.
Any ideas?
Function 1 - (does not work with project trigger timmed event DOES work manually executing)
function getEmails() {
/*var ss = SpreadsheetApp.getActiveSheet();*/
var ss = SpreadsheetApp.openById("1Y-MY-SPREADSHEET-ID-8q9q98k");
var threads = GmailApp.search("is:starred in:WebOppCRM");
var row = ss.getLastRow();
for (var i=0; i<threads.length; i++) {
/* COMMENT OUT TO FOCUS ON PURPOSE OF QUESTION
var start = new Date();
if (isTimeUp_(start)) {
Logger.log("Time up");
break;
}
*/
var messages = threads[i].getMessages();
var newrow = row + 1;
for (var j=0; j<messages.length; j++) {
var msg = messages[j].getPlainBody();
var dat = messages[j].getDate();
var msgParse ='=SPLIT($A'+ (newrow) + ', "|")';
ss.appendRow([msg, dat, msgParse]);
newrow = newrow +1;
Utilities.sleep(100);// pause in the loop for 200 milliseconds
}
threads[i].moveToTrash();
}
Function 2 - does work with project triggers or manual
function deleteForever() {
var threads = GmailApp.search("in:trash");
for (var i = 0; i < threads.length; i++) {
Gmail.Users.Messages.remove("tannerfasteners#gmail.com", threads[i].getId());
}
}
You're missing this function. It's working here, also with a time driven trigger.
function isTimeUp_(start) {
var now = new Date();
return now.getTime() - start.getTime() > 300000; // alert above 3 minutes, to keep it under 5 minutes
}
I really would rather accept an answer by another, however nothing above even addressed my problem as to what I specifically laid out.
The answer was to give focus to the specific sheet when it was not opened by me.
I Realized
code was executing at the top of the script
code and functions below were executing
The script has not instructed WHAT Sheet to use
Kind of obvious now too - when moving from
var ss = SpreadsheetApp.getActiveSheet();
to the following and having the sheet closed
var ss = SpreadsheetApp.openById("-My-Sheet-ID-Removed-");
There was no way the data could write without knowing what sheet to write to.
Modifying my script and adding the line marked as /SOLUTION>/ below worked to set an active sheet
var ss = SpreadsheetApp.openById("-My-ID-Here");
/*SOLUTION>*/ SpreadsheetApp.setActiveSheet(ss.getSheetByName('data'))
var threads = GmailApp.search("is:starred in:WebOppCRM");
var row = ss.getLastRow();
This resolved my specific problem and question as to why it worked when the sheet was open ( because the sheet was active by my being on it ) but partially stopped when sheet was closed ( because the sheet was obviously no longer active )
Hopefully this saves someone some debug time.