I'm having a nightmare doing a lot of scenarios using Apps Script, but nothing works! I have a function that makes a GET request returns an array of cards. Now, sometimes I need this card refreshes again to fetch the new content.
function listTemplatesCards(){
var getAllTemplates = getTemplates();
var allTemplates = getAllTemplates.templates;
var theUserSlug = getAllTemplates.user_slug;
var templateCards = [];
//There are templates
if(allTemplates.length > 0){
allTemplates.forEach(function(template){
templateCards.push(templateCard(template, theUserSlug).build());
});
return templateCards;
}
}
This function is called on onTriggerFunction. Now, if I moved to another card and I wanted to back again to the root but in clean and clear way, I use this but it doesn't work:
//Move the user to the root card again
var refreshNav = CardService.newNavigation().popToRoot();
return CardService.newActionResponseBuilder().setStateChanged(true).setNavigation(refreshNav).build();
Simply, what I want is once the user clicks on Refresh button, the card refreshes/updates itself to make the call again and get the new data.
The only way I've found to do this is to always use a single card for the root. In the main function (named in the appscript.json onTriggerFunction), return only a single card, not an array of cards. You can then use popToRoot().updateCard(...) and it works.
I struggled with this for over a day, improving on Glen Little's answer so that its a bit more clear.
I have my root card to be refreshed defined in a funciton called: onHomepage.
I update the appscript.json manifest to set the homepageTrigger and onTriggerFunction to return the function that builds my root card.
"gmail": {
"homepageTrigger": {
"enabled": true,
"runFunction":"onHomepage"
},
"contextualTriggers":[
{
"unconditional":{},
"onTriggerFunction": "onHomepage"
}
]
}
Then it is as simple as building a gotoRoot nav button function that will always refresh the root page.
function gotoRootCard() {
var nav = CardService.newNavigation()
.popToRoot()
.updateCard(onHomepage());
return CardService.newActionResponseBuilder()
.setNavigation(nav)
.build();
}
As far as gmail addons are considered, cards are not refreshed but updated with new cards. And it is pretty simple.
//lets assume you need a form to be updated
function updateProfile() {
//ajax calls
//...
//recreate the card again.
var card = CardService.newCardBuilder();
//fill it with widgets
//....
//replace the current outdated card with the newly created card.
return CardService.newNavigation().updateCard(card.build());
}
A bad hack that works for my Gmail add-on:
return CardService.newActionResponseBuilder()
.setStateChanged(true) // this doesn't seem to do much. Wish it would reload the add-on
.setNotification(CardService.newNotification()
.setText('Created calendar event')
)
// HACK! Open a URL which closes itself in order to activate the RELOAD_ADD_ON side-effect
.setOpenLink(CardService.newOpenLink()
.setUrl("https://some_site.com/close_yoself.html")
.setOnClose(CardService.OnClose.RELOAD_ADD_ON))
.build();
The contents of close_yoself.html is just:
<!DOCTYPE html>
<html><body onload="self.close()"></body></html>
So, it looks like Google has considered and solved this issue for an ActionResponse which uses OpenLink, but not for one using Navigation or Notification. The hack above is definitely not great as it briefly opens and closes a browser window, but at least it refreshes the add-on without the user having to do so manually.
Related
I am trying to get the scope of gdrive to create a file from some form values with my addon.
To achieve this, I added a handler to the manifest and implemented the corresponding function.
"onItemsSelectedTrigger": {
"runFunction": "onDriveItemsSelected"
}
In the function I can use the following as ID of the first selected item. (I currently check Mimetype to keep it simple...)
createFolderID = e['drive']['selectedItems'][0].id;
Now I have two problems:
1.
When clicking a folder within the gdrive - the event function seems to await a built card as return value. I just want to use the selected folder (or ideally the folder where I am currently "in", via getparent?), without needing an additional card. If I return null, the card is created anyway above my addon card and shown with "No content shown for this message".
Is there away to avoid this?
2.
I need to inject the folder ID of the selected folder into my form (which I created with CardService at start of the addon). Declaring a "global" var does not seem to work,I assume that the cloud context will not preserve the variable value. The value is needed as parameter to a created document of my addon.
Can anyone point me into the right direction to store this folder Id until the user runs the addons action?
EDIT:
/**
* Get the selected folder to create the offer in
*/
function onDriveItemsSelected(e) {
// We check only the first selection
if (e['drive']['selectedItems'][0].mimeType == "application/vnd.google-apps.folder")
{
createFolderTitle = e['drive']['selectedItems'][0].title;
createFolderID = e['drive']['selectedItems'][0].id;
Logger.log(e['drive']['selectedItems'][0].title + " selected. ID: " + createFolderID)
PropertiesService.getUserProperties().setProperty('selectedFolderId', createFolderID);
}
}
The following snippet is contained in the manifest to link the function to selection events.
"drive": {
"homepageTrigger": {
"runFunction": "initForm"
},
"onItemsSelectedTrigger": {
"runFunction": "onDriveItemsSelected"
}
}
I use this for catching the selection event. But the card on the right side is then overlayed with an empty card with the message I already mentioned.
You can always pass a default parameter to a function.
Example:
function createForm(selectedFolderId = PropertiesService.getUserProperties().getProperty("selectedFolderId")) {
let form;
// Create the form
return form;
}
References:
PropertiesService
Update:
When you use OnItemsSelectedTrigger you must return an array of Card objects.
I have a very basic Google Workspace Add-on that uses the CalendarApp class to toggle the visabilty of a calendar’s events when a button is pressed, using the setSelected() method
The visabilty toggling works, but the change in only reflected in the UI when the page is refreshed. Toggling the checkbox manually in the UI reflects the change immediately without needing to refresh the page.
Is there a method to replicate this immediate update behaviour via my Workspace Add-On?
A mwe is below.
function onDefaultHomePageOpen() {
// create button
var action = CardService.newAction().setFunctionName('toggleCalVis')
var button = CardService.newTextButton()
.setText("TOGGLE CAL VIS")
.setOnClickAction(action)
.setTextButtonStyle(CardService.TextButtonStyle.FILLED)
var buttonSet = CardService.newButtonSet().addButton(button)
// create CardSection
var section = CardService.newCardSection()
.addWidget(buttonSet)
// create card
var card = CardService.newCardBuilder().addSection(section)
// call CardBuilder.call() and return card
return card.build()
}
function toggleCalVis() {
// fetch calendar with UI name "foo"
var calendarName = "foo"
var calendarsByName = CalendarApp.getCalendarsByName(calendarName)
var namedCalendar = calendarsByName[0]
// Toggle calendar visabilty in the UI
if (namedCalendar.isSelected()) {
namedCalendar.setSelected(false)
}
else {
namedCalendar.setSelected(true)
}
}
In short: Create a chrome extension
(2021-sep-2)Reason: The setSelected() method changes ONLY the data on server. To apply the effect of it, you need to refresh the page. But Google Workspace Extension "for security reason" does not allow GAS to do that. However in an Chrome Extension you can unselect the checkbox of visibility by plain JS. (the class name of the left list is encoded but stable for me.) I have some code for Chrome Extension to select the nodes although I didn't worked it out(see last part).
(2021-jul-25)Worse case: Default calendars won't be selected by getAllCalendars(). I just tried the same thing as you mentioned, and the outcome is worse. I wanted to hide all calendars, and I am still pretty sure the code is correct, since I can see the calendar names in the console.
const allCals = CalendarApp.getAllCalendars()
allCals.forEach(cal => {console.log(`unselected ${cal.setSelected(false).getName()}`)})
Yet, the principle calendar, reminder calendar, and task calendar are not in the console.
And google apps script dev should ask themselves: WHY DO PEOPLE USE Calendar.setSelected()? We don't want to hide the calendar on the next run.
In the official document, none of these two behaviour is mentioned.
TL;DR part (My reason for not using GAS)
GAS(google-apps-script) has less functionality. For what I see, google is trying to build their own eco-system, but everything achievable in GAS is also available via javascript. I can even use typescript and do whatever I want by creating an extension.
GAS is NOT easy to learn. The learning was also painful, I spent 4 hours to build the first sample card, and I can interact correctly with the opened event after 9 hours. The documentation is far from finished.
GAS is poorly supported. The native web-based code editor (https://script.google.com/) is not build for coding real apps, it loses the version control freedom in new interface. And does not support cross-file search. Instead of import, codes run from top to bottom in the list, which you need to find that by yourself. (pass along no extension, no prettier, I can tolerate these)
In comparison with other online JS code editors, like codepen / code sandbox / etcetera it does so less function. Moreover, VSCode also has a online version now(github codespaces).
I hope my 13 hours in GAS are not totally wasted. As least whoever read this can just avoid suffering the same painful test.
Here's the code(typescript) for disable all the checks in Chrome.
TRACKER_CAL_ID_ENCODED is the calendar ID of which I don't want to uncheck. Since it is not the major part of this question, it is not very carefully commented.
(line update: 2022-jan-31) Aware that the mutationsList.length >= 3 is not accurate, I cannot see how mutationsList.length works.
Extension:
getSelectCalendarNode()
.then(unSelectCalendars)
function getSelectCalendarNode() {
return new Promise((resolve) => {
document.onreadystatechange = function () {
if (document.readyState == "complete") {
const leftSidebarNode = document.querySelector(
"div.QQYuzf[jsname=QA0Szd]"
)!;
new MutationObserver((mutationsList, observer) => {
for (const mutation of mutationsList) {
if (mutation.target) {
let _selectCalendarNode = document.querySelector("#dws12b.R16x0");
// customized calendars will start loading on 3th+ step, hence 3, but when will they stop loading? I didn't work this out
if (mutationsList.length >= 3) {
// The current best workaround I saw is setTimeout after loading event... There's no event of loading complete.
setTimeout(() => {
observer.disconnect();
resolve(_selectCalendarNode);
}, 1000);
}
}
}
}).observe(leftSidebarNode, { childList: true, subtree: true });
}
};
});
}
function unSelectCalendars(selectCalendarNode: unknown) {
const selcar = selectCalendarNode as HTMLDivElement;
const calwrappers = selcar.firstChild!.childNodes; // .XXcuqd
for (const calrow of calwrappers) {
const calLabel = calrow.firstChild!.firstChild as HTMLLabelElement;
const calSelectWrap = calLabel.firstChild!;
const calSelcted =
(calSelectWrap.firstChild!.firstChild! as HTMLDivElement).getAttribute(
"aria-checked"
) == "true"
? true
: false;
// const calNameSpan = calSelectWrap.nextSibling!
// .firstChild! as HTMLSpanElement;
// const calName = calNameSpan.innerText;
const encodedCalID = calLabel.getAttribute("data-id")!; // const decodedCalID = atob(encodedCalID);
if ((encodedCalID === TRACKER_CAL_ID_ENCODED) !== calSelcted) {
//XOR
calLabel.click();
}
}
console.log(selectCalendarNode);
return;
}
There is no way to make a webpage refresh with Google Apps Script
Possible workarounds:
From the sidebar, provide users a link that redirects them to the Calendar UI webpage (thus a new, refreshed version of it will be opened)
Install a Goole Chrome extension that refreshes the tab in specified intervals
I am trying to embed GoodData dashboard to an iframe in my application and it works well but each tab on that dashboard has different number of reports on it and I'd like to make the iframe height dynamic based on the actual dashboard content.
Is there a way how to do it? Does GoodData somehow propagate the space needed to render the dashboard?
Thank you.
In fact there is a postMessage() sent event called 'ui.frameinfo' which you could use to detect the dashboard tab height (when using dashboard.html). It is sent every time the tab changes its height.
The following listener should print out the iframe's internal height:
window.addEventListener('message', function(e) {
var message;
try {
message = JSON.parse(e.data);
} catch (e) {
// valid messages are JSON
message = {};
}
// drop other than GoodData events
if (!message.gdc) return;
if (message.gdc.name === 'ui.frameinfo') {
console.log('frame height:', message.gdc.data.height);
}
}
Note that this is not an official feature (yet) and potentially subject to change.
I am not sure what the proper heading / title for this question should be. I am new to WinJS and am coming from a .NET webform and winclient background.
Here is my scenario. I have a navigation WinJS application. My structure is:
default.html
(navigation controller)
(settings flyout)
pages/Home.html
pages/Page2.html
So at the top of the default.js file, it sets the following variables:
var app = WinJS.Application;
var activation = Windows.ApplicationModel.Activation;
var nav = WinJS.Navigation;
It seems like I cannot use these variables anywhere inside my settings flyout or any of my pages:ready functions. They are only scoped to the default.js?
In the same regard, are there resources on the interwebs (links) that show how to properly share variables, events, and data between each of my "pages"?
The scenario that I immediately need to overcome is settings. In my settings flyout, I read and allow the user to optionally set the following application setting:
var applicationData = Windows.Storage.ApplicationData.current;
var localSettings = applicationData.localSettings;
localSettings.values["appLocation"] = {string set by the user};
I want to respond to that event in either my default.js file or even one of my navigation pages but I don't know where to "listen". My gut is to listen for the afterhide event but how do I scope that back to the page where I want to listen from?
Bryan. codefoster here. If you move the lines you mentioned...
var app = WinJS.Application;
var activation = Windows.ApplicationModel.Activation;
var nav = WinJS.Navigation;
...up and out of the immediate function, they'll be in global scope and you'll have access to them everywhere. That's one of the first things I do in my apps. You'll hear warnings about using global scope, but what people are trying to avoid is the pattern of dropping everything in global scope. As long as you control what you put in there, you're fine.
So put them before the beginning of the immediate function on default.js...
//stuff here is scoped globally
var app = WinJS.Application;
var activation = Windows.ApplicationModel.Activation;
var nav = WinJS.Navigation;
(function () {
//stuff here is scoped to this file only
})();
If you are saving some data and only need it in memory, you can just hang it off the app variable instead of saving it into local storage. That will make it available to the whole app.
//on Page2.js
app.myCustomVariable = "some value";
//on Page3.js
if(app.myCustomVariable == "some value") ...
Regarding your immediate need:
like mentioned in the other answer, you can use datachanged event.
Regards sharing variables:
If there are variables that you would like to keep global to the application, they can be placed outside the anonymous function like mentioned in the Jeremy answer. Typically, that is done in default.js. Need to ensure that scripts using the global variables are placed after the script defining the global variable - in default.html. Typically - such variable will point to singleton class. For example: I use it in one of my apps to store authclient/serviceclient for the backend service for the app. That way - the view models of the multiple pages need not create instance of the object or reference it under WinJS namespace.
WinJS has also concept of Namespace which lets you organize your functions and classes. Example:
WinJS.Namespace.define('Utils.Http',
{
stringifyParameters: function stringifyParameters(parameters)
{
var result = '';
for (var parameterName in parameters)
{
result += encodeURIComponent(parameterName) + '=' + encodeURIComponent(parameters[parameterName]) + '&';
}
if (result.length > 0)
{
result = result.substr(0, result.length - 1);
}
return result;
},
}
When navigating to a page using WinJS.Navigation.navigate, second argument initialState is available as options parameter to the ready event handler for the page. This would be recommended way to pass arguments to the page unless this it is application data or session state. Application data/session state needs to be handled separately and needs a separate discussion on its own. Application navigation history is persisted by the winjs library; it ensures that if the app is launched again after suspension - options will be passed again to the page when navigated. It is good to keep the properties in options object as simple primitive types.
Regards events:
Typically, apps consume events from winjs library. That can be done by registering the event handler using addEventListener or setting event properties like onclick etc. on the element. Event handlers are typically registered in the ready event handler for the page.
If you are writing your own custom control or sometimes in your view model, you may have to expose custom events. Winjs.UI.DOMEventMixin, WinJS.Utilities.createEventProperties can be mixed with your class using WinJS.Class.mix. Example:
WinJS.Class.mix(MyViewModel,
WinJS.Utilities.createEventProperties('customEvent'),
WinJS.UI.DOMEventMixin);
Most often used is binding to make your view model - observable. Refer the respective samples and api documentation for details. Example:
WinJS.Class.mix(MyViewModel,
WinJS.Binding.mixin,
WinJS.Binding.expandProperties({ items: '' }));
Here is what I ended up doing which is kinda of a combination of all the answers given:
Created a ViewModel.Settings.js file:
(function () {
"use strict";
WinJS.Namespace.define("ViewModel", {
Setting: WinJS.Binding.as({
Name: '',
Value: ''
}),
SettingsList: new WinJS.Binding.List(),
});
})();
Added that file to my default.html (navigation container page)
<script src="/js/VMs/ViewModel.Settings.js"></script>
Add the following to set the defaults and start 'listening' for changes
//add some fake settings (defaults on app load)
ViewModel.SettingsList.push({
Name: "favorite-color",
Value: "red"
});
// listen for events
var vm = ViewModel.SettingsList;
vm.oniteminserted = function (e) {
console.log("item added");
}
vm.onitemmutated = function (e) {
console.log("item mutated");
}
vm.onitemchanged = function (e) {
console.log("item changed");
}
vm.onitemremoved = function (e) {
console.log("item removed");
}
Then, within my application (pages) or my settings page, I can cause the settings events to be fired:
// thie fires the oniteminserted
ViewModel.SettingsList.push({
Name: "favorite-sport",
Value: "Baseball"
});
// this fires the itemmutated event
ViewModel.SettingsList.getAt(0).Value = "yellow";
ViewModel.SettingsList.notifyMutated(0);
// this fires the itemchanged event
ViewModel.SettingsList.setAt(0, {
Name: "favorite-color",
Value: "blue"
});
// this fires the itemremoved event
ViewModel.SettingsList.pop(); // removes the last item
When you change data that needs to be updated in real time, call applicationData.signalDataChanged(). Then in the places that care about getting change notifications, listen to the datachanged on the applicationData object. This is also the event that is raised when roaming settings are synchronized between computers.
I've found that many times, an instant notification (raised event) is unnecessary, though. I just query the setting again when the value is needed (in ready for example).
I am trying to make a library, went well so far, but after adding a few functions it went bad.
When I run the script form the editor it is written in the script works. But when I try to test it the script cannot recognize the server handlers, giving an error: Unknown macro handler_function_name
I checked, all the names in the handlers correspond to names of functions. I read that some people had problems because the code was in different files, moved all the code in the same file the problem is still there.
It does not behave like that for all the handlers...
What else could be the reason for this?
edit:
The app creates additional panels during as a response to "clicks". Handlers of elements on those panels are the ones who's macros (that is handler functions) the app is not able to "find".
How can this be solved?
(except for the solution to put all the panels in the original panel and then change visibility, this works as far as handlers go but raises other problems)
So to put some code here, this is very very simple code...
function notWorkingGUI(){
var app=UiApp.createApplication();
var appPanel=app.createVerticalPanel().setId("appPanel");
var handler1=app.createServerHandler("handlerFunction1").addCallbackElement(appPanel);
var firstButton=app.createButton("Button 1", handler1);
appPanel.add(firstButton);
app.add(appPanel);
SpreadsheetApp.getActive().show(app);
}
function handlerFunction1(e){
var app=UiApp.getActiveApplication();
var appPanel2=app.createVerticalPanel().setId("appPanel2").setStyleAttribute("zIndex", 0).setStyleAttribute("position", "fixed");
var handler2=app.createServerHandler("handlerFunction2").addCallbackElement(appPanel2);
var secondButton=app.createButton("Button 2", handler2);
var label=app.createLabel("This should get visible after the click").setId("label").setVisible(false);
appPanel2.add(secondButton).add(label);
app.add(appPanel2);
return app;
}
function handlerFunction2(e){
var app=UiApp.getActiveApplication();
app.getElementById("label").setVisible(true);
return app;
}
This will work as expected when executed from the editor in which it is written, that is it will show firstButton then secondButton and finaly the label, however if it would be published as a library and invoked from an other script it would only recognise functionHandler1, that is show firstButton, secondButton but after a click on the secondButton an error message will be seen.
However if the script would be written like this:
function workingGUI(){
//previous first part
var app=UiApp.createApplication();
var appPanel=app.createVerticalPanel().setId("appPanel");
var handler1=app.createServerHandler("handlerFunction1a").addCallbackElement(appPanel);
var firstButton=app.createButton("Button 1", handler1);
//previous second part
var appPanel2=app.createVerticalPanel().setId("appPanel2").setStyleAttribute("zIndex", 0).setStyleAttribute("position", "fixed");
var handler2=app.createServerHandler("handlerFunction2a").addCallbackElement(appPanel2);
var secondButton=app.createButton("Button 2", handler2).setId("button2");
appPanel.add(firstButton);
app.add(appPanel);
SpreadsheetApp.getActive().show(app);
}
function handlerFunction1a(e){
var app=UiApp.getActiveApplication();
var label=app.createLabel("This should get visible after the click").setId("label").setVisible(false);
app.getElementById("appPanel2").add(app.getElementById("button2")).add(label);
app.add(app.getElementById("appPanel2"));
return app;
}
function handlerFunction2a(e){
var app=UiApp.getActiveApplication();
app.getElementById("label").setVisible(true);
return app;
}
Note that all handlers must be defined in the main function, meaning that also all the elements using those handlers and all the callback elements have to be defined here.
Then it would work even as a library, however for some reason this makes the script much much slower even for such a simple example.
The issue is here:
http://code.google.com/p/google-apps-script-issues/issues/detail?id=1346
It is calling the local code rather than the library code.
I wonder if it is still slow if you add a stub function in the local code?
i.e.
function runthis() {
library.createGUI();
}
function myevent() {
library.myevent();
}
I worked around this problem, it makes the script a bit slower but if you define all the handlers (that implies all the UI elements) in the original function it will work.