Google App Script to trigger on cell value change - google-apps-script

I am a newbie and have been using a simple App Script to send out emails with triggers onEdit and onChange. However, my Worksheet has over ten sheets and any edits/changes (done by me or by computations) in any of the sheets sends out an email, causing unintended spam! To avoid this, if I could use some code that sends the email based only on ANY CHANGE to a specific cell's value, in a specific sheet, my problem would be solved. My outgoing email message is short and the whole message is in just ONE cell (C2). If I can add a line of code which monitors for ANY change in that cell C2, and sends out an email if there is a change, that's it! I'd be done. My Script is as follows:
function sendEmail(){
var ss = SpreadsheetApp.getActiveSpreadsheet()
var sheet1=ss.getSheetByName('Email');
var emailAddress = sheet1.getRange(2,1).getValue();
var subject = sheet1.getRange(2,2).getValue();
var message = sheet1.getRange(2,3).getValue();
MailApp.sendEmail(emailAddress, subject, message);
}

Answer:
You can do this with an onEdit() and a conditional.
Code Example:
function onEdit(e) {
const specificSheet = "Email" // for example
const specificCell = "C2" // for example
let sheetCheck = (e.range.getSheet().getName() == specificSheet)
let cellCheck = (e.range.getA1Notation() == specificCell)
if (!(sheetCheck && cellCheck)) {
return
}
else {
sendEmail()
}
}
Rundown of this function:
Defines the sheet and A1 notation of the specific cell to check
Gets the Sheet and the A1 notation of the cell which was just edited
Returns if either the Sheet or the Cell are not the defined specific cell (using De Morgan's law)
Runs sendEmail() if the cell and Sheet are correct
References:
Event Objects | Apps Script | Google Developers
Simple Triggers | Apps Script | Google Developers
De Morgan's laws - Wikipedia

Working from the answer, this is how it ended up for me since I had several ranges to check:
function onEdit(e){
if(wasEdited(e, range1)){ // e.g. range1 = "Sheet1!A5"
// handle range1 change
}
if(wasEdited(e, range2)){ // e.g. range2 = "Sheet1!A7"
// handle range2 change
}
}
function wasEdited(e, range){
let tab = getTabFromA1Range(range)
let cell = getRangeFromA1Range(range)
return e.range.getSheet().getName() == tab && e.range.getA1Notation() == cell
}
function getTabFromA1Range(a1Range){
return a1Range.substring(0, a1Range.indexOf("!"))
}
function getRangeFromA1Range(a1Range){
return a1Range.substring(a1Range.indexOf("!")+1)
}

On further research, the following solution seems to work the best:
function sendEmail(){
Utilities.sleep(30000);
var ss=SpreadsheetApp.getActiveSpreadsheet();
var data=ss.getActiveSheet().getActiveCell().getA1Notation();
var sheetname = ss.getActiveSheet().getName();
var sheet1=ss.getSheetByName('Email');
var emailAddress = sheet1.getRange(2,1).getValue();
var subject = sheet1.getRange(2,2).getValue();
var message = sheet1.getRange(2,3).getValue();
if(data.indexOf('A:C')!=-1.23456789) {
MailApp.sendEmail(emailAddress, subject, message);
}
};
The key seems to be the "if statement" on line 10. Please note the time delay of half a minute I added to the script. This is because without it, on the trigger activating, the previous email was going out instead of the current one. Obviously my app has a slight delay in syncing and the trigger fired before all the current data got populated in the relevant cell!

Related

Getting google spreadsheets to send me an email

I have a spreadsheet on Google spreadsheets that has "D2" through all of "D" (because it will expand as used and when I add more) with a drop down box for priority status. The drop down has "Low, Medium, High". I want to get a script to send me an email when that priority gets edited to "High"
This is the script I did last night at 2AM half asleep.
{
var ss = SpreadsheetApp.getActive();
var sheet = ss.getSheetByName("TC52 Bugs or Issues and Improvements.");
var valueToCheck = sheet.getRange(D).getValue();
var rangeEdit = e.range.getA1Notation();
if(rangeEdit == "D")
{
if(valueToCheck > High)
{
MailApp.sendEmail("austin.hendrix#decathlon.com", "High Priority please check now.", "Check spreadsheet" + valueToCheck+ ".");
}
}
}````
This article has a tutorial.
On that note, something along the lines of (Heavily edited by MetaMan)
function sendEmails() {
var sheet = SpreadsheetApp.getActiveSheet();
var dataRange = sheet.getRange(1,4,sh.getLastRow()) or getRange('D1:D'+sh.getLastRow()));
var data = dataRange.getValues();
for (var i in data) {
var row = data[i];
var emailAddress = "youremail#email.com"; // First column
var message = "High priority set with data in A column "+sheet.getRange(("A"+i+1)).getValues()[0]; // Second column
var subject = 'High priority set in D'+i;
if(row[0]=="High"){
MailApp.sendEmail(emailAddress, subject, message);
}
}
}
The function will send you an email when column D is edited to "High".
You will need to fill in 'Your Spreadsheet name' the recipient email address and what ever subject you wish. Also you can edit the body as well. The current body just tells you which row issue the email. Alternately, if you wish to provide more information I'll do it for you.
function onMyEdit(e) {
const sh = e.range.getSheet();
if(sh.getName() == 'Your Sheet Name' && e.range.columnStart == 4 && e.range.rowStart >1 && e.value == 'High') {
GmailApp.sendEmail('recipient','subject',`row ${e.range.rowStart} has been set to high`)
}
}
Since sending an email requires permission you will have to create an installable trigger for this function.
You can do it manually or use a function like the one below which will prevent you from creating more that one trigger.
function createTrigger() {
const ts = ScriptApp.getProjectTriggers().map(t=>t.getHandlerFunction());
if(!~ts.indexOf('onMyEdit')) {
ScriptApp.newTrigger('onMyEdit').forSpreadsheet(SpreadsheetApp.getActive().getId()).onEdit().create();
}
}
ScriptApp
Please Note: that you cannot run onEdit triggered functions from the Script Editor or even from a menu. They require the onEdit trigger which populates the single parameter e with the event object. You can run them from another function as long as it provides the event object.

Google Apps Script - How to execute functions one after another?

1) My Goal
I'm trying to write a Google Apps Script that runs 3 functions step by step with a small pause after each step
Basically: 1) Fill cells with specific content 2) Send content from cells to email address 3) delete content from cells
2) My Challenge
Google Apps Script is running all functions simultanioulsy
3) What have I tried?
SpreadsheetApp.flush(); as suggested here - but did not work out
4) My Code
function pastecontent() {
// Fetch spreadsheet
var sheet = SpreadsheetApp.getActiveSpreadsheet();
// copy paste from first row to all others
var source = sheet.getRange("D6:F6");
source.copyTo (sheet.getRange("D7:F22"));
}
function sendEmails() {
// pause for X seconds
Utilities.sleep(3000);
// Fetch spreadsheet
var sheet = SpreadsheetApp.getActiveSpreadsheet();
// get the range and values in one step
var values = sheet.getRange("D7:F22").getValues();
// Send Mail
var message = values + " https://docs.google.com/spreadsheets/d/[....]";
var emailAddress = "XYZ#gmail.com";
var subject = "Test Mail";
if (cell != "") {
MailApp.sendEmail(emailAddress, subject, message);
}
}
function clearcontent() {
// pause for X seconds
Utilities.sleep(8000);
// Fetch spreadsheet
var sheet = SpreadsheetApp.getActiveSpreadsheet();
// clear cells
sheet.getRange("D7:F22").clearContent();
}
Thanks for your support
Joe
Main function to call all functions:
function main() {
pastecontent();
sendEmails();
clearcontent();
}
Sample Log:
I haven't changed anything in your code aside from the email and it went smoothly.
Just make sure to have a main function that calls them one by one.
Aside from that, I'm not seeing any issues with your code
Nitpick: The cell variable wasn't defined before sending the email. Declaration wasn't included in the code provided so I declared it in mine. You might want to add that if you don't have it in yours.
I think you can organize your code like this
function mainFunction(){
pastecontent();
Utilities.sleep(200);// pause in the loop for 200 milliseconds
sendEmails();
Utilities.sleep(200);/
clearcontent();
}

Stand Alone SpreadsheetApp.getActiveSheet() always return the first sheet

SpreadsheetApp.getActiveSheet() will always return the first sheet rather than the active sheet which is most likely a bug in new sheets.
function testActiveSheet(){
var ssCentral = SpreadsheetApp.openById("xxxxxxxxxxx");
var ssCentralName = ssCentral.getName();
SpreadsheetApp.setActiveSpreadsheet(ssCentral);
Logger.log('the ssCentralName is ' + ssCentralName);
var currentActiveSheet = ssCentral.getActiveSheet();
var currentActiveSheetName = currentActiveSheet.getName();
Logger.log('the currentActiveSheetName is ' + currentActiveSheetName);
}
despite the active sheet is not the first one, it still returns the first one.
If the script is stand-alone and not bound to a specific spreadsheet, an active Sheet would not exist. This is not a bug but expected behaviour. To expand an active sheet is what a user is looking at, not what the script is currently working in; or as said in the official documentation, "the active sheet in a spreadsheet is the sheet that is being displayed in the spreadsheet UI."
If the script is not bound then there is no sheet being displayed to any user and therefore the function just returns the first sheet! Hope that makes sense, let me know if you have any follow-up questions.
See here for details.
in response to #Chris
this is the SCRIPT B
function callActivateNewMember(){
LibraryName.activateNewMember()
}
this is the standalone SCRIPT A
function activateNewMember(){
// DECLARE THE CURRENT ACTIVE WORKSHEET as ssCentral
var ssCentral = SpreadsheetApp.openById("xxxxxxxxxxxxxx");
// check if TeamList sheet is active - if not land on Teamlist and ask to select a
new member by his/her first name
// goto and activate TeamList
var teamListSheet = ssCentral.getSheetByName('TEAM LIST');
var teamListSheetIndex = teamListSheet.getIndex()-1;
Logger.log('the teamListSheetIndex is ' + teamListSheetIndex);
var currentSheet = SpreadsheetApp.getActiveSheet().getName();
Logger.log('the currentSheet is ' + currentSheet);
if(currentSheet !='TEAM LIST'){
ssCentral.setActiveSheet(ssCentral.getSheets(
[teamListSheetIndex]).getRange('B5').activate();
Logger.log('WRONG TABLE');
return;
}
var teamListSheetLastRow = teamListSheet.getLastRow();
var nberRows = teamListSheetLastRow-7
// CHECK IF THE CELL IS IN THE FIRST COL AND IS NOT EMPTY
// ------------------get the current row of the member selected
var MemberRow = ssCentral.getCurrentCell().getRow();
var MemberCol = ssCentral.getCurrentCell().getColumn();
if(MemberRow <8 ||MemberCol>1 ){
Logger.log('RIGHT TABLE - WRONG CELL SELECTED');
return;
}
var memberCellValue = ssCentral.getCurrentCell().getValue();
if(memberCellValue=='' ){
Logger.log('CELL SELECTED EMPTY');
return;
}
// VALIDATION : IS THIS MEMBER WAS ALREADY ACTIVATED
//-----------get the column with “Reporting Activated” as header
var repActiveCol = teamListSheet.getRange("D7").getColumn();
//-----------check if the cell of “Reporting Activated” is Y
var checkY = teamListSheet.getRange(MemberRow, repActiveCol).getValue();
//-----------If checkY = “Y” then Alert OK
if (checkY == "Y") {
Logger.log('MEMBER WAS ALREADY ACTIVATED');
return;
}
// rest of the function
}
after considering the answers from Chris I decided to change the strategy
I now call the function in the standalone SCRIPT along with 2 parameters I catch from the SCRIPT bound to the Spreadsheet: ActiveSheet + ActiveCell.
then I return an error message based on the situation and display alert based on the situation.
it works now but I am not totally satisfied as I wanted to show the most limited code in the SCRIPT bound to the Spreadsheet.
thanks again to Chris for taking the time to explain to me the issue I faced. if you pass by Bali - Indonesia just let me know I'd be glad to have a coffee with you. We have a dev center here.
If your gs script was created from within a Google Sheet you can use
var sheet = SpreadsheetApp.getActiveSheet();
to get the currently selected sheet

Check color in Google Sheets

I'm struggling a bit with Google App Script. I'm looking to write a simple script that checks when a user changes a colour, and, if it's wrong, corrects it.
Here's my code so far:
function onEdit(e) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheets()[0];
var r = sheet.getActiveRange();
if (r.getBackground() != '#D9EAD3') {
var ui = SpreadsheetApp.getUi();
ui.alert('Wrong colour!');
r.setBackground('#D9EAD3');
}
}
It almost works...However.
it only works when text is changed, not colour (there doesn't appear to be an onFormat option)
it appears to change cell a1 every time.
Can anyone help? Thanks.
EDIT
That's a great help - thanks, Sandy. My code now looks like this:
function formatWasChanged(e) {
var r = e.range;
if (r.getBackground() != '#D9EAD3' && r.getBackground() != '#FFFFFF') {
var ui = SpreadsheetApp.getUi();
ui.alert('Wrong colour!');
r.setBackground('#D9EAD3');
}
}
I've also defined this as a project trigger, called on change. Unfortunately, it's still not running when I test it on my spreadsheet - var r = e.range; appears to fail when I run it in the console.
You can also use an installable "Change" trigger. That can check for:
Quote from Google Documentation:
e.changeType - The type of change (EDIT, INSERT_ROW, INSERT_COLUMN, REMOVE_ROW, REMOVE_COLUMN, INSERT_GRID, REMOVE_GRID, FORMAT, or OTHER)
Note the FORMAT type of change.
Google Documentation - Change Trigger
You must click the 'Resources' menu, and choose 'Current Project Triggers' in the code editor.
function formatWasChanged(e) {
Logger.log('e.changeType: ' + e.changeType);
if (e.changeType === 'FORMAT') {
Logger.log('Its a FORMAT changeType: ');
};
};
If you do stay with the onEdit() trigger,
Don't use:
var r = sheet.getActiveRange();
To get the range that was edited, use:
var r = e.range;
Google documentation:
e.range - A Range object, representing the cell or range of cells that were edited

Google Spreadsheet- Notify by email when someone edits a cell in a row

If someone edits the first row, an e-mail should be sent to A. If someone edits the second row, an e-mail should be sent to B and so on.
if (edited cell was in range - first row) {
//do something }
if (edited cell was in range- second row) {
//do something }
and so on.
Have already tried scripts available in Stack overflow, all are based on active cell. What if someone drags a value in a range of rows. The script would send an e-mail to the first active row cell only.
Found this, but fails when values are dragged over cells and I need over a range of each row.
function emailNotification() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getActiveSheet();
var cell = ss.getActiveCell().getA1Notation();
var cellvalue = ss.getActiveCell().getValue().toString();
var recipient = "me#gmail.com";
var subject = 'Update to '+sheet.getName();
MailApp.sendEmail(recipient, subject, "roster update");
};
Apps Script has simple triggers for edit events.
https://developers.google.com/apps-script/guides/triggers/
From the onEdit docs:
function onEdit(e){
// Set a comment on the edited cell to indicate when it was changed.
var range = e.range;
range.setNote('Last modified: ' + new Date());
}
You will notice when this trigger is fired it passes onEdit an object that contains a lot of info on this edit. You can view the object at:
https://developers.google.com/apps-script/guides/triggers/events
This object contains the Range object of the edit. The range object docs are at:
https://developers.google.com/apps-script/reference/spreadsheet/range
From the range you can get which cells were edited with the getA1Notation() method and the values using the getValues() method.