Let's say certain cells will be protected if it matches certain condition
function A() {
var sh = SpreadsheetApp.getActiveSpreadsheet();
var ss = sh.getActiveSheet();
var protection;
for(var i = 3;i<ss.getLastRow();i=i+1){
if(ss.getRange(i,4).getValue()==7){ // check if the condition is matched
for(var j=5; j<=20; j=j+2){
if(ss.getRange(i,j).isBlank()){
protection = ss.getRange(i,j).protect(); // protect that certain cell
protection.removeEditors(protection.getEditors());
if (protection.canDomainEdit())
protection.setDomainEdit(false);
}
}
}
}
Now I want to delete the protection I made on that cell if the condition changed, let's say the upcoming code will be:
if(ss.getRange(i,4).getValue()<=6){
...
How can I only delete the protection applied to that cell instead of all the protection in the entire sheet?
Thank you
I tried
getProtections().remove()
but it does not target certain cells right?
I believe your goal is as follows.
You want to unprotect the cells when the value of column "D" is less than 6.
About Now I want to delete the protection I made on that cell if the condition changed, when I saw your showing script for protecting the cells, it seems that when the if-statement of if(ss.getRange(i,4).getValue()==7){} is true, the empty cells of the row are protected. From this situation, in your goal, when the if statement of if(ss.getRange(i,4).getValue()<=6){} is true, you want to remove all protected cells of the row.
If my understanding is correct, how about the following sample script? In this case, the protected ranges can be retrieved with getProtections(SpreadsheetApp.ProtectionType.RANGE).
Sample script 1:
In this sample, when this script is run, the protected cells are unprotected by searching the values of column "D".
function myFunction() {
var sh = SpreadsheetApp.getActiveSpreadsheet();
var ss = sh.getActiveSheet();
var protects = ss.getProtections(SpreadsheetApp.ProtectionType.RANGE).reduce((o, p) => {
var row = p.getRange().getRow();
o[row] = o[row] ? [...o[row], p] : [p];
return o;
}, {});
var values = ss.getRange(3, 4, ss.getLastRow() - 2, 1).getValues();
values.forEach(([d], i) => {
if (d <= 6) {
var obj = protects[i + 3];
if (obj) {
obj.forEach(p => p.remove());
}
}
});
}
Sample script 2:
In this sample, when the cells of column "D" is edited, the script is run by the installable OnEdit trigger. So, please install the OnEdit trigger to the function installedOnEdit. When you use this script, please edit the cells of column "D". By this, the script is run. When this script is directly run by the script editor, an error occurs. Please be careful about this.
function installedOnEdit(e) {
var sheetName = "Sheet1"; // Please set your sheet name.
var { range } = e;
var sheet = range.getSheet();
if (sheet.getSheetName() != sheetName || range.columnStart != 4 || range.rowStart < 3 || range.getValue() > 6) return;
var protects = sheet.getProtections(SpreadsheetApp.ProtectionType.RANGE).reduce((o, p) => {
var row = p.getRange().getRow();
o[row] = o[row] ? [...o[row], p] : [p];
return o;
}, {});
var obj = protects[range.rowStart];
if (obj) {
obj.forEach(p => p.remove());
}
}
Note:
When I saw your script A(), I thought that when the script is modified, the process cost might be able to be reduced a little. So, how about the following modification?
function A() {
var sh = SpreadsheetApp.getActiveSpreadsheet();
var ss = sh.getActiveSheet();
var values = ss.getRange(3, 1, ss.getLastRow() - 2, 20).getValues();
values.forEach((r, i) => {
if (r[3] == 7) {
for (var j = 4; j < 20; j = j + 2) {
if (r[j].toString() == "") {
protection = ss.getRange(i + 3, j + 1).protect();
protection.removeEditors(protection.getEditors());
if (protection.canDomainEdit())
protection.setDomainEdit(false);
}
}
}
});
}
References:
getProtections(type)
reduce()
forEach()
Installable Triggers
Related
The script inserted copies the values in range Sheet1!A5:F5 then transpose pastes into the first row and second column of Sheet2.
The script is able to copy and transpose paste under the last row of the second column on continuous use until it is reset.
.
Current Result Before Paste - Copies Range Sheet1 A5:F5
Current Result After Paste - Pastes to Range 2nd Column in Sheet2
.
.
.
My aim would be:
for the script to work with the checkbox in cell B2.
to create a target range for the values to be pasted to:
Sheet2! B7:B - hopefully still allowing the transpose paste to function through getLastRow()+1.
Expected Result Before Paste - Checkbox = True, Copying Range Sheet1 A5:F5
Expected Result After Paste - Paste to Range Sheet2 B7:B
Have tried inserting:
if (sheet.getSheetName() != "*DuplioMltSlds" || range.getA1Notation() != "D13" || !range.isChecked()) return;
as well as changing the function to onEdit.
Have also tried various var row = ss.getRange("Sheet2!B7:B") options, however, have not been able find a suitable result.
Sample Sheet
Script source and script:
function copyTransposeAndDelete () {
var ss = SpreadsheetApp.getActiveSpreadsheet ();
var source = ss.getRange("Sheet1!A5:F5");
var destSheet = ss.getSheetByName("Sheet2");
var destRange = destSheet.getRange(destSheet.getLastRow()+1, 2, source.getWidth(), source.getHeight());
destRange.setValues(transpose(source.getValues()));
}
function transpose(array) {
return array[0].map(function(row, i) {
return array.map(function(col) {
return col[i];
});
});
}
I believe your goal is as follows.
You want to copy and paste the cells "A5:F5" of "Sheet1" to the cell "B2" of "Sheet2". In this case, you want to copy the values by transposing.
When the values are copied again, you want to paste the transposed values to (the last row + 1) of column "B" of "Sheet2".
You want to run the script when the checkbox of cell "B2" of "Sheet1" is checked.
In this case, how about the following modification?
Modified script:
function onEdit(e) {
// Ref: https://stackoverflow.com/a/44563639
Object.prototype.get1stNonEmptyRowFromBottom = function (columnNumber, offsetRow = 1) {
const search = this.getRange(offsetRow, columnNumber, this.getMaxRows()).createTextFinder(".").useRegularExpression(true).findPrevious();
return search ? search.getRow() : offsetRow;
};
var { source, range } = e;
var sheet = range.getSheet();
if (sheet.getSheetName() != "Sheet1" || range.getA1Notation() != "B2" || !range.isChecked()) return;
var srcRange = sheet.getRange("A5:F5");
var dstSheet = source.getSheetByName("Sheet2")
var row = dstSheet.get1stNonEmptyRowFromBottom(2) + 1;
var dstRange = dstSheet.getRange("B" + (row < 7 ? 7 : row));
srcRange.copyTo(dstRange, SpreadsheetApp.CopyPasteType.PASTE_VALUES, true);
}
In your situation, I thought that copyTo(destination, copyPasteType, transposed) might be suitable. Because, in this method, the values can be copied by transposing.
When the OnEdit trigger is used, you can use the event object. By this, the process cost will become low a little. Ref
Note:
When you use this script, please check the checkbox of "B2" of "Sheet1". By this, the script is run. When you directly run the script with the script editor, an error like TypeError: Cannot destructure property 'source' of 'e' as it is undefined. occurs. Please be careful about this.
References:
copyTo(destination, copyPasteType, transposed)
Event Objects
Added:
If you want to put the values to the column "B" of "Sheet2" even when the column "B" of "Sheet2" has already had the values after row 7, how about the following sample script?
Sample script:
function onEdit(e) {
// Ref: https://stackoverflow.com/a/44563639/7108653
Object.prototype.get1stEmptyRowFromTop = function (columnNumber, offsetRow = 7) {
const range = this.getRange(offsetRow, columnNumber, 2);
const values = range.getDisplayValues();
if (values[0][0] && values[1][0]) {
return range.getNextDataCell(SpreadsheetApp.Direction.DOWN).getRow() + 1;
} else if (values[0][0] && !values[1][0]) {
return offsetRow + 1;
}
return offsetRow;
};
var { source, range } = e;
var sheet = range.getSheet();
if (sheet.getSheetName() != "Sheet1" || range.getA1Notation() != "B2" || !range.isChecked()) return;
var srcRange = sheet.getRange("A5:F5");
var dstSheet = source.getSheetByName("Sheet2")
var row = dstSheet.get1stEmptyRowFromTop(2);
var dstRange = dstSheet.getRange("B" + row);
srcRange.copyTo(dstRange, SpreadsheetApp.CopyPasteType.PASTE_VALUES, true);
}
function myfunk() {
const ss = SpreadsheetApp.getActive();
const sh = ss.getSheetByName("Sheet1")
const vs = sh.getRange("A4:F4").getValues().flat().map(e => [e]);
const dsh = ss.getSheetByName("Sheet2");
dsh.getRange(7,2,vs.length,vs[0].length).setValues(vs)
}
I am trying to protect a range through google apps script in my google sheet. Whenever the data point in column O is checked i.e. value is yes. I would like that row be protected.
Secondly, I have prior protected columns E:H and M:N, which should stay protected.
There are around 1000 rows, 10-15 new rows that need to be protected daily. By the code I have currently written, it removes the protection and then re-adds it, which takes a lot of time. If I remove the part where it removes the rights then it still re-adds the same protection regardless.
Is there anyway to check if the cell is protected, if protected move onto the next row?
Secondly, I'm unable to provide access to the other emails i.e. "add editors"
function removecompleted(){
var ss = SpreadsheetApp.getActiveSpreadsheet()
var purchased = ss.getSheetByName("Purchased Inventory")
last_row_purchased = purchased.getLastRow()
var emails = [
'user#domain.com'
];
var protections = purchased.getProtections(SpreadsheetApp.ProtectionType.RANGE);
for (var i = 0; i < protections.length; i++) {
var protection = protections[i];
if (protection.canEdit()) {
protection.remove();
}
}
for(var i = 1; i < last_row_purchased; i++) {
if(purchased.getRange(i, 15).getValue() == "Yes")
{
target_range = purchased.getRange(i, 1, 1, 15)
protection = target_range.protect().setDescription('Sample protected range');
var me = Session.getEffectiveUser();
protection.addEditor(me);
protection.removeEditors(protection.getEditors())
protection.addEditors(emails);
protection.removeEditors(protection.getEditors());
if (protection.canDomainEdit()) {
protection.setDomainEdit(false);
}
}
}
var range = purchased.getRange('E:H');
var protection = range.protect().setDescription('Always Protected');
var range = purchased.getRange('M:N');
var protection = range.protect().setDescription('Always Protected');
}
You can do this on edit, but rather than checking to see if the cell is protected (which is not that much faster than just setting the protection anyway) you can just check the row of each protected range and remove the protection if its row is equal to the row of the cell that was just unticked:
function onEdit(e) {
// List of sheet names the function should run on
const sheets = ["Sheet1", "Sheet2", "Sheet3", "etc"]
const sheet = e.source.getActiveSheet()
// if current sheet is not in permitted sheets, return
if (!~sheets.indexOf(sheet.getName())) {
return
}
const row = e.range.getRow()
if (e.range.getColumn() === 15) {
const ss = e.source
if (e.value === "TRUE") {
range = sheet.getRange(`${row}:${row}`)
const me = Session.getEffectiveUser().getEmail()
const protection = range.protect()
protection.getEditors().forEach(user => {
protection.removeEditor(user.getEmail())
})
protection.addEditor(me)
}
else {
const protections = sheet.getProtections(SpreadsheetApp.ProtectionType.RANGE)
protections.forEach(function(p) {
if (p.getRange().getRow() === row) {
if (row === 1 && p.getRange().getColumn() !== 1) return
p.remove()
}
})
}
}
}
Code Rundown:
Get the row number of the edited range
Check if the edited cell was column 15
If the value of the cell is "TRUE", protect the row, and remove all editors aside yourself.
If the value of the cell is "FALSE", then get all the sheet's protections, check the value of their row, and if the row matches the edited cell's row, then remove that protection
NB : The final check also has a condition to check if the column is not equal to 1 so to not remove the protections on E:H or M:N.
References:
Class Protection
Class: Sheet
Class: Range
I have two working functions. One of them is myFunction with a trigger. Is protects the row of the cell when any information is entered in this cell in Column4.
function myFunction(e) {
const sheetNames = ['Sheet1', 'Sheet2', 'Sheet3']; // Please set the sheet names you want to run the script.
const range = e.range;
const sheet = range.getSheet();
const value = range.getValue();
const row = range.getRow();
if (!sheetNames.includes(sheet.getSheetName()) || range.getColumn() != 4 || row == 2 || value == "") return;
const p = sheet.getRange(`B${row}:D${row}`).protect();
const owner = Session.getActiveUser().getEmail();
p.getEditors().forEach(f => {
const email = f.getEmail();
if (email != owner) p.removeEditor(email);
});
}
Another function is an onEdit function. It adds date in Column1 when I enter information in Column4. The date appears in the same row with the cell in Column4.
function onEdit() {
var colToCheck = 4;
// Offset from the input [row, column]
var dateOffset = [0, -3];
// Sheets to proceed on
var sheetNames = ['Sheet1', 'Sheet2', 'Sheet3'];
var sheet = SpreadsheetApp.getActive().getActiveSheet();
var name = sheet.getName();
if (sheetNames.indexOf(name) > -1) {
var cell = sheet.getActiveCell();
var col = cell.getColumn();
if (col == colToCheck) {
var dateTimeCell = cell.offset(dateOffset[0], dateOffset[1]);
dateTimeCell.setValue(new Date());
}
}
}
How these two functions can be combined in one sheet?
I just added two separate functions to one sheet: the one with trigger and the one onEdit as two different codes. And they work as I need. So, we do not need to combine them somehow. They just work one after another. First, the one onEdit function works, and as it adds info to the necessary cell, the function with trigger starts working.
I am trying to create a sort function using Google Apps Script. I have about 20 columns in a Google sheet and want users to have the ability to sort the sheet by the click of a button rather than using the filter view because they keep on messing it up.
Rather than having 20 buttons for each column, I want one button with script which links to a dropdown list of Named Ranges being the same as the column headers.
Not sure if this is possible but this is a sample of my sheet:
I am struggling to get this script to work:
function sortByRangeName(rangeName){
var ss = SpreadsheetApp.getActive();
var sheet = ss.getSheetByName('Sheet 1');
var namedRange = ss.getRangeByName();
var startCol = namedRange.getColumn();
var lastCol = namedRange.getLastColumn();
var range = sheet.getRange('E1');
var value = range.getValue();
var rangeName = ss.getRangeByName(value);
var columnForSorting = (startCol <= dataRange.getLastColumn()) ? startCol : null;
if (namedRange && (startCol == lastCol) && columnForSorting) {
dataRange.sort({column: columnForSorting, ascending: false});
}
else {
throw new Error(Utilities.formatString("Range name: %s, startCol: %s, lastCol: %s, columnForSorting: %s", header, startCol, lastCol, columnForSorting));
}
}
This is a link to my spreadsheet:
Sample Spreadsheet
Try an Installable onEdit() with this function:
function sortByColumn(e) {
const sh=e.range.getSheet();
if(sh.getName()=='Sheet 1' && e.range.columnStart==5 && e.range.rowStart==1 && e.value) {
const hA=sh.getRange(2,1,1,sh.getLastColumn()).getValues()[0];
const col={};
hA.forEach(function(h,i){col[h]=i+1;});
const rg=sh.getRange(3,1,sh.getLastRow()-2,sh.getLastColumn());
rg.sort({column:col[e.value],ascending:true});
}
}
I believe your goal as follows.
You want to sort the range of "A3:E" with each column using the named ranges when the dropdown list at the cell "E1" on "Sheet 1" is selected.
Modification points:
In your script,
At var namedRange = ss.getRangeByName(), the argument is not used.
dataRange is not declared.
When above issues are resolved, the script works. But in this case, unfortunately, your goal cannot be achieve.
In order to achieve above goal, in this answer, the OnEdit event trigger is used.
In your dropdown list, it seems that there is the names including the space. Please be careful this. For this, I used trim().
Modified script:
Please copy and paste the following script to the script editor on the Google Spreadsheet. And, please select the dropdown list. By this, the values are sorted using the named ranges with the column selected by the dropdown list.
function onEdit(e) {
var range = e.range;
var sheet = range.getSheet();
if (sheet.getSheetName() != "Sheet 1" || range.getA1Notation() != "E1") return;
sheet.getDataRange().offset(2, 0).sort({
column: e.source.getRangeByName(e.value.trim()).getColumn(),
ascending: false
});
}
Note:
When the issues are removed from your script, it becomes as follows. I thought that knowing the modification points in your script might be help to study the script. So I also added this.
function sortByRangeName(rangeName){
rangeName = "Branch"; // This is a sample value
var ss = SpreadsheetApp.getActive();
var sheet = ss.getSheetByName('Sheet 1');
var namedRange = ss.getRangeByName(rangeName.trim()); // Modified
var startCol = namedRange.getColumn();
var lastCol = namedRange.getLastColumn();
var range = sheet.getRange('E1');
var value = range.getValue();
var rangeName = ss.getRangeByName(value);
var dataRange = sheet.getDataRange().offset(2, 0); // Added
var columnForSorting = (startCol <= dataRange.getLastColumn()) ? startCol : null;
if (namedRange && (startCol == lastCol) && columnForSorting) {
dataRange.sort({column: columnForSorting, ascending: false});
} else {
throw new Error(Utilities.formatString("Range name: %s, startCol: %s, lastCol: %s, columnForSorting: %s", header, startCol, lastCol, columnForSorting));
}
}
References:
Simple Triggers
Event Objects
getRangeByName(name)
trim()
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.