2

This is my first post and I have searched for days and can not find a solution for this problem.

I have a custom menu in sheets that pops up a .showModalDialog html. A user fills that out with information and clicks submit. This runs a function in the back end that creates folders/files and adds the user data to various sheets etc.

All this is working I have a Ui.alert that I am using to check the data entered is correct and for some reason the function is being trigger twice and the UI.alert is popping up again as a result. I have a fail safe that checks if one of the fields exists so it doesn't write again but the pop up is a really bad user experience.

Any help would be much appreciated. Function to create custom menu:

function onOpen() { 
  SpreadsheetApp.getUi() 
      .createMenu('TOA - Menu')
      .addItem('Add New Account', 'addAccount')
      .addItem('Update Brand', 'updateBrand')
      .addItem('Go Live', 'goLive')
      .addToUi();
}

Function to bring up form:

function addAccount() {
  const html = HtmlService.createHtmlOutputFromFile('newAccount')
      .setTitle('Add New Account')
      .setWidth(1000)
      .setHeight(800);;
  SpreadsheetApp.getUi() 
      .showModalDialog(html, 'Add New Account');
}

Code for the form:

<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <style>
      #itemDisplay {
        display: none;
      }
      #modDisplay {
        display: none;
      }
      #priceDisplay {
        display: none;
      }
      #businessTypeDisplay {
        display: none;
      }
    </style>

  </head>
  <body>
    <h1>
        Add New Account
    </h1>

  <form id="myForm" onsubmit="handleNewAccountFormSubmit(this);"> 
    <div>
    <label for="newBrand">Brand:</label>
    <input name="newBrand" type="text" placeholder="Brand Name" required>
    </div>

    <div>
    <p>Location</p>
    <label for="country">Country:</label>
    <select name="country" id="" onchange="" required>
      <option value="" disabled selected>Select Country</option> 
      <option value="US">US</option>     
    </select>

    <label for="state">State/Province:</label>
    <input name="state" type="text" placeholder="State or province" required>

    <label for="city">City:</label>
    <input name="city" type="text" placeholder="City" required>


    <div>
    <label for="businessType">Business Type:</label>
    <select name="businessType" id="businessTypeSelect" onchange="businessOtherDisplay(this.value);" required>
      <option value="" disabled selected>Select Request Reason</option> 
      <option value="americanDiner">American Diner</option>
      <option value="pizzaParlor">Pizza Parlor</option>
      <option value="coffeeShop">Coffee Shop</option>
      <option value="candyShop">Candy Store</option>
      <option value="iceCreamParlor">Ice Cream Parlor</option>
      <option value="burgerShop">Burger Shop</option>
      <option value="otherNon_AmericanDiner">Other non-American Diner (Foreign servings)</option>
      <option value="other">Other (not listed above)</option>
    </select>
    </div>

    <div id="businessTypeDisplay">
    <label for="businessTypeOther">Business Type Other:</label>
    <input name="businessTypeOther" type="text" placeholder="Business type if not listed above">
    </div>

    <div>
    <label for="integration">Integration:</label>
    <select name="integration" required>
      <option value="" disabled selected>Select Request Reason</option> 
      <option value="square">Square</option>
      <option value="clover">Clover</option>
      <option value="cloverPilot">Clover Pilot</option>
      <option value="stripe">Stripe</option>
      <option value="gPay">GPAY</option>
      <option value="others" >Others</option>
    </select>
    </div>

    <label for="menuSource">File Attachment/Source:</label>
    <input name="menuSource" type="text" placeholder="Path to menu" required url>

    <div>
    <p>Do you need an item hidden/disabled?</p>
    <label for="yes">Yes</label>
    <input name="disableItemOption" type="radio" value="yes" onclick="showItem()">
    <label for="no">No</label>
    <input name="disableItemOption" type="radio" value="no" checked onclick="hideItem()">
    </div>

    <div id="itemDisplay">
    <label for="itemDisable">Which item(s) should be disabled?</label>
    <textarea id="disabledItem" name="itemDisable" cols="40" rows="5"></textarea>
    </div>

    <div>
    <p>Do you need a modifier hidden/disabled?</p>
    <label for="yes">Yes</label>
    <input name="disableModOption" type="radio" value="yes" onclick="showMod()">
    <label for="no">No</label>
    <input name="disableModOption" type="radio" value="no" checked onclick="hideMod()">
    </div> 

    <div id="modDisplay">
    <label for="modDisable">Which modifier(s) should be disbaled?</label> 
    <textarea id="disabledMod" name="modDisable" cols="40" rows="5"></textarea>
    </div>   

    <div>
    <p>Do you need to update a price?</p>
    <label for="yes">Yes</label>
    <input name="updatePrice" type="radio" value="yes" onclick="showPrice()">
    <label for="no">No</label>
    <input name="updatePrice" type="radio" value="no" checked onclick="hidePrice()">
    </div>

    <div id="priceDisplay">
    <label for="priceUpdate">Which item/modifier needs a price update?</label>
    <textarea id="updatedPrice" name="priceUpdate" cols="40" rows="5" priceUpdate></textarea>
    </div>  

    <div>
    <label for="otherUpdates">Any other information needed on the menu?</label>  
    <input name="otherUpdates" type="text" placeholder="List other instructions here">
    </div>

    <div>
    <label for="specialInstructions">Are there special instructions/notes for this brand?</label>  
    <input name="specialInstructions" type="text" placeholder="List special instructions here">
    </div>

    <input id="submitButton" type="submit" value="Submit">
    <input type="button" value="Cancel" onclick="google.script.host.close()">
    </div>
  </form>  
  <script>

      function handleNewAccountFormSubmit(formObject) {
        document.getElementById('submitButton').disabled=true;
        google.script.run.withSuccessHandler().processNewAccountForm(formObject);        
      }

      function disableSubmit() {
       document.getElementById('submitButton').disabled=true;
       document.getElementById('submitButton').value='Sending...';
      }

      function showItem(){
        document.getElementById('itemDisplay').style.display ='block';         
        document.getElementById('disabledItem').required = true;         
      };
      function hideItem(){
        document.getElementById('itemDisplay').style.display = 'none';
        document.getElementById('disabledItem').required = false; 
      };
      function showMod(){
        document.getElementById('modDisplay').style.display ='block';
        document.getElementById('disabledMod').required = true; 
      };
      function hideMod(){
        document.getElementById('modDisplay').style.display = 'none';
        document.getElementById('disabledMod').required = false; 
      };

       function showPrice(){
        document.getElementById('priceDisplay').style.display ='block';
        document.getElementById('updatedPrice').required = true; 
      };
      function hidePrice(){
        document.getElementById('priceDisplay').style.display = 'none';
        document.getElementById('updatedPrice').required = false; 
      };

      function businessOtherDisplay(value) {

       if(value === "other") {
          document.getElementById('businessTypeDisplay').style.display = 'block';
        } else {
        document.getElementById('businessTypeDisplay').style.display = 'none';
        };
      };

    </script>
  </body>
</html>

And the code to handle the logic

function processNewAccountForm(formObject) {  

  const ui = SpreadsheetApp.getUi();
  const ass = SpreadsheetApp.getActiveSpreadsheet();
  const ss = ass.getActiveSheet();
  const timestamp = Utilities.formatDate(new Date(), "GMT+8", "MM/dd/yyyy HH:mm:ss");
  const userEmail = Session.getActiveUser().getEmail();
  const brandName = formObject.newBrand;

// Add alert to check data entered is correct
  const response = ui.alert('Please confirm the following information is correct:', 
                            ' :  ' + formObject.newBrand + 
                            '\n:  ' + formObject.country +
                            '\n:  ' + formObject.state + 
                            '\n:  ' + formObject.city +                       
                            '\n :  ' + formObject.businessType +
                            '\n:  ' + formObject.integration + 
                            '\n :  ' + formObject.menuSource +
                            '\n     /?:  ' + formObject.disableItemOption +
                            '\n ()   ?:  ' + formObject.itemDisable +
                            '\n     /?:  ' + formObject.disableModOption +  
                            '\n    ?:  ' + formObject.modDisable +  
                            '\n      ?:  ' + formObject.updatePrice +  
                            '\n /    ?:  ' + formObject.priceUpdate + 
                            '\n      ?: ' + formObject.otherUpdates + 
                            '\n   /   ?:  ' + formObject.specialInstructions                            
                            , ui.ButtonSet.YES_NO);

    if(response === ui.Button.YES) {
       var lock = LockService.getScriptLock();
  lock.waitLock(60000);
  try {   
    const brandColumn = ss.getRange('D:D');
    const brandValues = brandColumn.getValues();
    let i = 1;
    //      Check for exisiting brand name
    for(i=1; i < brandValues.length; i++) {
        if(brandValues[i].toString().toLowerCase().trim() == brandName.toString().toLowerCase().trim() && ss.getRange(i+1,5).getValue() == 'New Brand'){       
        ui.alert("Brand name already created");
        return;
        }       
    };  
//  Create folder and PDF with build instructions
      const parentFolder = DriveApp.getFolderById("RemovedfolderID");// how does this work with Shared drives? Create and move?
    //      const parentFolder = DriveApp.getFolderById("RemovedfolderID"); < ---- Team drive ID (notworking..)  My folder ->  RemovedfolderID

      const newFolder = parentFolder.createFolder(brandName);
      const docFile = newFolder.createFile(brandName+'.pdf', 
                            ' :  ' + formObject.newBrand + 
                            '\n:  ' + formObject.country +
                            '\n:  ' + formObject.state + 
                            '\n:  ' + formObject.city +                       
                            '\n :  ' + formObject.businessType +
                            '\n:  ' + formObject.integration + 
                            '\n :  ' + formObject.menuSource +
                            '\n     /?:  ' + formObject.disableItemOption +
                            '\n ()   ?:  ' + formObject.itemDisable +
                            '\n     /?:  ' + formObject.disableModOption +  
                            '\n    ?:  ' + formObject.modDisable +  
                            '\n      ?:  ' + formObject.updatePrice +  
                            '\n /    ?:  ' + formObject.priceUpdate + 
                            '\n      ?: ' + formObject.otherUpdates + 
                            '\n   /   ?:  ' + formObject.specialInstructions, 
                               MimeType.PDF);       
      const fileURL = docFile.getUrl();

  //  add header row to spreadsheet 
            //   Create Spreadsheet in Brand folder. Activity log.
      const name = brandName + " Activity Log";
      const id = newFolder.getId();
      const resource = {
        title: name,
        mimeType: MimeType.GOOGLE_SHEETS,
        parents: [{id: id}]      
      };

      const fileJson = Drive.Files.insert(resource);
      const fileId = fileJson.id;

      const lastRow = ss.getLastRow();

        const newEntry = [
         lastRow, 
         timestamp, 
         timestamp, 
         formObject.newBrand, 
         'New Brand', 
         formObject.businessType, 
         formObject.integration, 
         '=HYPERLINK("'+formObject.menuSource+'")', 
         userEmail,
         fileURL,
         ,
         ,
         ,
         ,
         formObject.city,
         formObject.state,
         formObject.country,
         fileId         

    ];   

    const newSheet = SpreadsheetApp.openById(fileId);    
    const sheetRange = newSheet.getSheetByName("Sheet1").getRange(1,1,1,18);  
    const headers = [
      ['',
       '   ',
       ' ',
       ' ',
       ' ',
       ' ',
       '',
       ' ',
       ' :',
       ' ',
       ' ?',
       ' ',
       ' ',
       '    ',
       '',
       '/',
       '',
       ' ']
      ];

     sheetRange.setValues(headers);

//    Add data to last row in main tracker  
      ss.appendRow(newEntry);

//  Copy data to spreadsheet brand 
      const activitySheet = newSheet.getSheetByName("Sheet1") 
      activitySheet.appendRow(newEntry);
//    Flush changes before releasing lock
      SpreadsheetApp.flush();
  } catch(e) {
    ui.alert("System is Busy, Please try again in a moment.");
    return
    } finally {

    lock.releaseLock();

    return
  }

  } else {
//     action to take if info is incorrect? or No is clicked 
  };  

};

I know that multiple triggers have been a known issue as per posts from Cooper and others here and here but I can't seem to be able to adopt them for my solution.

Thanks in advance for any ideas.

  • It's not a known issue. The issues you linked are explicitly due to Google forms submit and not due to a webapp form submit. May be the user is hitting the submit button twice? – TheMaster Mar 11 '20 at 12:00
  • @TheMaster Oh okay. Sorry I thought it was similar. Sorry i am very new to apps scripts and programming in general. Maybe I can add a function to disable the button? Thoughts? – Noah Liberty Mar 11 '20 at 12:06
  • I tried onsubmit="handleNewAccountFormSubmit(this); disableSubmit()" calling the function: function disableSubmit() { document.getElementById('submitButton').disabled=true; document.getElementById('submitButton').value='Sending...'; } And that didnt work for me. Still getting the pop up. Is there a better way to disable the button? – Noah Liberty Mar 11 '20 at 12:26
  • Do it inside `handleNewAccFS()` function before calling gsrun – TheMaster Mar 11 '20 at 12:57
  • @TheMaster I tried that and no joy. Everything runs fine with that code...just twice. It isn't every time its about 50%. – Noah Liberty Mar 11 '20 at 13:10
  • Try adding a `alert('inside handleNewAccountFormSubmit')` client side. This way we can decide whether the problem is in client or server side. If the alert is called twice then submit button is hit twice for whatever reason. – TheMaster Mar 11 '20 at 14:37
  • @TheMaster Just the one alert so something else is triggering it twice. I'm not sure the Lock is working either as I am fairly new to Lock Service. – Noah Liberty Mar 11 '20 at 15:18
  • Could you downgrade to rhino engine from v8 and try? See [tag info page](https://stackoverflow.com/tags/google-apps-script/info) for more details. – TheMaster Mar 11 '20 at 16:05
  • Alternatively remove lock and try again. – TheMaster Mar 11 '20 at 16:13
  • @TheMaster I did and converted code. Still is popping up twice. It's still firing twice for some strange reason. I have checked for duplication of the function loops basically everything I can... – Noah Liberty Mar 11 '20 at 16:25
  • Have you tried removing all locks? – TheMaster Mar 11 '20 at 17:04
  • Have you noticed that there is nothing in your form? – Cooper Mar 11 '20 at 20:10
  • I did. I removed all locks and used rhino. I do need the locks as there will be a team of 8-10 using this solution and that could scale. @Cooper haha I did notice that haha! I removed it as it is work data(not so sensitive though). I can edit and add it in but the problem isn't that it's not running it's that it is running twice. Do you think that would help? – Noah Liberty Mar 12 '20 at 00:58
  • How about this: `
    .....
    `
    – Cooper Mar 12 '20 at 01:07
  • Hi @NoahLiberty could you share a sample sheet so we can actually copy and test your problem? – Raserhin Mar 12 '20 at 09:40
  • @Cooper Hey thanks for the help. I tried that both using an aux function and stand alone and it didnt work. If I remove the function in the
    tag and trigger it straight from the button it wont fire at all. If I use both or .parentNode in the onsubmit it still fires twice. I am so confused! I have a demo coming up tomorrow haha and while it's not the end of the world would be nice to have it sorted.
    – Noah Liberty Mar 12 '20 at 12:12
  • @Raserhin I can not as We have DLP here at work.. I wish I could. Let me see if I can include more info and details. – Noah Liberty Mar 12 '20 at 12:14
  • @Rasherhin I have added more information. Everything goes in .gs except the .html for the form – Noah Liberty Mar 12 '20 at 12:24

1 Answers1

2

These are the changes I'd make to the dialog:

 <input id="btn1" type="button" value="Submit" onClick="handleNewAccountFormSubmit(this.parentNode);" />
    <input type="button" value="Cancel" onclick="google.script.host.close()">
    </div>
  </form>  
  <script>

      function handleNewAccountFormSubmit(formObject) {
        document.getElementById('btn1').disabled=true;
        google.script.run.processNewAccountForm(formObject);        
      }

I'm not saying that your way is wrong. That's just the way I would try to do it. But realistically it's a fairly large dialog and your just going to have to dig in and figure it out. If I were doing it and I was having a lot of problems I'd probably start with a simpler version and get it to work and then slowly add more features.

This is actually a fairly difficult time two write code in the middle of a transition between two runtimes. I've just noticed that I've lost my content assist in some areas in ES5 but they now work in ES6 so things can be difficult to deal with.

halfer
  • 19,824
  • 17
  • 99
  • 186
Cooper
  • 59,616
  • 6
  • 23
  • 54
  • I was unable to get rid of the second pop up. I ended up pivoting to a Web App as I needed to have people running the script and writing to protected cells. I have a new question up regarding that [here](https://stackoverflow.com/questions/60771819/response-and-error-handling-in-google-apps-scripts-dopost-withfailurehandler) if you are interested in having a look. – Noah Liberty Mar 20 '20 at 09:52