onEdit function update undo history - google-apps-script

I need to store the last modification user and date of a sheet in developer metadata.
I use the onEdit function to update these metadata at each edit of a sheet (please see the code below)
My problem is the metadata creation/update is taken into account in the undo history.
So users need to ctrl+Z three times for one cancel...
Do you know a way to avoid this behavior?
function onEdit(e) {
// Prevent errors if no object is passed.
if (!e) return;
var dateMetadata;
var dateKey = 'lastModificationDate';
var userMetadata;
var userKey = 'lastModificationUser';
// Get the active sheet.
var sheet = e.source.getActiveSheet();
var user = e.user.getEmail();
var date = Math.floor(Date.now() / 1000);
metadatas = sheet.getDeveloperMetadata();
for each (var m in metadatas) {
if (m.getKey() === dateKey) {
dateMetadata = m;
}
if (m.getKey() === userKey) {
userMetadata = m;
}
}
if (dateMetadata) {
dateMetadata.setValue(date);
} else {
sheet.addDeveloperMetadata(dateKey, date);
}
if (userMetadata) {
userMetadata.setValue(user);
} else {
sheet.addDeveloperMetadata(userKey, user);
}
}

After the script was run, when one time "ctrl+Z" is pushed, you want to undo the put value.
In your current script, after a value is put to a cell, the undo can be done by pushing 3 times "ctrl+Z".
If my understanding is correct, how about this answer? Please think of this as just one of several possible answers.
Modification points:
When the Spreadsheet is opened from outside and modify the Spreadsheet with the script, even when "ctrl+Z" is pushed, the undo cannot be done. In your case, I thought that this can be used.
Use var sheet = SpreadsheetApp.openById(id).getSheetByName(sheetName) instead of var sheet = e.source.getActiveSheet().
Modified script:
When your script is modified, it become as follows.
From:
function onEdit(e) {
To:
function installedOnEdit(e) {
In this case, please install the installable OnEdit event trigger for the function of installedOnEdit. By this, openById can be used.
And also, please modify as follows.
From:
var sheet = e.source.getActiveSheet();
To:
var id = e.source.getId();
var sheetName = e.source.getActiveSheet().getSheetName();
var sheet = SpreadsheetApp.openById(id).getSheetByName(sheetName);
Note:
When the installable OnEdit event trigger is used, the double run of functions can be prevented by changing the function name from onEdit to installedOnEdit. Ref
In this case, unfortunately, it cannot confirm using the event object whether "ctrl+Z" was pushed. By this, the added developer metadata cannot be canceled with "ctrl+Z". Please be careful this.
As another method, of course, I think that you can also use Sheets API in your situation.
References:
Simple Triggers
Installable Triggers
Asynchronous Processing using Event Triggers
openById(id)
getSheetByName(name)
If I misunderstood your question and this was not the direction you want, I apologize.

Related

How to prevent any editor from adding new sheet in google sheets workbook

How can I prevent any anonymous editor from adding a new sheet inside the workbook. I wish to allow them to edit just one single sheet, but some editors mistakenly mess up the workbook by inserting unwanted sheets. thanks in advance. i tried the script in this link below but it does not seem to work.
Prevent users from creating new sheets in a shared google spreadsheet
// Deletes any tab named "SheetN" where N is a number
function DeleteNewSheets() {
var newSheetName = /^Sheet[\d]+$/
var ssdoc = SpreadsheetApp.getActiveSpreadsheet();
var sheets = ssdoc.getSheets();
// is the change made by the owner ?
if (Session.getActiveUser().getEmail() == ssdoc.getOwner().getEmail()) {
return;
}
// if not the owner, delete all unauthorised sheets
for (var i = 0; i < sheets.length; i++) {
if (newSheetName.test(sheets[i].getName())) {
ssdoc.deleteSheet(sheets[i])
}
}
}
I created this spreadsheet https://docs.google.com/spreadsheets/d/1EdF8I0tyfQagfw1cxHCWgsr2umXi6-vbu7GvM8SJQSM/edit#gid=0 with the code above and set the triggers, but anyone could still create a new sheet.
I believe your current situation and goal are as follows.
There is a sheet in Google Spreadsheet.
You don't want to make the anonymous users insert a new sheet.
Issue and workaround:
In the current stage, unfortunately, there is no method for clearly detecting the edit of an anonymous user. In your script, I thought that the script can be used for the special permitted users. In this case, the anonymous users cannot be included. I thought that this might be the reason for your issue. So in order to achieve your goal, it is required to prepare a workaround. In this answer, I would like to propose a workaround.
When the sheet insert is detected, OnChange trigger can be used. But, the identification of the user is a bit difficult. When a function installedOnChange(e) is executed by the OnChange trigger, the following condition can be obtained. And, in order to check the active user, Session.getActiveUser().getEmail() and Session.getEffectiveUser().getEmail() are used.
When the owner inserts a new sheet,
e of installedOnChange(e) has the property of "user":{"email":"### owner's email ###","nickname":"### owner name ###"}.
Session.getActiveUser().getEmail() and Session.getEffectiveUser().getEmail() return the owner's email.
When the special permitted user inserts a new sheet,
e of installedOnChange(e) has the property of "user":{"email":"","nickname":""}. In this case, no email address and no name are returned.
Session.getActiveUser().getEmail() returns empty, and Session.getEffectiveUser().getEmail() returns the owner's email.
When the anonymous user inserts a new sheet,
e of installedOnChange(e) has the property of "user":{"email":"### owner's email ###","nickname":"### owner name ###"}.
Session.getActiveUser().getEmail() and Session.getEffectiveUser().getEmail() return the owner's email.
In this case, the situation is the same with the owner. But, in order to identify this, here, the simple trigger is used. Because the anonymous user cannot use the simple trigger. For example, when the anonymous user edits a cell, the simple trigger is not run. This situation is used.
When these conditions are reflected in a sample script, it becomes as follows.
Usage:
1. Prepare sample script.
Please copy and paste the following script to the script editor of Spreadsheet. And, please directly run onOpen function with the script editor. By this, the initial sheet name is stored in PropertiesService. And also, when the Spreadsheet is opened, onOpen is run. By this, the initial condition can be saved.
function onOpen() {
PropertiesService.getScriptProperties().setProperty("sheetName", JSON.stringify(SpreadsheetApp.getActiveSpreadsheet().getSheets().map(s => s.getSheetName())));
}
function onSelectionChange(e) {
CacheService.getScriptCache().put("simpleTrigger", JSON.stringify(e), 30);
}
function deleteSheet(e) {
if (e.changeType != "INSERT_GRID") return;
const sheetNames = JSON.parse(PropertiesService.getScriptProperties().getProperty("sheetName"));
e.source.getSheets().forEach(s => {
if (!sheetNames.includes(s.getSheetName())) e.source.deleteSheet(s);
});
}
function installedOnChange(e) {
const lock = LockService.getDocumentLock();
if (lock.tryLock(350000)) {
try {
Utilities.sleep(3000); // Please increase this wait time when the identification is not correct.
const c = CacheService.getScriptCache();
const simpleTrigger = c.get("simpleTrigger");
const activeUser = Session.getActiveUser().getEmail();
const effectiveUser = Session.getEffectiveUser().getEmail();
if (activeUser && effectiveUser && simpleTrigger) {
// Operation by owner.
// do something.
PropertiesService.getScriptProperties().setProperty("sheetName", JSON.stringify(SpreadsheetApp.getActiveSpreadsheet().getSheets().map(s => s.getSheetName())));
} else if (!activeUser && effectiveUser && simpleTrigger) {
// Operation by permitted user.
// do something.
deleteSheet(e); // If you want to make the permitted user not insert new sheet, please use this line.
} else {
// Operation by anonymous user.
// do something.
deleteSheet(e);
}
c.remove("simpleTrigger");
} catch (e) {
throw new Error(JSON.stringify(e));
} finally {
lock.releaseLock();
}
} else {
throw new Error("timeout");
}
}
In this sample script, the owner, the special permitted user, and the anonymous user are identified. And, the owner can insert a new sheet. But, when the special permitted user and the anonymous user insert a new sheet, the inserted sheet is deleted. By this, as a workaround, your goal might be able to be achieved.
In this script, onSelectionChange is used as a simple trigger for detecting the anonymous user.
3. Install OnChange trigger as an installable trigger.
Please install a trigger to the function installedOnChange as the OnChange installable trigger. Ref
3. Testing.
In order to test this script, please insert a new sheet by the special permitted user and the anonymous users. By this, the inserted sheet is deleted. And, when the owner inserts a new sheet, the inserted sheet is not deleted.
Note:
In this sample script, in order to check whether the simple trigger is executed, Utilities.sleep(3000) is used. So, the time for identifying the user is a bit long. I thought that this might be a limitation.
For example, if you are not required to identify the users who insert a new sheet, you can also use the following simple script. Before you use this script, please install OnChange trigger to installedOnChange and run onOpen with the script editor. In this sample script, all users cannot insert a new sheet.
function onOpen() {
PropertiesService.getScriptProperties().setProperty("sheetName", JSON.stringify(SpreadsheetApp.getActiveSpreadsheet().getSheets().map(s => s.getSheetName())));
}
function installedOnChange(e) {
const lock = LockService.getDocumentLock();
if (lock.tryLock(350000)) {
try {
if (e.changeType != "INSERT_GRID") return;
const sheetNames = JSON.parse(PropertiesService.getScriptProperties().getProperty("sheetName"));
e.source.getSheets().forEach(s => {
if (!sheetNames.includes(s.getSheetName())) e.source.deleteSheet(s);
});
} catch (e) {
throw new Error(JSON.stringify(e));
} finally {
lock.releaseLock();
}
} else {
throw new Error("timeout");
}
}
References:
Installable Triggers
Simple Triggers
getActiveUser()
getEffectiveUser()

Google Apps Script takes very long (or doesn't run at all)

I have a simple script that checks the cell contents for how many dashes it contains and changes the cell colour accordingly. The function trigger is set to onEdit().
For some reason I don't know, this script takes at least a few seconds to run, if it even runs at all.
I would like to ask for help to explain why this takes very long to execute.
Thank you in advance.
function checkSix() {
var sheet = SpreadsheetApp.getActiveSheet();
var currentCell = sheet.getCurrentCell();
var contents = currentCell.getValue();
var amt = contents.replace(/[^-]/g, "").length;
if(amt >= 6)
currentCell.setBackground('red');
else if(amt == 5)
currentCell.setBackground('yellow');
else
currentCell.setBackground('white');
}
I thought that your script is correct. When I tested it, the process time was about 1 second. Although I'm not sure whether this is the direct solution of your issue, can you test the following modification?
Modification points:
In your script, the if statement of the multiple conditional branches is used.
From the benchmark, when the ternary operator is used, a process cost can be reduced a little.
Your function of checkSix() is executed by OnEdit trigger of onEdit.
From the benchmark, when the event object is used, a process cost can be reduced a little.
From or doesn't run at all, I thought that several users might use Google Spreadsheet.
In this case, I would like to propose to use LockService.
When above points are reflected to your script, it becomes as follows.
Modified script:
From your question, it supposes that in your situation, checkSix is called from onEdit as follows. Please be careful this. When the lock service is not used, the script becomes as follows.
function onEdit(e) {
checkSix(e);
}
function checkSix(e) {
var currentCell = e.range;
var amt = e.range.getValue().replace(/[^-]/g, "").length;
currentCell.setBackground(amt >= 6 ? 'red' : amt == 5 ? 'yellow' : 'white');
}
When the lock service is used, the script becomes as follows.
function onEdit(e) {
checkSix(e);
}
function checkSix(e) {
var lock = LockService.getDocumentLock();
if (lock.tryLock(10000)) {
try {
var currentCell = e.range;
var amt = e.range.getValue().replace(/[^-]/g, "").length;
currentCell.setBackground(amt >= 6 ? 'red' : amt == 5 ? 'yellow' : 'white');
} catch(er) {
throw new Error(er);
} finally {
lock.releaseLock();
}
}
}
Note:
In this answer, it supposes that your script is only the script shown in your question. If you run other functions when onEdit is run, my proposal might not be useful. Please be careful this.
If above modification was not the direct solution of your issue, as a test case, I would like to propose to try to create new Spreadsheet and test it again.
References:
Benchmark: Conditional Branch using Google Apps Script
Benchmark: Event Objects for Google Apps Script
Event Objects
function checkSix(e) {
const sh = e.range.getSheet();
if(sh.getName() == 'Your Sheet Name' && e.value ) {
let contents = e.value;
let l = contents.replace(/[^-]/g,'').length;
if(l>=6) {
e.range.setBackground('red');
}else if(l==5) {
e.range.setBackground('yellow');
} else {
e.range.setBackground('while');
}
}
Since you changed the name you will require an installable onEdit trigger. Or call it from onEdit(e) function.

onEdit specific cell copy data from one google sheets to another

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.

Is there an event that triggers when changing permission for a sheet in Google Sheets?

onEdit and onChanged does not seem to trigger when removing or giving permission to a user.
I have not seen any event related to that in the documentation.
If there is no such events, is there a way to programatically track permission changes?
Edit: I am sorry if the question was not clear but I meant sheet protection permissions for individual sheets inside a workbook instead of Drive sharing permission for the workbook.
If I understand you correctly, you want to keep track of changes in Sheet protections. As you said, there are no Apps Script triggers that can track this.
Workaround (time-based trigger and Properties Service):
As a workaround, I'd propose doing the following:
Create a time-based trigger that will fire a function periodically (every 1 minute, for example). You can do this either manually, or programmatically, by running this function once:
function createTrigger() {
ScriptApp.newTrigger("trackProtections")
.timeBased()
.everyMinutes(1)
.create();
}
This will fire the function trackProtections every minute. This function's purpose is to track changes to the sheet protections since last time it was fired (in this example, 1 minute ago).
In the triggered function, retrieve the current sheet protections, store them in script properties, and compare the previously store protections to the current ones (check editors, etc.). You could use JSON.stringify() and JSON.parse() to be able to store these protections to script properties (which only accept strings) and convert them back. It could be something along the following lines (check inline comments):
function trackProtections() {
var scriptProperties = PropertiesService.getScriptProperties(); // Get script properties (old protections)
var ss = SpreadsheetApp.getActive();
var sheets = ss.getSheets(); // Get all sheets in spreadsheet
sheets.forEach(function(sheet) { // Iterate through each sheet in the spreadsheet
var sheetName = sheet.getName();
// Get sheet current protection (null if it's unprotected):
var protection = sheet.getProtections(SpreadsheetApp.ProtectionType.SHEET)[0];
// Get previous editors of current sheet (stored in script properties):
var oldEditors;
if (scriptProperties.getProperty(sheetName) !== null) {
oldEditors = scriptProperties.getProperty(sheetName).split(',');
}
// Get current editors of current sheet:
var newEditors;
if (protection) {
newEditors = protection.getEditors().map(function(editor) {
return editor.getEmail();
});
}
if (oldEditors && !newEditors) { // Protection for the sheet was removed
scriptProperties.deleteProperty(sheetName); // Remove old property (protection doesn't exist anymore)
Logger.log('protection for ' + sheetName + ' was removed!');
} else if (!oldEditors && !newEditors) { // There were and there aren't any protections for the sheet
Logger.log('there are no protections');
} else if (!oldEditors && newEditors) { // Protection for the sheet was added
Logger.log('protection for ' + sheetName + ' was added!');
scriptProperties.setProperty(sheetName, newEditors.toString()); // Add script property with current editors
} else {
if (newEditors.sort().join(',') !== oldEditors.sort().join(',')) { // Check if old and current editors are the same
var addedEditors = newEditors.filter(function(editor) {
return oldEditors.indexOf(editor) === -1; // Return editors that are in current protection but not in old one (added)
});
var removedEditors = oldEditors.filter(function(editor) {
return newEditors.indexOf(editor) === -1; // Return editors that were in old protection but not in current one (removed)
});
Logger.log('protection for ' + sheetName + ' was modified!');
Logger.log('these editors were added: ' + JSON.stringify(addedEditors));
Logger.log('these editors were removed: ' + JSON.stringify(removedEditors));
scriptProperties.setProperty(sheetName, newEditors.toString()); // Add script property with current editors
}
}
});
}
This function iterates through all sheets in the spreadsheet, and looks for changes in the protection since last execution (whether the sheet became protected, or unprotected, or whether the editors changed).
In this sample, the protections are getting stored in script properties the following way: each property has the name of the sheet as the key and the emails of the different editors of the sheet as value (comma-separated). The script is using toString and split to store the array of editor emails in the property, because script properties only accept strings as value.
You could also keep track of other protection settings that change (description, etc.) which would make the script a bit more complex (and you'd probably have to use JSON.stringify and JSON.parse, but I hope this simplified example will be enough for you to understand the workflow.
The function should also check if there is a change between the current protections and the ones that were previously stored, and do whatever you had planned to do when sheet protections changed. Since I don't know what you wanted to do in this situation, I'm just using Logger.log to log the changes that happened to the protection since last execution.
Reference:
Class Protection
Class ClockTriggerBuilder
Class PropertiesService

Function to get an email notification if a cell is changed to include today's date?

Looking to get an email notification if any cell in a google sheet is changed to include today's date, in EITHER FORMAT such as 6/7 or 6/7/16 .
New to this, and I searched heavily but am confused on editing functions.
Also for this, should I use onEdit as the trigger?
Any help is greatly appreciated<3
As you are going to use a MailApp class (or GmailApp class), you have to use an installable trigger, because normal spreadsheet triggers (onEdit, onChange) do not work with classes that need authentication.
With installable onChange or onEdit trigger there's no easy way to know which cell was changed. You can't just do:
function myOnChange(event){
event.range; // To get the modified range.
}
That's a limitation. (simple triggers, installable triggers)
I have a quite similar problem, but the cells I'm monitoring are in a specific column. I look though the column to see if I find the content I want and then I call other functions if that's positive.
Take a look at this code and adapt this to your case. Its pretty close!
function onEditTrigger(){
if(todaysDateIsOnTargetColumn()){
sendMail('recipient#example.com', 'Subject', 'replyto#example.com', '<p>text</p>' );
}
}
function todaysDateIsOnTargetColumn(){
var range = getSheetByName('Your Sheet Name').getRange('A1:A'); // in fact, here you can use the range you want!
var todaysFormattedDate = Utilities.formatDate(Date.new(), "GMT", "dd/MM/yy");
return foundMatchingValueOnRange(range, todaysFormattedDate); // This function will see if the value (today's date) is inside the range.
}
function sendEmail(recipientEmail, emailSubject, emailReplyTo, emailHtmlBody) {
MailApp.sendEmail({
to: recipientEmail,
subject: emailSubject,
replyTo: emailReplyTo,
htmlBody: emailHtmlBody
});
}
// Utils (these can be useful in other projects)
function foundMatchingValueOnRange(range, value){
var foundMatchingValue = false;
var uniqColumnValues = flatten(range.getValues());
var valueIndex = uniqColumnValues.indexOf(value)
if(valueIndex >= 0){
foundMatchingValue = true
}
return foundMatchingValue;
}
function flatten(matrix){
return [].concat.apply([], matrix);
}
function getSheetByName(name){
var thisSpreadsheet = SpreadsheetApp.getActiveSpreadsheet();
var sheet = thisSpreadsheet.getSheetByName(name);
return sheet;
}
Then, after that, you have to install the trigger on the sheet:
On the Script edition page, got to Resources > Current Project's triggers and do:
I know that's not exactly what you're looking for, but google spreadsheets have some restrictions when it comes to triggers and permissions working together!
Hope that helps!