Google Apps Script - Make onEdit() recognize setValue() changes - google-apps-script

I have a Spreadsheet with some functions. One of them is a onEdit(event) function that copies some values to other sheets based on conditions. This is the code (simplified but with the important parts intact):
function onEdit(event) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var s = event.source.getActiveSheet();
var r = event.range;
if(s.getName() === "Lista" && r.getColumn() === 9 && r.getValue() === "Posicionada") {
var sheetname = s.getRange(r.getRow(),3).getValue();
var columnRef = s.getRange(r.getRow(),4).getValue();
var row = s.getRange(r.getRow(),5).getValue();
var targetSheet = ss.getSheetByName("Mapa " + sheetname);
var headers = targetSheet.getRange(1, 1, 1, targetSheet.getLastColumn());
for (var i = 0; i < headers; i++) {
if (headers[i] === columnRef) {
break;
}
}
var column;
if (columnRef === "A1") {
column = 2;
}
else if (columnRef === "A2") {
column = 3;
}
else if (columnRef === "B1") {
column = 4;
}
else if (columnRef === "B2") {
column = 5;
}
if (sheetname === "N2") {
row = row - 30;
}
if (sheetname === "N3") {
column = column - 10;
row = row - 42;
}
targetSheet.getRange(row,column).setValue(s.getRange(r.getRow(), 1, 1, 1).getValue());
}
}
The code works as it should when I manually edit the cell. But, I have a code that edit the cell when the user press a button in a sidebar, this is the code:
function positionMU(){
var ss = SpreadsheetApp.getActiveSpreadsheet();
var cell = ss.getActiveCell().activate();
var cellLevel = cell.offset(0,2);
var cellLetter = cell.offset(0,3);
var cellNumber = cell.offset(0,4);
var cellStatus = cell.offset(0,8);
var dbq = "Posicionada";
var fora = "Pendente de recebimento";
if (cellStatus.getValue() == "Aguardando posicionamento"){
cellStatus.setValue(dbq); //attention in this line
}
else if (cellStatus.getValue() == "Aguardando saĆ­da"){
cellStatus.setValue(fora);
var cellExitDate = cell.offset(0,6);
cellExitDate.setValue(getDate());
}
}
As you can see, this function change the cell content with setValue(), but, when I use this function, the value of the cell changes, but the onEdit() trigger doesn't work.
How can I make the onEdit() trigger recognize changes made with setValue()?

You are right. onEdit() only triggers if the range is edited manually. As can be seen here, onEdit() triggers when a value is changed by the user.
I tested the function by making function to insert values into a column for which my onEdit responds and nothing happens. Including various other techniques that I could think of. Best thing to do here is to suggest this as an enhancement on App Script's Issue Tracker.
However, I made it work by writing another function to be called when another function in the script makes changes to the sheet. These are the test functions I wrote:
function addValues()
{
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName("Sheet1");
var range = sheet.getDataRange();
var book = "Book";
var cancel = "Cancel";
var maxRow = range.getLastRow()+1;
for(var i=0; i<4; i++)
{
if (i%2 == 0)
{
sheet.getRange(maxRow, 1).setValue(book);
autoChanges(maxRow);
}else{
sheet.getRange(maxRow, 1).setValue(cancel);
autoChanges(maxRow);
}
maxRow++;
}
}
autoChanges function:
function autoChanges(row)
{
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName("Sheet1");
var range = sheet.getDataRange();
var data = range.getValues();
var response = "";
sheet.getRange(row, 2).protect();
response = data[row-1][0];
if (response == "Book")
{
sheet.getRange(row, 2).canEdit();
}else{
sheet.getRange(row, 2).setValue("--NA--");
}
}
Not the most elegant solution but this seems to be the only workaround for what you are trying to do.

There are some very good reasons why calling range.setValue() doesn't trigger the onEdit event, and most of them have to do with infinite recursion. In fact, you call setValue() yourself WITHIN onEdit(). This would trigger a recursive call, and from what I can see, you have no provision for handling the base case and thus your code would explode if setValue() did what you want.
Why not simply take all of your code out of your event handler, and put it into another function:
function onEdit (e) {
return handleEdits(e.range);
}
function handleEdits(r) {
s = r.getSheet();
ss = s.getParent();
//the rest of your code should drop right in.
}
then, inside your autoChanges function, go ahead and call handleEdits, passing it an appropriate range, after your call to setValue().

If you like to play with fire, and I personally do, you can call the onEdit(e) function after you make a change. Just send in an object whatever is called and used by the e object.
For me, I just needed to add:
var e={};
e.range=the range you are making a change to in the script;
e.source = SpreadsheetApp.getActiveSpreadsheet();//or make sure you have the sheet for wherever you are setting the value
e.value=whatever value you are setting
e.oldValue=if you onEdit needs this, set it here
//if you are using any of the other standard or special properties called by your onEdit...just add them before calling the function.
onEdit(e);//call the function and give it what it needs.

Related

How to copy all data from source sheet to destination sheet?

I've made a Google Sheet that serves to keep track of lot numbers within a manufacturing setting. Right now, when the onEdit trigger is enabled, the data from the source sheet only doubles first row of data in the destination sheet. Here's the link to the Sheet. This is the code I have so far:
function onEdit(e) {
if (e.range.getA1Notation() == 'N2') {
if (/^\w+$/.test(e.value)) {
eval(e.value)();
e.range.clear();
}
}
}
function Reset() {
var s = SpreadsheetApp.getActiveSheet();
if( s.getName() == "Input" ) {
var dataRange = s.getRange('A5:V14');
var values = dataRange.clearContent();
s.getRange("C5:C14").setValue('-');
s.getRange("F5:F14").setValue('-');
s.getRange("I5:I14").setValue('-');
s.getRange("L5:L14").setValue('-');
}
}
function Submit() {
const ss = SpreadsheetApp.getActive();
const sh = ss.getSheetByName('Input');
const targetSheet = ss.getSheetByName('Database');
const data = sh.getRange(5,1,1,23).getValues(); // A5:W14
targetSheet.getRange(targetSheet.getLastRow()+1,3,data.length,data[0].length).setValues(data);
}
If I understand your question correctly you are worried that when you select Submit, there is a double insert of data into the Database sheet.
It can be assumed that this is due to the Submit function being run twice. The first time when checking /^\w+$/.test(e.value) and the second time directly when you run eval(e.value)().
I'm not sure if this is a bug or a feature :-)
Try this code, it works:
function onEdit(e) {
if (e.range.getA1Notation() == 'N2') {
var val = e.value;
switch(val) {
case 'Reset': Reset();
break;
case 'Submit': Submit();
break;
}
e.range.clear();
}
}
Upd.
The double insertion is due to the fact that you are using 2 onEdit() triggers. Have a look at Edith / Triggers of the current project. Delete it.
Upd2.
If you want only filled rows out of 10 to appear on the Database sheet then use this code. It filters the rows based on whether there is some text in column H, such as Adult or Pediatric.
function Submit() {
var ss = SpreadsheetApp.getActive();
var sh = ss.getSheetByName('Input');
var targetSheet = ss.getSheetByName('Database');
var data = sh.getRange(5,1,10,23).getValues();
Logger.log(data);
var fData = data.filter(data => (data[13] != ""));
Logger.log(fData);
targetSheet.getRange(targetSheet.getLastRow()+1,3,fData.length,fData[0].length).setValues(fData);
}

How to automatically merge cells in Google Sheets upon creation of new Sheet

I am trying to figure out a way to make Google Sheets automatically merge Cells A1-C1 when a new sheet is created. My coworker and I have been trying to figure out the script that would make this happen, but everything we have tried only changes the previous Sheet we were working on, not the new one.
So far these are the two scripts we have tried, just to get some sort of result we are looking for:
function formatCells() {
var ss = SpreadsheetApp.getActiveSpreadsheet ();
var s = ss.getSheetByName('Combined')
var range = s.getDataRange()
var values = range.getValues();
for( var row = values.length -1; row >= 0; --row)
if (values[row][1] == 'Hello')
{s.getRange(row+1,1).mergeAcross();
}
}
and
function newSheetTrigger() {
var ss = SpreadsheetApp.getActive();
ScriptApp.newTrigger('newSheet')
.forSpreadsheet(ss)
.onChange()
.create();
}
function newSheet(e){
if (e.changeType == 'INSERT_GRID') {
SpreadsheetApp.flush();
SpreadsheetApp.getActiveSheet().getRange('A1:C1').merge();
}
}
Does anyone have an idea of where we went wrong?
The problem is that theonChange trigger is not able to detect the active sheet correctly
Retrieving the active sheet on trigger will always return you the first sheet, as you can easily verify with
function myFunction(e) {
Logger.log(e.changeType);
if(e.changeType=="INSERT_GRID"){
Logger.log(SpreadsheetApp.getActive().getActiveSheet().getName());
}
}
So you need to implement a workaround.
For example:
Strore the present sheet names in Script properties
When the trigger fires and the condition e.changeType=="INSERT_GRID" is fullfilled:
Compare the currently present sheet number to the one stored in script properties to evaluate either a new sheet has been inserted
If the sheet number increased - find the name of the new sheet with indexOf()
Merge cells on the new sheet and update the script properties
Code snippet:
var ss = SpreadsheetApp.getActive();
//run me once
function firstSetUp(){
var sheets = ss.getSheets();
var names = [];
for (var i = 0; i < sheets.length; i++){
names.push(sheets[i].getName())
}
PropertiesService.getScriptProperties().setProperty("sheets", JSON.stringify(names) );
}
//run me on trigger
function newSheet(e) {
if(e.changeType=="INSERT_GRID"){
var newSheets = ss.getSheets();
var oldSheetNames = JSON.parse(PropertiesService.getScriptProperties().getProperty("sheets"));
Logger.log(oldSheetNames);
var length = oldSheetNames.length;
Logger.log("length : " + length);
if (length != newSheets.length){
for (var i = 0; i < newSheets.length; i++){
if(oldSheetNames.indexOf(newSheets[i].getName()) == -1){
var newSheet = newSheets[i];
Logger.log(newSheet.getName());
newSheet.getRange('A1:C1').merge();
oldSheetNames.push(newSheet.getName());
PropertiesService.getScriptProperties().setProperty("sheets", JSON.stringify(oldSheetNames));
break;
}
}
}
}
}

How to freeze a range of cells when a checkbox is clicked?

Screenshot for the sheet:
I just need someone to help me write simple code to freeze a range of cells when a certain checkbox is clicked.
I would like it so that when I click on the 'Complete' checkbox, all of the ones above it cannot be edited or changed anymore. Vise Versa when the 'Complete' checkbox is unchecked the ones above are editable. That simple.
The purpose of the sheet is to take attendance for a class. When I am done taking the attendance I don't want to be able to change it anymore (or risk clicking on the wrong checkbox). That's why the complete button is there.
Can anyone write the code for me, please?
(Freeze or seal or protect)
This code is not working (I am a beginner so sorry)
function onEdit() {
var sheet = SpreadsheetApp.getActive();;
var completedRow = sheet.getDataRange();
for (i = 2; i < 18; i++){
var isComplete = source.getRange(countRow, i).getValue();
if (isComplete === true){
source.getRange(2, i, countRow-1).protect();
}
}
}
Your code reflects the basic logic, though there are some syntax flaws. Hopefully this answer will help you understand and adapt that syntax.
The code doesn't doesn't take advantage of the Event Objects that are available to onEdit(e), which include the row, column and value of the edited cell. It's not compulsory to use the Event objects, but they certainly make life easier.
countRow isn't defined; and because you are working with a spreadsheet of finite length (20 rows); it is probably unnecessary. But it is a sensible idea to allow for bigger spreadsheets. Maybe something like var countRow = sheet.getLastRow(); would be a good alternative Doc Ref.
isComplete - we know that this is always on row 20; we also know that it will have a value of "true" or "false". So, you don't need a loop to define this row.
At some stage, you may want to "unprotect" a column; say at the start of a new term or year; so it's likely that checking row 20 for a value of "false" could be useful.
Your goal can probably be achieved in many ways. The following should be considered as just one option.
The main function is setup in an onEdit(e) simple trigger.
I also setup a custom menu (using onOpen) that gives you access to view all the protected columns, and to remove protection if you need to.
I've also left some Logger.log statements in the code that may enable you to check the value of certain fields at key stages of the code.
All-in-all, this code follows the same logic as your code, but with some more detail.
One last thing, this code is designed to work on a specific sheet by virtue of var sheet = ss.getSheetByName(sheetname); but you could just as easily change this to var sheet = SpreadsheetApp.getActiveSheet(); to make it work on multiple sheets in your spreadsheet.
function onEdit(e) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheetname = "Sheet1";
var sheet = ss.getSheetByName(sheetname);
// set variable for last column
//Logger.log(JSON.stringify(e))
// set variables for edited cells,
var edittedRow = e.range.rowStart;
var edittedColumn = e.range.columnStart;
var newValue = e.value;
var headerrange = sheet.getRange(1, edittedColumn);
var headervalue = headerrange.getDisplayValue();
//Logger.log("DEBUG: The header range is "+headerrange.getA1Notation()+", and the value is "+headervalue);
// test if edit row =20, and the checkbox was ticked
if (edittedRow === 20 && newValue === "TRUE") {
//Logger.log("DEBUG: The 'ON' leg applies");
//Logger.log("DEBUG: edittedRow = "+edittedRow+", Editted column = "+edittedColumn+", and value = "+newValue);
// define the range to protect
var protectRangeOn = sheet.getRange(1, edittedColumn, 19, 1);
// protect the range - warning only.
protectRangeOn.protect().setDescription(headervalue)
.setWarningOnly(true);
//Logger.log("DEBUG1: protection set for "+protectRangeOn.getA1Notation());
}
//test if edit row=20, and the checkbox was unticked
if (edittedRow === 20 && newValue === "FALSE") {
//Logger.log("DEBUG: The 'OFF' leg applies");
//Logger.log("DEBUG: edittedRow = "+edittedRow+", Editted column = "+edittedColumn+", and value = "+newValue);
// define the range to unprotect
var protectRangeOff = sheet.getRange(1, edittedColumn, 19, 1);
var protections = sheet.getProtections(SpreadsheetApp
.ProtectionType.RANGE)
for (var i = 0; i < protections.length; i++) {
Logger.log("protections range name = " + protections[i]
.getDescription() + " - Header value = " + headervalue);
if (protections[i].getDescription() === headervalue) {
//Logger.log("DEBUG: OFF matches")
protections[i].remove();
}
}
//Logger.log("DEBUG2: protection unset for "+protectRangeOff.getA1Notation());
}
}
// Add a custom menu to the active spreadsheet to access Utilities
function onOpen(e) {
SpreadsheetApp.getUi()
.createMenu('Protection Utilities')
.addItem('Show all protections', 'uigetprotections')
.addItem('Remove all protections', 'removeallprotections')
.addToUi();
}
function removeallprotections() {
// remove all protections
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheetname = "Sheet1";
var sheet = ss.getSheetByName(sheetname);
var protections = ss.getProtections(SpreadsheetApp.ProtectionType
.RANGE);
Logger.log(protections);
for (var i = 0; i < protections.length; i++) {
var protection = protections[i];
Logger.log(protection.getEditors())
if (protection.canEdit()) {
protection.remove();
}
}
// Display confirmation dialog
var ui = SpreadsheetApp.getUi();
var response = ui.alert('REMOVE ALL PROTECTION',
'Confirmed: Removed all protections', ui.ButtonSet.OK);
}
function uigetprotections() {
// generate a list of all RANGE protections
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheetname = "Sheet1";
var sheet = ss.getSheetByName(sheetname);
var protections = ss.getProtections(SpreadsheetApp.ProtectionType
.RANGE);
//Logger.log(protections);
var ui = SpreadsheetApp.getUi();
var protectioninfo = "";
if (protections.length != 0) {
for (var p = 0; p < protections.length; p++) {
//Logger.log("DEBUG: Date = "+protections[p].getDescription()+", Range = "+protections[p].getRange().getA1Notation());
protectioninfo = protectioninfo + "Date: " + protections[p]
.getDescription() + ", Range = " + protections[p].getRange()
.getA1Notation() + "\n";
}
var response = ui.alert('SHOW ALL PROTECTIONS', protectioninfo, ui
.ButtonSet.OK);
} else {
var response = ui.alert('SHOW ALL PROTECTIONS',
"There were no protected ranges", ui.ButtonSet.OK);
}
}

Cannot get getColumn and getRow methods to return any values

I am new to Google apps script (and javascript for that matter).
In the code below I keep getting "No logs found. Use Logger API to add logs to your project.", tried adding an "event" variable to the onEdit() trigger with no avail.
function onEdit() {
var test = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("test");
var activeCell = test.getActiveCell();
var col = activeCell.getColumn();
var row = activeCell.getRow();
Logger.Log(col);
Logger.Log(row);
if(col == 1 && row == 1) {
var testVar = 1;
Logger.log(testVar);
}
}
Thanks.
You can play around with this to learn more about the event object. It really has a lot of information in it right of out the box.
function onEdit(e) {
if(e.range.getSheet().getName()!='test'){return;}//keeps other pages from affecting the performance of the onEdit function
var activeCell = e.range;//typically e.range is one cell
var col = e.range.columnStart;//there's also a columnEnd
var row = e.range.rowStart;//there's also a rowEnd
Logger.log(JSON.stringify(e));//take a look at this and see what else is there
Logger.Log(col);
Logger.Log(row);
if(col == 1 && row == 1 && e.range.getSheet().getName()=='test') {
var testVar = 1;
Logger.log(testVar);
}
}

How can I change the name "onEdit" to other name?

Can anyone help me do this?
My problem is that all of my three (3) codes has the same name "onEdit".
How can I run three (3) of of them?
There is a suggestion that I need to change their name, but when I change the names my codes wont run.
Can anyone post an example code for me, for my reference?
Please?
function onEdit() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getActiveSheet();
var lastRow = sheet.getLastRow();
for (p=1 ; p<=lastRow ; p++) { // p <= lastRow
var status = sheet.getRange("C"+p).getValue(); // Change P to the completed column
if (status == "no") { // status == "no"
sheet.hideRows(p);
}
}
}
function onEdit() {
var spreadsheet = SpreadsheetApp.getActive();
var sheet = spreadsheet.getActiveSheet();
var cell = spreadsheet.getActiveCell();
var col = cell.getColumn();
var row = cell.getRow();
var rows = [1, 2, 3];
// This is a list of the rows that should blink if edited.
if (col === 7 && rows.indexOf(row) !== -7 && sheet.getName() === 'Sheet1') {
// If the edited cell is in column A (1) and if the edited cell
// is one of the rows listed
for (var num = 0; num < 50; num++) {
var colour = num%2 === 0
? 'GOLD'
: 'WHITE';
// Using ? and : like this is called a ternary operation. It's a
// shorter form of if. ifStatement ? true : false.
sheet.getRange('G' + row + ':G' + row).setBackground(colour);
// Get the range for the edited row and set the bg colour
SpreadsheetApp.flush();
Utilities.sleep(500);
}
}
}
Two functions in one onEdit()
Here's an example that combines two functions into one onEdit() simple trigger.
function onEdit(e){
var ss=e.source;
var rg=e.range;
var sh=rg.getSheet();
var name=sh.getName();
Logger.log('Name: %s',name);
var includedSheets=['Sheet45','Sheet46']
if(includedSheets.indexOf(name)==-1){
return;
}
if(name=='Sheet45'){
Sheet45(e);//You could put the code right here but I wanted to make it clear that it's two different operation. This one deletes a row if all conditions are met.
}
if(name=='Sheet46'){
Sheet46(e);//This one deletes a column if all conditions are met
}
}
function Sheet45(e){//You can name them whatever you want
var sh=e.range.getSheet();
var row=e.range.getRow();
var col=e.range.getColumn();
Logger.log('Name: %s',e.range.getSheet().getName());
if(col==1 && row==4 && e.value=='delete'){//if column1 and row4 is changed to 'delete'
sh.deleteRow(row);//the it deletes row 4
}
}
function Sheet46(e){
var sh=e.range.getSheet();
var row=e.range.getRow();
var col=e.range.getColumn();
Logger.log('Name: %s',e.range.getSheet().getName());
if(row==1 && col==4 && e.value=='delete'){//if column4 and row1 is changed to 'delete'
var rg=sh.getDataRange();//then it deletes column 4
var vA=rg.getValues();
for(var i=0;i<vA.length;i++){
vA[i].splice(3,1);
Logger.log('vA[%s]: %s',i,vA[i]);
}
}
rg.clear();
sh.getRange(1,1,vA.length,vA[0].length).setValues(vA);
}
There are a lot of different ways to do it. I like to return as quickly as possible on sheets that are not involved in any of the functions.
Simple Triggers
Spreadsheet Documentation
You probably already know that you can't run these onEdit() functions directly unless you provide the event object.
Your two functions combined
I modified the second one a bit because it didn't make sense to me. So you change it since you probably understand what you want.
function onEdit(e){
oE1(e);
oE2(e);
}
function oE1(e) {//this works on all sheet
var ss=e.source;
var rg=e.range;
var sh=rg.getSheet();
var name=sh.getName();
var lastRow = sh.getLastRow();
for(var row=1;row<=lastRow;row++) {
var status=sh.getRange(row,3).getValue();//Column3 is C
if (typeof(status)=="string" && status.toLowerCase()=="no") {
sh.hideRows(row);
}
}
}
function oE2(e) {//this only works on sheet 1
var ss=e.source;
var rg=e.range
var sh=rg.getSheet();
var row=rg.getRow();
var col=rg.getColumn();
var cell=sh.getRange(row,col);
var rows = [1, 2, 3];
if(col==1 && rows.indexOf(row)!=-1 && sh.getName()=='Sheet1') {
sh.getRange(row,7).setBackground((rows.indexOf(row)%2==0)?'Gold':'White');
}
}
It would be nice to rework the organization so that you can return more quickly for sheets that are not involved with either function. It's really helpful to look at the executions page when debugging these functions.