Custom functions workaround for Google Workspace add-ons - google-apps-script

I'm looking for a workaround to the limitation that custom functions are not available in Google Workspace Add-ons. As proven by #Rubén in this SO answer.
I am building a Google Workspace Add-on. I want to get data from a specified range in the Spreadsheet, then run a function in the code, then output the data back to the user in the Spreadsheet.
I'm guessing I might use SpreadsheetApp.getActiveRange() to facilitate the spreadsheet data interactions. Something similar to the following pseudocode.
Pseudocode
const sourceRange = SpreadsheetApp.getActiveRange();
// do stuff
const destinationRange = foo;
destinationRange.setValues( bar, );
Is this a viable path forward?

Yes, you might use code like the shown in the question in Workspace add-ons for Google Sheets to make that your add-on UI interacts with the active spreadsheet.
The below code snippet is a complete helper function used in sample Workspace Add-on provided by Google Developers. It was taken from Translate text from Google Docs, Sheets, and Slides.
Breadcrumb: Samples by project type > Workspace Add-ons > Translate text
Specific URL: https://developers.google.com/apps-script/add-ons/translate-addon-sample#code.gs.
Please note that it uses
var ranges = SpreadsheetApp.getActive().getSelection().getActiveRangeList().getRanges();
/**
* Helper function to get the text of the selected cells.
* #return {CardService.Card} The selected text.
*/
function getSheetsSelection(e) {
var text = '';
var ranges = SpreadsheetApp.getActive().getSelection().getActiveRangeList().getRanges();
for (var i = 0; i < ranges.length; i++) {
const range = ranges[i];
const numRows = range.getNumRows();
const numCols = range.getNumColumns();
for (let i = 1; i <= numCols; i++) {
for (let j = 1; j <= numRows; j++) {
const cell = range.getCell(j, i);
if (cell.getValue()) {
text += cell.getValue() + '\n';
}
}
}
}
if (text !== '') {
var originLanguage = e.formInput.origin;
var destinationLanguage = e.formInput.destination;
var translation = LanguageApp.translate(text, e.formInput.origin, e.formInput.destination);
return createSelectionCard(e, originLanguage, destinationLanguage, text, translation);
}
}

Related

Export Google Docs comments into Google Sheets - The Basics

Trying to produce a Comment Log in a Google sheet based on a Google Doc.
I applied the script as suggested by NaziA in this link. And I activated the DriveAPI service.
function listComments() {
// Change docId into your document's ID
// See below on how to
var docId = '1fzYPRldd16KjsZ6OEtzgBIeGO8q5tDbxaAcqvzrJ8Us';
var comments = Drive.Comments.list(docId);
var hList = [], cList = [];
// Get list of comments
if (comments.items && comments.items.length > 0) {
for (var i = 0; i < comments.items.length; i++) {
var comment = comments.items[i];
// add comment and highlight to array's first element
hList.unshift([comment.context.value]);
cList.unshift([comment.content]);
}
// Set values to A and B
var sheet = SpreadsheetApp.getActiveSheet();
sheet.getRange("A1:A" + hList.length).setValues(hList);
sheet.getRange("B1:B" + cList.length).setValues(cList);
}
}
I used the suggested code verbatim with one change. I replaced the DocumentID with the ID from the new Google sheet I was using as a target. After I approved the permission, it executed without errors but did not write any values to the Google Sheet. I had a demo Google Doc as a source with few comments.
Screenshot of Comment Doc
Any suggestions?

Unprotecting protected sheet so that others can run script and then protect the sheet again

I have compiled a list of scripts that I run on a sheet. I'm not a programmer I'm still learning so I have used some code from other people.
The following are the only unprotected ranges B2:C2,N5:N43 but for the other scripts to run the whole sheet needs to be unprotected and protected again.
Using Google Apps Script, you can modify your scripts so that before running they unprotect your sheets and ranges, and after running they re-protect them. You could use code such as the following:
function unProtectAndProtect() {
var sheetProtections = SpreadsheetApp.getActive().getProtections(SpreadsheetApp.ProtectionType.SHEET);
var rangeProtections = SpreadsheetApp.getActive().getProtections(SpreadsheetApp.ProtectionType.RANGE);
var protectionData = {
sheetProtections: [],
rangeProtections: []
};
for (var i=0; i<sheetProtections.length; i++) {
var protection = {};
protection['editors'] = sheetProtections[i].getEditors();
protection['description'] = sheetProtections[i].getDescription();
protection['range'] = sheetProtections[i].getRange();
protection['unprotected ranges'] = sheetProtections[i].getUnprotectedRanges();
protection['candomainedit'] = sheetProtections[i].canDomainEdit();
protection['iswarningonly'] = sheetProtections[i].isWarningOnly();
sheetProtections[i].remove();
protectionData.sheetProtections.push(protection);
}
for (var i=0; i<rangeProtections.length; i++) {
var protection = {};
protection['editors'] = rangeProtections[i].getEditors();
protection['description'] = rangeProtections[i].getDescription();
protection['range'] = rangeProtections[i].getRange();
protection['unprotected ranges'] = rangeProtections[i].getUnprotectedRanges();
protection['candomainedit'] = rangeProtections[i].canDomainEdit();
protection['iswarningonly'] = rangeProtections[i].isWarningOnly();
rangeProtections[i].remove();
protectionData.rangeProtections.push(protection);
}
try {
/**
*
* HERE YOU CAN RUN YOUR SCRIPT
*
**/
catch(e) {
Logger.log("Caught exception: " + e.toString());
}
for (var i=0; i<protectionData.sheetProtections.length; i++) {
var sheet = protectionData.sheetProtections[i]['range'].getSheet();
var protection = sheet.protect()
.setDescription(protectionData.sheetProtections[i]['description'])
.setRange(protectionData.sheetProtections[i]['range'])
.setUnprotectedRanges(protectionData.sheetProtections[i]['unprotected ranges'])
.setDomainEdit(protectionData.sheetProtections[i]['candomainedit'])
.setWarningOnly(protectionData.sheetProtections[i]['iswarningonly']);
var protectionEditors = protectionData.sheetProtections[i]['editors'];
// add Editors
for (var j=0; j<protectionEditors.length; j++) {
protection.addEditor(protectionEditors[j]);
}
}
for (var i=0; i<protectionData.rangeProtections.length; i++) {
var range = protectionData.rangeProtections[i]['range'];
var protection = range.protect()
.setDescription(protectionData.rangeProtections[i]['description'])
.setDomainEdit(protectionData.rangeProtections[i]['candomainedit'])
.setWarningOnly(protectionData.rangeProtections[i]['iswarningonly']);
var protectionEditors = protectionData.rangeProtections[i]['editors'];
// add Editors
for (var j=0; j<protectionEditors.length; j++) {
protection.addEditor(protectionEditors[j]);
}
}
}
The idea would be to actually run your script's code where the HERE YOU CAN RUN YOUR SCRIPT comment is located, which is the point at which the protections are removed from the Sheet and saved in memory. Afterwards, they are retrieved from memory and put back into the Sheet.
You must be careful, however, of the actual script exceeding the runtime limit (see quotas). If this situation occurs, the script will halt without re-setting your protections.
In case you are interested about protections in Google Apps Scripts, I suggest you check out the following link:
Class Protection

Google slides auto update links /tables linked from google sheet

I have a Google Slides presentation that has some linked cells/table to data in Google Sheets. Currently, I have to manually click each linked cell/table to update values.
I need a script for Google Slides that would auto-update / batch-update / refresh these links, so that the values/tables get auto-updated. Is that possible?
Both yes and no.
Charts
Yes! those can be batch updated:
function onOpen() {
SlidesApp.getUi() // Or DocumentApp or FormApp.
.createMenu('Update Charts')
.addItem("Update now !!!!!", 'refreshCharts').addToUi();
}
function refreshCharts(){
var gotSlides = SlidesApp.getActivePresentation().getSlides();
for (var i = 0; i < gotSlides.length; i++) {
var slide = gotSlides[i];
var sheetsCharts = slide.getSheetsCharts();
for (var k = 0; k < sheetsCharts.length; k++) {
var shChart = sheetsCharts[k];
shChart.refresh();
}
}
}
Source: https://stackoverflow.com/a/48254442/
Shapes/Tables
No: https://issuetracker.google.com/issues/64027131
Update From Google 24/5/2019
https://gsuiteupdates.googleblog.com/2019/05/bulk-update-docs-slides.html
Has anyone gotten this to work?
function refreshCharts(){
var gotSlides = SlidesApp.getActivePresentation().getSlides();
for (var i = 0; i < gotSlides.length; i++) {
var slide = gotSlides[i];
var sheetsCharts = slide.getSheetsCharts();
for (var k = 0; k < sheetsCharts.length; k++) {
var shChart = sheetsCharts[k];
shChart.refresh();
}
}
}
I just get this error message
"
Unable to refresh chart. Please verify that the chart is a valid chart in Google Sheets."
The following example setup assumes there are five slides with some linked charts:
first slide - main slide title/subtitle (no chart)
second slide - contains a chart linked from the chart of a pivot table that counts the R&D staff and what they planned to do from the Google Form responses sheet
third slide - contains a chart linked from the chart of a pivot table that counts the IME staff and what they planned to do from the Google Form responses sheet
fourth slide - contains a chart linked from the chart of a pivot table that counts the PMO staff and what they planned to do from the Google Form responses sheet
fifth slide - contains a chart linked from the chart of a pivot table that counts the total staff and what they planned to do from the Google Form responses sheet
The function below will update the slides with linked charts.
For it to work, it requires the Advanced Google Services: Google Slides API https://developers.google.com/slides/.
You can activate this advanced feature from your Google Apps Script IDE under Resources > Advanced Google Services... > Google Slides API. The API version is set to v1. If you don't enable it, the script will complain Slides is not defined at updateSlideCharts(...)
See the link below for more detail about RefreshSheetsChartRequest in Google Slides API v1: https://developers.google.com/slides/reference/rest/v1/presentations/request#RefreshSheetsChartRequest
function updateSlideCharts() {
var presentation = SlidesApp.openById(YOUR_SLIDE_ID); //you need to get this slide id from your slide URL
//if empty
if (presentation == null) throw new Error('Presentation was not found');
Logger.log("%s id = %s", presentation.getName(), presentation.getId());
var slides = presentation.getSlides();
if (slides == null) throw new Error('Slides were not found');
Logger.log("Total of slides in %s: %d", presentation.getName(), slides.length);
var presentationId = presentation.getId();
var presentationRndChartId = slides[1].getSheetsCharts()[0].getObjectId();
Logger.log("ObjectId of \"%s\": %s", slides[1].getSheetsCharts()[0].getTitle(), presentationRndChartId);
var presentationImeChartId = slides[2].getSheetsCharts()[0].getObjectId();
Logger.log("ObjectId of \"%s\": %s", slides[2].getSheetsCharts()[0].getTitle(), presentationImeChartId);
var presentationPmoChartId = slides[3].getSheetsCharts()[0].getObjectId();
Logger.log("ObjectId of \"%s\": %s", slides[3].getSheetsCharts()[0].getTitle(), presentationPmoChartId);
var presentationStaffChartId = slides[4].getSheetsCharts()[0].getObjectId();
Logger.log("ObjectId of \"%s\": %s", slides[4].getSheetsCharts()[0].getTitle(), presentationStaffChartId);
var requests = [{
refreshSheetsChart: {
objectId: presentationRndChartId
}
}];
// Execute the request.
var batchUpdateResponse = Slides.Presentations.batchUpdate({
requests: requests
}, presentationId);
Logger.log('Refreshed linked Sheets charts for \"%s\"', slides[1].getSheetsCharts()[0].getTitle());
requests = [{
refreshSheetsChart: {
objectId: presentationImeChartId
}
}];
// Execute the request.
batchUpdateResponse = Slides.Presentations.batchUpdate({
requests: requests
}, presentationId);
Logger.log('Refreshed linked Sheets charts for \"%s\"', slides[2].getSheetsCharts()[0].getTitle());
requests = [{
refreshSheetsChart: {
objectId: presentationPmoChartId
}
}];
// Execute the request.
batchUpdateResponse = Slides.Presentations.batchUpdate({
requests: requests
}, presentationId);
Logger.log('Refreshed linked Sheets charts for \"%s\"', slides[3].getSheetsCharts()[0].getTitle());
var requests = [{
refreshSheetsChart: {
objectId: presentationStaffChartId
}
}];
// Execute the request.
var batchUpdateResponse = Slides.Presentations.batchUpdate({
requests: requests
}, presentationId);
Logger.log('Refreshed linked Sheets charts for \"%s\"', slides[4].getSheetsCharts()[0].getTitle());
}
Linked Table
As of Aug 2021, there is still no .refresh() function for linked tables, but if your use case allows you to know the source spreadsheet and range at script writing, you can update a linked table by reading the text values, font colors, font styles, etc., from the spreadsheet and writing them to the table. Something like this:
function updateSheetsChart()
{
// The range on the source spreadsheet
var sourceRange = SpreadsheetApp.openById(SOURCE_SPREADSHEET_ID).getRange(SOURCE_TABLE_RANGE)
var source = {
'values': sourceRange.getDisplayValues(),
'backgrounds': sourceRange.getBackgrounds(),
'textStyles': sourceRange.getTextStyles(),
'fontColors': sourceRange.getFontColors()
}
// The linked table on the presentation
var table = SlidesApp.getActivePresentation().getPageElementById(SHEETS_TABLE_OBJECT_ID).asTable()
var columnCount = table.getNumColumns()
var rowCount = table.getNumRows()
for (var col = 0; col < columnCount; col++)
{
for (var row = 0; row < rowCount; row++)
{
var cell = table.getCell(row, col)
// Cell text
var cellText = cell.getText()
cellText.setText(source.values[row][col])
// Background color
cell.getFill().setSolidFill(source.backgrounds[row][col])
// Font style (bold)
var cellTextStyle = cellText.getTextStyle()
cellTextStyle.setBold(source.textStyles[row][col].isBold())
// Text color
cellTextStyle.setForegroundColor(source.fontColors[row][col])
}
}
}
You can add more lines for font size, italics, link URLs, etc. See the documentation for Range, Sheets TextStyle, and Slides TextStyle classes for the corresponding methods.
There are plenty of limitations: this won't copy full rich text (multiple colors or font styles in one cell), for example.
Also see this answer for a simpler workaround (but it doesn't transfer the cell formatting).

Creating Google Docs file by getting data from Google Sheets.

I am an experienced programmer who isn't experienced with using the script editor of Google Drive.
Since I need to make some reports, I was wondering about ways to exploit the script functionale of Google Drive to ease my process.
So my goal is there's this format that I created in Words, and for some parts of the Words, I need to put in each student's score. However, as doing this manually is very demanding, i was wondering ways to utilize google sheets and google docs for this.
So I was wondering if there's a way for me to get certain data from the spreadsheet (one column for each doc) and put the numbers in the appropriate space in the google docs file, and save it in google drive or send it as an email. Then, I will repeat this process for each column in the spreadsheet until everyone's report has been created.
If you professional programmers can help me out here it will be deeply appreciated. I never had any experience with google script editor and I do not know where to start. Thank you!
You may check this Script for generating Google documents from Google spreadsheet data source tutorial.
/**
* Generate Google Docs based on a template document and data incoming from a Google Spreadsheet
*
* License: MIT
*
* Copyright 2013 Mikko Ohtamaa, http://opensourcehacker.com
*/
// Row number from where to fill in the data (starts as 1 = first row)
var CUSTOMER_ID = 1;
// Google Doc id from the document template
// (Get ids from the URL)
var SOURCE_TEMPLATE = "xxx";
// In which spreadsheet we have all the customer data
var CUSTOMER_SPREADSHEET = "yyy";
// In which Google Drive we toss the target documents
var TARGET_FOLDER = "zzz";
/**
* Return spreadsheet row content as JS array.
*
* Note: We assume the row ends when we encounter
* the first empty cell. This might not be
* sometimes the desired behavior.
*
* Rows start at 1, not zero based!!! 🙁
*
*/
function getRowAsArray(sheet, row) {
var dataRange = sheet.getRange(row, 1, 1, 99);
var data = dataRange.getValues();
var columns = [];
for (i in data) {
var row = data[i];
Logger.log("Got row", row);
for(var l=0; l<99; l++) {
var col = row[l];
// First empty column interrupts
if(!col) {
break;
}
columns.push(col);
}
}
return columns;
}
/**
* Duplicates a Google Apps doc
*
* #return a new document with a given name from the orignal
*/
function createDuplicateDocument(sourceId, name) {
var source = DocsList.getFileById(sourceId);
var newFile = source.makeCopy(name);
var targetFolder = DocsList.getFolderById(TARGET_FOLDER);
newFile.addToFolder(targetFolder);
return DocumentApp.openById(newFile.getId());
}
/**
* Search a paragraph in the document and replaces it with the generated text
*/
function replaceParagraph(doc, keyword, newText) {
var ps = doc.getParagraphs();
for(var i=0; i<ps.length; i++) {
var p = ps[i];
var text = p.getText();
if(text.indexOf(keyword) >= 0) {
p.setText(newText);
p.setBold(false);
}
}
}
/**
* Script entry point
*/
function generateCustomerContract() {
var data = SpreadsheetApp.openById(CUSTOMER_SPREADSHEET);
// XXX: Cannot be accessed when run in the script editor?
// WHYYYYYYYYY? Asking one number, too complex?
//var CUSTOMER_ID = Browser.inputBox("Enter customer number in the spreadsheet", Browser.Buttons.OK_CANCEL);
if(!CUSTOMER_ID) {
return;
}
// Fetch variable names
// they are column names in the spreadsheet
var sheet = data.getSheets()[0];
var columns = getRowAsArray(sheet, 1);
Logger.log("Processing columns:" + columns);
var customerData = getRowAsArray(sheet, CUSTOMER_ID);
Logger.log("Processing data:" + customerData);
// Assume first column holds the name of the customer
var customerName = customerData[0];
var target = createDuplicateDocument(SOURCE_TEMPLATE, customerName + " agreement");
Logger.log("Created new document:" + target.getId());
for(var i=0; i<columns.length; i++) {
var key = columns[i] + ":";
// We don't replace the whole text, but leave the template text as a label
var text = customerData[i] || ""; // No Javascript undefined
var value = key + " " + text;
replaceParagraph(target, key, value);
}
}
As #James Donnellan stated, please check the official documentation on how to use the service which allows scripts to create, access, and modify Google Sheets files.

How to share a private spreadsheet on a public site using Awesome Table gadget

I would like to run the google awesome table gadget for a public site by accessing a private sheet url. This is to protect the data, otherwise the sheet url is visible to the public and they can copy the whole sheet. I want users to get the information only through the site.
How can I accomplish this. Is there a way to run the gadget like app script where it run as myself.
Do I need to modify the gadget xml to access my private sheet like app script?
You can use a proxy script between your private sheet and the public site. This will not hide the sheet URL, but avoids the need to share the sheet itself.
In the documentation for Awesome Tables, see "Use row-level permissions", which describes how to set up a proxy script. Instead of controlling display of specific rows, however, this simple proxy will serve your entire table, while hiding the underlying spreadsheet from the public.
Set up your Awesome Table spreadsheet & gadget normally. There is no special configuration of your data required.
Deploy the Simple Proxy script as a web app.
Copy the script below into a new script in your account.
Run it once to authorize it.
Deploy it as a web app, "Execute as me", Access to "anyone, including anonymous".
Copy the public URL of the app.
On the "Advanced parameters" tab of the Awesome Table gadget, paste the public URL of your Simple Proxy into the "Apps Script Proxy URL" field.
Simple Proxy.gs
This script was adapted from Romain's original, removing the Domain-only features that provided user-level data filtering.
// Simple proxy for AwesomeTables
// Adapted from https://script.google.com/d/1UfKnjB6jcemv5-BRP-ckaI5UCoEQI2KuvFdjNzmLpyadelNLCwpvaFsO/edit
function doGet(e) {
var ssUrl = e.parameter.url;
var sheetName = e.parameter.sheet;
var a1Notation = e.parameter.range;
var sheet = SpreadsheetApp.openByUrl(ssUrl).getSheetByName(sheetName);
var range = sheet.getRange(a1Notation);
var data = range.getValues();
var dt = {cols:[], rows:[]};
for(var i = 0; i < data[0].length; i++) {
dt.cols.push({id:i, label:data[0][i] + ' ' + data[1][i], type: 'string', isNumber:true, isDate:true});
}
for(var i = 2; i < data.length; i++) {
var row = [];
for(var j = 0; j < data[i].length; j++) {
if(isNaN(data[i][j])) dt.cols[j].isNumber = false;
if(data[i][j] instanceof Date == false) dt.cols[j].isDate = false;
else if(data[i][j].getFullYear() == 1899) {
dt.cols[j].isDate = false;
data[i][j] = data[i][j].getHours()+':'+(data[i][j].getMinutes()<10?'0':'')+data[i][j].getMinutes();
}
else data[i][j] = "Date("+data[i][j].getTime()+")";
row.push({v:data[i][j]});
}
dt.rows.push({c:row});
}
for(var i = 0; i < data[0].length; i++) {
if(dt.cols[i].isDate) dt.cols[i].type = 'datetime';
else if(dt.cols[i].isNumber) dt.cols[i].type = 'number';
}
var output = e.parameters.callback + '(' + JSON.stringify({
dataTable: dt
}) + ')';
return ContentService.createTextOutput(output).setMimeType(ContentService.MimeType.JAVASCRIPT);
}
Caveats
A user will be able to view the URL of your spreadsheet in the HTML source for the page hosting Awesome Tables. If you have enabled sharing, the spreadsheet could be wide open to them.
A single Simple Proxy can serve ALL spreadsheets that your account has access to. This is both a feature and a risk that you should be aware of.