Google Sheets script to hide columns not working on onEdit() - google-apps-script

I have the following code to show or hide columns/rows in a sheet based on the contents of the first row/column (whether it's more than or less than 0) which works fine when called from an installable trigger, but as an onEdit function, it doesn't work.
Ideally, I'd like to avoid using installable triggers, as that doesn't copy with the sheet and it's primarily intended as a template to be repeatedly copied.
The data is held in the sheet "input" and then provides data for queries in the sheet "table" (which is protected); ideally, it should either run on edit (on the sheet input) or on a one-minute interval (which is the preferable option, but not if it involves using the triggers program as outlined above).
The current version as below doesn't create any error messages but doesn't function as intended either.
function onEdit(){
hideEmptyRows();
hideCols();
}
function hideCols() {
var sh = SpreadsheetApp.getActive().getSheetByName('table');
var totalsRow = 1;
sh.getRange(totalsRow, 3, 1, sh.getMaxColumns()-3).getValues()[0]
.forEach(function(el, i) {
if (!el || el >= 1) sh.showColumns(i + 3)
});
sh.getRange(totalsRow, 3, 1, sh.getMaxColumns()-3).getValues()[0]
.forEach(function(el, i) {
if (!el || el <= 0) sh.hideColumns(i + 3)
})
}
function hideEmptyRows() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var s = ss.getSheetByName("Table"); // Enter sheet name
var row = s.getRange('A:A').getValues(); // Enter
column letter that has the text "hide" and "unhide"
s.showRows(1, s.getMaxRows());
for(var i=0; i< row.length; i++){s.showRows(i+1, 1); if(row[i] == 0) { s.hideRows(i+1, 1); } // Value to hide
}}

Explanation:
Your code should be working fine.
However, it is suggested to take advantage of the event object e. In your current solution, if you make any changes to another sheet, onEdit might alter the sheet table. Maybe this is a desired behaviour but I wouldn't recommend it.
Another improvement you can make in this particular project, using the event object, is to execute the forEach part when there is a change in the first row. Namely, there is no need to execute a for loop that performs calculations for row 1, if for example, row 2 is edited.
Solution:
Here I offer you a more maintainable/futureproof solution:
function onEdit(e) {
const sheet_name = 'table';
const row = e.range.getRow();
const col = e.range.getColumn();
const sh = e.source.getActiveSheet();
const totalsRow = 1;
if(sh.getName() == sheet_name && row == totalsRow ){
sh.getRange(totalsRow, 3, 1, sh.getMaxColumns()-3).getValues()[0]
.forEach(function(el, i) {
if (!el || el <= 0) sh.hideColumns(i + 3);
if (!el || el > 0) sh.showColumns(i + 3);
})
}
}

Related

Google Sheets: Append then Delete rows based on Tickbox condition

I'm attempting to create a spreadsheet to organise products ordered at my workplace.
When an order is received a team member would add the details to the sheet; when it is collected they'd fill out date and ID then tick the order complete. See Attached
What I want to happen next is that the row containing the complete details from that order is appended to a second page in the sheet and the original row is deleted.
I can't make sense of how to get this to run automatically when the box is checked; so far I have been compiling a script to run from a button press:
function runFiling() {
function moveRows() {
var ss = SpreadsheetApp.getActive();
var osh = ss.getSheetByName('Current');
var dsh = ss.getSheetByName('Collected');
var srg = osh.getDataRange('H2:H');//You might want to specify a more unique range. This just gets all of the data on the sheet
var svA = srg.getValues();
var d=0;//deleted row counter
for(var i=1;i<svA.length;i++) {
if(svA[i][7] =='TRUE') {
dsh.appendRow(svA[i]);//append entire row to Sheet2
osh.deleteRow(i-d+1);//accounts for the difference between length of array and number of remaining row.
d++;
}
}
}
}
However even this fails to Append or Delete anything although no errors are found/returned.
If anyone can suggest a way to fix the above or, preferably, how to make the script work when the box is ticked your help will be most appreciated.
Try it this way using an onEdit(e) function
function onEdit(e) {
const sh = e.range.getSheet();
if (sh.getName() == 'Current' && e.range.columnStart == 7 && e.range.rowStart > 1 && e.value == "TRUE") {
const dsh = ss.getSheetByName('Collected');
const vs = sh.getRange(e.range.rowStart, 1, 1, sh.getLastColumn()).getValues()
dsh.getRange(dsh.getLastRow() + 1, 1, vs.length, vs[0].length).setValues(vs);
sh.deleteRow(e.range.rowStart);
}
}
This will accomplish the task line by line as the check boxes are checked off by the user.

Transferring Rows between Sheets via a like Identifier

Evening everyone!
I asked this about a week back, but I think the thread got lost in the ether. We came close, but I'm trying to create a function where "Transfer a range of Rows from sheet 1 to sheet 2. Sheet 1 has order IDs in column E. G will have =unique to show me all current order IDs, with check boxes next to each unique reference. Check the box next to which ones you want to CUT over > Select a menu run add on > Run Script > all Rows from A:E that match the desired ID are moved".
[Picture Reference]
Sheet Reference
function onEdit(e) {
e.source.toast('Entry')
const sh = e.range.getSheet();
if(sh.getName() == "Reference" && e.range.columnStart == 8 && e.range.rowStart > 1 && e.value == "TRUE") {
e.source.toast('Gate1')
let rg = sh.getRange(e.range.rowStart,1,1,5)
let vs = rg.getValues();
const osh = e.source.getSheetByName("Processing");
osh.getRange(osh.getLastRow() + 1,1,1,5).setValues(vs);
rg.deleteCells(SpreadsheetApp.Dimension.ROWS);
e.range.setValue("FALSE");
}
}
Here is what we had so far. Please let me know if anyone can help, thank you!
To get all rows that match the unique ID whose checkbox was ticked, use Array.filter(), like this:
/**
* Simple trigger that runs each time the user hand edits the spreadsheet.
*
* #param {Object} e The onEdit() event object.
*/
function onEdit(e) {
if (!e) {
throw new Error(
'Please do not run the onEdit(e) function in the script editor window. '
+ 'It runs automatically when you hand edit the spreadsheet.'
+ 'See https://stackoverflow.com/a/63851123/13045193.'
);
}
moveRowsByUniqueId_(e);
}
/**
* Triggers on a checkbox click and moves rows that match a unique ID.
*
* #param {Object} e The onEdit() event object.
*/
function moveRowsByUniqueId_(e) {
let sheet;
if (e.value !== 'TRUE'
|| e.range.rowStart <= 1
|| e.range.columnStart !== 8
|| (sheet = e.range.getSheet()).getName() !== 'Reference') {
return;
}
e.source.toast('Moving rows...');
const uniqueId = e.range.offset(0, -1).getValue();
const range = sheet.getRange('A2:E');
const values = range.getValues();
const targetSheet = e.source.getSheetByName('Processing');
const _matchWithId = (row) => row[4] === uniqueId;
const valuesToAppend = values.filter(_matchWithId);
if (uniqueId && valuesToAppend.length) {
appendRows_(targetSheet, valuesToAppend);
range.clearContent();
const remainingValues = values.filter((row) => !_matchWithId(row));
range.offset(0, 0, remainingValues.length, remainingValues[0].length)
.setValues(remainingValues);
e.source.toast(`Done. Moved ${valuesToAppend.length} rows.`);
} else {
e.source.toast('Done. Found no rows to move.');
}
e.range.setValue(false);
}
For that to work, you will need to paste the appendRows_() and getLastRow_() utility functions in your script project.
It work almost like asked but :
it's using a personal lib (available below)
didn't make the part realtiv of removing range and aggregate result, I hope i can add it to the lib some day. However, empty cell are fill with -
for an obscure reason, it doesn't like the TRUE/FALSE cell, but work like a charm with 1/0 or any other texte value, regex, ...
Additional error handling are to be added if not any match or others possibilites
function onEdit(e){
console.log(SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Reference").getRange("H3").getValue())
var tableReference = new TableWithHeaderHelper(SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Reference").getRange("A1").getDataRegion());
var tableReferenceId = new TableWithHeaderHelper(SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Reference").getRange("G11").getDataRegion());
var tableProcessing = new TableWithHeaderHelper(SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Processing").getRange("A1").getDataRegion());
// get value
var id = tableReferenceId.getTableWhereColumn("Move to Sheet").matchValue(1).getWithinColumn("Unique Filter").cellAtRow(0).getValue();
var tableWithinId = tableReference.getTableWhereColumn("Shipment ID").matchValue(id)
for(var i=0 ; i < tableWithinId.length() ; i++){
var rangeRowWithinId = tableWithinId.getRow(i);
tableProcessing.addNewEntry(rangeRowWithinId);
for(var cell in rangeRowWithinId.getRange()) cell.setValue("-");
}
//reset value
tableReferenceId.getTableWhereColumn("Move to Sheet").matchValue(1).getWithinColumn("Move to Sheet").cellAtRow(0).setValue(0)
}
See below the app script file you need to create in order to use this utils function:
https://github.com/SolannP/UtilsAppSsript/blob/main/UtilsGSheetTableHelper.gs

Count the number of changes in a column - Google Script / Sheets

First of all, I ask you to excuse me if I make some language-mistake (I'm Italian!).
I'm trying to write a script for a Google Sheet that can help me to track the number of changes of a column. I would like a counter that grows everytime a value of a cell changes. Ex:
-Column A: the cell A3 changes from "2020" to "2021"
-Column B: the cell B3 changes from 0 to 1 (o from 2 to 3, a simple +1 on the value).
I wrote this code but I cannot understand where is the error.
function onEdit(e) {
incrementCounter_(e);
}
function incrementCounter_(e) {
var stw = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Foglio2");
var c1 = stw.getRange(2, 1);
var c2 = stw.getRange(2, 2);
if (!e || !e.range) {
return;
}
var sheet = e.range.getSheet();
for (var i=2; i<=6; i++){
if (sheet.getName() === stw && e.range.getA1Notation() === stw.getrange().getvalue(i,1)) {
var cell = stw.getrange().getvalue(i,2);
cell.setValue((Number(cell.getValue()) || 0) + 1);
}
}
}
Thanks for the help!
There is no need to use two functions for this.
Code
// Copyright 2020 Google LLC.
// SPDX-License-Identifier: Apache-2.0
function onEdit(e) {
var sheet = e.range.getSheet();
var sheetName = sheet.getName();
if (sheetName == "Foglio2" && e.range.getColumn() == 1) {
var row = e.range.getRow();
var val = sheet.getRange(row, 2).getValue();
sheet.getRange(row, 2).setValue(val + 1);
}
}
Explanation
What you want can be achieved by using the onEdit(e) trigger. The above function makes use of the e event object and checks where exactly the edit is being made. As it can be seen, the for loop is not needed in this situation as in order to get the column and row needed the getColumn() and getRow() methods have been used.
In this situation, the script checks if the sheet in which the edit has been made is Foglio2 and if an edit has been made on the A column. If the condition checks, it increments the corresponding value from the B column.
Note
Please note that getValue(value1, value2) is not a valid method and if you want to get the value for that specific range, you must pass the two parameters to the getRange() method.
Reference
Apps Script Event Objects;
Apps Script Sheet Class;
Apps Script Range Class - getValue().
Do you mean this?
function onEdit(e) {
if (e.range.getA1Notation() === 'A3') {
var range = SpreadsheetApp.getActiveSheet().getRange('B3');
var value = range.getValue();
value++;
range.setValue(value);
}
}

Formula-based cell changes in Google Sheets is not firing onEdit script

I'm completely new to Google script writing, but I've used various posts here to piece together what I need: something that will add a time stamp to a row when a certain column changes. Here's what I'm currently using:
function onEdit() {
var s = SpreadsheetApp.getActiveSheet();
if( s.getName() == "test" ) { //checks that we're on the correct sheet
var r = s.getActiveCell();
if( r.getColumn() == 16 ) { //checks the column
var nextCell = r.offset(0, 1);
if( nextCell.getValue() === '' ) //is empty?
nextCell.setValue(new Date());
}
}
}
This works perfectly when I manually change the data; however, the column that the script is monitoring pulls data from another sheet and this fails to fire the trigger/script. How can I get around this so that cells with formulas (that reference other sheets) will still fire my script?
Any help is greatly appreciated. Thanks!
The onEdit trigger works only when an actual user edits the spreadsheet. Depends of your use case, but you can be use a Time-driven trigger and set it in a recurring interval and with a function monitor the column for changes, here's an example:
function monitorColumn() {
// Add the trigger from the Resources menu in the Script Editor
// Script Editor > Resources > Current oroject's triggers > Add trigger
// [monitorColumn] - [Time-driven] - [Hour timer] - [Every hour]
var s = SpreadsheetApp.getActiveSpreadsheet();
var ss = s.getSheetByName("test"); // Get the sheet by name
// Get the values of the columns P & Q with getRange(row, column, numRows, numColumns)
var columnValues = ss.getRange(1, 16, ss.getLastRow(), 2).getValues();
// Loop through the values
for (var i = 0; i < columnValues.length; i++) {
// If cell in column P is empty and cell in column Q is not empty set todays date
if(columnValues[i][0] != "" && columnValues[i][1] == ""){
// While a range index starts at 1, 1, the JavaScript array will be indexed from [0][0].
ss.getRange(i+1,17).setValue(new Date());
}
}
}

How to apply a loop on a script for multiple/various sheet names in Google spreadsheet?

I'm still learning GoogleApp scripting. Can anyone guide me in the right direction how to apply the same codes on a Google spreadsheet with multiple sheets with different sheet names? Maybe I need a script for a loop?
Thank you for your help in advance!
This is the script I have so far:
function MakeRowGray() {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Sheet1');
var columnD = sheet.getRange(2, 2, sheet.getLastRow()-1, 1); // Row B
var dValues = columnD.getValues();
var columnE = sheet.getRange(2, 3, sheet.getLastRow()-1, 1); // Row C
var eValues = columnE.getValues();
for (var i = 2; i < dValues.length + 2; i++) {
if (dValues[i-2][0].toUpperCase() == 'Y' && eValues[i-2][0].toUpperCase() == 'Y') { // Checks for 'Y' in both D and E columns (Participated & Received)
// If they're both yes, make them gray...
sheet.getRange(i, 1, 1, 7).setBackgroundColor("#CCCCCC"); // Make A through H gray
}
else if (dValues[i-2][0].toUpperCase() == 'Y' && eValues[i-2][0].toUpperCase() != 'Y' && eValues[i-2][0].toUpperCase() != 'W' && eValues[i-2][0].toUpperCase() != 'W?') // IN PROGRESS CODE -- MAKE ROW BLUE??
{
sheet.getRange(i, 1, 1, 7).setBackgroundColor("#AAAAFF"); // Make A through H blue
}
else if (dValues[i-2][0].toUpperCase() == 'Y' && eValues[i-2][0].toUpperCase() == 'W?') // Not sure if Waiting or not (W?)
{
sheet.getRange(i, 1, 1, 7).setBackgroundColor("#FFBB00"); // Make A through H slightly orange
}
else if (dValues[i-2][0].toUpperCase() == 'X' && eValues[i-2][0].toUpperCase() == 'X') {
sheet.getRange(i, 1, 1, 7).setBackgroundColor("#FF0000"); // Red
}
else if (dValues[i-2][0].toUpperCase() == 'Y' && eValues[i-2][0].toUpperCase() == 'W') {
sheet.getRange(i, 1, 1, 7).setBackgroundColor("#FFFF00"); // Yellow
}
else
{ // Reset...
sheet.getRange(i, 1, 1, 7).setBackgroundColor("#FFFFFF");
}
}
};
You've identified the need to change a function you've written so it can be applied in a broader way than it currently supports. This type of work is generally referred to as refactoring.
In your case, this could be the thought process to follow...
Since you want to do the same thing on multiple sheets, generalize the current function to operate on an arbitrary Sheet. The definition of the MakeRowGray() function should be changed to accept a sheetName as a parameter. If you still want the existing behavior to be preserved, i.e. calling MakeRowGray() without any parameter will operate on Sheet1, that can be accomodated.
function MakeRowGray(sheetName) {
sheetName = sheetName || 'Sheet1'; // Default to operate on Sheet1
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName);
...
Make that change, and test it. Does the function still behave as it used to? Can you pass in the name of one of your other sheets, and does it work there?
Next, write a new function that handles the problem of iterating through your various sheets. This function will pass off work to the refactored MakeRowGray().
function makeAllSheetsGray() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheets = ss.getSheets();
for (var i=0; i<sheets.length; i++) {
MakeRowGray( sheet.getName() );
}
}
Test that this function does what you expect. Does it find all sheets, regardless of name? Do the right names get passed to MakeRowGray()?
Improve things / tidy up.
This is an important step for future maintenance and re-usability.
Do the function names make sense?
For example, MakeRowGray() does not clearly indicate what the function is actually doing, probably because of previous refactoring. A name like conditionallyColorSheetRows would be an improvement. The new function, makeAllSheetsGray(), should adapt as well, since it was based on the previous inappropriate name.
Do variable names make sense?
Are you doing work you don't need to?
For example, in makeAllSheetsGray() we're getting an array of Sheet instances, then passing the name of the sheet to MakeRowGray() where we use the name to get a handle on a Sheet instance. A further refactoring to use just the Sheet instances would save some processing. There may be reasons to leave things as they are, but since Google Apps Scripts have limited execution time, it's always smart to look for ways to reduce cycles.