onEdit trigger based on OUTCOME (VALUE) from formula result - sorting

I am trying to create an AUTOSORT onEdit() script that is only triggered when a value in Column D (4) matches a certain text. In this pattern, when the onEdit trigger becomes active, range.sort({column:11,ascending:true}) is used in the active sheet, but is limited to a preselected range of sheets.
I have a script for when a cell is changed completely, but unfortunately doesn't run when the value is changed due to a formula.
What I would like to have is a script that checks the outcome of a formula for a specific TEXT in Column D (4) whenever it is changed.
What I have so far (not working at this moment):
function onEdit(e) {
multiSortColumns(e);
}
function multiSortColumns(e) {
if (e.range.columnStart == 4 && e.range.getValue() == 'CHANGE OF DATE') {
var sheets = ["Sheet1", "Sheet2", , ,];
var sheet = e.range.getSheet();
if (sheets.includes(sheet.getSheetName())) {
var range = sheet.getRange("A5:bY600");
range.sort({ column: 11, ascending: true });
e.source.toast('Sort complete.');
}
}
}

Because the value in column D is not manually edited but through a formula or function, onEdit(e) doesn't consider that as an event as explained in the limitations below:
- Script executions and API requests do not cause triggers to run. For example, calling Range.setValue() to edit a cell does not cause the spreadsheet's onEdit trigger to run.
But let's say the cell that is manually edited is in column A and based on that, the cell value in the same row in column D changes to "CHANGE OF DATE", you can adjust your code by catching that edit event in column A :
function onEdit(e) {
multiSortColumns(e);
}
function multiSortColumns(e) {
//var refcell = SpreadsheetApp.getActiveSheet().getActiveCell();
if (e.range.columnStart == 1 && e.range.offset(0,3).getValue() == 'CHANGE OF DATE') { //i.e. col 1 is edited and col 1+3 shows "CHANGE OF DATE"
var sheets = ["Sheet1", "Sheet2", , ,];
var sheet = e.range.getSheet();
if (sheets.includes(sheet.getSheetName())) {
var range = sheet.getRange("A5:bY600");
range.sort({ column: 11, ascending: true });
e.source.toast('Sort complete.');
}
}
}
Your column D is still used as a pre-condition for sorting but using offset() relative to column A where the edit event actually happened.

Related

GAS: copy one data validation (dropdown) in N number of rows and set unique value in each dropdown

Edited for clarity and to add images
Using Google Apps Script, how can I:
copy a range from Section 2 sheet (G11:H11, with a checkbox & dropdown) into range G12:G25 N number of times (based on the number of non-empty rows in MASTER DROPDOWN sheet under same header title as 'Section 2'!A2) and then,
set a different value in each dropdown (each unique value listed in MASTER DROPDOWN sheet under the correct header).
For example, first image is "MASTER DROPDOWN" sheet.
This second image is "Section 2" sheet. The user can add or delete items on the list using the buttons on the right side of the page.
And this last image is "Section 2" sheet. I cannot understand how to write the code for this... When user presses "Reset list" button, I want to copy checkbox and dropdown menu (from G11:H11) N number of times (N=3 based on number of items from MASTER DROPDOWN under Section 2). In each dropdown, I want to set value with each item from the original list in the MASTER DROPDOWN sheet. This process should be dynamic and work on Section 1 and Section 3 sheet (not in worksheet currently).
Any advice on the script verbage to search/learn about this type of functionality, or some direction on the script for this is much appreciated. Here's a link to my code that I have so far...
https://docs.google.com/spreadsheets/d/1ZdlJdhA0ZJOIwLA9dw5-y5v1FyLfRSywjmQ543EwMFQ/edit?usp=sharing
function newListAlert (){
var ui = SpreadsheetApp.getUi();
var response = ui.alert("Are you sure you want to delete your current list and create a new one?",ui.ButtonSet.YES_NO);
if(response == ui.Button.YES) {
newList();
} else {
}
}
function newList() {
var ss = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
var range = ss.getRange("G11:H25");
var options = {contentsOnly: true, validationsOnly: true};
//clear current list
range.clear(options);
//add new item to list in first row of range
addNewItem();
//copy new datavalidation row above based on number of non-empty rows in MASTER DROPDOWN with same header as active sheet (-1)
var datass = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("MASTER DROPDOWN");
var range = ss.getRange("A2");
if (range.getCell(1,1)){
var section = datass.getRange(1,1,1,datass.getLastColumn()).getValues();
var sectionIndex = section[0].indexOf(range.getValue()) + 1;
var validationRange = datass.getRange(4,sectionIndex,19);//19 columns: checklist has a maximum of 18 rows (+ 1 for "select option")
}
}
In your situation, how about modifying newList() as follows?
Modified script:
function newList() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getActiveSheet();
var sheetName = sheet.getSheetName();
sheet.getRange("G11:H25").clear({ contentsOnly: true, validationsOnly: true });
var srcSheet = ss.getSheetByName("MASTER DROPDOWN");
var values = srcSheet.getDataRange().getValues();
var obj = values[0].map((_, c) => values.map(r => r[c])).reduce((o, [h, , , ...v], i) => {
if (h != "") {
v = v.filter(String);
v.shift();
o[h] = { values: v, range: srcSheet.getRange(4, i + 1, v.length + 1) };
}
return o;
}, {});
if (obj[sheetName]) {
var validationRule = SpreadsheetApp.newDataValidation()
.setAllowInvalid(false)
.setHelpText('Select an option from the menu. To add more options to the dropdown list, go to MASTER DROPDOWN tab.')
.requireValueInRange(obj[sheetName].range, true)
.build();
var d = obj[sheetName].values.map(_ => [validationRule]);
var v = obj[sheetName].values.map(e => [e]);
sheet.getRange(sheet.getLastRow() + 1, 8, obj[sheetName].values.length).setDataValidations(d).setValues(v).offset(0, -1).insertCheckboxes();
}
}
When this script is run, the values for DataValidations are retrieved from the sheet "MASTER DROPDOWN", and using the sheet name, the dataValidation rules are created, and put to the column "H". And also, the checkboxes are put to the column "G" of the same rows of the dataValidations.
In this case, for example, when you add a new sheet of "Section 1" and run newList(), the dropdown list including "Engineering" and "Design" is put to the column "H" and the checkboxes are also put to the column "G".
Note:
In this modification, the sheet name like "Section 2" is used for searching the column of "MASTER DROPDOWN" sheet. So please be careful about this.
And, from your current script, the last row is used for putting to the dropdown list and checkboxes. So when you want to modify this, please modify the above script.
This sample script is for your sample Spreadsheet. So when your actual Spreadsheet is changed, this script might not be able to be used. Please be careful this.
References:
setDataValidations(rules)
insertCheckboxes()

Google Sheets - QUERY, adding a cell to hide a row

I'm hoping to use a Google Form to have my students sign up for activities. I added a column to the right of the form data where I can put an "x" once I have met with them. I have a separate tab called "Ordered" where I use QUERY to sort and show only the entries without an x. Once I meet with a student, I can put an x on the original data tab, and the entry will hide from the Ordered tab (kind of a queue for my students).
I would love to not have to switch between the 2 tabs every time I have to check someone off. Is there a way to add a similar column on the Ordered tab that will hide the finished entry?
Here is the sheet I'm referring to:
https://docs.google.com/spreadsheets/d/1fQHF0EoGLk5NEI6GvyRk4InBl-uEq0jq7qqmyOmFjy8/edit#gid=379798836
I'm sharing this sheet with other teachers who aren't familiar with spreadsheets, so the solution has to be ludite friendly.
As #Aerials said, you are likely to run into circular dependency issues here. Because of this, I'd suggest using Apps Script and get rid of the QUERY formula. You want to do the following:
Every time someone submits the form, the Ordered sheet gets updated with the new submission data.
Every time an x is added to the Form Responses 1 sheet, the corresponding row in Ordered gets removed.
Every time an x is added to the Ordered sheet, the corresponding row in Ordered gets removed, and an x gets added to the corresponding row in Form Responses 1.
A possible way to go would be something along the following lines (open the script bound to your spreadsheet by clicking Tools > Script editor):
Install an onFormSubmit trigger so that Ordered gets updated with new data every time the form is submitted. The trigger can be installed manually or programmatically, copying this function to your script and running it once:
function onFormSubmit(e) {
var ss = SpreadsheetApp.getActive();
ScriptApp.newTrigger("copyAndFilterData")
.forSpreadsheet(ss)
.onFormSubmit()
.create();
}
Once the trigger is installed, the function copyAndFilterData will execute every time the form is submitted. This function should updated Ordered based on the data in Form Responses 1, removing all the submission that have been marked as complete x. This function could be the following:
function copyAndFilterData() {
var ss = SpreadsheetApp.getActive();
var sheet1 = ss.getSheetByName("Form Responses 1");
var sheet2 = ss.getSheetByName("Ordered");
var sourceData = sheet1.getDataRange().getValues();
var filteredData = sourceData.filter(sourceRow => sourceRow[5] !== "x")
.sort((a, b) => a - b)
.map(sourceRow => {
sourceRow.pop();
sourceRow.splice(1, 1);
return sourceRow;
});
sheet2.getRange(1, 1, sheet2.getLastRow(), 4).clear({contentsOnly: true});
var destRange = sheet2.getRange(1, 1, filteredData.length, filteredData[0].length)
destRange.setValues(filteredData);
}
At this point, another piece of functionality would be needed: update the spreadsheet based on the addition or removal of x on any of both sheets. For this, an onEdit trigger would be needed, like this:
function onEdit(e) {
var editedSheet = e.range.getSheet();
if (editedSheet.getName() === "Form Responses 1") copyAndFilterData();
else if (editedSheet.getName() === "Ordered") removeRows(e, editedSheet);
}
Once this function is copied and the project saved, every time Form Responses 1 is edited, the Ordered sheets gets updated (function copyAndFilterData), and every time Ordered is edited, the function removeRows is fired. This function should do two things: (1) remove the row which was marked as complete in Ordered (x added) and (2) add the corresponding x to Form Responses 1. The submission dates can be compared to identify the row:
function removeRows(e, editedSheet) {
var range = e.range;
var column = range.getColumn();
var row = range.getRow();
var value = range.getValue();
if (column == 5 && row > 1 && value == "x") {
var date = editedSheet.getRange(row, 1).getValue();
var formSheet = e.source.getSheetByName("Form Responses 1")
var values = formSheet.getRange(2, 1, formSheet.getLastRow() - 1).getValues();
var markedRow = values.findIndex(value => value[0].getTime() === date.getTime());
formSheet.getRange(markedRow + 2, 6).setValue("x");
editedSheet.deleteRow(row);
}
}
Reference:
Overview of Google Apps Script
Simple Triggers
Installable Triggers

How do I reveal a hidden row based on a cell value?

I have created a sheet which has a dropdown with values "Select One:" "Push" "Pull" in cell B23. I would like row 24 to be hidden by default. Upon selection of the value "Pull" from the dropdown, I would like row 24 to be unhidden.
Is there a way to unhide row 24 if the selection in cell B23 = Pull?
You want to:
Show row 24 if dropdown in B23 is set to Pull.
Hide row 24 in any other case.
If that's the case, you can copy this simple onEdit trigger to the script bound to your spreadsheet (the sheet with the data is called Sheet1 in this sample, please change this accordingly if that's not the case):
function onEdit(e) {
var ss = SpreadsheetApp.getActive();
var sheet = ss.getSheetByName("Sheet1"); // Please change accordingly
var dropdown = sheet.getRange("B23").getValue();
var row = 24;
if (dropdown == "Pull") sheet.showRows(row);
else sheet.hideRows(row);
}
As you can see, there is no need to use a for loop, which will slow down the execution (specially considering this will run every time the file is edited).
In order to make sure row 24 will be hidden or shown accordingly the first time you open your file, you can attach an onOpen trigger to the function, like this:
function onOpen(e) {
onEdit(e)
}
Reference:
onEdit
onOpen
hideRows(rowIndex)
showRows(rowIndex)
I hope this is of any help.
use this script:
function onEdit(e)
{
var ss=SpreadsheetApp.getActiveSpreadsheet();
var sh=ss.getSheetByName("Sheet1");
var rg=sh.getDataRange();
var vA=rg.getValues();
var xx=sh.getRange("B23").getValue();
for(var i=0;i<vA.length;i++)
{
var row=i+1;
switch(xx)
{
case 'Pull':
if(row=24){sh.showRows(row);}
break;
case 'Select One:':
if(row=24){sh.hideRows(row);}
break;
case 'Push':
if(row=24){sh.hideRows(row);}
break;
default:
}
}
}

Have Google Sheet Linked to Google Form Sort By Date Automatically

I use a Google Form for people to request days off. An add-on called Form Approvals is used to send emails to certain people who can approve or deny the request. In the Google Sheet listing the responses, new entries keep going to the bottom.
Is there a way to make new entries from the Google Form to be sorted automatically by the date of the day off in the Google Sheet?
I found this script, but it doesn't work:
function onEdit(event){
var sheet = event.source.getActiveSheet();
var editedCell = sheet.getActiveCell();
var columnToSortBy = 2;
var tableRange = sheet.getDataRange();
if(editedCell.getColumn() == columnToSortBy){
var range = sheet.getRange(tableRange);
range.sort( { column : columnToSortBy } );
}
}
Also, is there a way to specify which sheet tab for the script to run on?
Try this:
function onEdit(e){
var sh=e.range.getSheet();
if(sh.getName()!="Your desired sheet name")return;
if(e.range.columnStart==2){
sh.getDataRange().sort({ column:2});
}
}
A lot of new programmers try to run these onEdit(e) functions from the script editor. Unfortunately, that doesn't work because the e parameter is expecting to be populated by the event trigger. Without the event object you'll normally get an error like Cannot read property range from undefined because e has not been populated by the event trigger.
I test them by making sure I'm editing the correct sheet and correct range and I use the e.source.toast() function to provide me with feed back sort of like the console.log() does.
If you want to learn more about the event object then try adding a Logger.log(JSON.stringify(e)); to the first line after the function declaration. And then get it to run by editing the appropriate sheet in the appropriate way and go to view log to see the results.
If your sheet is populated by a form and you want to sort the data every time a new form is submitted - you need to use the onFormSubmit trigger.
Google Forms populates the destination spreadsheet chronologically, in order to avoid interference you can use a sync sheet to which the data is transferred on every form submit and which you can sort as desired.
Sample:
function myFunction() {
var ss=SpreadsheetApp.getActiveSpreadsheet();
var sheet=ss.getActiveSheet();
if(sheet.getName()=="Name of Tab to copy and sort"){
var lastRow=sheet.getLastRow();
var lastCol=sheet.getLastColumn();
var range=sheet.getRange(lastRow,1,1,lastCol);
var secondarySheetId="XXX";//Paste here the Id of the secondary spreadsheet
var secondarySheet=SpreadsheetApp.openById(secondarySheetId).getSheetByName("Name of tab of your choice");
secondarySheet.getRange(secondarySheet.getLastRow()+1,1,1,lastCol).setValues(range.getValues());
SpreadsheetApp.flush();
var secondaryRange = secondarySheet.getDataRange();
var columnToSortBy=2; //adapt to your needs
secondaryRange.sort( { column : columnToSortBy } );
}
}
Atach this script to the destination spreadsheet, insert the Id of a secondary spreadsheet (which you have to create first), save the script and bind an installable onFormSubmit trigger to the script through Edit->Current project's triggers->New trigger.
I figured it out using https://www.idiotinside.com/2018/06/08/sort-multiple-columns-google-sheets-apps-script/ :)
For my spreadsheet:
The name of the sheet I want to sort (not every sheet in the spreadsheet) is called "Requests".
Dates are in Column B which is column number 2.
Dates are sorted with oldest at the top.
Be sure to edit SHEET_NAME, SORT_DATA_RANGE, and SORT_ORDER to your needs.
Use this script with an onEdit trigger:
SHEET_NAME = "Requests";
SORT_DATA_RANGE = "A2:L1500";
SORT_ORDER = [
// {column: 1, ascending: true}, // 1 = column number for Column A, sorting by ascending order
// {column: 3, ascending: false}, // 3 = column number for Column C, sorting by descending order
{column: 2, ascending: true},
];
function onEdit(e){
multiSortColumns();
}
function multiSortColumns(){
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName(SHEET_NAME);
var range = sheet.getRange(SORT_DATA_RANGE);
range.sort(SORT_ORDER);
ss.toast('Sorting by Date completed.');
}
Then go to https://script.google.com/ and create a trigger for the above script. Under "Select event type" use "On form submit".
This is working well so far :)

Applying Google Apps Script for Dynamic Data Validation to Existing Sheet

So I'm using this script (credit to Chicago Computer Classes) for populating dynamic data validation of a Google Sheets cell based on what the user entered in a different cell.
For example, if they enter the sport "Football" in one cell, the next cell has data validation for "CFB, CFL, or NFL" but if they enter "Basketball" in the first cell then the second cell's data validation changes to "ABL, CBB, NBA, or WNBA" for examples.
The script is working fantastic and you are welcome to play with the sheet here
However ... here's my problem:
I have an existing spreadsheet with 9000 rows of data. I would like to apply this new data validation scheme to this spreadsheet. The script is triggered with the onEdit() function which works great when you are entering things one row at a time. But if I try to copy and paste a whole bunch of rows in the first column, only the first row of the second column triggers the onEdit and gets the new data validation while all the other rows of the second column are unchanged. I've also tried to "Fill Down" or "Fill Range" on the first column and they have the same result where the first row in the selected range gets the new data validation but the rest of the selection is unchanged.
And while it would work just fine if I was manually entering rows, I really don't feel like doing that 9000 times :)
How do I modify the script to trigger the function with data that's copy/pasted or filled down?
Thanks!
Script here:
function onEdit(){
var tabLists = "Leagues";
var tabValidation = "2018";
var ss = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
var datass = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(tabLists);
var activeCell = ss.getActiveCell();
if(activeCell.getColumn() == 6 && activeCell.getRow() > 1 && ss.getSheetName() == tabValidation){
activeCell.offset(0, 1).clearContent().clearDataValidations();
var makes = datass.getRange(1, 1, 1, datass.getLastColumn()).getValues();
var makeIndex = makes[0].indexOf(activeCell.getValue()) + 1;
if(makeIndex != 0){
var validationRange = datass.getRange(3, makeIndex, datass.getLastRow());
var validationRule = SpreadsheetApp.newDataValidation().requireValueInRange(validationRange).build();
activeCell.offset(0, 1).setDataValidation(validationRule);
}
}
}
You should use the event object, which will provide you with the range that was edited. What you're doing now is looking only at the "active cell", which doesn't leverage the benefits of the event object, and can also lead to bugginess when you make rapid changes.
Using the event object, when you make an edit to multiple cells at once (from copy/paste), you can then loop through the range and set your validations.
function onEdit(e) {
var editedRange = e.range;
var ss = editedRange.getSheet();
var tabValidation = "2018";
if(editedRange.getColumn() == 6 && editedRange.getRow() > 1 && ss.getSheetName() == tabValidation) {
var tabLists = "Leagues";
var tabListsSheet = e.source.getSheetByName(tabLists);
var makes = tabListsSheet.getRange(1, 1, 1, tabListsSheet.getLastColumn()).getValues(); // This won't change during execution, so call only once
var activeCell = editedRange.getCell(1,1); // Start with the first cell
var remainingRows = editedRange.getHeight();
while(remainingRows > 0) {
var cellValue = activeCell.getValue();
activeCell.offset(0, 1).clearContent().clearDataValidations(); // Always clear content & validations
if (cellValue != "") { // Add validations if cell isn't blank
var makeIndex = makes[0].indexOf(cellValue) + 1;
if(makeIndex != 0) {
var validationRange = tabListsSheet.getRange(3, makeIndex, tabListsSheet.getLastRow()-2);
var validationRule = SpreadsheetApp.newDataValidation().requireValueInRange(validationRange).build();
activeCell.offset(0, 1).setDataValidation(validationRule);
}
}
activeCell = activeCell.offset(1, 0); // Get the next cell down
remainingRows--; // Decrement the counter
}
}
}

Resources