Insert page breaks in Google Docs with App script - google-apps-script

I want to create a Google Docs file, provided that the number of pages must be even. I used the following code:
function myFunction(){
var data = doc.getAs("application/pdf").getDataAsString();
var pages = data.match(/\/Contents/g).length;
Logger.log(pages);
if(pages % 2 !== 0)
{
var searchText = '----- End -----';
var res = Docs.Documents.get(docId);
let offset = 0;
const requests = res.body.content.reduce((ar, e) => {
if (e.paragraph) {
e.paragraph.elements.forEach(f => {
if (f.textRun) {
const re = new RegExp(searchText, "g");
let p = null;
while (p = re.exec(f.textRun.content)) {
ar.push({insertPageBreak: {location: {index: p.index + offset}}});
}
}
})
}
offset = e.endIndex;
return ar;
}, []).reverse();
Docs.Documents.batchUpdate({requests: requests}, docId);
}
When running, I get the error: Preventing GoogleJsonResponseException: API call to sheets.spreadsheets.batchUpdate failed with error: Must specify at least one request.
After some research, I fixed the line
Docs.Documents.batchUpdate({requests: requests}, docId);
to
if (requests.length > 0) {
Docs.Documents.batchUpdate({requests: requests}, docId);}
}
Now the code is running normally but in the generated Google Docs file, there are still no page breaks.
I need help. Thank you.

From your showing script, I thought that you might my this answer https://stackoverflow.com/a/65745933 . If my understanding is correct, I thought that the reason for your current issue of Preventing GoogleJsonResponseException: API call to sheets.spreadsheets.batchUpdate failed with error: Must specify at least one request. is due to that in your Google Document has no value of var searchText = '----- End -----'. For example, when you put the value of ----- End ----- to the last page of your Google Document, this script can be used. But, from your error message, I'm worried that the value might not be included in your Google Document.
So, in your situation, I thought that you might want to insert the page break to the last page. In this case, how about the following modifications? At the following modifications, the page break is inserted at the last of the document.
Pattern 1:
In this pattern, Docs API is used.
From:
var searchText = '----- End -----';
var res = Docs.Documents.get(docId);
let offset = 0;
const requests = res.body.content.reduce((ar, e) => {
if (e.paragraph) {
e.paragraph.elements.forEach(f => {
if (f.textRun) {
const re = new RegExp(searchText, "g");
let p = null;
while (p = re.exec(f.textRun.content)) {
ar.push({insertPageBreak: {location: {index: p.index + offset}}});
}
}
})
}
offset = e.endIndex;
return ar;
}, []).reverse();
Docs.Documents.batchUpdate({requests: requests}, docId);
To:
var res = Docs.Documents.get(docId);
var requests = [{ insertPageBreak: { location: { index: res.body.content.pop().endIndex - 1 } } }];
Docs.Documents.batchUpdate({ requests }, docId);
Pattern 2:
In this pattern, Docs API is not used.
From:
var searchText = '----- End -----';
var res = Docs.Documents.get(docId);
let offset = 0;
const requests = res.body.content.reduce((ar, e) => {
if (e.paragraph) {
e.paragraph.elements.forEach(f => {
if (f.textRun) {
const re = new RegExp(searchText, "g");
let p = null;
while (p = re.exec(f.textRun.content)) {
ar.push({insertPageBreak: {location: {index: p.index + offset}}});
}
}
})
}
offset = e.endIndex;
return ar;
}, []).reverse();
Docs.Documents.batchUpdate({requests: requests}, docId);
To:
var body = doc.getBody();
doc.getBody().insertPageBreak(body.getChildIndex(body.appendParagraph("")));
From your script, unfortunately, I cannot know your doc. So, I guessed doc as Class Document object. Please be careful about this.
Reference:
insertPageBreak(childIndex)

Related

How to create a list of all folders in a shared goggle drive folder

I'm trying to create a listing of all my google drive folders and I have the following script and a link to the question where I got the script from. I'm just not sure how to implement it. I put the Drive ID in line 6 and then ran it and got this error; Can anyone tell me where I'm going wrong?
List every file and folder of a shared drive in a spreadsheet with Apps Script
11:08:37 AM Error
ReferenceError: gobj is not defined
getFoldersInASharedFolder # Folder Listing.gs:12
This is the script
function getFoldersInASharedFolder() {
let tr = [];
let token = '';
let page = 0;
do {
let r = Drive.Files.list({ corpora: 'drive', includeItemsFromAllDrives: true, supportsTeamDrive: true, supportsAllDrives: true, driveId: "???", pageToken: token,q: "mimeType = 'application/vnd.google-apps.folder'" });
let obj = JSON.parse(r);
tr.push(obj)
token = obj.nextPageToken
} while (token != null)
let folder = DriveApp.getFolderById(gobj.globals.testfolderid);
folder.createFile(`SharedDriveList ${Utilities.formatDate(new Date(), Session.getScriptTimeZone(), "MM/dd/yyyy HH:mm:ss")}`, JSON.stringify(tr), MimeType.PLAIN_TEXT);
let html = '<style>td,th{border:1px solid black;font-size: 16px;}</style><table><tr><th>Title</th><th>Id</th><th>Path</th></tr>';
tr.forEach((o, i) => {
o.items.forEach(item => {
if (item.mimeType = "application/vnd.google-apps.folder") {
html += `<tr><td>${item.title}</td><td>${item.id}</td><td>${getPathAllDrivesFromId(item.id)}</td></tr>`;
}
})
});
html += '</table><input type="button" value="exit" onclick="google.script.host.close()" />';
SpreadsheetApp.getUi().showModelessDialog(HtmlService.createHtmlOutput(html).setHeight(500).setWidth(1200), `Folders in: ${JSON.parse(Drive.Drives.get("driveid")).name}`);
}
The error message occurs because
let folder = DriveApp.getFolderById(gobj.globals.testfolderid);
use the a nested property of gobj as parameter but gobj was not declared.
You can fix this error either by properly declaring gobj or by replacing gobj.globals.testfolderid by the folder id (properly set as a string).
The comments explain how to use this function
function getFoldersInASharedFolder() {
const sharedriveid = "";//add the shared drive id here
const storagefilefolderid = ""; //this where I was storing the response which I used when I was first building the script. It's not necessary to do this if you don't wish to. You can just comment that code out of the script
let tr = [];
let token = '';
let page = 0;
do {
let r = Drive.Files.list({ corpora: 'drive', includeItemsFromAllDrives: true, supportsTeamDrive: true, supportsAllDrives: true, driveId: sharedriveid, pageToken: token,q: "mimeType = 'application/vnd.google-apps.folder'" });//drive id for the shared drive that you wish all of the folders from
let obj = JSON.parse(r);
tr.push(obj)
token = obj.nextPageToken
} while (token != null)
let folder = DriveApp.getFolderById(storagefilefolderid);//the folder id for the file that stores the results
folder.createFile(`SharedDriveList ${Utilities.formatDate(new Date(), Session.getScriptTimeZone(), "MM/dd/yyyy HH:mm:ss")}`, JSON.stringify(tr), MimeType.PLAIN_TEXT);
let html = '<style>td,th{border:1px solid black;font-size: 16px;}</style><table><tr><th>Title</th><th>Id</th><th>Path</th></tr>';
tr.forEach((o, i) => {
o.items.forEach(item => {
if (item.mimeType = "application/vnd.google-apps.folder") {
html += `<tr><td>${item.title}</td><td>${item.id}</td><td>${getPathAllDrivesFromId(item.id)}</td></tr>`;
}
})
});
html += '</table><input type="button" value="exit" onclick="google.script.host.close()" />';
SpreadsheetApp.getUi().showModelessDialog(HtmlService.createHtmlOutput(html).setHeight(500).setWidth(1200), `Folders in: ${JSON.parse(Drive.Drives.get(sharedrivedid)).name}`);
}

Debug/Fix "Exceeded maximum execution time" error Google Addon

I've created my first Google Workspace Addon which I have deployed to the associated domain successfully although still working on it due to this error.
Without errors, functionally it's (was) working well, however, I now keep getting "Exceeded maximum execution time" errors.
This error is frustratingly intermittent, but it recently started occuring around 90%+ of the time despite not occurring previously for weeks. Reverting to previous versions of my code hasn't fixed the issue.
Most if not all existing answers regarding this error assume the script is genuinely exceeding the time limit for scripts (either 6 minutes or 30 minutes).
To be clear - this is not what's happening here. The script doesn't run for anywhere close to the maximum time allowed (as far as I'm aware). It actually runs for around 45 seconds before giving the error.
It may somehow be running in the background and causing a legitimate Timeout error but I need to figure out if this is the case and the cause of the error and then fix it.
I've contacted Google but their relevant support who are knowledgeable about this and who could help are unfortunately very difficult to get hold of.
In Cloud Logging for this error, I'm seeing (obfuscated id's):
{
"insertId": "28z8v2anf1xq6",
"jsonPayload": {
"context": {
"reportLocation": {
"filePath": "[unknown file]",
"functionName": "[unknown function]"
}
},
"message": "Exceeded maximum execution time",
"serviceContext": {
"service": "AKfycbx-SO5mXBNoe2leEH4wrj02t9fZZwkq5BQlJPuBaNM"
}
},
"resource": {
"type": "app_script_function",
"labels": {
"function_name": "fileMakerPro",
"invocation_type": "unknown",
"project_id": "order-management-713317"
}
},
"timestamp": "2022-03-10T10:09:58.827Z",
"severity": "ERROR",
"labels": {
"script.googleapis.com/process_id": "fIxbX5X5IjrIqLLK-XQgJiJV0U9vQMEAEA1GOxTqw-XzqsaVlKTNK2UcymM7feuXp-qZLshvBUg61TS6u28l_v6szoAq8QBBOvGudISVj0yceQXPKpIHO6HJ2G-uxuqy4xcnv-NzDfBTMbJH7VmK_AjZd6a5KVA-LnhtOE_28mCn_zTpI5AC3-BhX_lcCC1p-3QsMX6blhMZSYgVTTo1T_Z9SovufUWinpJieIbio-L8wzQPkjLYM2l9s5RHGuKMcT3LbvUO7fFS4DV1z_xfaR_nU1LqeayAk1aouGSXc",
"script.googleapis.com/user_key": "wPo7ZL4DSIqpPrZfeqo7E7yAArA430YCNnzbVYhyuVlR6nKybMkISeaDZoigRDx0gAJXIjF49EoL",
"script.googleapis.com/deployment_id": "wkq5BQlJPcbx-SO5mEHAKfywrj02t9fZZ4uBXBNoe2leaNM",
"script.googleapis.com/project_key": "Xf9t90MIwJJbMnu2Z9hQ48TM6DPTcW9w3"
},
"logName": "projects/order-management-317713/logs/script.googleapis.com%2Fconsole_logs",
"receiveTimestamp": "2022-03-10T10:09:59.313440894Z"
}
Which is obscure and isn't helpful at all with the "unknowns".
The function it references, for reference:
async function fileMakerPro(e) {
console.time("fileMakerPro");
//Order Management Named Ranges
let OM_NR = await getNamedRangesAsObject_(ss);
let orderNumber, orderFolder, orderFolderName;
let loadingDate, loadingDateFormatted, dueDate, dueDateFormatted;
let sheetGoodsTable = OM_NR.goodsTable.ranges.filter(function (x) { /* here, x is an array, not an object */
return !(x.every(element => element === (undefined || null || '')))
});
//Get order number if exists or show warning and return
if (orderNumber = OM_NR.OrderNo.range.toString()) {
} else {
ui.alert(`⚠️ ORDER NUMBER \(${OM_NR.OrderNo.rangeA1.toString()}\) appears missing, please correct and try again.`);
return;
}
//Get loading date if exists or show warning and return
if (loadingDate = OM_NR.transportLoadingDate.range) {
loadingDateFormatted = `${loadingDate.getDate()}-${loadingDate.getMonth() + 1}-${loadingDate.getFullYear()}`;
monthName = monthNames[loadingDate.getMonth()];
//Set Due Date 30 days after Loading Date
dueDate = new Date(loadingDate.getFullYear(), loadingDate.getMonth(), loadingDate.getDate() + 30);
dueDateFormatted = `${dueDate.getDate()}-${dueDate.getMonth() + 1}-${dueDate.getFullYear()}`;
} else {
ui.alert(`⚠️ LOADING DATE \(${OM_NR.transportLoadingDate.rangeA1.toString()}\) appears to be missing, please correct and try again.`);
return;
}
let rootFolder = DriveApp.getFileById(sheetId).getParents().next();
let tempFolder = DriveApp.getFolderById("1f4Ll-KODzmvBIuunaCS_7anMCX-w86S6");
let monthFolders = rootFolder.getFolders();
let tickBoxes = e.formInputs.generateDocsCheckboxes;
let packingListTemplateId = '1cZUB4U59Z56456gdgdghhka2QQ9sUNIlQSHYy1M';
let tickCMR = false, tickInvoice = false;
//Find existing order folder
let orderSearch = await searchFolders(monthFolders, monthName, orderNumber);
//Set Order Folder Name if both values present
orderFolderName = `${loadingDate.getDate()}-${loadingDate.getMonth() + 1}-${loadingDate.getFullYear()} \(${orderNumber}\)`;
if (tickBoxes !== undefined) {
if (tickBoxes.indexOf('tickBoxCMRnote') > -1) {
tickCMR = true;
}
if (tickBoxes.indexOf('tickBoxInvoice') > -1) {
tickInvoice = true;
}
} else {
let confirm = ui.alert(`⚠️ No documents selected. Update Order ${orderNumber} Snapshot and Packing List only?`, Browser.Buttons.YES_NO);
if (confirm === ui.Button.YES) {
fileMakerPro();
} else {
ss.toast(`⚠️ Update cancelled.`);
return;
}
}
if (!orderSearch.foundMonthFolder) {
ss.toast(`⚙️ Creating Month Folder...`);
monthFolder = rootFolder.createFolder(monthName);
} else {
monthFolder = orderSearch.foundMonthFolder;
}
if (!orderSearch.foundOrderFolder) {
ss.toast(`⚙️ Creating Order Folder...`);
orderFolder = monthFolder.createFolder(orderFolderName);
} else {
if (!orderSearch.foundOrderFolder.getName().includes(loadingDateFormatted)) {
ui.alert(`⚠️ Duplicate Order Number ${orderNumber} found: ${orderSearch.foundMonthFolder.getName()} / ${orderSearch.foundOrderFolder.getName()}.\n\n Please change Order Number or remove duplicate Order Folder.`);
return;
}
orderFolder = orderSearch.foundOrderFolder;
}
if (tickInvoice && tickCMR) {
assembleCMR(orderFolder, orderNumber, tempFolder, loadingDateFormatted, sheetGoodsTable, OM_NR);
assembleInvoice(orderFolder, orderNumber, tempFolder, loadingDateFormatted, dueDateFormatted, sheetGoodsTable, OM_NR);
ss.toast(`✅ Documents Saved`);
} else {
if (tickCMR) {
assembleCMR(orderFolder, orderNumber, tempFolder, loadingDateFormatted, sheetGoodsTable, OM_NR);
ss.toast(`✅ CMR Document Saved`);
}
if (tickInvoice) {
assembleInvoice(orderFolder, orderNumber, tempFolder, loadingDateFormatted, dueDateFormatted, sheetGoodsTable, OM_NR);
ss.toast(`✅ Invoice Document Saved`);
}
}
let existingOrderSnapshot = orderFolder.getFilesByName(`${orderNumber} - ORDER SNAPSHOT - DO NOT EDIT`);
let existingPackingList = orderFolder.getFilesByName(`${orderNumber} - PACKING LIST`);
let packingListId = await createPackingList(existingPackingList, orderFolder, orderNumber, packingListTemplateId, sheetId, OM_NR);
createOrderSnapshot(existingOrderSnapshot, orderFolder, orderNumber, sheetId, packingListId);
syncOrderNumbers(orderSearch.orderNumbers);
console.timeEnd("fileMakerPro");
return;
}
I suspect maybe the following function could be causing the Timeout since the loop may be malfunctioning but not sure how to check or fix:
async function writePackingList(OM_NR, packingList, packingListId){
let PL_NR = await getNamedRangesAsObject_(packingList);
for (let nr in PL_NR) {
Logger.log(nr);
if (nr == "loadingAddress") {
packingList.getRangeByName(`${nr}`).setValue(OM_NR[`${nr}`].range);
} else if (nr) {
packingList.getRangeByName(`${nr}`).setValue(OM_NR[`${nr}`].range);
} else {
break;
}
}
ss.getRangeByName("transportTotalNetWeight").setFormula(`=IMPORTRANGE("${packingListId}", "PL!I15")`)
ss.getRangeByName("transportTotalGrossWeight").setFormula(`=IMPORTRANGE("${packingListId}", "PL!H15")`)
}
Any help would be appreciated as I've hit a brick wall with this. I need to figure out how to debug this error, understand why it's happening and fix it so it doesn't happen when fully deployed.
Thanks
EDIT to include additional functions:
//Make copy of Invoice template doc, get body, replace all placeholders, return
async function assembleInvoice(orderFolder, orderNumber, tempFolder, loadingDateFormatted, dueDateFormatted, sheetGoodsTable, OM_NR) {
console.time("assembleInvoice");
ss.toast(`⚙️ Assembling Invoice Document...`);
let prefix = orderNumber + " - ";
let fileName = "INVOICE";
let templateTempCopy;
//Blue Invoice Template
let invoiceTemplate1 = DriveApp.getFileById("C2j_q3nUP4UTUR1KiCt07r1lATPKSjzEm-EeY109T8B4");
//Red Invoice Template
let invoiceTemplate2 = DriveApp.getFileById("1uczJT-F-JzKzaBwh_CdhJNcFkgDHfGjypJYom4vqGIo");
//Choose Invoice template based on Movement Type
let staraMovementType = OM_NR.staraMovementType.range;
if (staraMovementType.toString().includes('GB SHIPPING TO')) {
templateTempCopy = invoiceTemplate1.makeCopy(tempFolder);
} else {
templateTempCopy = invoiceTemplate1.makeCopy(tempFolder);
}
//Open working copy
let workingCopy = DocumentApp.openById(templateTempCopy.getId());
//Get body from working copy
let documentBody = workingCopy.getBody();
//Populate table
let allTables = documentBody.getTables();
let invoiceGoodsTable = allTables[3];
let itemRowTemplate = invoiceGoodsTable.getRow(invoiceGoodsTable.getNumRows() - 2);
let totalsRowTemplate = invoiceGoodsTable.getRow(invoiceGoodsTable.getNumRows() - 1);
for (let n in sheetGoodsTable) {
let tableRow = invoiceGoodsTable.appendTableRow(itemRowTemplate.copy());
tableRow.getCell(0).replaceText("{itemNo}", sheetGoodsTable[n][1]);
tableRow.getCell(1).replaceText("{itemDesc}", sheetGoodsTable[n][0]);
tableRow.getCell(2).replaceText("{commodityCode}", sheetGoodsTable[n][2]);
tableRow.getCell(3).replaceText("{cartons}", sheetGoodsTable[n][4]);
tableRow.getCell(4).replaceText("{qtyKgs}", sheetGoodsTable[n][5]);
tableRow.getCell(5).replaceText("{price}", sheetGoodsTable[n][3]);
tableRow.getCell(6).replaceText("{total}", sheetGoodsTable[n][5] * sheetGoodsTable[n][3]);
}
let totalsRow = invoiceGoodsTable.appendTableRow(totalsRowTemplate.copy());
totalsRow.getCell(3).replaceText("{cartons}", OM_NR.transportCasesCartons.range);
totalsRow.getCell(4).replaceText("{qtyKgs}", OM_NR.transportTotalNetWeight.range);
totalsRow.getCell(6).replaceText("{total}", OM_NR.transportInvoiceTotal.range);
itemRowTemplate.removeFromParent();
totalsRowTemplate.removeFromParent();
let importerAddress = `${OM_NR.importerConsigneeIntoEU.range}, ${OM_NR.importerAddress.range}`;
documentBody.replaceText("{currency}", OM_NR.staraInvoiceCurrency.range);
documentBody.replaceText("{shipToAddress}", importerAddress);
documentBody.replaceText("{billToAddress}", importerAddress);
documentBody.replaceText("{exporterCustomsInvoiceNo}", OM_NR.exporterCustomsInvoiceNo.range);
documentBody.replaceText("{exporterVAT}", OM_NR.exporterVAT.range);
documentBody.replaceText("{exporterEORI}", OM_NR.exporterEORI.range);
documentBody.replaceText("{staraOrderNo}", OM_NR.staraOrderNo.range);
documentBody.replaceText("{staraCustomerOrderNo}", OM_NR.staraCustomerOrderNo.range);
documentBody.replaceText("{transportLoadingDate}", loadingDateFormatted);
documentBody.replaceText("{dueDate}", dueDateFormatted);
documentBody.replaceText("{paymentTerms}", "30 DAYS");
documentBody.replaceText("{staraInvoiceNo}", OM_NR.staraInvoiceNo.range);
documentBody.replaceText("{incoTerms}", `${OM_NR.orderIncoterm.range} ${OM_NR.orderCountryOfDestination.range}`);
documentBody.replaceText("{transportTotalGrossWeight}", OM_NR.transportTotalGrossWeight.range);
documentBody.replaceText("{transportTotalNetWeight}", OM_NR.transportTotalNetWeight.range);
documentBody.replaceText("{transportTruckRef}", OM_NR.transportTruckRef.range);
documentBody.replaceText("{transportSeal1}", OM_NR.transportSeal1.range);
documentBody.replaceText("{transportSeal2}", OM_NR.transportSeal2.range);
documentBody.replaceText("{transportInvoiceTotal}", OM_NR.transportInvoiceTotal.range);
documentBody.replaceText("{staraInvoiceCurrency}", OM_NR.staraInvoiceCurrency.range);
workingCopy.saveAndClose();
//Gets a 'blob' of the completed document
let completedDoc = templateTempCopy.getAs('application/pdf');
//Check for existing PDF and move to trash (not fully delete)
let foundFile = orderFolder.getFilesByName(prefix + fileName);
if (foundFile.hasNext()) {
foundFile.next().setTrashed(true);
}
try {
//Create PDF
ss.toast(`📝 Saving ${fileName} PDF...`);
orderFolder.createFile(completedDoc).setName(prefix + fileName);
deleteTempFiles(templateTempCopy.getId());
} catch (err) {
showError(err);
} finally {
ss.toast(`✅ Invoice Document Saved`);
console.timeEnd("assembleInvoice");
}
return;
}
async function createPackingList(existingPackingList, orderFolder, orderNumber, packingListTemplateId, sheetId, OM_NR) {
console.time("createPackingList");
let packingListId;
//Get current sheet container objects and data
if (existingPackingList.hasNext()) {
packingListId = existingPackingList.next().getId();
} else {
ss.toast(`📃 Creating Packing List...`);
let newPackingList = DriveApp.getFileById(packingListTemplateId);
let newPackingListCopy = newPackingList.makeCopy(`${orderNumber} - PACKING LIST`, orderFolder)
packingListId = newPackingListCopy.getId();
}
let packingList = SpreadsheetApp.openById(packingListId);
addImportrangePermission(packingListId, sheetId);
writePackingList(OM_NR, packingList, packingListId);
ss.toast(`✅ Packing List Created/Updated`);
console.timeEnd("createPackingList");
return packingListId;
}
async function createOrderSnapshot(existingOrderSnapshot, orderFolder, orderNumber, sheetId, packingListId) {
console.time("createOrderSnapshot");
let snapshotExists = existingOrderSnapshot.hasNext();
//If snapshot exists, delete.
if (snapshotExists) {
existingOrderSnapshot.next().setTrashed(true);
ss.toast(`📷 Updating Order Snapshot...`);
} else {
ss.toast(`📷 Creating Order Snapshot...`);
}
//Make copy of sheet in order folder
let currentSheet = DriveApp.getFileById(sheetId);
let sheetCopy = currentSheet.makeCopy(`${orderNumber} - ORDER SNAPSHOT - DO NOT EDIT`, orderFolder);
let sheetCopyId = sheetCopy.getId();
let targetSpreadsheet = SpreadsheetApp.openById(sheetCopyId);
//let targetSpreadsheet = SpreadsheetApp.openById(DriveApp.getFileById(sheetId).makeCopy(`${orderNumber} - ORDER SNAPSHOT - DO NOT EDIT`, orderFolder).getId());
addImportrangePermission(targetSpreadsheet.getRangeByName("CONTROL").getValue(), sheetCopyId);
addImportrangePermission(packingListId, sheetCopyId);
targetSpreadsheet.getRangeByName("transportTotalNetWeight").setFormula(`=IMPORTRANGE("${packingListId}", "PL!I15")`)
targetSpreadsheet.getRangeByName("transportTotalGrossWeight").setFormula(`=IMPORTRANGE("${packingListId}", "PL!H15")`)
targetSpreadsheet.getRangeByName("topRow").clearDataValidations()
targetSpreadsheet.getRangeByName("sheetBody").clearDataValidations()
//DELETE UNNEEDED TABS FROM COPY
targetSpreadsheet.deleteSheet(targetSpreadsheet.getSheetByName("PC"));
targetSpreadsheet.deleteSheet(targetSpreadsheet.getSheetByName("C1"));
targetSpreadsheet.deleteSheet(targetSpreadsheet.getSheetByName("C2"));
targetSpreadsheet.deleteSheet(targetSpreadsheet.getSheetByName("SFC"));
if (snapshotExists) {
ss.toast(`✅ Order Snapshot Updated`);
} else {
ss.toast(`✅ Order Snapshot Created`);
}
console.timeEnd("createOrderSnapshot");
return;
}
async function addImportrangePermission(sourceSheetId, authSheetId) {
console.time("addImportrangePermission");
if (sourceSheetId) {
// donor or source spreadsheet id
// adding permission by fetching this url
let url = `https://docs.google.com/spreadsheets/d/${authSheetId}/externaldata/addimportrangepermissions?donorDocId=${sourceSheetId}`;
let token = ScriptApp.getOAuthToken();
let params = {
method: 'post',
headers: {
Authorization: 'Bearer ' + token,
},
muteHttpExceptions: true
};
UrlFetchApp.fetch(url, params);
}
console.timeEnd("addImportrangePermission");
return;
}
Issue:
In Workspace add-ons, callback functions executed when an Action triggers are limited to 30 seconds of execution time:
The Apps Script Card service limits callback functions to a maximum of 30 seconds of execution time. If the execution takes longer than that, your add-on UI may not update its card display properly in response to the Action.
Note:
Since you didn't provide the code related to the functions which are taking most time (e.g. assembleCMR, assembleInvoice, createPackingList) I cannot make any suggestion on that, but in any case you should try to improve the efficiency of those functions, or split it all into several Actions.
Reference:
Callback functions

trouble with "statistics" property when using with YouTube.Search.list in Google App Script

I cannot seem to get this script to work for 'statistics':
function searchByKeyword2() {
var results = YouTube.Search.list('statistics', {q: 'dogs', maxResults: 5, });
for(var i in results.items) {
var item = results.items[i];
Logger.log(item);
}
}
I can use 'id', 'snippet', or 'id, snippet', but I cannot get it to work with 'statistics'. I've been looking an answer for hours, but I haven't found anything. Any clues?
Per the API documentation, YouTube.Search includes results for Videos, Channels, and Playlists. Not all of these resources have statistics nodes, and thus the YouTube.Search endpoint does not allow querying for the statistics node - only id and snippet.
For collections which track statistics, like Videos, you query them directly to access statistics. Since Videos.list does not search, you need to first search and then provide the relevant video IDs. Note that you can change the search order (and many other search properties) - full details available in the API reference - but the default sort is 'relevance'.
As an example:
function getVideoStatistics(videoIds) {
const options = {
id: videoIds.join(","),
fields: "nextPageToken,items(id,statistics)"
};
const results = [];
do {
var search = YouTube.Videos.list('statistics', options);
if (search.items && search.items.length)
Array.prototype.push.apply(results, search.items);
options.pageToken = search.nextPageToken;
} while (options.pageToken);
return results;
}
function getVideosFromQuery(query, maxResults) {
const options = {
q: query,
maxResults: maxResults,
type: 'video',
fields: "nextPageToken,pageInfo/totalResults,items(id/videoId,snippet(title,channelTitle))"
};
const results = [];
do {
var search = YouTube.Search.list('snippet', options);
if (search.items && search.items.length)
Array.prototype.push.apply(results, search.items);
options.pageToken = search.nextPageToken;
} while (options.pageToken && results.length < search.pageInfo.totalResults && results.length < maxResults);
return results;
}
function foo() {
var someQuery = "something";
var searchResults = getVideosFromQuery(someQuery, 50);
var ids = searchResults.map(function (videoSearchResult) {
return videoSearchResult.id.videoId;
});
var stats = getVideoStatistics(ids);
console.log({message:"query video statistics", searchResults: searchResults, statistics: stats});
}

DataStudio returns random error id when using custom connector

I am using script.google.com to create a custom connector that can read CSV data from drive.google.com and send the data to Googles data studio.
When running the connector and inserting a simple table inside the data studio, I receive a simple that the request could not be processed because of an server error. The error id is changing every time I "re-publish" the script.
This is
function getData(request) {
var dataSchema = [];
request.fields.forEach(function(field) {
for (var i = 0; i < csvDataSchema.length; i++) {
if (csvDataSchema[i].name === field.name) {
dataSchema.push(csvDataSchema[i]);
break;
}
}
});
csvFile = UrlFetchApp.fetch("https://drive.google.com/uc?export=download&id=" + request.configParams.documentId);
var csvData = Utilities.parseCsv(csvFile);
var data = [];
csvData.forEach(function(row) {
data.push({
values: row
});
});
console.log( {
schema: dataSchema,
rows: data
} );
return {
schema: dataSchema,
rows: data
};
};
This is the csvDataSchema:
var csvDataSchema = [
{
name: 'date',
label: 'Date',
dataType: 'STRING',
semantics: {
conceptType: 'DIMENSION'
}
},
{
name: 'nanoseconds',
label: 'nanoseconds',
dataType: 'NUMBER',
semantics: {
"isReaggregatable": true,
conceptType: 'METRIC'
}
},{
name: 'size',
label: 'Size of Testfile in MByte',
dataType: 'STRING',
semantics: {
"isReaggregatable": false,
conceptType: 'DIMENSION'
}
}
];
And this is the result of the getData function, stringified:
{"schema":[{"name":"date","label":"Date","dataType":"STRING","semantics":{"conceptType":"DIMENSION"}},{"name":"size","label":"Size of Testfile in MByte","dataType":"STRING","semantics":{"isReaggregatable":false,"conceptType":"DIMENSION"}}],"rows":[{"values":["2017-05-23",123,"1"]},{"values":["2017-05-23",123,"1"]}]}
It perfectly fits to the reference. I am providing more information, but following the tutorial it should work, anyways.
Those are the fields provided in request:
And this is what getDate returns:
So, what I am wondering first is: Why is there a random error id? And what could be wrong with my script?
You should only return fields/columns included in request. Currently, data contains all fields that are in csvFile. Depending on your chart element in your dashboard, request will most likely contain only a subset of your full schema. See example implementation at the Data Studio Open Source repo.
If this does not solve the problem, you should setup error handing and check if the error is occurring at any specific line.
#Minhaz Kazi gave the missing hint:
As I did not "dynamically" filled the response object in getData, I always returned all three columns.
With my code above the only thing I had to do is adding the third column as a dimension or a metric.
So I changed my code to dynamically return the columns so it will fit to the response. For this I had to implement an function that will transform the CSV-data into an object.
This is the getData() function now:
function getData(request) {
var url = "https://drive.google.com/uc?export=download&id="
+ request.configParams.documentId;
var csvFile = UrlFetchApp.fetch(url);
var csvData = Utilities.parseCsv(csvFile);
var sourceData = csvToObject(csvData);
var data = [];
sourceData.forEach(function(row) {
var values = [];
dataSchema.forEach(function(field) {
switch(field.name) {
case 'date':
values.push(row.date);
break;
case 'nanoseconds':
values.push(row.nanoseconds);
break;
case 'size':
values.push(row.size);
break;
default:
values.push('');
}
});
data.push({
values: values
});
});
return {
schema: dataSchema,
rows: data
};
};}
And this is the function to convert the CSV data to an object:
function csvToObject(array) {
var headers = array[0];
var jsonData = [];
for ( var i = 1, length = array.length; i < length; i++ )
{
var row = array[i];
var data = {};
for ( var x = 0; x < row.length; x++ )
{
data[headers[x]] = row[x];
}
jsonData.push(data);
}
return jsonData;
}
(it's based on a so-solution from here, I modified it to fit my source CSV data)

Export Form responses as csv Google Apps Scripts

Is there is a fast way to programmatically export all responses from a Google Form to a csv? Something like "Export responses to csv" invoked via Scripts.
Right now I'm doing it in a rock art way:
Iterate over the forms I want to export (~75)
Open each form var form = FormApp.openById(formId);
Get responses: var formReponses = form.getResponses(); (from 0 to 700 responses each form)
Iterate over responses and get item responses: var preguntes = formReponses[r].getItemResponses();
For each itemResponse, convert it to csv/json
Export responses to a drive file
This is extremly slow and additionally it hangs over and over, so I had to export responses in chunks of 50 responses and save them in Drive separated files. On next execution (after letting servers to cool down for a while), I'm executing the script again, skipping the number of responses found on the chunk file.
Additionally I'm not sure that Google keeps the responses order when doing form.getResponses(); (actually I've found that if the form has been modified, the order is not the same)
Is there a better way to do it?
Whith the help of #JackBrown I've managed to write a Chrome extension to download responses (maybe soon in github). This will wait for each download in the formIds object until finished and then prompt for the next one:
'use strict';
function startDownload() {
const formIds = {
'Downloads-subfolder-here': {
'Download-filename-here': '1-cx-aSAMrTK0IHsQkE... {form-id here}',
'Another-filename-here': '...-dnqdpnEso {form-id here}',
// ...
},
'Another-subfolder-here': {
'Download-filename-here': '1-cx-aSAMrTK0IHsQkE... {form-id here}',
'Another-filename-here': '...-dnqdpnEso {form-id here}',
// ...
},
};
const destFolders = Object.keys(formIds);
const downloads = [];
for (let t = 0, tl = destFolders.length; t < tl; t += 1) {
const destFolder = destFolders[t];
const forms = Object.keys(formIds[destFolder]);
for (let f = 0, fl = forms.length; f < fl; f += 1) {
const formName = forms[f];
downloads.push({
destFolder,
formName,
url: `https://docs.google.com/forms/d/${formIds[destFolder][formName]}/downloadresponses?tz_offset=-18000000`,
filename: `myfolder/${destFolder}/${formName.replace(/\//g, '_')}.csv`,
});
}
}
const event = new Event('finishedDownload');
const eventInterrupt = new Event('interruptedDownload');
let currId;
chrome.downloads.onChanged.addListener((downloadDelta) => {
if (downloadDelta.id === currId) {
if (downloadDelta.state && downloadDelta.state.current === 'complete') {
document.dispatchEvent(event);
} else if (downloadDelta.state && downloadDelta.state.current === 'interrupted') {
console.log(downloadDelta);
document.dispatchEvent(eventInterrupt);
}
}
});
downloads.reduce((promise, actual) => {
return promise.then((last) => (last ? new Promise((resolve) => {
const { url, filename, destFolder, formName } = actual;
function listener() {
document.removeEventListener('finishedDownload', listener);
document.removeEventListener('interruptedDownload', listener);
resolve(true);
};
function interrupt() {
document.removeEventListener('finishedDownload', listener);
document.removeEventListener('interruptedDownload', listener);
resolve(false);
}
console.log(`Processant ${destFolder}, ${formName}: ${url}`);
document.addEventListener('finishedDownload', listener);
document.addEventListener('interruptedDownload', interrupt);
chrome.downloads.download({ url, filename }, (downloadId) => {
currId = downloadId;
if (!downloadId) {
console.log();
console.log('Error downloading...');
console.log(runtime.lastError);
resolve();
}
});
}) : Promise.resolve(false)));
}, Promise.resolve(true));
}
chrome.browserAction.onClicked.addListener((/*tab*/) => startDownload());