Google Sheets - make application scripts run on phone App version - google-apps-script

I have created a Google Sheets spreadsheet and some scripts to help me track the attendance of members at a gym.
My project has the following scripts associated:
- a trigger that sends periodic events based on some checks I do and also updates some cells. This works fine I scheduled it to run daily and it seems to work properly even if my PC isn't on :D
- I have also added a onEdit function that's supposed to remind the user that he/she has modified a cell and let him know that he can undo in order to revert the changes. This is the function:
function onEdit(e) {
var ui = SpreadsheetApp.getUi();
var cell = e.range;
var actual_cell = e.source.getActiveRange().getA1Notation();
var row = cell.getRow();
var column = cell.getColumn();
var old_content = e.oldValue;
ui.alert('Modificare celula' + ' ' + actual_cell + '.' + ' UNDO to rever the changes')
}
This, however, does not work on the mobile version: Sheets for Android.
Is there any way I can make my app fully functional on the mobile version as well? I want to have that warning message that tells the user a cell was modified on the mobile version as well. How could I do that?
Also another question regarding the trigger that sends periodic emails.
I have used a Time Based daily trigger that runs some function. Do I have to be logged on my account at all times on my PC(or any other device) in order for this script to run periodically? Or is it on the cloud and it will run even if I'm not logged on any device?

To be able to trigger the script bound to the spreadsheet from a mobile App (Android or iOS), you have to deploy the script as a API executable (Publish -> Deploy as API executable from menu bar in script) [1].
As stated by a Googler in this comment [2], since last year you can't display any element from the Ui class [3] in mobile versions.
About your other question: It doesn't matter if your computer is on, the script/trigger will run either way.
[1] https://developers.google.com/apps-script/api/quickstart/target-script
[2] https://b.corp.google.com/issues/36763692#comment6
[3] https://developers.google.com/apps-script/reference/base/ui

Related

How to transfer data from webapp to addon

Project_1 is a container-bound script. A container is a readable spreadsheet (Template).
Code_1:
function doPost(e) {
return HtmlService.createHtmlOutput(JSON.stringify(e));
}
The user makes a copy of the Template, deploys the script (Project_1) as a webapp with permissions: "Execute as: Me, Who has access: Anyone". The user is the owner of Project_1.
Project_2 is a script deployed as an add-on. The user from point 1 is not the owner of Project_2.
Code_2:
function sendPost() {
var sheetURL = SpreadsheetApp.getActiveSpreadsheet().getUrl();
var webAppUrl = "https://script.google.com/macros/s/###/exec"; // 7: Part_1 - WebApp: Tester
// var auth = ScriptApp.getOAuthToken();
// var header = { 'Authorization': 'Bearer ' + auth };
var payload = { scriptName: 'updateData', sheetURL: 'sheetURL' };
var options = {
method: 'post',
// headers: header,
muteHttpExceptions: true,
payload: payload
};
var resp = UrlFetchApp.fetch(webAppUrl, options);
var respTxt = resp.getContentText();
console.log('resp: ' + respTxt);
}
function doPost(ev) {
var respWebapp = func(ev);
}
The user installs an add-on (Project_2).
The flow in the direction of addon -> webapp is fine: when sendPost() starts, it sends a request to the webapp and receives a response with the necessary data_1 in response.
The flow in the direction of "someone on the web" -> webapp also flows well: when requesting a webapp_url receives the transferred data_2.
I am trying to transfer data_2 to an addon.
I read a lot about scripts.run, but it seems that this option is not applicable in such a situation.
There is also nowhere to add an eventListener.
I would not want to deploy webapp from my account, so as not to spend my quota for simultaneous executions (<= 30).
Also I would not like to do a sidebar, etc. in the spreadsheet and try to screw eventListener to html. I assume that with this approach, the listener (if it is possible to add it there at all) will be active only when ui is active (the spreadsheet is open and the sidebar is active). Data can come at any time of the day and must be immediately transferred to the addon.
Added:
I feel like I'm stumped. Therefore I reaches out to the community in the hope that someone would suggest a workaround or a new strategy for this initial data. By initial data I mean provide the opportunity for more than 30 users to exchange messages in both directions Spreadsheet <--> External service (for example, Telegram) and at the same time not fall under the limit of 30 simultaneous script executions.
Added_2:
I'm going to assign a bounty, so I'm transferring here from the comments what is missing in the post and updating the question itself.
I rejected the option with immediate entry into the sheet, because this will cause constant calls to the spreadsheet and slow down the performance of the system as a whole.
I am aware of the existence of Google cloud functions and Google compute engine, but would like to stay within the free quotas.
QUESTION: How to transfer data from webapp to addon and execute func () or which workaround to use to achieve the goals?
Here is a list of your requirements:
Trigger add-on code to run from some external request, not using the add-on user interface or time based trigger.
Code runs from the user's account, using their quota
Run the add-on code regardless of whether the user is using the add-on or not. For example, their Google Sheet is closed, and the user may even be signed out.
I only know of one way to do that, and it's with a Sheet's add-on by triggering the "On Change" event by setting a value in a Sheet cell using the Sheets API. The Sheets API must use a special option to set the value "As the User."
The special setting is:
valueInputOption=USER_ENTERED
That option will trigger the "On Change" event even if the Sheet is closed.
Obviously the script making the request needs authorization from the user to set a value in a cell of the Sheet.
If the script sending the request is outside of the user's account then you'd need to use OAuth.
The add-on would need to install an "On Change" trigger for the Sheet and the function that the trigger is bound to would need to determine whether the change was from the special cell designated for this special functionality.
If the request to set a value in the users Sheet is from outside of that users Google account, then the user of the Sheet would need to somehow authorize the OAuth credentials for the Sheets API to make a change to the Sheet.
Depending upon the programming language being used with the Google Sheets API, there may be a Sheets API Library specifically for that language. You can also use the Sheets REST API.
There is an example here on StackOverflow of using the Sheets REST API from Apps Script, but if the external request is from some code that isn't Apps Script, it won't be exactly the same.
I understand that the solutions proposed in the comments, by others and myself, can't work in your scenario because it can't stand an average delay of 30 seconds. In that case I strongly advise you to set up a Cloud project that can be used as an instant server, as opposed to triggers/apps/etc.
In "Code_1" and "Code_2" use a shared data store. In other words, instead of directly passing the data from "Code_1" to "Code_2", make that Code_1 write to the datastore and "Code_2" read from it.
One possibility among many is to use a spreadsheet as a database. In this case you might use on change triggers to do some action when the spreadsheet is changed by one of the "Code_1" scripts and/or use time-driven triggers to do some action with certain frequency or at certain datetime.

How do I publish a single Google Scripts function?

So I have a single function that I want to be made available for ALL of my Google Sheets.
It is very simple. Simple enough that I'll post it here in it's entirety.
function swapMonthAndDay() {
// The code below will swap the month and day for any Date objects in the selected cells.
var range = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet().getActiveRange()
var values = range.getValues();
values.forEach(function(row, y){
row.forEach(function(value, x){
if (value instanceof Date){
var month = value.getMonth() + 1,
day = value.getDate();
value.setMonth(day - 1);
value.setDate(month);
}
});
});
range.setValues(values)
}
I was able to get the result that I want via the "test as add-on..." from the run menu. But this is temporary as the term "test" implies. So, this leads me to believe that I need to publish this as a Sheets add-on. But, I really don't want to publish this to the web store. Too many steps, authentications, certificates etc to get there.
Okay I've finally figured it out.
Turns out, it has nothing to do with the Chrome Web Store. There is the GSuite Marketplace and the old and depreciating Chrome Web store. Currently, the deploy as add-on button links through to the Chrome Web Store deployment flow.
It all relies in the Cloud Project settings.
https://developers.google.com/gsuite/add-ons/how-tos/publish-addons
https://developers.google.com/gsuite/add-ons/how-tos/publish-for-domains
It needs to be accessed via Resources > Cloud Platform project..., within the menu when editing the script.

How do I execute a google script as 'owner' of a spreadsheet the user cannot view

Hopefully this is quite a simple question!
I've made a Google Script that writes to cells in a separate sheet "MasterSheet" (helped by several useful Q&As from here). This will ultimately be deployed embedded to multiple different sheets that I'm giving to individual users.
It works perfectly when the user has edit permissions on "MasterSheet", but I need that to remain private - i.e. not even viewable to anyone but me.
As background: In each 'user sheet', IMPORTRANGE is used to import the columns from 'MasterSheet' that that user is allowed to view, and then the script allows the user to add a comment to the MasterSheet.
I can view MasterSheet to see all the columns with comments from various users on one unified sheet, but the individual users shouldn't be able to view this.
The specific script for writing to the sheet is fairly generic:
function saveCommentToMasterSheet(form){
var company = form.company,
contact = form.contactselect,
comment = form.message ,
ss = SpreadsheetApp.openById("MASTERSHEET_ID").getSheetByName('MasterSheet');
if(company=="all"){
var row = findCell(contact);
} else {
var row = findCell2(contact,company);
}
//^The above finds the specific row number relating to the entry the user wants to comment on.
var cell2 = ss.getRange(row,11);
// ^In this case '11' is the column related to this sheet's specific user, I've made separate sheets for each user that are identical except this column number
cell2.setValue(comment);
}
I believe that I could make MasterSheet editable to anyone with the link, but I'd rather avoid that, particularly as the script is embedded in each spreadsheet so if the users just looked at it they'd find the MasterSheet id.
I understand that it's possible to run a script as me using the execute API, but if I'm honest, I struggled a little to figure out make that work.
Sorry if I'm asking a simple question - I've given it a good search and can't figure it out.
Many thanks!
Alex
N.B. This Running a google script from within a spreadsheet, but as a different user? looks like a similar question, but I'd really like to keep the comment system within the user's spreadsheet.
You can make a POST request to the master spreadsheet from the spreadsheets distributed to the users:
Apps Script Documentation - UrlFetchApp.fetch()
function saveCommentToMasterSheet(form) {//Function in the
//spreadsheets distributed to the users
var options,responseCode,url;
url = "https://script.google.com/macros/s/File_ID/exec";//Get from publishing
options = {};
options.method = 'post';
options.payload = form;
responseCode = UrlFetchApp.fetch(url, options).getResponseCode();
Logger.log('responseCode: ' + responseCode);//View the Logs
};
The above code will trigger the doPost(e) function in the master spreadsheet, and put the data into the event object e.
Then you can get the data out of e and write the data directly to what is the active spreadsheet, which is the master spreadsheet. Publish the Web App to run as "Me".
There are two versions of the published Web App; the "dev" version and the "exec" version. The "dev" version is always live with the latest changes, but should never be used in production. The "exec" version has a new version every time that you publish a the script again. To use the latest "exec" version in production, you must keep publishing the latest code.

How do I convert this google script to work offline?

I am trying to automate data entry while roaming offline using my Chromebook.
I know that google drive is enabled offline and a standalone script in GAS should in theory do the trick but im not sure how to put the pieces together. So far I have the below code which works perfectly online (gets stuck in "running" offline) and I've got the GAS app installed. Any guidance would be greatly appreciated!
function onOpen() {
var ui = SpreadsheetApp.getUi();
// Or DocumentApp or FormApp.
ui.createMenu('Invoice/Receipt System')
// creates a menu item "Submit Order"
.addItem('Record Invoice', 'menuItem1')
.addToUi();
}
function menuItem1() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var is = ss.getSpreadsheetByName("Template_Invoice");
var lastID = is.getRange("j6");
var nextID = is.getRange("j7");
var lastIDValue = lastID.getValue();
var source = ss.getSpreadsheetByName("Key_Invoice");
// sets the 'Key_DailyInput' Sheet as source
var target = ss.geSpreadsheetByName("DataBase_Invoice");
// sets 'Key_DailyInput' sheet as the target for copying data to.
var sourceData = source.getSheetValues(5,1,source.getLastRow(),15);
// sets range to gather source 'Key_DailyInput' data by finding last row, and Line 5 to Column 15
target.getRange(target.getLastRow()+1, 1, sourceData.length,15).setValues(sourceData);
// finds last row of target 'Orders' and writes to +1 row past last row up to column 15 using setValues of sourceData
// Following simply clears DailyInput so new data can be entered
is.getRange('C5:c8').clearContent();
is.getRange('G7:G8').clearContent();
is.getRange('B12:h28').clearContent();
is.getRange('b31:b34').clearContent();
// increases value by +1 so next Sales Order ID is incremented by 1
var cell = is.getRange("j6");
var cellValue = cell.getValue();
cell.setValue(cellValue + 1);
nextID.setValue(lastIDValue + 1);
}
As stated in other responses, the answer appears to be 'No'. However, while researching I did find the Command Line Interface for Apps Script (clasp) to manage and edit your projects offline. I'll post it here hoping it will be helpful to Apps Script developers.
CLASP Features
Develop Locally. clasp lets you write code on your own computer and upload it to Apps Script via command line when you're done. You can also download existing Apps Script projects and then edit them locally. Once the code is local, you can use your favorite development tools like git to work on Apps Script projects.
* Manage Deployment Versions.
* Create, update, and view multiple deployments of your project.
* Structure Code. clasp automatically converts your flat project on script.google.com into folders.
You can find more information on clasp at https://developers.google.com/apps-script/guides/clasp. However, you'll also need to activate the Linux(beta) on your Chromebook utilizing these instructions.
Short answer
Google Apps Script can't be ran offline because they run on the server-side.
Explanation
From https://developers.google.com/apps-script/overview
Google Apps Script is a scripting language based on JavaScript that
lets you do new and cool things with Google Apps like Docs, Sheets,
and Forms. There's nothing to install — we give you a code editor
right in your browser, and your scripts run on Google's servers.

Google Apps Script - Editors Execute Script on Spreadsheet

I created a Google spreadsheet to help my team keep track of their calendar for the upcoming week. The spreadsheet has 2 tabs: an "Overview" tab and a "Details" tab. On the "Details" tab, the top row lists all of the weekdays through the end of the year and the left column lists the team members names. For each day, the team member writes what school they will be at and the description of support.
The "Overview" tab has the same structure as the "Details" tab (i.e., dates across the top and names listed on the left), but provides a visual summary of where each person will be. I wrote a script that takes the school name from the "Details" tab and enters it in the cell that corresponds to the correct person/date on the "Overview" tab. Then it takes the description of support and creates a Note on the "Overview" tab with the description. It also shades the cell on the Overview tab so I can see that support is planned. So from the "Overview" tab, all I have to do is mouse over the cell and it will show me the planned support.
It works great in my own account. However, I have shared the spreadsheet with some team members and it is not working. I don't want my team to be able to edit the "Overview" tab because I only want them to input their information on the "Details" tab. If I give them editing rights to the "Overview" tab, then they can change the Notes, cell colors, sheet structure, etc (that is why I want the Overview tab as view-only). So, I protected the "Overview" tab so they cannot make edits. However, because they cannot edit this sheet, it seems that the script will not run and update the Overview tab when they edit the Details tab. I assume this is because they don't have permission (because the sheet is protected). When I remove the sheet protection, the script runs just fine for them.
Any thoughts on how I can get around this? I really need to keep the Overview tab View-only. Thanks.
If, as you stated in comments, you use an installable on Edit trigger the problem you describe should not occur since "When a script runs because of a trigger, the script runs using the identity of the person who installed the trigger, not the identity of the user whose action triggered the event. This is for security reasons. For example, if you install a trigger in your script, but your coworker performs the action that triggers the event, the script runs under your identity, not your coworker's identity. For complete information on this, see Understanding Permissions and Script Execution."
see doc here
EDIT : sorry, I didn't see the issue about this special case : issue 1562 posted on july 2012, status "triaged"
EDIT 2 : I tried #tracon6 suggestion to remove the protection temporarily but it doesn't work either... the script generates an error when trying to apply the protection.
but
as an EDIT 3
I found a workaround that works ! We can add an editor just for the time we write to the targetSheet and remove it right after... using a flush in between it works.
here is the code I used to test :
function onEditInstallable(event) {
var sheet = event.source.getActiveSheet();
if(sheet.getName()=='Sheet1'){return};
var r = event.source.getActiveRange();
var column = r.getColumn();
var ss = SpreadsheetApp.getActiveSpreadsheet();
var targetSheet = ss.getSheetByName("TFA");
var targetSheet = ss.getSheetByName("Sheet1");
var user = SpreadsheetApp.getActive().getEditors()[1];
var permission = targetSheet.getSheetProtection();
permission.addUser(user);
targetSheet.setSheetProtection(permission);
SpreadsheetApp.flush();
targetSheet.getRange(1,column).setValue('value change on sheet 2, column : '+column);
SpreadsheetApp.flush();
permission.removeUser(user)
targetSheet.setSheetProtection(permission)
}
EDIT 4 :
Since it seems that many editors could have access to these sheets (see last comments), there has to be a way to know who is active on the spreadsheet. In a non domain environment this is not possible with a triggered function so I would suggest a small uiApp with a list from which the user has to choose, trigger that with an installable onOpen and store the value to use it in the main function.
Heres is a piece of code to handle that aspect.
function SpecialonOpen(){
var s = SpreadsheetApp.getActive();
var app = UiApp.createApplication().setTitle('Please select your email in this list').setWidth(300).setHeight(300);
var list = app.createListBox().setName('list')
var editors = s.getEditors();
for(var n in editors){
list.addItem(editors[n].getEmail());
}
var handler = app.createServerHandler('getMail').addCallbackElement(list);
var btn = app.createButton('select',handler);
s.show(app.add(list).add(btn))
}
function getMail(e){
var email = e.parameter.list;
var editors = ScriptProperties.getProperty('currentEditors')||' ';
if(editors.indexOf(email)==-1){
editors+=(','+email);
ScriptProperties.setProperty('currentEditors',editors);
}
var app = UiApp.getActiveApplication().close();
var editors = ScriptProperties.getProperty('currentEditors').split(',');
if(editors[0].length<2){editors.splice(0,1)};
Logger.log(editors)
return app
}
You should also add a function to reset this list at some moment... don't know what would be best ? on a daily base maybe ? I'll let this to you ;-)
Do the Overview tab and the Details tab need to be in the same spreadsheet?
Perhaps you can have the Overview tab in a spreadsheet that only you can access and have the Details tab in a spreadsheet that everyone else can access -- then you can modify your script to use the information in the new "Details Spreadsheet" to update the new "Overview Spreadsheet".
Alternatively, in your script, you could turn off the protection on the Overview tab, then run the main part of the script, then turn on the protection on the Overview tab again before the script ends (see https://developers.google.com/apps-script/reference/spreadsheet/page-protection#setProtected(Boolean))
Admittedly, neither of these options are as clean the onEdit option.
Here is a hacky workarround you can try: publish the script as a content service with anonymous access, and call it with urlFetch from your trigger, passing the necessary param to your service.
This should cause the code to execute in a different context where the effective user is the script publisher and not te user. Will be slower thou.
Why don't you protect the Overview sheet using the options in the Data tab and set permissions for edit to 'Only you'. This will protect the sheet from everyone, even editors, leaving the sheet in View Only mode for everyone except yourself.