"SpreadsheetApp.getUi() cannot be called from this context"
Asked Answered
A

4

18

In a Google Sheets spreadsheet, I want to show a modal dialog created from HTML, then run a function, then close that HTML prompt automatically.

The dialog should stay until the function finishes executing, then automatically disappear.

This process has to be repeated every 3 hours, and the script needs to run as me (as I have edit permissions that other users do not) so simple triggers probably won't work (I've read that you must create an installable trigger if you want the function to run as you and not whoever the current user is at the given time)

I currently have:

  1. A .gs function Magic_Telling, that creates a modal dialog by using an HTML file
  2. An HTML file, Prompt_Styling, that contains the css / html styling for the prompt. This HTML file then calls a .gs function All_In that processes the rows

My code:

Magic_Telling Creates the modal dialog from HTML file.

function Magic_Telling() {
var UI = SpreadsheetApp.getUi();
var newline = '\n'
// Display a modal dialog box with custom HtmlService content.
var htmlOutput = HtmlService.createHtmlOutputFromFile('PromptStyling')
    .setSandboxMode(HtmlService.SandboxMode.IFRAME)
    .setWidth(300)
    .setHeight(100);
UI.showModalDialog(htmlOutput, ' ');
}


Prompt_Styling HTML file for styling prompt + script that runs the function All_In that will process rows

<html>
<head>
// some irrelevant stuff here
</head>

<script>
window.onload = function() {    
google.script.run
    .withSuccessHandler(closeDialog)
    .All_In();
    };

window.closeDialog = function() {
    google.script.host.close();
    };

</script>
</html>

All_In Function to process rows

function All_In() {

UnlockRowBlocks();
UnhideRowBlocks();
LockRowBlocks();
HideRowBlocks();

}

When I run MagicTelling from the script editor, it works beautifully. The entire sequence executes (prompt shown, All_In executed, prompt disappeared). Perfect.

I then created an installable trigger by going to Script Editor > Resources > Current project's triggers and added a trigger to run Magic_Telling every 3 hours. (I presume this is an "installable trigger")

But I get this error message:

Cannot call SpreadsheetApp.getUi() from this context.

...when the function reaches the first line of Magic_Telling

What should I do to get around this?

Allelomorph answered 20/3, 2016 at 17:36 Comment(0)
P
25

Ui Dialogs can not be called by time triggered functions, they have to be triggered by a user action, that's to say a click on a menu item or some sort of button that calls the function showing the UI.

Pentose answered 20/3, 2016 at 17:47 Comment(4)
Thanks @Sergeinsas that was helpful information that I didn't know. Couple of follow-ups: 1. I have to manually click something every 3 hours if I want this to work!? Can you think of any workaround that will avoid this? 2. If I understand you correctly, if I create a button and assign Magic_Telling to it, then the three functions should at least execute? (but of course it would be done manually, which sucks)Allelomorph
You could use a sidebar with client side JavaScript that could update every 3 hours automatically... this would be less obvious than a modal dialog but could meet your requirements.Pentose
Ah, thanks for that (though I have no clue how to do that). How about this for a potential workaround? Can I store a 0 value somewhere in spreadsheet cell A1, store a timestamp in cell A2, then calculate (using a formula) the duration between now() and the timestamp value, and if that duration exceeds 3 hours, then edit the 0 value to 1? Then run the function Magic_Telling every time the value in cell A1 == 1? Can you think of any more elegant way to do this? P.S. - Checked out your book, looks very usefulAllelomorph
Wanted to add to the discussion. I was getting this error, but also when triggered from the context menu I got an error like no permission to call showSidebar. This is kinda unrelated but I ended up in this thread multiple times. For me the solution was this: I was setting explicit scopes in appsscript.json and needed to add https://www.googleapis.com/auth/script.container.ui to the oauthScopes array. Thought it might help someone!Advance
B
6

If this error happened then check the triggers or close the script editor tab and refresh google sheets then open the script project .

Make sure that the Apps Script is bound script and not standalone script . or the getUi() won't work .

Burette answered 28/12, 2021 at 17:31 Comment(3)
Your answer could be improved with additional supporting information. Please edit to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers in the help center.Damp
Your answer could be improved with additional supporting information. Please edit to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers in the help center.Wanting
Just to add to this information... close the script editor tab because it might have been detached from your associated spreadsheet tab, and reopened the script again.Selle
C
5

Simple case getting the 'Cannot call SpreadsheetApp.getUi() from this context.'-Error for everybody who just got started with scripting using the Tools > Script editor Menu.

In this case you work with a standalone script only, meaning, your script is just attached to one document or spreadsheet. The standalone script allows i.e. to simply call doc = DocumentApp.getActiveDocument(), the active Document the script is attached to.

It happened to me that I used var ui = SpreadsheetApp.getUi(); while getting the ERROR message in quest here... and it took me hours to find out what went wrong with this simple line as I went down all the way to Oauth Scopes and the Developer Console.

So, it might be helpful for some beginners to know that I actually used the var ui = SpreadsheetApp.getUi(); within a Document-Script. Very clear I got the error, but ... Hope this is helpful for some simple scripters!

PS. I hope it is needless to mention that using a var ui = DocumentApp.getUi(); within a SpreadSheet will produce a similar Error message.

Conventual answered 1/7, 2021 at 3:13 Comment(3)
It'd be good if you formatted code with backticks like this var ui to make it easier to read your answer.Swagsman
In this case the script is a bounded or contained script, not a standalone script.Teleutospore
For those who didn't undersand: the function should be DOCUMENTApp, and not SPREADSHEETApp. This helped me! Took hours here too. Thanks!Chloroplast
T
1

If you need to periodically show a message or notification to a user, instead of using a time-driven trigger for calling the Class Ui, use a sidebar and client-side code, i.e. setTimeout in a recursive function, to call a server side function that calls the Class Ui. You might also show the message in the sidebar.

In the case of spreadsheets another option might be use Spreadsheet.toast. Another option is to edit the document. This might work in small documents where the edited section is shown all the time.


When running function calling Class Ui it will fail if the corresponding document editor UI and the Google Apps Script Editor hasn't a connection between an active document, form, slide or spreadsheet and the script.

Time-Driven triggers have a connection with the container / bounded file but there isn't one with the document editor UI, no matter if the script was opened from the document editor UI at the time that the time-driven trigger was executed.

This error will happen too when calling Class Ui from a standalone project because there is no connection with a document editor user interface. While the Google Apps Script editor might look as a "document editor", Class Ui doesn't work with it as the Class Ui can only be called from DocumentApp, FormApp, SlidesApp and the SpreadsheetApp classes.

Below is a simple sample. It adds a custom menu used to open a sidebar. The sidebar holds the client-side code that will open a modal dialog every 10 seconds for 3 times. The client-side code has a timer function that holds a setTimeout which calls the controller function which calls the server-side function and updates a counter used to limit the number of times that the server-side function will be executed. Steps to use this code:

  1. Create a new spreadsheet
  2. Click Extensions > Apps Script
  3. Remove the default content from default .gs file and add the Code.gs code.
  4. Add two html files and name them sidebar and 'modalDialog.
  5. Add the html sidebar.html and modalDialog.html to the corresponding files.
  6. Run onOpen or reload the spreadsheet and click My Menu > Show sidebar (reloading the spreadsheet will will close the script editor) and authorize the script.
  7. On the spreadsheet, click My Menu > Show sidebar

Code.gs

/**
 * Adds a custom menu to show the sidebar
 */
function onOpen(e) {
  SpreadsheetApp.getUi()
    .createMenu('My Menu')
    .addItem('Show Sidebar', 'showSidebar')
    .addToUi()
}

/**
 * Shows the sidebar
 */
function showSidebar() {
  const myHttpOutput = HtmlService.createHtmlOutputFromFile('sidebar')
    .setTitle('My Sidebar')
  SpreadsheetApp.getUi().showSidebar(myHttpOutput);
}

/**
 * Shows the modal dialog. To be called from client-side code.
 */
function showModalDialog() {
  const myHttpOutput = HtmlService.createHtmlOutputFromFile('modalDialog')
    .setWidth(400)
    .setHeight(150)
  SpreadsheetApp.getUi().showModalDialog(myHttpOutput, 'My Modal'); 
}

sidebar.html

<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
  </head>
  <style>
    .error {
      color : red;
      background-color : pink;
      border-style : solid;
      border-color : red;
    }
  </style>  
  <body>
    <h1>Timed Modal Dialog Controller</h1>
    <div id="sidebar-status">
      The modal dialog will be shown after the specifed timeout interval.
    </div>
    <script>

      const defaultInterval = 10000;
      let count = 0;
      
      /**
       * Run initializations on sidebar load.
       */
      (() => {

        timer(); 

      })();

      /**
       * Calls the controller function at the given interval.
       *
       * @param {Number} interval   (optional) Time in ms between polls. Default is 2s (2000ms)
       *                            
       */
      function timer(interval) {
        interval = interval || defaultInterval;
        setTimeout(() => {   
          controller();
        }, interval);
      };

      /**
       * Calls the server side function that uses Class Ui to
       * show a modal dialog.
       */  
      function controller(){
        /** Maximum number of iterations */
        const max = 3; 
    
        if(count < max){
          google.script.run
            .withSuccessHandler(() => {
              const msg = `Counter: ${++count}`;
              showStatus(msg);
              timer();
              })
            .withFailureHandler(error => {
                const msg = `<div class="error">${error.message}</div>`;
                showStatus(msg);
              })
            .showModalDialog();
        } else {
          const msg = `<p>Maximum reached.</p>`;
          showStatus(msg)
        }
      }
      /**
       * Displays the given status message in the sidebar.
       *
       * @param {String} msg The status message to display.
       */
      function showStatus(msg) {
        const status = document.querySelector('#sidebar-status');
        status.innerHTML = msg;
      }
    </script>  
  </body>
</html>

modalDialog.html

<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
  </head>
  <body>
    <h1>Attention!</h1>
    <p>It's time to take a break.</p>
  </body>
</html>

It can be easily adapted to be used on a document, form or presentation.

If the manifest is being edited manually, please be sure to include https://www.googleapis.com/auth/script.container.ui in the list of OAuth scopes besides other required according to the type of document to which the script will be bounded.

If you need to work with a standalone script, instead of a bounded script, you should use it as and Editor add-on.

Reference

Related

Teleutospore answered 17/9, 2022 at 14:48 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.