I am trying to develop google calendar add-on like zoom meeting.
In appsscript.json file, below code is there.
"calendar": {
"conferenceSolution": [{
"onCreateFunction": "createConference",
"id": "1",
"name": "Meeting",
"logoUrl": "https://companyxyz.com/images/logo192.png"
}],
"eventOpenTrigger": {
"runFunction": "buildSimpleCard"
},
"currentEventAccess": "READ_WRITE"
}
}
In Calendar.gs, below code is there.
function createConference(e) {
Logger.log(e);
var dataBuilder = ConferenceDataService.newConferenceDataBuilder();
return dataBuilder.build();
}
/**
* Build a simple card with a button that sends a notification.
* This function is called as part of the eventOpenTrigger that builds
* a UI when the user opens a Calendar event.
*
* #param e The event object passed to eventOpenTrigger function.
* #return {Card}
*/
function buildSimpleCard() {
var buttonAction = CardService.newAction()
.setFunctionName('onSaveConferenceOptionsButtonClicked')
.setParameters(
{'phone': "1555123467", 'adminEmail': "joyce#example.com"});
var button = CardService.newTextButton()
.setText('Add new attendee')
.setOnClickAction(buttonAction);
var buttonSet = CardService.newButtonSet()
.addButton(button);
var section = CardService.newCardSection()
.setHeader("addon")
.addWidget(buttonSet);
var card = CardService.newCardBuilder()
.addSection(section)
//.setFixedFooter(footer);
return card.build();
// Check the event object to determine if the user can set
// conference data and disable the button if not.
// if (!e.calendar.capabilities.canSetConferenceData) {
// button.setDisabled(true);
// }
// ...continue creating card sections and widgets, then create a Card
// object to add them to. Return the built Card object.
}
/**
* Callback function for a button action. Sets conference data for the
* Calendar event being edited.
*
* #param {Object} e The action event object.
* #return {CalendarEventActionResponse}
*/
function onSaveConferenceOptionsButtonClicked(e) {
var parameters = e.commonEventObject.parameters;
// Create an entry point and a conference parameter.
var phoneEntryPoint = ConferenceDataService.newEntryPoint()
.setEntryPointType(ConferenceDataService.EntryPointType.PHONE)
.setUri('tel:' + parameters['phone']);
var adminEmailParameter = ConferenceDataService.newConferenceParameter()
.setKey('adminEmail')
.setValue(parameters['adminEmail']);
// Create a conference data object to set to this Calendar event.
var conferenceData = ConferenceDataService.newConferenceDataBuilder()
.addEntryPoint(phoneEntryPoint)
.addConferenceParameter(adminEmailParameter)
.setConferenceSolutionId(1)
.build();
return CardService.newCalendarEventActionResponseBuilder()
.setConferenceData(conferenceData)
.build();
}
I have published this add-on from Publish->Deploy from menifest.
Executing this code giving me error of ReferenceError: ConferenceDataService is not defined.
I have searched all the possible references, but not able to get any solution.
Please suggest me proper solution for this.
According to this comment from this issue here, it looks like there has been a change regarding this.
When testing the above code, the ReferenceError: ConferenceDataService is not defined. is not displayed anymore and the code runs as expected.
For other methods specific to the ConferenceDataService you can check the documentation here.
Reference
Apps Script ConferenceDataService.
Cannot create conference with 3rd party Conference Solution from conference solutions Dropdown in Google Calendar
Related
I have a sheet with a button (drawing) that executes an apps script. The script reads the data on the sheet and does some work.
I keep making the same mistake. I change some data in a cell then push the button. Since the cell only saves on lost focus, and the button push does not cause a change of focus, the data has not actually changed. I have to make the change to the field, click on another field then push the button. There does not seem to be anyway to save all changes before the button press.
How can I detect that I am mid edit and save what I have first then run my script. Or is there a way to disable the button until edit is done?
Here is some sample code to implement checkbox buttons.
Use Insert > Checkbox to place checkboxes in cells, then specify the location of a checkbox in the cellToWatch parameter, and give the name of the function to run when that checkbox is ticked through the action.run parameter.
You can specify multiple checkboxes by including more objects in the triggers array.
/**
* Checkbox buttons
*
* Use Insert > Checkbox instead of Insert > Drawing
* to implement clickable buttons in Google Sheets.
*
* Checkboxes work in Sheets on mobile as well as Sheets on web.
* Functions run this way work without explicit end-user authorization,
* but in a limited access mode where they cannot call services that
* require authorization.
*
* #see https://stackoverflow.com/a/67160138/13045193
*/
/**
* Simple trigger that runs each time the user edits the spreadsheet.
*
* #param {Object} e The onEdit() event object.
*/
function onEdit(e) {
if (!e) {
throw new Error('Please do not run the script in the script editor window. It runs automatically when you edit the spreadsheet.');
}
checkboxButtons_(e);
}
/**
* Runs a function when a cell value changes.
*
* #param {Object} e The onEdit() event object.
*/
function checkboxButtons_(e) {
// version 1.5, written by --Hyde, 19 April 2021
// - generalize
try {
const sheet = e.range.getSheet();
const triggers = [
////////////////////////////////
// [START modifiable parameters]
{
description: 'Shows a message when the checkbox in Sheet1!B2 is ticked.',
cellToWatch: e.source.getRange('Sheet1!B2'),
triggerValue: true,
resetValue: false,
action: {
run: exampleFunction_,
parameters: {
exampleMessage: "It's alive!",
},
},
messagePost: '',
event: e,
},
{
description: 'Clears some cells when the checkbox in Sheet1!B3 is ticked.',
cellToWatch: e.source.getRange('Sheet1!B3'),
triggerValue: true,
resetValue: false,
action: {
run: exampleFunctionClearRanges_,
parameters: {
rangeListToClear: sheet.getRangeList(['C7', 'E7', 'G7', 'C8', 'E8', 'G8']),
},
},
messagePost: 'Cleared six cells.',
event: e,
},
// [END modifiable parameters]
////////////////////////////////
];
triggers.some(function (trigger) {
if (sheet.getSheetId() !== trigger.cellToWatch.getSheet().getSheetId()
|| e.range.getA1Notation() !== trigger.cellToWatch.getA1Notation()
|| e.range.getValue() !== trigger.triggerValue) {
return false;
}
trigger.action.run(trigger.action.parameters, trigger);
trigger.cellToWatch.setValue(trigger.resetValue);
if (trigger.messagePost) {
showMessage_(trigger.messagePost);
}
return true;
});
} catch (error) {
showAndThrow_(error);
}
}
/**
* Example function that shows a message in a toast.
*
* #param {Object} parameters The trigger.action.parameters object from checkboxButtons_().
* #param {Object} event The event object from checkboxButtons_().
* #return {Object} The original event object, for chaining.
*/
function exampleFunction_(parameters, event) {
showMessage_(parameters.exampleMessage);
return event;
}
/**
* Example function that clears all ranges in a range list in one go.
*
* #param {Object} parameters The trigger.action.parameters object from checkboxButtons_().
* #param {Object} event The event object from checkboxButtons_().
* #return {Object} The original event object, for chaining.
*/
function exampleFunctionClearRanges_(parameters, event) {
parameters.rangeListToClear.clearContent();
return event;
}
/**
* Shows error.message in a pop-up and throws the error.
*
* #param {Error} error The error to show and throw.
*/
function showAndThrow_(error) {
// version 1.0, written by --Hyde, 16 April 2020
// - initial version
var stackCodeLines = String(error.stack).match(/\d+:/);
if (stackCodeLines) {
var codeLine = stackCodeLines.join(', ').slice(0, -1);
} else {
codeLine = error.stack;
}
showMessage_(error.message + ' Code line: ' + codeLine, 30);
throw error;
}
/**
* Shows a message in a pop-up.
*
* #param {String} message The message to show.
* #param {Number} timeoutSeconds Optional. The number of seconds before the message goes away. Defaults to 5.
*/
function showMessage_(message, timeoutSeconds) {
// version 1.0, written by --Hyde, 16 April 2020
// - initial version
SpreadsheetApp.getActive().toast(message, 'Checkbox buttons', timeoutSeconds || 5);
}
You can use a checkbox to trigger the function using onEdit(e), this way you are forced to unselect the input cell before running the function.
Or you can keep using the button, but checking that the checkbox is selected, otherwise the function returns a warning message or something.
Is it possible to have the button just select a hidden checkbox which runs the code using the onEdit script?
I'm not sure if onEdit would trigger and I'm also not sure if the selected field would un-select but it seems to be worth a try.
I've used the documentation posted here https://github.com/gsuitedevs/apps-script-oauth1
I am having issues getting the function to authorize. I am new to working with API's so please bear with me. Trying to do a simple get request from twitter but the first part isn't going through. Any idea where things are going wrong? *Note ive loaded the Oauth1 library.
function getTwitterService() {
// Create a new service with the given name. The name will be used when
// persisting the authorized token, so ensure it is unique within the
// scope of the property store.
var service = OAuth1.createService('twitter')
// Set the endpoint URLs.
service.setAccessTokenUrl('https://api.twitter.com/oauth/access_token')
service.setRequestTokenUrl('https://api.twitter.com/oauth/request_token')
service.setAuthorizationUrl('https://api.twitter.com/oauth/authorize')
// Set the consumer key and secret.
service.setConsumerKey('myKey')
service.setConsumerSecret('mySecret')
// Set the name of the callback function in the script referenced
// above that should be invoked to complete the OAuth flow.
.setCallbackFunction('authCallback')
// Set the property store where authorized tokens should be persisted.
.setPropertyStore(PropertiesService.getUserProperties());
function authCallback(request) {
var twitterService = getTwitterService();
var isAuthorized = twitterService.handleCallback(request);
if (isAuthorized) {
return Logger.log('Success! You can close this tab.');
} else {
return Logger.log('Denied. You can close this tab');
}
}
function makeRequest() {
Logger.log(authorizationUrl);
var twitterService = getTwitterService();
var response = twitterService.fetch("https://api.twitter.com/1.1/followers/list.json?screen_name='xyz'");
var post = response.getContentText();
Logger.log(post);
}
}
The getTwitterService() method should return the service object.
Currently, all the other methods in the snippet are declared inside the getTwitterService method.
function getTwitterService() {
return OAuth1.createService('twitter')
.setAccessTokenUrl('https://api.twitter.com/oauth/access_token')
.setRequestTokenUrl('https://api.twitter.com/oauth/request_token')
.setAuthorizationUrl('https://api.twitter.com/oauth/authorize')
.setConsumerKey('myKey')
.setConsumerSecret('mySecret')
.setCallbackFunction('authCallback')
.setPropertyStore(PropertiesService.getUserProperties());
}
I followed official guides https://developers.google.com/gmail/add-ons/how-tos/compose and https://developers.google.com/gmail/add-ons/guides/quickstart
Here is my appsscript.json:
{
"oauthScopes": [
"https://www.googleapis.com/auth/gmail.addons.execute",
"https://www.googleapis.com/auth/gmail.addons.current.action.compose"
],
"gmail": {
"name": "Gmail Add-on Quickstart",
"logoUrl": "https://www.gstatic.com/images/icons/material/system/2x/bookmark_black_24dp.png",
"contextualTriggers": [{
"unconditional": {
},
"onTriggerFunction": "createReplyDraft"
}],
"openLinkUrlPrefixes": [
"https://mail.google.com/"
],
"primaryColor": "#4285F4",
"secondaryColor": "#4285F4"
}
}
and Code.gs as:
var composeAction = CardService.newAction()
.setFunctionName('createReplyDraft');
var composeButton = CardService.newTextButton()
.setText('Compose Reply')
.setComposeAction(composeAction, CardService.ComposedEmailType.REPLY_AS_DRAFT);
// ...
/**
* Creates a draft email (with an attachment and inline image)
* as a reply to an existing message.
* #param {Object} e data passed by the compose action.
* #return {ComposeActionResponse}
*/
function createReplyDraft(e) {
// Activate temporary Gmail add-on scopes, in this case to allow
// a reply to be drafted.
var accessToken = e.messageMetadata.accessToken;
GmailApp.setCurrentMessageAccessToken(accessToken);
// Creates a draft reply.
var messageId = e.messageMetadata.messageId;
var message = GmailApp.getMessageById(messageId);
var draft = message.createDraftReply('',
{
htmlBody: "Kitten!"
}
);
// Return a built draft response. This causes Gmail to present a
// compose window to the user, pre-filled with the content specified
// above.
return CardService.newComposeActionResponseBuilder()
.setGmailDraft(draft).build();
}
I want to open the "Reply" panel of my Gmail and paste some content into it.
It is giving Error following error: with the add-on.
Runtime error.
Access denied: : Cannot compose without user interaction.. [line: 27, function: createReplyDraft, file: Code]
I had already reinstalled module many times and also tried to give full scope permission of "https://mail.google.com/".
Looking at your code it looks like you are trying to call your CreateReplyDraft function as soon as the add-on is loaded. Google does not allow that, the user would have to click a button in the UI to track creating a draft.
Have you tried adding https://www.googleapis.com/auth/gmail.readonly to your scope?
You cannot trigger compose action directly like this. You have to create a button widget which has linked compose action(your createReplyDraft function) with it. So when the user clicks on that button, compose action will be triggered.
I run a Google Apps script that uploads a file to the user's Google Drive file:
function doGet(e) {
var blob = UrlFetchApp.fetch(e.parameters.url).getBlob();
DriveApp.createFile(blob);
return HtmlService.createHtmlOutput("DONE!");
}
My site loads a popup window that runs a Google Apps Script with that code. Works fine.
Now, how do I communicate back to my site that they user has successfully uploaded the file? As in, how can I communicate back to my server that the user has run doGet()?`
Some type of response handling must exist?
Full working code (test it out on JSBin):
<!DOCTYPE html>
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.js"></script>
</head>
<body>
<div class="google-upload" data-url="https://calibre-ebook.com/downloads/demos/demo.docx">
<span style="background-color: #ddd">Upload</span>
</div>
<script>
$(function() {
$(".google-upload").click(function() {
var url = "https://script.google.com/macros/s/AKfycbwsuIcO5R86Xgv4E1k1ZtgtfKaENaKq2ZfsLGWZ4aqR0d9WBYc/exec"; // Please input the URL here.
var withQuery = url + "?url=";
window.open(withQuery + $('.google-upload').attr("data-url"), "_blank", "width=600,height=600,scrollbars=1");
});
});
</script>
</body>
</html>
So to clarify, I want a way to find out whether if the user has successfully uploaded the file. Something like:
request.execute(function(response) {
if (response.code == 'uploaded') {
// uploaded, do stuff
} else {
// you get the idea...
}
});
Adding a bounty for a complete solution to this.
Rather than returning a HtmlService object, you can pass data using jQuery's $.getJSON method and retrieve data from the doGet function with ContentService. Google Apps Script does not accept CORS, so using JSONP is the best way to get data to and from your script. See this post for more.
Working CodePen Example
I split your HTML and scripts for clarity. None of the HTML changed from your original example.
Code.gs
function doGet(e) {
var returnValue;
// Set the callback param. See https://stackoverflow.com/questions/29525860/
var callback = e.parameter.callback;
// Get the file and create it in Drive
try {
var blob = UrlFetchApp.fetch(e.parameters.url).getBlob();
DriveApp.createFile(blob);
// If successful, return okay
// Structure this JSON however you want. Parsing happens on the client side.
returnValue = {status: 'okay'};
} catch(e) {
Logger.log(e);
// If a failure, return error message to the client
returnValue = {status: e.message}
}
// Returning as JSONP allows for crossorigin requests
return ContentService.createTextOutput(callback +'(' + JSON.stringify(returnValue) + ')').setMimeType(ContentService.MimeType.JAVASCRIPT);
}
Client JS
$(function() {
$(".google-upload").click(function() {
var appUrl = "https://script.google.com/macros/s/AKfycbyUvgKdhubzlpYmO3Marv7iFOZwJNJZaZrFTXCksxtl2kqW7vg/exec";
var query = appUrl + "?url=";
var popupUrl = query + $('.google-upload').attr("data-url") + "&callback=?";
console.log(popupUrl)
// Open this to start authentication.
// If already authenticated, the window will close on its own.
var popup = window.open(popupUrl, "_blank", "width=600,height=600,scrollbars=1");
$.getJSON(popupUrl, function(returnValue) {
// Log the value from the script
console.log(returnValue.status);
if(returnValue.status == "okay") {
// Do stuff, like notify the user, close the window
popup.close();
$("#result").html("Document successfully uploaded");
} else {
$("#result").html(returnValue);
}
})
});
});
You can test the error message by passing an empty string in the data-url param. The message is returned in the console as well as the page for the user.
Edit 3.7.18
The above solution has problems with controlling the authorization flow. After researching and speaking with a Drive engineer (see thread here) I've reworked this into a self-hosted example based on the Apps Script API and running the project as an API executable rather than an Apps Script Web App. This will allow you to access the [run](https://developers.google.com/apps-script/api/reference/rest/v1/scripts/run) method outside an Apps Script web app.
Setup
Follow the Google Apps Script API instructions for JavaScript. The Apps Script project should be a standalone (not linked to a document) and published as API executable. You'll need to open the Cloud Console and create OAuth credentials and an API key.
The instructions have you use a Python server on your computer. I use the Node JS server, http-server, but you can also put it live online and test from there. You'll need to whitelist your source in the Cloud Console.
The client
Since this is self hosted, you'll need a plain HTML page which authorizes the user through the OAuth2 API via JavaScript. This is preferrable because it keeps the user signed in, allowing for multiple API calls to your script without reauthorization. The code below works for this application and uses the authorization flow from the Google quickstart guides.
index.html
<body>
<!--Add buttons to initiate auth sequence and sign out-->
<button id="authorize-button" style="display: none;">Authorize</button>
<button id="signout-button" style="display: none;">Sign Out</button>
<button onclick="uploadDoc()" style="margin: 10px;" id="google-upload" data-url="https://calibre-ebook.com/downloads/demos/demo.docx">Upload doc</button>
<pre id="content"></pre>
</body>
index.js
// Client ID and API key from the Developer Console
var CLIENT_ID = 'YOUR_CLIENT_ID';
var API_KEY = 'YOUR_API_KEY';
var SCRIPT_ID = 'YOUR_SCRIPT_ID';
// Array of API discovery doc URLs for APIs used by the quickstart
var DISCOVERY_DOCS = ["https://script.googleapis.com/$discovery/rest?version=v1"];
// Authorization scopes required by the API; multiple scopes can be
// included, separated by spaces.
var SCOPES = 'https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/script.external_request';
var authorizeButton = document.getElementById('authorize-button');
var signoutButton = document.getElementById('signout-button');
var uploadButton = document.getElementById('google-upload');
var docUrl = uploadButton.getAttribute('data-url').value;
// Set the global variable for user authentication
var isAuth = false;
/**
* On load, called to load the auth2 library and API client library.
*/
function handleClientLoad() {
gapi.load('client:auth2', initClient);
}
/**
* Initializes the API client library and sets up sign-in state
* listeners.
*/
function initClient() {
gapi.client.init({
apiKey: API_KEY,
clientId: CLIENT_ID,
discoveryDocs: DISCOVERY_DOCS,
scope: SCOPES
}).then(function () {
// Listen for sign-in state changes.
gapi.auth2.getAuthInstance().isSignedIn.listen(updateSigninStatus);
// Handle the initial sign-in state.
updateSigninStatus(gapi.auth2.getAuthInstance().isSignedIn.get());
authorizeButton.onclick = handleAuthClick;
signoutButton.onclick = handleSignoutClick;
// uploadButton.onclick = uploadDoc;
});
}
/**
* Called when the Upload button is clicked. Reset the
* global variable to `true` and upload the document.
* Thanks to #JackBrown for the logic.
*/
function updateSigninStatus(isSignedIn) {
if (isSignedIn && !isAuth) {
authorizeButton.style.display = 'none';
signoutButton.style.display = 'block';
uploadButton.style.display = 'block'
uploadButton.onclick = uploadDoc;
} else if (isSignedIn && isAuth) {
authorizeButton.style.display = 'none';
signoutButton.style.display = 'block';
uploadButton.style.display = 'block';
uploadDoc();
} else {
authorizeButton.style.display = 'block';
signoutButton.style.display = 'none';
uploadButton.style.display = 'none';
isAuth = false;
}
}
/**
* Sign in the user upon button click.
*/
function handleAuthClick(event) {
gapi.auth2.getAuthInstance().signIn();
isAuth = true; // Update the global variable
}
/**
* Sign out the user upon button click.
*/
function handleSignoutClick(event) {
gapi.auth2.getAuthInstance().signOut();
isAuth = false; // update the global variable
}
/**
* Append a pre element to the body containing the given message
* as its text node. Used to display the results of the API call.
*
* #param {string} message Text to be placed in pre element.
*/
function appendPre(message) {
var pre = document.getElementById('content');
var textContent = document.createTextNode(message + '\n');
pre.appendChild(textContent);
}
/**
* Handle the login if signed out, return a Promise
* to call the upload Docs function after signin.
**/
function uploadDoc() {
console.log("clicked!")
var docUrl = document.getElementById('google-upload').getAttribute('data-url');
gapi.client.script.scripts.run({
'scriptId':SCRIPT_ID,
'function':'uploadDoc',
'parameters': [ docUrl ]
}).then(function(resp) {
var result = resp.result;
if(result.error && result.error.status) {
// Error before the script was Called
appendPre('Error calling API');
appendPre(JSON.parse(result, null, 2));
} else if(result.error) {
// The API executed, but the script returned an error.
// Extract the first (and only) set of error details.
// The values of this object are the script's 'errorMessage' and
// 'errorType', and an array of stack trace elements.
var error = result.error.details[0];
appendPre('Script error message: ' + error.errorMessage);
if (error.scriptStackTraceElements) {
// There may not be a stacktrace if the script didn't start
// executing.
appendPre('Script error stacktrace:');
for (var i = 0; i < error.scriptStackTraceElements.length; i++) {
var trace = error.scriptStackTraceElements[i];
appendPre('\t' + trace.function + ':' + trace.lineNumber);
}
}
} else {
// The structure of the result will depend upon what the Apps
// Script function returns. Here, the function returns an Apps
// Script Object with String keys and values, and so the result
// is treated as a JavaScript object (folderSet).
console.log(resp.result)
var msg = resp.result.response.result;
appendPre(msg);
// do more stuff with the response code
}
})
}
Apps Script
The Apps Script code does not need to be modified much. Instead of returning using ContentService, we can return plain JSON objects to be used by the client.
function uploadDoc(e) {
Logger.log(e);
var returnValue = {};
// Set the callback URL. See https://stackoverflow.com/questions/29525860/
Logger.log("Uploading the document...");
try {
// Get the file and create it in Drive
var blob = UrlFetchApp.fetch(e).getBlob();
DriveApp.createFile(blob);
// If successful, return okay
var msg = "The document was successfully uploaded!";
return msg;
} catch(e) {
Logger.log(e);
// If a failure, return error message to the client
return e.message
}
}
I had a hard time getting CodePen whitelisted, so I have an example hosted securely on my own site using the code above. Feel free to inspect the source and take a look at the live Apps Script project.
Note that the user will need to reauthorize as you add or change scopes in your Apps Script project.
In my understanding now, only one doGet() can trigger unique doPost() in a Google Apps Script application.
I would like to perform a Software Publisher System that user upload the file or fill up revision information in forms and push submit to the next step. The final page will show the input information, send email to guys and complete all operation.
But how do I enter next form after the submit button pushed?
I have tried a method that creating the 2nd step and 3rd step forms in the doPost(), and using try...catch to difference which step form triggered the current step, like the following code.
(Because any steps can't get the callback item throw by non-previous step, then it arises an exception)
It works very well but I think it doesn't make sens and very silly. Have any better solutions? Thanks, please.
//---------------------------------------------------------------------------
function doGet(e)
{
var app = UiApp.createApplication().setTitle("AP Publisher");
createFileUploadForm(app);
return app;
}
//---------------------------------------------------------------------------
function doPost(e)
{
var app = UiApp.getActiveApplication();
try {
// 2nd step form
var fileBlob = e.parameter.thefile;
createRevisionForm();
}
catch(error) {
try {
// 3rd step form
createConfirmForm(e);
}
catch(error2) {
//Complete
sendMail(e);
modifySitePageContent(e);
saveHistoryFile(e);
showConfirmedInfo(e);
}
}
return app;
}
This answer is copied entirely from create a new page in a form dynamically, based on data of the prev. page.
Using the UiApp service, you have one doGet() and one doPost() function... but here's a way to extend them to support a dynamic multi-part form. (The example code is borrowed from this answer.)
Your doGet() simply builds part1 of your form. In the form, however, you need to identify your form by name, like this:
var form = app.createFormPanel().setId("emailCopyForm");
You doPost() then, will pass off handling of the post operation to different functions, depending on which form has been submitted. See below. (Also included: reportFormParameters (), a default handler that will display all data collected by a form part.)
/**
* doPost function with multi-form handling. Individual form handlers must
* return UiApp instances.
*/
function doPost(eventInfo) {
var app;
Logger.log("Form ID = %s", eventInfo.parameter.formId);
// Call appropriate handler for the posted form
switch (eventInfo.parameter.formId) {
case 'emailCopyForm':
app = postEmailCopyForm(eventInfo);
break;
default:
app = reportFormParameters (eventInfo);
break;
}
return app;
}
/**
* Debug function - returns a UiInstance containing all parameters from the
* provided form Event.
*
* Example of use:
* <pre>
* function doPost(eventInfo) {
* return reportFormParameters(eventInfo);
* }
* </pre>
*
* #param {Event} eventInfo Event from UiApp Form submission
*
* #return {UiInstance}
*/
function reportFormParameters (eventInfo) {
var app = UiApp.getActiveApplication();
var panel = app.createVerticalPanel();
panel.add(app.createLabel("Form submitted"));
for (var param in eventInfo.parameter) {
switch (param) {
// Skip the noise; these keys are used internally by UiApp
case 'lib':
case 'appId':
case 'formId':
case 'token':
case 'csid':
case 'mid':
break;
// Report parameters named in form
default:
panel.add(app.createLabel(" - " + param + " = " + eventInfo.parameter[param]));
break;
}
}
app.add(panel);
return app;
}
To generate each form part, subsequent form handlers can use the data retrieved in previous parts to dynamically add new Form objects to the ui.
I think it would be simpler to use 3 (or more) different panels in your doGet function with all the items you need and to play with their visibility.
At first only the 1rst panel would be visible and, depending on user input (using client Handlers to handle that) show the next ones (and eventually hide the first one).
In the end the submit button will call the doPost and get all data from the doGet.
First a tip of my hat to Mogsdad. His post(s) were guiding lights in the darkly documented path that led me here. Here is some working code
that demonstrates a multiple page form, i.e. it does the initial doGet() and then lets you advance back and forth doing multiple doPost()'s. All this is done in a single getForm() function called by both the standard doGet() and the doPost() functions.
// Muliple page form using Google Apps Script
function doGet(eventInfo) {return GUI(eventInfo)};
function doPost(eventInfo) {return GUI(eventInfo)};
function GUI (eventInfo) {
var n = (eventInfo.parameter.state == void(0) ? 0 : parseInt(eventInfo.parameter.state));
var ui = ((n == 0)? UiApp.createApplication() : UiApp.getActiveApplication());
var Form;
switch(n){
case 0: {
Form = getForm(eventInfo,n); // Use identical forms for demo purpose only
} break;
case 1: {
Form = getForm(eventInfo,n); // In reality, each form would differ but...
} break;
default: {
Form = getForm(eventInfo,n) // each form must abide by (implement) the hidden state variable
} break;
}
return ui.add(Form);
};
function getForm(eventInfo,n) {
var ui = UiApp.getActiveApplication();
// Increment the ID stored in a hidden text-box
var state = ui.createTextBox().setId('state').setName('state').setValue(1+n).setVisible(true).setEnabled(false);
var H1 = ui.createHTML("<H1>Form "+n+"</H1>");
var H2 = ui.createHTML(
"<h2>"+(eventInfo.parameter.formId==void(0)?"":"Created by submission of form "+eventInfo.parameter.formId)+"</h2>");
// Add three submit buttons to go forward, backward and to validate the form
var Next = ui.createSubmitButton("Next").setEnabled(true).setVisible(true);
var Back = ui.createSubmitButton("Back").setEnabled(n>1).setVisible(true);
var Validate = ui.createSubmitButton("Validate").setEnabled(n>0).setVisible(true);
var Buttons = ui.createHorizontalPanel().add(Back).add(Validate).add(Next);
var Body = ui.createVerticalPanel().add(H1).add(H2).add(state).add(Buttons).add(getParameters(eventInfo));
var Form = ui.createFormPanel().setId((n>0?'doPost[':'doGet[')+n+']').add(Body);
// Add client handlers using setText() to adjust state prior to form submission
// NB: Use of the .setValue(val) and .setValue(val,bool) methods give runtime errors!
var onClickValidateHandler = ui.createClientHandler().forTargets(state).setText(''+(parseInt(n)));
var onClickBackHandler = ui.createClientHandler().forTargets(state).setText(''+(parseInt(n)-1));
Validate.addClickHandler(onClickValidateHandler);
Back.addClickHandler(onClickBackHandler);
// Add a client handler executed prior to form submission
var onFormSubmit = ui.createClientHandler()
.forTargets(state).setEnabled(true) // Enable so value gets included in post parameters
.forTargets(Body).setStyleAttribute("backgroundColor","#EEE");
Form.addSubmitHandler(onFormSubmit);
return Form;
}
function getParameters(eventInfo) {
var ui = UiApp.getActiveApplication();
var panel = ui.createVerticalPanel().add(ui.createLabel("Parameters: "));
for( p in eventInfo.parameter)
panel.add(ui.createLabel(" - " + p + " = " + eventInfo.parameter[p]));
return panel;
}
The code uses a single "hidden" state (here visualized in a TextBox) and multiple SubmitButton's to allow the user to advance forward and backward through the form sequence, as well as to validate the contents of the form. The two extra SubmitButton's are "rewired" using ClientHandler's that simply modify the hidden state prior to form submission.
Notes
Note the use of the .setText(value) method in the client handler's. Using the Chrome browser I get weird runtime errors if I switch to either of the TextBox's .setValue(value) or .setValue(value, fireEvents) methods.
I tried (unsuccessfully) to implement this logic using a Script Property instead of the hidden TextBox. Instead of client handlers, this requires using server handlers. The behavior is erratic, suggesting to me that the asynchronous server-side events are occurring after the form submission event.