A sheet populated with results of a query, from a separate workbook. User can select values from a drop down for any cell. When onEdit called, value is transferred to the main workbook, which then replicates back to the current cell in the active sheet via the query. Trying to prevent cells with existing values from being edited - ie - user can only put a value in a currently blank cell, and not change an existing. Because the query repopulates with each change in the master, locations of empty cells constantly change as they are filled or if new rows are added in the master.
(code below is a mashup from this video and one similar about unique ID's..(https://www.youtube.com/watch?v=ucLO4iHP2Kw))
Tried reading the current range of data into an array, and chugging through to set protection on any non-null cell. This works, but a 3x60 range of cells takes over 2 minutes to set protection on the non-blanks. Not a very good wait time before the user can move on to edit another cell!
Following is current code triggered on onEdit() The section after the "else" is where I want things to bail out by rejecting the user selection/input.
Any attempt to enter something different or change a value in a non-empty cell results in the query generating an error because it doesn't want to overwrite the cell. Hitting the "Del" key clears the cell, and the query then will refresh everything. Optimally, it would be great if the section after "else" just did the same - hit the "Del" key!
function syncData(e){
var src = e.source.getActiveSheet();
var r = e.range;
if (e.oldValue == null){
r.clear();
let id = src.getRange(r.rowStart,1).getValue();
var db = SpreadsheetApp.openById("SPREADSHEET ID").getSheetByName("SHEET NAME");
var ids = db.getRange("A:A").getValues();
let row = 0;
for (row; row < ids.length; row++){
if (ids[row][0] === id)
break;
}
row++;
db.getRange(row,r.columnStart).setValue(e.value);
}
else{
e.setValue(e.oldValue);
}
}
Of course I came to a solution after posting here!
If the rest worked, all I needed to do was post e.oldValue to the main spreadsheet, rather than e.value. That would repopulate the view sheet with the original value, preventing the user from making a change.
Here's the modified code:
function NewsyncData(e){
var src = e.source.getActiveSheet();
var r = e.range;
r.clear();
let id = src.getRange(r.rowStart,1).getValue();
var db = SpreadsheetApp.openById("SPREADSHEET ID").getSheetByName("SHEET NAMES");
var ids = db.getRange("A:A").getValues();
let row = 0;
for (row; row < ids.length; row++){
if (ids[row][0] === id)
break;
}
row++;
if (e.oldValue == null){
db.getRange(row,r.columnStart).setValue(e.value);
}
else{
db.getRange(row,r.columnStart).setValue(e.oldValue);
}
}
Related
I'm trying to build an automated list with google sheets. The first sheet(A) is for input of production data of a week. The second sheet(B) should be the data archive. Thus i want the content from sheet A copied to sheet B and then deleted in sheet a. It should be copied in the next empty range in sheet B.
My problem must be inside the notation of the "while" or / and the "if" but nothing seems to work properly.
The while checks if sheet A is already emptied, if not the "if" function checks a specific range in sheet B if it is empty. If thats the case it should be copying the data and then delete it. Else the column of the range in sheet b is changed to the next range (spaltennummer + 6).
While troubleshooting it either stays in the while (finds no empty range?) or it runs through without any effect. I tried "== 0", "== """, isblank and so on. (every option available?). Google didnt seem able to provide me an answer...
Thanks for ur help.
Code:
function myFunction() {}
function leerzelle(){
var spaltennummer = 4;
var rangeDatenarchiv = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Tabellenblatt1").getRange(8,spaltennummer,15,5);
var rangeDateneingabe = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Dateneingabe").getRange("J8:N22");
var values = rangeDateneingabe.getValues();
while(rangeDateneingabe !== 0){
if(rangeDatenarchiv == 0) {
rangeDatenarchiv.setValues(values);
rangeDateneingabe.setValues("");
}
else{
spaltennummer = spaltennummer + 6;
}
}
}
Try this:
Code:
function leerzelle() {
var spaltennummer = 4;
// Data entry
var rangeDateneingabe = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Dateneingabe").getRange("J8:N22");
var values = rangeDateneingabe.getValues();
// While we have value in data entry sheet
while (!rangeDateneingabe.isBlank()) {
// Update archive everytime you increment in else
var rangeDatenarchiv = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Tabellenblatt1").getRange(8, spaltennummer, 15, 5);
// If archive range is blank, move values
if (rangeDatenarchiv.isBlank()) {
rangeDatenarchiv.setValues(values);
rangeDateneingabe.clearContent();
}
else {
spaltennummer = spaltennummer + 6;
}
}
}
Note:
You just can't compare a range to a number, use isBlank to check if it doesn't have values instead.
You can't use setValues("") on a range to remove the contents, use clearContent instead to delete those values.
You need to redeclare archive range everytime you loop using the incremented column number
References:
isBlank
clearContent
I'm trying to build a schedule for employees to add appointments to a rep calendar. The goal is once a day has 5 appointments, it gets blacked out, and locked down. I had this working with data validation, but then we decided to add employee names to a dropdown housed in each cell. Since each cell can only have one data validation rule (as far as I can tell) I'm having to use a script to protect/unprotect the cells. I've almost got everything working, but my function seems to only protect the given range. When I added the else clause to unprotect the range given <5 appointments, it runs without failure, but does't actually change anything. (I suspect it protects, then immediately unprotects the range.) Currently, the function only works on Monday (B5:B27).
The code I'm using is:
function onEdit() {
var ss = SpreadsheetApp.getActive();
var maxAppointments = ss.getRange('G3')
if (countMon >= maxAppointments) {
var countMon = ss.getRange('B3').getValue();
var mon = ss.getRange('B5:B27');
var protectMon = mon.protect().setDescription('Protect Monday').setRangeName('monday');
protectMon.removeEditors(protectMon.getEditors());
protectMon.addEditors(['mgr1#domain.com', 'mgr2#domain.com', 'mgr3#domain.com']);
if (protectMon.canDomainEdit()) {
protectMon.setDomainEdit(false);
}
}
else {
var monProtections = ss.getActiveSheet().getProtections(SpreadsheetApp.ProtectionType.RANGE);
for (var i = 0; i < monProtections.length; i++) {
if ( monProtections[i].getRangeName() == 'monday') {
monProtections[i].remove();
}
}
}
};
I also created a simplified example sheet with personal info scrubbed.
You can use this sample code: (this should work for columns Monday-Friday)
function onEdit(e) {
var ss = e.source; // get spreadsheet
var sheet = ss.getActiveSheet();
var cell = e.range; // get edited range
if(sheet.getName() != "Rep1"){
return;
}
var maxAppointments = sheet.getRange('G3').getValue();
var row = cell.getRow();
var col = cell.getColumn();
//Check if edited cell is within B5:F27
if(row>=5 && row<=27 && col>=2 && col<=6){
//Get current date user count
var count = sheet.getRange(3,col).getValue();
if(count>=maxAppointments){
//Select Row 5 to Row 27
var range = sheet.getRange(5,col,23);
// Protect range
var protection = range.protect();
var name = 'Column'+col;
protection.setDescription(name);
Logger.log(protection.getEditors());
protection.removeEditors(protection.getEditors());
var me = Session.getEffectiveUser();
protection.addEditor(me);
Logger.log(protection.getEditors());
if (protection.canDomainEdit()) {
protection.setDomainEdit(false);
}
}else{
var protections = sheet.getProtections(SpreadsheetApp.ProtectionType.RANGE);
Logger.log("Length"+protections.length);
for (var i = 0; i < protections.length; i++) {
var protection = protections[i];
Logger.log(protection.getDescription());
if (protection.getDescription() == 'Column'+col) {
Logger.log("remove");
protection.remove();
}
}
}
}
}
I added some guarding in your current onEdit(), such as modified cell should be in sheet "Rep1", modified cells should be within B5:F27 (done by checking row and column index)
What it does?
Check if modified cell is in Sheet Rep1 and within B5:F27 by getting its sheet name using Sheet.getName(), Row and Column Index using Range.getRow()/Range.getColumn()
If current user count on a specific day is >= the max allowed user count, protect the specific column from row5 to row 27 using Sheet.getRange(row, column, numRows) and Range.protect(). Set the description to "Column"+column index
If current user count a specific day is < the max allowed user count, remove the protection if there is any. Protection to be removed will be based on the protection description set in step 2
Sample Output:
-> This is the view from a non-editor user after protection. Notice that column B and D are not editable (Data validation drop-down list was hidded)
Note:
In the sample code, I just set my primary user as the editor after removing other editors. You can just apply your configuration in your original code where there are multiple editors.
I also did not modify your conditional formatting which sets the cell background to black, maybe you could revisit that since it always change the cell background of columns B-D even though not all columns were locked. See the sample output where columns B and D are locked but not column C
This code works fine when data is edited in Column 3 or being copy-pasted but if the cursor remains at column 1 at the time of the whole row being copy/pasted, it won't update and secondly, if salesforce sends data to column 3, it doesn't work that time too, please help me here.
function onEdit() {
var s = SpreadsheetApp.getActiveSheet();
var sName = s.getName();
var r = s.getActiveCell();
var row = r.getRow();
var ar = s.getActiveRange();
var arRows = ar.getNumRows()
// Logger.log("DEBUG: the active range = "+ar.getA1Notation()+", the number of rows = "+ar.getNumRows());
if( r.getColumn() == 3 && sName == 'Sheet1') { //which column to watch on which sheet
// loop through the number of rows
for (var i = 0;i<arRows;i++){
var rowstamp = row+i;
SpreadsheetApp.getActiveSheet().getRange('F' + rowstamp.toString()).setValue(new Date()).setNumberFormat("MM/dd/yyyy hh:mm"); //which column to put timestamp in
}
}
}//setValue(new Date()).setNumberFormat("MM/dd/yyyy hh:mm:ss");
Explanation:
Three important things to know:
As it is also stated in the official documentation, the onEdit triggers are triggered upon user edits. This function won't be triggered by formula nor another script. If salesforce or any other service except for the user, edits column C the onEdit trigger is not going to be activated. Workarounds exist, but these workarounds depend on the context of your specific problem. I would advice you to search or ask a specific question about it.
Regarding the other issue you have, you should get rid of active ranges and take advantage of the event object. This object contains information regarding the edit/edits user made.
As it is recommended by the Best Practices you should not set values in the sheet iteratively but you can to that in one go by selecting a range of cells and set the values. In your case, you want to set the same value in all of the cells in the desired range, hence setValue is used instead of setValues. But the idea is to get rid of the for loop.
Solution:
function onEdit(e) {
var s = e.source.getActiveSheet();
var sName = s.getName();
var ar = e.range;
var row = ar.getRow();
var arRows = ar.getNumRows()
if( ar.getColumn() == 3 && sName == 'Sheet1') {
s.getRange(row,6,arRows).setValue(new Date()).setNumberFormat("MM/dd/yyyy hh:mm");
}
}
Note:
Again, onEdit is a trigger function. You are not supposed to execute it manually and if you do so you will actually get errors (because of the use of the event object). All you have to do is to save this code snippet to the script editor and then it will be triggered automatically upon edits.
I am stuck with, at first sight, simple script.
I want to clear a content from cell S when T has value "Copied".
What I have at the moment is this:
function onEdit(e) {
if(e.range.columnStart === 20) {
e.range.offset(0,-1).clearContent();
}
}
I am not sure how to include IF. Also, bear in mind that T column has a formula, so I don't edit it manually, and with this script, it doesn't work.
It doesn't have to be OnEdit, I can set a trigger to run the script every minute which is even better, but it is important to filter it by the value Copied.
To explain a bit more how my file works (example):
1) I add a comment in the cell S5.
2) My second script runs every minute where it copies values from column S to column V.
3) In the column T, I have the formula (=IF(V5<>"",IF(RegExMatch(S5,V5),"Copied",""),"")), which means if the value exist in the column V5 add Copied in cell T5.
4) I am looking for a solution that when cell T:T has "Copied", delete the cell range S:S
Thank you millions!
As #TheWizEd points out the value in T is dependant on the result in another cell. However an OnEdit function does not necessarily have to respond to the range where the change was made. I've used this code to use the OnEdit event to evaluate the values in Column T and then make the relevant change to values in Column S.
Column T uses a for loop to go through the various row, but the relevant value is pushed to array. This allows a single setValues to be executed at the end of the function.
The function should be assigned to the OnEdit trigger for the Spreadsheet.
function so_53469142() {
// Setup spreadsheet and target sheet
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName("so_53469142");
// get the last row of data
var lastRow = sheet.getLastRow();
// get range for Column S & T
var values = sheet.getRange(1, 19, lastRow, 2).getValues();
// set counter variable
var i = 0;
var dataArray = [];
var masterArray = [];
// start loop
for (i = 0; i < lastRow; i++) {
// Logger.log("i="+i+", S = "+values[i][0]+", T = "+values[i][1]);//DEBUG
// empty the array
dataArray = [];
// test value of first row in T
if (values[i][1] === "Copied") {
// If value = "Copies then push blank onto array for Column S
dataArray.push("");
} else {
// else push existing value for column S
dataArray.push(values[i][0]);
}
// make the array 2D
masterArray.push(dataArray);
}
// Update values in Column S
sheet.getRange(1, 19, lastRow).setValues(masterArray);
}
I am very new at writing code and have a limited understanding so please be gentle!
I have been trying to expand on the code made on this question.
I have had limited success and am now stuck, I was hoping someone would be kind enough to point me in the right direction or tell me what I am doing wrong.
So, the scenario: A need to have an auto-incrementing "Job Reference" for booking in netbook repairs to the IT dept I work for. This booking is made via a Google Form and I can get the code in the link to work perfectly but! I was hoping to have a little more than a simple count - 1,2,3,4,5 and so on. Ideally the job ref would be displayed as JR0001, JR0002 and so on.
So, my attempt at code!
The code which user: oneself submitted which works perfectly.
function onFormSubmit(e) {
// Get the active sheet
var sheet = SpreadsheetApp.getActiveSheet();
// Get the active row
var row = sheet.getActiveCell().getRowIndex();
// Get the next ID value. NOTE: This cell should be set to: =MAX(A:A)+1
var id = sheet.getRange("P1").getValue();
// Check of ID column is empty
if (sheet.getRange(row, 1).getValue() == "") {
// Set new ID value
sheet.getRange(row, 1).setValue(id);
}
}
The first thing I tried was to simply add another variable and add it to the .setValue
function onFormSubmit(e) {
// Get the active sheet
var sheet = SpreadsheetApp.getActiveSheet();
// Get the active row
var row = sheet.getActiveCell().getRowIndex();
// Get the next ID value. NOTE: This cell should be set to: =MAX(A:A)+1
var id = sheet.getRange("X1").getValue();
// Set Job Reference
var jobref = "JR";
// Check of ID column is empty
if (sheet.getRange(row, 1).getValue() == "") {
// Set new ID value
sheet.getRange(row, 1).setValue(jobref+id);
}
}
This worked as far as getting "JR1" instead of 1 but the auto-increment stopped working so for every form submitted I still had "JR1" - not really auto-increment!
I then tried setting up another .getValue from the sheet as the Job Ref
function onFormSubmit(e) {
// Get the active sheet
var sheet = SpreadsheetApp.getActiveSheet();
// Get the active row
var row = sheet.getActiveCell().getRowIndex();
// Get the next ID value. NOTE: This cell should be set to: =MAX(A:A)+1
var id = sheet.getRange("X1").getValue();
// Set Job Reference
var jobref = sheet.getRange("X2").getValue();
// Check of ID column is empty
if (sheet.getRange(row, 1).getValue() == "") {
// Set new ID value
sheet.getRange(row, 1).setValue(jobref+id);
}
}
Same result - a non incrementing "JR1"
I then tried concatenating the working incrementing number with my job ref cell and then calling that in the script.
function onFormSubmit(e) {
// Get the active sheet
var sheet = SpreadsheetApp.getActiveSheet();
// Set Job Reference
var jobref = sheet.getRange("X2").getValue();
// Get the active row
var row = sheet.getActiveCell().getRowIndex();
// Get the next ID value. NOTE: This cell should be set to: =MAX(A:A)+1
var id = sheet.getRange("X1").getValue();
// Check of ID column is empty
if (sheet.getRange(row, 1).getValue() == "") {
// Set new ID value
sheet.getRange(row, 1).setValue(jobref);
}
}
Same result the value doesn't increment
I don't understand why the number stops incrementing if I add other variables and don't understand how to add the leading zeros to the incrementing number. I get the feeling that I am trying to over complicate things!
So to sum up is there a way of getting an auto-incrementing ref that is 6 characters long - in this scenario first form JR0001 second submit JR0002 and so on.
I would really like some pointers on where I am going wrong if possible, I do want to learn but I am obviously missing some key principles.
here is a working solution that uses the "brand new" Utilities.formatString() that the GAS team just added a few days ago ;-)
function OnForm(){
var sh = SpreadsheetApp.getActiveSheet()
var startcell = sh.getRange('A1').getValue();
if(! startcell){sh.getRange('A1').setValue(1);return}; // this is only to handle initial situation when A1 is empty.
var colValues = sh.getRange('A1:A').getValues();// get all the values in column A in an array
var max=0;// define the max variable to a minimal value
for(var r in colValues){ // iterate the array
var vv=colValues[r][0].toString().replace(/[^0-9]/g,'');// remove the letters from the string to convert to number
if(Number(vv)>max){max=vv};// get the highest numeric value in th column, no matter what happens in the column... this runs at array level so it is very fast
}
max++ ; // increment to be 1 above max value
sh.getRange(sh.getLastRow()+1, 1).setValue(Utilities.formatString('JR%06d',max));// and write it back to sheet's last row.
}