0

I have a script that automatically hides columns with products on specified sheets based on the checklist on sheet "General". The logic is as follows: If the product is unchecked on tab "General" the first row for the corresponding column across all tabs returns value "FALSE". The script is triggered and hides all columns that have "FALSE" in the first row and shows those columns that have "TRUE".

The issue is that when a lot of products are checked/unchecked the script times out and does not go through all tabs (30 sec timeout for onEdit()). Triggering the script via a Custom menu or any other way is not an option for us...

Is there any workaround solution for this? Or perhaps the script can be somehow optimized to fit the 30 sec limitation?

    function  onEdit(e) { 
 var ss = e.source;
var range = e.range;
 var activeSheet = range.getSheet();
  if (activeSheet.getSheetName() != "General" || ![ "B22","B23","B24","B25","B26","B27","B28","B29", "B30","B31","B32","B33","B34","B35","B36","B37","B38","B39","B40","B41","B42", "B43","B44", "B45", "B46"].some(f => f == range.getA1Notation())) return;
 ss.toast('Script start'); 
  // 2. Retrieve the values of the 1st row of sheet "Hide sheet", and the columns are hidden or shown by the values.
 var ss = SpreadsheetApp.getActiveSpreadsheet();
  var only = [
    'Sheet1',
    'Sheet2',
    'Sheet5',
     'Sheet6',
     'Sheet8',
     'Sheet9',
     'Sheet10',
     'Sheet11',
     'Sheet12'
  ];
  only.forEach(function(name) {
  var sheet = ss.getSheetByName(name);
  sheet
    .getRange(1, 1, 1, sheet.getLastColumn())
    .getValues()[0]
    .forEach((e, i) => sheet[e === true ? "showColumns" : "hideColumns"](i + 1));
  })
}
Marios
  • 26,333
  • 8
  • 32
  • 52
Kate Bedrii
  • 103
  • 7

1 Answers1

1

Explanation / Issue:

I think your code is pretty much optimized.

Well, some things could be further optimized, for example:

![ "B22","B23",..."B45", "B46"].some(f => f == range.getA1Notation()))

could be replaced with a simple comparison:

!(row >= 22 && row <=46 && col == 2)

or instead of an array you can define a string and use the includes or you can use includes for the array as well, but the string version would be:

!"B22,B23,..B46".includes(range.getA1Notation())

However, even if you make all these optimizations, the runtime would probably go from 30+ seconds to 25 or so. My point is, in the short term future you will face the same issues again assuming you want to add more sheets or more columns in the sheets etc.

My approach here would be to take advantage of the full 6 or 30 minutes quota that Google gives you instead of the limited 30 seconds quota given for onEdit triggers.

The logic is the following:

  • Rename onEdit(e) to myInstallableEdit(e) or any name you want except for onEdit. This will allow you to create an installable trigger for myInstallableEdit(e).

  • The installable onEdit trigger will allow you to create time-driven triggers. The idea is to execute the part of the code that takes a lot of time with a time-driven trigger which has a quota of 6 or 30 minutes (depending on your account).

  • As soon as there is a successful edit in the sheet (based on the conditions you have specified already), create a time trigger for the function you want to run that is going to be executed only one time within the next minute. Sure, you have to wait less than 60 seconds for your function to start running and that is a limitation, but at least your function will have 6 minutes to run and this will solve your issues.

Workaround Solution:

Manually execute only and once the createOnEditTrigger function. This will create an installable onEdit trigger for myInstallableEdit. Therefore, myInstallableEdit is allowed to create time driven triggers for functionToRun which is the function which needs the most time to finish:

function createOnEditTrigger() { const ss = SpreadsheetApp.getActive(); ScriptApp.newTrigger('myInstallableEdit') .forSpreadsheet(ss).onEdit().create(); }

function  myInstallableEdit(e) { 
  const ss = e.source;
  const range = e.range;
  const activeSheet = range.getSheet();
  const row = range.getRow();
  const col = range.getColumn();
  if (activeSheet.getSheetName() != "General" || !(row >= 22 && row <=46 && col == 2)) return;
  ss.toast('Script start'); 
  // 2. Retrieve the values of the 1st row of sheet "Hide sheet", and the columns are hidden or shown by the values.
  // run functionToRun with a time-trigger 
   deleteTriggers();
   ScriptApp.newTrigger("functionToRun")
  .timeBased()
  .after(1)
  .create();
}

function functionToRun(){
  const ss = SpreadsheetApp.getActive();
  const only = ['Sheet1', 'Sheet2', 'Sheet5',
                'Sheet6', 'Sheet8', 'Sheet9',
                'Sheet10', 'Sheet11','Sheet12'].
  map(name=>ss.getSheetByName(name));
  only.forEach(sheet => {
               sheet
               .getRange(1, 1, 1, sheet.getLastColumn())
  .getValues()[0]
  .forEach((e, i) => sheet[e === true ? "showColumns" : "hideColumns"](i + 1));
  });
}

function deleteTriggers() {
  const triggers = ScriptApp.getProjectTriggers();
  for (let i = 0; i < triggers.length; i++) {
    if ( triggers[i].getHandlerFunction() == "functionToRun") {
      ScriptApp.deleteTrigger(triggers[i]);
    }
  }
}

I also made some optimizations in your current solution, but if these optimization don't work for you, then use your current code, and just adapt the logic of the time-driven trigger creation upon edits.

Marios
  • 26,333
  • 8
  • 32
  • 52
  • Thanks for taking a look into this, @Marios! I was also thinking about the installable trigger, however as far as I know it will prompt user to authorize the script first which won't really work for my case as spreadsheet users are not tech-savvy at all :( Anyway, I gave it a shot and for some reason the script won`t hide the columns while the execution transcript shows that it ran successfully – Kate Bedrii Feb 04 '21 at 11:40
  • @KateBedrii 1) [Installable triggers always run under the account of the person who created them](https://developers.google.com/apps-script/guides/triggers/installable). Meaning that if **you** create the installable trigger, it will also work for the other people who haven't created the `onEdit` trigger, but of course they need to have edit access to the edit range but also the range they interact. 2) I forgot to define variable `ss` in the `functionToRun` function. If you added it and it still does not work please go to the execution page and show me the error message. – Marios Feb 04 '21 at 11:47
  • yup, I caught that one :) Here's the error I`m getting in the executions: _Exception: This script has too many triggers. Triggers must be deleted from the script before more can be added. at myInstallableEdit(Code:28:4)_ – Kate Bedrii Feb 04 '21 at 11:55
  • @KateBedrii can you please delete all the triggers as you can [only have 20 per script](https://developers.google.com/apps-script/guides/services/quotas)? Go to the triggers page and delete every trigger. Of course there are scripts to do that like [this](https://stackoverflow.com/a/47217237/11225291) but if you think the manual way will be faster in your case, then choose that instead. – Marios Feb 04 '21 at 11:57
  • It looks like the issue was that each time the function triggers it creates a "FunctionToRun" trigger and once there 20+ of them, the script fils to run. I resolved this by adding the following at the end of the script: `var triggers = ScriptApp.getProjectTriggers(); for (var i = 0; i < triggers.length; i++) { var thf = triggers[i].getHandlerFunction() //String Returns the function that will be called when the trigger fires. // if it's the one I kept making above if (thf == "functionToRun") { ScriptApp.deleteTrigger(triggers[i]); }` – Kate Bedrii Feb 04 '21 at 13:13