I have a changelog in my spreadsheet to keep track of information that is added, edited, or removed. The issue is that the changelog cannot detect if a user "Delete row". Is it even possible to have the changelog track deleted row or am I out of luck? Thanks 🙏.
function onEdit(e){
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheetNameTracker = ss.getActiveSheet().getName();
if (sheetNameTracker !== "Changelog") {
let sheet = ss.getSheetByName('Changelog');
var range = e.range.getA1Notation();
var user = e.user.getUsername();
var function_source = "onEdit";
var time = new Date();
var changeType = 'EDIT';
console.log(user);
var oldValue = e.oldValue;
var newValue = e.value;
var lastRow = sheet.getLastRow();
sheet.getRange(lastRow+1,1,1,8).setValues([[time.toLocaleString(),function_source,changeType,sheetNameTracker,range,user,oldValue,newValue]]);
}
}
Example Google Sheet with Appscript
About I am asking if there is a way I can have changelog track deleted row. Or is it not possible?, in the current stage, the event object of the OnEdit trigger doesn't include the property of changeType. So, in this case, it is required to use the installable OnChangge trigger. When this is reflected in your script, how about the following modification?
Modified script:
After you copy and paste the following modified script, please install OnChange trigger to the function onChange. By this, when a row is removed, the value of REMOVE_ROW is returned as e.changeType.
function onEdit(e) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheetNameTracker = ss.getActiveSheet().getName();
if (sheetNameTracker !== "Changelog") {
let sheet = ss.getSheetByName('Changelog');
var range = e.range && e.range.getA1Notation(); // Modified
var user = e.user.getUsername();
var function_source = "onEdit";
var time = new Date();
var changeType = e.changeType || 'EDIT'; // Modified
console.log(user);
var oldValue = e.oldValue;
var newValue = e.value;
var lastRow = sheet.getLastRow();
sheet.getRange(lastRow + 1, 1, 1, 8).setValues([
[time.toLocaleString(), function_source, changeType, sheetNameTracker, range, user, oldValue, newValue]
]);
}
}
// Please install OnChange trigger to this function.
function onChange(e) {
if (e.changeType == "EDIT") return;
onEdit(e);
}
In this modified script, when a row is removed, onChange is automatically run by the installable onChange trigger. And, the event object is sent to onEdit, and the log is recorded by your script.
Note:
In the case of the OnEdit trigger, even when a row is deleted, this trigger is not fired while the trigger is fired when a cell is edited. On the other hand, in the case of the installable OnChange trigger, the trigger is fired for both the editing cell and removing a row. In this modification, this situation is used.
In the case of OnChange trigger, in the current stage, the event object of OnChange trigger doesn't include range object. By this, the removed row is not known while it can be found that a row is removed. If you want to know the removed row, as a workaround, it is required to compare the old sheet and the new sheet before and after the edit. I'm worried that in this case, the script might be complicated. So, in this answer, I proposed a simple modification.
Related
I want to create a script that will display the cell address of the active cell in A1, however, this is proving difficult because I can't even get the onSelectionChange example on the Google Apps Script guide to work. Whenever I run it, the error I get is TypeError: Cannot read property 'range' of undefined.
This is the code:
/**
* The event handler triggered when the selection changes in the spreadsheet.
* #param {Event} e The onSelectionChange event.
*/
function onSelectionChange(e) {
// Set background to red if a single empty cell is selected.
var range = e.range;
if(range.getNumRows() === 1
&& range.getNumColumns() === 1
&& range.getCell(1, 1).getValue() === "") {
range.setBackground("red");
}
}
About your error message of TypeError: Cannot read property 'range' of undefined, I thought that you might have directly run the function of onSelectionChange with the script editor. If it's so, such error occurs because the event object e is not given.
onSelectionChange is used as the simple trigger. In this case, when you want to retrieve the cell address of the active cell with onSelectionChange trigger, please select the cell on Google Spreadsheet. By this, the script is run by the onSelectionChange trigger.
Sample script:
For example, when the onSelectionChange trigger is run by selecting a cell, the sample script that the cell address is put to the cell as the A1Notation is as follows. When you use this script, please copy and paste the following script to the script editor of Spreadsheet and save it. And, please select a cell on the active Spreadsheet.
function onSelectionChange(e) {
var range = e.range;
range.setValue(range.getA1Notation());
}
Result:
When above script is used, the following result is obtained. You can see that in order to run the script, the cell is selected.
Note:
Even when you selected a cell, when the sample script is not run, please close the Spreadsheet and open the Spreadsheet again. In my environment, I confirmed that by this, the onSelectionChange trigger worked.
Reference:
onSelectionChange(e)
Added 1:
About the following your replying,
Thanks. This works and so did the red cell Google example. Although I did have to close the workbook and reopen it to get it to work. How can I set the range to a fixed cell so the active cell address only appears there?
When you want to run the script for only the specific range, how about the following sample script? From your replying, I used your script for this.
Sample script:
function onSelectionChange(e) {
var ranges = ["A2:B5", "D2:E5"]; // Please set the range you want to run the script.
var range = e.range;
var sheet = range.getSheet();
var check = ranges.some(r => {
var rng = sheet.getRange(r);
var rowStart = rng.getRow();
var rowEnd = rowStart + rng.getNumRows();
var colStart = rng.getColumn();
var colEnd = colStart + rng.getNumColumns();
return (range.rowStart >= rowStart && range.rowEnd < rowEnd && range.columnStart >= colStart && range.columnEnd < colEnd);
});
if (check) {
range.setBackground("red");
}
}
In this script, only when the cell in the range of var ranges = ["A2:B5", "D2:E5"] is selected, range.setBackground("red") is run. So please modify ["A2:B5", "D2:E5"] for your actual situation.
If you want to also check the sheet name, please modify above script as follows. In this case, when the cell in the ranges ["A2:B5", "D2:E5"] of the sheet ["Sheet2", "Sheet3"] is selected, range.setBackground("red") is run.
function onSelectionChange(e) {
var sheetNames = ["Sheet2", "Sheet3"]; // Please set the sheet names.
var ranges = ["A2:B5", "D2:E5"]; // Please set the range you want to run the script.
var range = e.range;
var sheet = range.getSheet();
if (!sheetNames.includes(sheet.getSheetName())) return;
var check = ranges.some(r => {
var rng = sheet.getRange(r);
var rowStart = rng.getRow();
var rowEnd = rowStart + rng.getNumRows();
var colStart = rng.getColumn();
var colEnd = colStart + rng.getNumColumns();
return (range.rowStart >= rowStart && range.rowEnd < rowEnd && range.columnStart >= colStart && range.columnEnd < colEnd);
});
if (check) {
range.setBackground("red");
}
}
Added 2:
About the following your replying,
No, sorry. I meant how can I make the active cell address appear in A1. If I click on cell G5, for example, 'G5' appears in A1.
In that case, how about the following sample script?
Sample script:
function onSelectionChange(e) {
var range = e.range;
range.getSheet().getRange("A1").setValue(range.getA1Notation());
}
In google sheets, I am trying to get one data to copy from one sheet to another.
I have this code which is working however I would like it to run onEdit when changing cell E4 in Googlesheet1. I am new at this and doesn't seem to get it to quite work with the solutions I found online.
function ExportRange() {
var destination = SpreadsheetApp.openById('googlesheet1');
var destinationSheet = destination.getActiveSheet();
var destinationCell = destinationSheet.getRange("AC3");
var cellData = '=IMPORTRANGE("https://docs.google.com/spreadsheets/googlesheet2", "AE10:AE9697")';
destinationCell.setValue(cellData);
}
Chose between a simple and installable onEdit trigger, depending on your requirements
For most applciaitons a simple onEdit trigger is sufficient, to use it you just need to rename your function ExportRange() to onEdit()
Take advantage of event objetcs that give you informaiton about the event that fired the trigger
So, the trigger onEdit can give you among others information about the event range - that is the range that has been edited
Now you can implement an if statement to specify that the rest of the funciton shall only be run if the event range and the corresponding sheet are as required
Sample:
function onEdit(event) {
var range = event.range;
var sheet = range.getSheet();
if(range.getA1Notation() == "E4" && sheet.getName() == "Googlesheet1"){
var destination = SpreadsheetApp.openById('googlesheet1');
var destinationSheet = destination.getActiveSheet();
var destinationCell = destinationSheet.getRange("AC3");
var cellData = '=IMPORTRANGE("https://docs.google.com/spreadsheets/googlesheet2", "AE10:AE9697")';
destinationCell.setValue(cellData);
}
}
Please note that this function can only be fired by the trigger in
case of an edit. If you try to run it manually, it will give you an
error because event (and thus event.range) will be undefined if
the funciton was not called by an edit event.
Please see example picture below.
I would like to use Google Sheets script editor to add a Filter view on cell B3, that filters on a custom formula =(B4=$B$1).
The plan is to add a onEdit function that checks whether the value in cell B1 has changed, and if so reset the filter view in cell B3.
This is breaking my brain! I can't seem to find a way to auto update/refresh the filter once my value in B1 has changed.
My code currently looks like this, but this doesn't re-add the custom formula. It just keeps showing me all the non-empty rows.
function onEdit(e){
var ss = SpreadsheetApp.getActiveSpreadsheet()
var sheetname = "Lactation";
var sheet = ss.getSheetByName(sheetname);
var erow = e.range.getRow();
//Logger.log("DEBUG: row = "+erow);
var ecolumn = e.range.getColumn();
//Logger.log("DEBUG: column = "+ecolumn);
if (erow == 1 & ecolumn == 2 ){
// there is a match so the cell is B2
//Logger.log("DEBUG: the cell is B2");
updatefilter();
}
else
{
// there is no match so the cell is NOT I3
//Logger.log("DEBUG: the cell is not I3");
}
}
function updatefilter() {
var spreadsheet = SpreadsheetApp.getActive();
spreadsheet.getRange('B3').activate();
var criteria = SpreadsheetApp.newFilterCriteria()
.setHiddenValues(['', 'No'])
.build();
spreadsheet.getActiveSheet().getFilter().setColumnFilterCriteria(2, criteria);
};
You want to modify the basic filter of the cells "B3:B" when the cell "B1" on the sheet Lactation is edited.
In this case, you want to use the OnEdit event trigger.
You want to use =(B4=$B$1), which is the custom formula, as the filter condition.
If my understanding is correct, how about this answer? Please think of this as just one of several possible answers.
Modification points:
You can take advantage of the event object as e of onEdit(e).
In order to use the custom function for the basic filter, you can use whenFormulaSatisfied.
Modified script:
In order to run the script, please edit the cell B1 on the sheet Lactation.
function onEdit(e){
var sheetName = "Lactation";
var editCell = "B1";
var range = e.range;
var sheet = range.getSheet();
if (range.getA1Notation() == editCell && sheet.getSheetName() == sheetName) {
updatefilter(sheet);
} else {
// do something
}
}
function updatefilter(sheet) {
sheet.getRange('B3').activate();
var criteria = SpreadsheetApp.newFilterCriteria().whenFormulaSatisfied("=(B4=$B$1)").build();
var filter = sheet.getFilter();
if (!filter) {
filter = sheet.getRange("B3:B").createFilter();
}
filter.setColumnFilterCriteria(2, criteria);
};
In this script, when the cell "B1" on the sheet of Lactation is edited, the function updatefilter is run by the OnEdit event trigger. And when the basic filter is existing, it is updated. When no filter is existing, the filter is set as new filter. The range of the filter is "B3:B". The custom function of =(B4=$B$1) is used as the criteria.
Note:
In above script, the simple trigger can be used. But if you use the methods for requiring to authorize, when an error occurs, please try to use Installable Triggers.
References:
Simple Triggers
Installable Triggers
Event Objects
whenFormulaSatisfied(formula)
If I misunderstood your question and this was not the direction you want, I apologize.
In google apps script, with onEdit trigger, how do I get the old values of all the cells if the edited range has multiple cells? In the Event object documentation https://developers.google.com/apps-script/guides/triggers/events it is specified that the oldValue attribute is "Only available if the edited range is a single cell".
In my case, I use onEdit from the Event object to run a function (that needs oldValue and newValue) only when a specific column is edited. It works fine when the user selects only one cell in my specific column, but if the user selects a few cells or the entire row for example, only data from the first selected cell is retrieved but I need to access the oldValue of my specific column.
You want to retrieve old values when the multiple cells are edited.
If my understanding is correct, how about this answer?
Issue:
Unfortunately, in the current stage, when the multiple cells are edited, there are no methods for retrieving all old values from the event object.
Workaround:
So as the current workaround, how about this flow?
Copy the active Spreadsheet.
This is run only one time.
When the cells are edited, the old values are retrieved by comparing the active Spreadsheet and copied Spreadsheet.
Update the copied Spreadsheet.
By above flow, the cycle for retrieving old values when the cells are edited can be created. When this flow is reflected to the script, it becomes as follows. Please think of this as just one of several answers.
Sample script:
When you use this script, please install the OnEdit event trigger to the function of onEditByTrigger() after copy and paste this script to the script editor of the container-bound script. By this, when the cells are edited, you can see the current and old values at the log.
var backupfilename = "backupfile";
function copyToo(srcrange, dstrange) {
var dstSS = dstrange.getSheet().getParent();
var copiedsheet = srcrange.getSheet().copyTo(dstSS);
copiedsheet.getRange(srcrange.getA1Notation()).copyTo(dstrange);
dstSS.deleteSheet(copiedsheet);
}
// This is run only one time.
function init() {
// Source
var srcss = SpreadsheetApp.getActiveSheet();
var range = srcss.getDataRange().getA1Notation();
var srcrange = srcss.getRange(range);
var srcsheetname = srcss.getName();
// Destination
var backupfile = DriveApp.getFilesByName(backupfilename);
var dstid = backupfile.hasNext()
? backupfile.next().getId()
: SpreadsheetApp.create(backupfilename).getId();
var dstss = SpreadsheetApp.openById(dstid).getSheets()[0]
var dstrange = dstss.getRange(range);
dstss.setName(srcsheetname);
copyToo(srcrange, dstrange);
PropertiesService.getScriptProperties().setProperty('backupfileid', dstid);
return dstid;
}
function onEditByTrigger(e) {
var columnNumber = 1; // If you want to retrieve the old values when the column "A" is edited, it's 1.
var source = e.source;
var range = e.range;
var dstid = PropertiesService.getScriptProperties().getProperty('backupfileid');
if (!dstid) {
dstid = init();
}
if (e.range.columnStart == columnNumber) {
var range = source.getSheetName() + "!" + range.getA1Notation();
var fields = "sheets(data(rowData(values(formattedValue,userEnteredFormat,userEnteredValue))))";
var currentValue = source.getRange(range).getValues();
var oldValue = SpreadsheetApp.openById(dstid).getRange(range).getValues();
Logger.log("currentValue %s", currentValue)
Logger.log("oldValue %s", oldValue)
}
// Update backup file
var range = e.source.getDataRange().getA1Notation();
var srcrange = e.source.getRange(range);
var dstrange = SpreadsheetApp.openById(dstid).getSheets()[0].getRange(range);
copyToo(srcrange, dstrange);
}
Note:
This is a sample script for retrieving the old values when the multiple cells were edited. So if you use this script, please modify this to your situation.
References:
Event Objects
Installable Triggers
If this was not the direction you want, I apologize.
I have a single IMPORTDATA formula in Cell A1 of a Google Sheet named "test-r" that I would like to be able to update the cell on a specific interval -- anywhere from seconds to hours.
After a good amount of research, I landed upon this suggestion in a previous post, but I am not having much success with it.
Here is how I modified the script in that post for my IMPORTDATA formula in Cell A1 of my sheet/tab name of "test-r"
function forceEval(sheetName, Row, Col){
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName("test-r");
var orig = sheet.getRange(1,1).getFormula();
var temp = orig.replace("=", "?");
sheet.getRange(row,col).setFormula(temp);
SpreadsheetApp.flush();
sheet.getRange(row,col).setFormula(orig);
}
function onEdit(e){
forceEval("test-r", 1, 1)
}
This has got to be 'operator error' on my part and I am new to this.
At the same time, I don't know if there is a more simplistic script for accomplishing my goal.
Any assistance would be appreciated.
You want to update the function of cell "A1" every specific interval time.
If my understanding is correct, how about this modification?
Modification points:
In your script, OnEdit event trigger is used. In this case, when the cell is edited, the trigger is fired.
I think that this might not be suitable for your situation.
How about using the time-driven trigger?
There are some misspellings in your script. By this, variables are not used, and several error occur.
When above points are reflected to your script, it becomes as follows.
Modified script 1:
function forceEval(){ // Modified
var sheetName = "test-r";
var cell = "A1";
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName(sheetName); // Modified
var orig = sheet.getRange(cell).getFormula(); // Modified
var temp = orig.replace("=", "?");
sheet.getRange(cell).setFormula(temp); // Modified
SpreadsheetApp.flush();
sheet.getRange(cell).setFormula(orig); // Modified
}
After you modified the script, in order to execute the function of forceEval() every specific interval time, please install the time-driven trigger.
In this script, #NAME? is displayed for an instant. If you don't want to do this, how about the following sample script?
Modified script 2:
function forceEval(){
var sheetName = "test-r";
var cell = "A1";
var ss = SpreadsheetApp.getActiveSpreadsheet();
var range = ss.getSheetByName(sheetName).getRange(cell);
var formula = range.getFormula();
range.clearContent();
SpreadsheetApp.flush();
range.setFormula(formula);
}
Install time-driven trigger:
In order to install the time-driven trigger, you can manually install it.
Also you can install it using the script. The following script installs the time-driven trigger for the function of forceEval. In this sample script, when setTrigger() is run, the function of forceEval() is run every 10 minutes. By this, the formula of the cell "A1" is updated every 10 minutes.
function setTrigger() {
var minutes = 10; // In this case, as a sample value, it's 10 minutes.
var functionName = "forceEval";
var triggers = ScriptApp.getProjectTriggers();
for (var i = 0; i < triggers.length; i++) {
if (triggers[i].getHandlerFunction() == functionName && triggers[i].getTriggerSource() == ScriptApp.TriggerSource.CLOCK) {
ScriptApp.deleteTrigger(triggers[i]);
}
}
ScriptApp.newTrigger(functionName).timeBased().everyMinutes(minutes).create();
}
Reference:
Time-driven triggers
If I misunderstood your question and this was not the direction you want, I apologize.
function Worksheet() {
var sheetName =
SpreadsheetApp.getActiveSpreadsheet().getSheetByName("WORKING SHEET");
var queryString = Math.random();
var cellFunction '={IMPORTRANGE("15MDYUIVjVcEs_7XI0Cbfo0FleO_Lvmbav_oybFgPUI8","WORKING SHEET!A2:Y10000");IMPORTRANGE("15MDYUIVjVcEs_7XI0Cbfo0FleO_Lvmbav_oybFgPUI8","WORKING SHEET!A10001:Y12000")}';//IMPORTRANGE FORMULA//
sheetName.getRange('A2').setValue(cellFunction);
};