├── LICENSE ├── README.md ├── columndictionary.gs ├── email.gs ├── getdocnamefromid.gs ├── main.gs ├── media ├── 01.png ├── 02.png ├── 03.png ├── 04.png ├── 05.png ├── 06.png ├── 07.png ├── 08.png ├── 09.png ├── 10.png ├── 11.png ├── 12.png ├── addsecondtrigger.png ├── addtrigger.png └── ludicrousmode.pdf └── parser.gs /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | MIT License 3 | 4 | Copyright (c) 2020 northwestcoder 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # appsheet-drivemerge 2 | 3 | aka a "Documentation Request App" 4 | 5 | Note: This is not an official Google product and is provided for educational purposes only. 6 | 7 | ## Overview 8 | 9 | - This Appsheet app and Apps Script solution lets your team manage a curated list of content from Google Drive, and then email PDFs of that content nicely packaged up for your audience. "Audience" could mean: 10 | 11 | - Onboarding for new hires 12 | - Technical content for training, business partners etc 13 | - Marketing and Product releases 14 | 15 | - Furthermore, this app *optionally* performs a find and replace on variables inserted into multiple Google Docs and Google Sheets that you have attached to your templates, then makes copies of the files during the find and replace and stores those copies in a Drive output folder you have specified, and finally sends the merged results to your audience as PDFs. 16 | 17 | ## High level steps 18 | 19 | 20 | #### 1. Copy [this](https://www.appsheet.com/samples/A-Google-Drive-to-Email-application?appGuidString=d03ddbdc-cec5-4f31-be0c-32dae49741cb) Appsheet app to your gsuite account. 21 | 22 | 23 | #### 2. Once copied, open Document sheets source. 24 | 25 | ![01.png](media/01.png) 26 | 27 | 28 | #### 3. Now in Google Sheets, go to the Tools menu and choose Script Editor. 29 | 30 | ![02.png](media/02.png) 31 | 32 | #### 4. Once Apps Script launches, paste in the entire contents of [getdocnamefromid.gs](https://raw.githubusercontent.com/northwestcoder/appsheet-drivemerge/main/getdocnamefromid.gs) (from this github project). Rename your project to any name you want. We named ours 'GetDocNameFromID'. Click the save icon. 33 | 34 | ![03.png](media/03.png) 35 | 36 | 37 | #### 5. Still in Apps Script, click the clock icon to set up a trigger. 38 | 39 | ![04.png](media/04.png) 40 | 41 | #### 6. On the triggers page, click "Add Trigger": 42 | 43 | 44 | ![addtrigger](media/addtrigger.png) 45 | 46 | 47 | #### 7. And make the screen look exactly like this (except for the name which again can be anything you want). 48 | 49 | ![05.png](media/05.png) 50 | 51 | #### 8. Click save. You will be prompted to authenticate and verify the services that your G Suite account will have access to. Confirm all of this. 52 | 53 | 54 | #### 9. Now we're going to repeat this process a second time. Back in Appsheet, find the data source called "Request", click source to open the Sheet. 55 | 56 | ![06.png](media/06.png) 57 | 58 | #### 10. Like we did above, in the sheet, go to the Tools menu and choose Script Editor. Now we are going to create *four files and then paste in the four remaining files in this git project*: 59 | 60 | - [columndictionary.gs](https://raw.githubusercontent.com/northwestcoder/appsheet-drivemerge/main/columndictionary.gs) 61 | - [email.gs](https://raw.githubusercontent.com/northwestcoder/appsheet-drivemerge/main/email.gs) 62 | - [main.gs](https://raw.githubusercontent.com/northwestcoder/appsheet-drivemerge/main/main.gs) 63 | - [parser.gs](https://raw.githubusercontent.com/northwestcoder/appsheet-drivemerge/main/parser.gs) 64 | 65 | #### 11. It turns out that Apps Script does not have strict approach to where your code should live, but we usually create multiple files whose purpose is modular - keep your room clean right? Basically, make your project look like the following; we called our project "Send Docs". 66 | 67 | ![07.png](media/07.png) 68 | 69 | 70 | #### As long as all the code is in there and we have a file with our entry function `function main(e) {}` and then we specify this function in our trigger (next), it doesn't really matter how you organize your code/functions in Apps Script :) 71 | 72 | 73 | #### 12. Once you have created the files, save your changes! Like we did before, we need to set up a trigger for this second Apps Script project by clicking on the clock icon. Use the same settings as we used the first time: 74 | 75 | ![addsecondtrigger](media/addsecondtrigger.png) 76 | 77 | 78 | ___ 79 | 80 | 81 | What you have just done above is create two script solutions that listen for data changes on Google Sheets. The first script listens for new Documents added to the document list (the Google Sheet "Documents") and verifies the document info. The second script listens for changes to Email Templates (the Google Sheet "Requests") and sends an email when certain conditions are met. 82 | 83 | Now let's go back to Appsheet. We're ready to test. 84 | 85 | 86 | #### When you first log into the app, click Start. you will then be prompted to create a new user record. This adds a new record to the Google Sheet named 'Globals'. 87 | 88 | 08 89 | 90 | #### Once you have done so, you can create A) Documents, B) Links, and C) Email Templates of documents and links. We have created a first Email Template to get you started as well as one placeholder Google Doc and three Links. 91 | 92 | 09 93 | 94 | #### You can now add documents to your Document Library as well as links to your Link Library. 95 | 96 | 97 | #### Data Elements 98 | 99 | - The app has a strong concept of *private - my stuff* versus *public - shared for all app users* for these data types: 100 | 101 | - Documents 102 | - Links 103 | 104 | ![10.png](media/10.png) 105 | 106 | 107 | - In contrast, all Email Templates are currently private and operate on a per-user basis. 108 | 109 | 110 | - In this manner you can now envision an environment where Documents and Links are shared across a community, but the email templates that use them are private and operate on a per-user basis. Neat. 111 | 112 | #### Document behavior, Email behavior, and various other treasure hunts 113 | 114 | - When adding a document to the Document Library, its Google Drive name _must_ include the phrase _[External]_ or else the app will reject it. We leave this as a fun exercise for you to figure out why, how, when and so forth. 115 | 116 | - The business premise here is that this Appsheet app and Google Apps Script are powerful bridges to your Google Drive experience, and you should take business caution before unleashing this experience on a large group of operators. 117 | - You don't want people sending confidential documents to your trading partners! 118 | - Note: you could easily send confidential docs from Drive directly without using this app, so this app is only an improvement over the curation experience for organizations managing Drive content. Or at the least, no worse from a security pov. 119 | 120 | - To send an email, you need at least one document or one link attached to the template. Then the red magic "send email" button will appear. 121 | 122 | - The display name "Email Templates" maps to the data source called "Requests". On Email Templates, in the data source , is a column called "LastSent" which toggles from its initial state of "Pending" to "SENDING EMAIL" when this script is triggered. On completion the script toggles this field back to "Pending". If you are testing this out, you can leave this Google Sheet open and you should see this change in realtime. 123 | 124 | - We have deliberately omitted any strong sense of security or user management in the Appsheet app. This is meant to be implemented by you, the designer. Each user does get a private record in the Google Sheet called "Globals" - this maps to "Settings" in the App: 125 | 126 | ![11.png](media/11.png) 127 | 128 | - A record is created when you first log in and click the "Start" icon. There is also a "Settings" page with one single choice: whether or not ludicrous mode is OFF or ON. We have a seperate document on [ludicrous mode](media/ludicrousmode.pdf). 129 | 130 | ![12.png](media/12.png) 131 | 132 | - Advanced extra credit: Appsheet has a [Rest API](https://help.appsheet.com/en/articles/1979979-invoking-the-api). Turn it on for this app! Now you can envision sending a post request which edits the table **Requests**, and you will be remotely generating Google Drive content as PDFs! The post request might look like so: 133 | 134 | ``` 135 | { 136 | "Action": "Edit", 137 | "Properties": { 138 | "Locale": "en-US", 139 | "Timezone": "Pacific Standard Time" 140 | }, 141 | "Rows": [ 142 | { 143 | "Key": "a316259a", 144 | "Request Subject": "Your New Email Subject" 145 | "SendTo": "yourrecipient@example.com", 146 | "LastSent": "SENDING EMAIL" 147 | } 148 | ] 149 | } 150 | ``` 151 | 152 | #### Run-as account of the Appsheet app and the Google Script in relation to documents you attach 153 | 154 | - Reminder: IF the owner of the Apps Script - let's call them jsmith@example.com - cannot see the Google Drive files that bobjones@example.com has attached to a template, THEN the email function of this app will fail silently. This is expected and proper behavior with this Google Appsheet app and Google Apps Script. 155 | 156 | #### Emails marked as spam by your client? 157 | 158 | Yes, it could happen for a few reasons. 159 | 160 | - This solutions runs as a g suite user account. If that account is named something ridiculous or is known for spamming in general, that might be a problem here. 161 | - Email clients like Outlook and Gmail have all kinds of clever checks to determine if an email is spam, things like what is the subject line, is there any "robotic language" or harsh html formatting, or forms-like formatting, etc. Do not for example, name your email subject "Your TPS Report" :) 162 | 163 | #### Silent Failures 164 | 165 | From the Appsheet apps point of view, most if not all failures will be silent. From the Appscript point of view, you can always go to the "Executions" page of your script to review activities and errors. We assume that you will be making changes to this script solution and as part of that process, will be adding `Logger.log("your logging");` liberally througout this code. 166 | 167 | 168 | -------------------------------------------------------------------------------- /columndictionary.gs: -------------------------------------------------------------------------------- 1 | // the column names - in any order - that we expect from the Google Sheet 2 | 3 | var requiredColumns = [ 4 | 'Request Subject', 5 | 'RequestedOn', 6 | 'RequestedBy', 7 | 'SendTo', 8 | 'LastSent', 9 | 'ReasonCode', 10 | 'DocumentList', 11 | 'DocumentNames', 12 | 'AttachmentType', 13 | 'Category', 14 | 'EmailBody', 15 | 'DocumentDescriptions', 16 | 'RequestIsActive', 17 | 'Ludicrous Mode', 18 | 'FindAndReplace', 19 | 'OutputFolder', 20 | 'Links', 21 | 'LinkNames', 22 | 'LinkDescriptions' 23 | ]; 24 | 25 | var columnMap = {}; 26 | 27 | function getColumnNumberByName(sheet, name) { 28 | 29 | var range = sheet.getRange(1, 1, 1, sheet.getMaxColumns()); 30 | var values = range.getValues(); 31 | for (var row in values) { 32 | for (var col in values[row]) { 33 | if (values[row][col] == name) { 34 | columnMap[name] = parseInt(col)+1; 35 | } 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /email.gs: -------------------------------------------------------------------------------- 1 | // returns our final email, will error on gmail max attachments > 25mb 2 | // a lot of hardwired html in here.. but it could be worse. 3 | 4 | function sendNotification(theemailsubject, fromemail, toemail, attachmentFiles, emailbody) { 5 | Logger.log("sending email now"); 6 | GmailApp.sendEmail(toemail, theemailsubject, '', { 7 | replyTo: fromemail, 8 | htmlBody: emailbody, 9 | attachments: attachmentFiles 10 | } ) 11 | } 12 | 13 | function createEmailBody(inboundEmailBody, 14 | documentNames, 15 | documentDescriptions, 16 | emaillinks, 17 | emaillinknames, 18 | emaillinkdetails) { 19 | 20 | var outboundEmailBody = inboundEmailBody.replace(/\n/g, '
'); 21 | 22 | if(emaillinks.length > 0) { 23 | 24 | } 25 | 26 | if(emaillinks[0].length > 0) { 27 | outboundEmailBody += createLinkTable(emaillinks, emaillinknames, emaillinkdetails); 28 | } 29 | 30 | if(documentNames[0].length > 0) { 31 | outboundEmailBody += `


List of files attached here individually or as a zip file:

`; 32 | for (var i in documentNames) { 33 | 34 | outboundEmailBody += "" + documentNames[i] + " - "; 35 | outboundEmailBody += documentDescriptions[i]; 36 | outboundEmailBody += "

"; 37 | } 38 | } 39 | 40 | outboundEmailBody += `


41 | 42 |
This email was generated with Google Workspace and the Appsheet no-code platform.



`; 43 | 44 | return outboundEmailBody 45 | 46 | } 47 | 48 | 49 | function createLinkTable(emaillinks, emaillinknames, emaillinkdetails) { 50 | 51 | var finallinktable = ` 52 |


53 | Some links that you might be interested in:

54 | 55 | 56 | 57 | 58 | 59 | `; 60 | 61 | for (var i in emaillinks) { 62 | finallinktable += ""; 63 | finallinktable += ""; 64 | finallinktable += ""; 65 | } 66 | 67 | finallinktable += "
NameDetails
" + emaillinknames[i] + "" + emaillinkdetails[i] + "
"; 68 | return finallinktable 69 | 70 | } -------------------------------------------------------------------------------- /getdocnamefromid.gs: -------------------------------------------------------------------------------- 1 | function getFileName(e) { 2 | 3 | var theSource = e.source; 4 | var theSheet = theSource.getActiveSheet(); 5 | var theActiveRange = theSheet.getActiveRange(); 6 | var theActiveRow = theActiveRange.getRow(); 7 | 8 | var thisDocID = theSheet.getRange(theActiveRow,2).getValue(); 9 | var thisDocName = DriveApp.getFileById(thisDocID).getName(); 10 | var thisDocType = DriveApp.getFileById(thisDocID).getMimeType(); 11 | 12 | if (thisDocName.toUpperCase().indexOf("[EXTERNAL]") > -1) { 13 | 14 | theSheet.getRange(theActiveRow,4).setValue(thisDocName); 15 | theSheet.getRange(theActiveRow,5).setValue(thisDocType); 16 | theSheet.getRange(theActiveRow,9).setValue("Accepted"); 17 | 18 | } else { 19 | 20 | theSheet.getRange(theActiveRow,4).setValue("FAILURE"); 21 | theSheet.getRange(theActiveRow,5).setValue(thisDocType); 22 | theSheet.getRange(theActiveRow,9).setValue("Rejected"); 23 | 24 | } 25 | 26 | 27 | } 28 | -------------------------------------------------------------------------------- /main.gs: -------------------------------------------------------------------------------- 1 | function main(e) { 2 | 3 | var FARMODE = false; 4 | 5 | var theSource = e.source; 6 | var theSheet = theSource.getActiveSheet(); 7 | var theActiveRange = theSheet.getActiveRange(); 8 | var theActiveRow = theActiveRange.getRow(); 9 | 10 | // this entire next section solves for people rearranging columns in their Google Sheet. 11 | // Instead of hardwiring column numbers, we refer them by name by creating a dictionary of columnsname:numbers 12 | // The names below are those we expect from the Appsheet app's Google Sheet data source 13 | // some of them are arrays that we split - also see columndictionary.gs 14 | 15 | // there are also various hardwired strings in the code below which correlate to the companion Appsheet app for this example 16 | 17 | for (i in requiredColumns) { 18 | columnMap[i] = getColumnNumberByName(theSheet,requiredColumns[i]); 19 | } 20 | var theemailsubject = theSheet.getRange(theActiveRow,columnMap["Request Subject"]).getValue(); 21 | var fromemail = theSheet.getRange(theActiveRow,columnMap["RequestedBy"]).getValue(); 22 | var toemail = theSheet.getRange(theActiveRow,columnMap["SendTo"]).getValue(); 23 | var lastSent = theSheet.getRange(theActiveRow,columnMap["LastSent"]).getValue(); 24 | var documentList = theSheet.getRange(theActiveRow,columnMap["DocumentList"]).getValue().split(" , "); 25 | var documentNames = theSheet.getRange(theActiveRow,columnMap["DocumentNames"]).getValue().split(" , "); 26 | var zippedOrNot = theSheet.getRange(theActiveRow,columnMap["AttachmentType"]).getValue(); 27 | var inboundEmailBody = theSheet.getRange(theActiveRow,columnMap["EmailBody"]).getValue(); 28 | var documentDescriptions = theSheet.getRange(theActiveRow,columnMap["DocumentDescriptions"]).getValue().split(" , "); 29 | var ludicrousMode = theSheet.getRange(theActiveRow,columnMap["Ludicrous Mode"]).getValue(); 30 | var findAndReplace = theSheet.getRange(theActiveRow,columnMap["FindAndReplace"]).getValue().split("\n"); 31 | var outputFolder = theSheet.getRange(theActiveRow,columnMap["OutputFolder"]).getValue(); 32 | var emaillinks = theSheet.getRange(theActiveRow,columnMap["Links"]).getValue().split(" , "); 33 | var emaillinknames = theSheet.getRange(theActiveRow,columnMap["LinkNames"]).getValue().split(" , "); 34 | var emaillinkdetails = theSheet.getRange(theActiveRow,columnMap["LinkDescriptions"]).getValue().split(" , "); 35 | // ok we're finished getting colummn values using named columns 36 | 37 | 38 | // if we are doing find/replace aka ludicrous mode we also need the following 39 | if (ludicrousMode == "ON" && outputFolder.length > 0) { 40 | FARMODE = true; 41 | var destinationFolder = DriveApp.getFolderById(outputFolder); 42 | var fileDatestampPart = new Date().toLocaleDateString(); 43 | var fileTimestampPart= new Date().toLocaleTimeString(); 44 | var fileTimestamp = fileDatestampPart + " " + fileTimestampPart; 45 | } 46 | 47 | // main checkpoint 48 | if (lastSent == "SENDING EMAIL" && fromemail.length > 0 && toemail.length > 0) { 49 | Logger.log("we have a send request") 50 | 51 | // if there is at least one document reference, launch our various file-related stuff 52 | // else skip to email and attach any links 53 | if (documentList[0].length > 0) { 54 | var blobs = []; 55 | for (var i in documentList) { 56 | 57 | thisfile = DriveApp.getFileById(documentList[i]); 58 | thisfileMimeType = DriveApp.getFileById(documentList[i]).getMimeType(); 59 | thisFileName = thisfile.getName(); 60 | 61 | // server side check to ensure we only ever send files whose name contains '[External]' 62 | if( thisFileName.toUpperCase().indexOf('[EXTERNAL]') > -1) { 63 | 64 | // only going to run find/replace on Google Docs currently 65 | if ( FARMODE && (thisfileMimeType == 'application/vnd.google-apps.document' || 66 | thisfileMimeType == 'application/vnd.google-apps.spreadsheet') 67 | ) { 68 | var newDriveFile = thisfile.makeCopy(thisFileName + " " + fileTimestamp, destinationFolder); 69 | var newDriveFileId = newDriveFile.getId(); 70 | Logger.log("We're going to find/replace now") 71 | parseResult = docParser(newDriveFileId, findAndReplace); 72 | blobs.push(parseResult); 73 | } else { 74 | Logger.log("No find/replace was found") 75 | blobs.push(thisfile.getBlob()); 76 | } 77 | } else { 78 | Logger.log("Skipped " + thisFileName + " as it did not include the phrase '[External]'"); 79 | } 80 | } 81 | } 82 | 83 | // generate an email 84 | var emailbody = createEmailBody(inboundEmailBody, 85 | documentNames, 86 | documentDescriptions, 87 | emaillinks, 88 | emaillinknames, 89 | emaillinkdetails); 90 | 91 | if (zippedOrNot == "Zipped PDF's" ) { 92 | var zippedBlobs = Utilities.zip(blobs,'yourfiles.zip'); 93 | sendNotification(theemailsubject, fromemail, toemail, zippedBlobs, emailbody); 94 | } 95 | else { 96 | sendNotification(theemailsubject, fromemail, toemail, blobs, emailbody); 97 | } 98 | 99 | // once we're done, let's set this row/cell value back from "SENDING EMAIL" to "Pending" 100 | theSheet.getRange(theActiveRow,6).setValue("Pending"); 101 | 102 | } 103 | 104 | else { 105 | Logger.log("no action taken"); 106 | } 107 | 108 | } 109 | 110 | 111 | -------------------------------------------------------------------------------- /media/01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/northwestcoder/appsheet-drivemerge/3c7c5df4332f306dd1b92ed499e3bf59848570af/media/01.png -------------------------------------------------------------------------------- /media/02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/northwestcoder/appsheet-drivemerge/3c7c5df4332f306dd1b92ed499e3bf59848570af/media/02.png -------------------------------------------------------------------------------- /media/03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/northwestcoder/appsheet-drivemerge/3c7c5df4332f306dd1b92ed499e3bf59848570af/media/03.png -------------------------------------------------------------------------------- /media/04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/northwestcoder/appsheet-drivemerge/3c7c5df4332f306dd1b92ed499e3bf59848570af/media/04.png -------------------------------------------------------------------------------- /media/05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/northwestcoder/appsheet-drivemerge/3c7c5df4332f306dd1b92ed499e3bf59848570af/media/05.png -------------------------------------------------------------------------------- /media/06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/northwestcoder/appsheet-drivemerge/3c7c5df4332f306dd1b92ed499e3bf59848570af/media/06.png -------------------------------------------------------------------------------- /media/07.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/northwestcoder/appsheet-drivemerge/3c7c5df4332f306dd1b92ed499e3bf59848570af/media/07.png -------------------------------------------------------------------------------- /media/08.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/northwestcoder/appsheet-drivemerge/3c7c5df4332f306dd1b92ed499e3bf59848570af/media/08.png -------------------------------------------------------------------------------- /media/09.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/northwestcoder/appsheet-drivemerge/3c7c5df4332f306dd1b92ed499e3bf59848570af/media/09.png -------------------------------------------------------------------------------- /media/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/northwestcoder/appsheet-drivemerge/3c7c5df4332f306dd1b92ed499e3bf59848570af/media/10.png -------------------------------------------------------------------------------- /media/11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/northwestcoder/appsheet-drivemerge/3c7c5df4332f306dd1b92ed499e3bf59848570af/media/11.png -------------------------------------------------------------------------------- /media/12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/northwestcoder/appsheet-drivemerge/3c7c5df4332f306dd1b92ed499e3bf59848570af/media/12.png -------------------------------------------------------------------------------- /media/addsecondtrigger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/northwestcoder/appsheet-drivemerge/3c7c5df4332f306dd1b92ed499e3bf59848570af/media/addsecondtrigger.png -------------------------------------------------------------------------------- /media/addtrigger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/northwestcoder/appsheet-drivemerge/3c7c5df4332f306dd1b92ed499e3bf59848570af/media/addtrigger.png -------------------------------------------------------------------------------- /media/ludicrousmode.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/northwestcoder/appsheet-drivemerge/3c7c5df4332f306dd1b92ed499e3bf59848570af/media/ludicrousmode.pdf -------------------------------------------------------------------------------- /parser.gs: -------------------------------------------------------------------------------- 1 | function docParser(newDriveFileId, parserpayload) { 2 | 3 | var mimeType = DriveApp.getFileById(newDriveFileId).getMimeType(); // 'application/vnd.google-apps.document' 4 | 5 | if (mimeType == 'application/vnd.google-apps.document') { 6 | Logger.log("Got to doc find and replace") 7 | var revisedDocumentFile = DocumentApp.openById(newDriveFileId); 8 | for (var i in parserpayload) { 9 | var regexer = parserpayload[i].toString().split('::'); 10 | revisedDocumentFile.getBody().replaceText('(\W|)'+regexer[0]+'(\W|)', regexer[1]); 11 | } 12 | revisedDocumentFile.saveAndClose(); 13 | return revisedDocumentFile.getBlob(); 14 | } 15 | else if (mimeType == 'application/vnd.google-apps.spreadsheet') { 16 | Logger.log("Got to spreadsheet find and replace") 17 | var revisedSpreadsheetFile = SpreadsheetApp.openById(newDriveFileId); 18 | 19 | sheetlist = revisedSpreadsheetFile.getSheets(); 20 | 21 | for (var sheet = 0; sheet < revisedSpreadsheetFile.getSheets().length ; sheet++ ) { 22 | var thissheet = revisedSpreadsheetFile.getSheetByName(sheetlist[sheet].getSheetName()); 23 | for (var parse = 0 ; parse < parserpayload.length ; parse++) { 24 | var regexer = parserpayload[parse].toString().split('::'); 25 | sheetParser(thissheet, regexer[0], regexer[1]); 26 | revisedSpreadsheetFile.waitForAllDataExecutionsCompletion; 27 | // sheets really make me nervous sometimes: 28 | Utilities.sleep(1000); 29 | } 30 | } 31 | 32 | // spreadsheets seem 'hot' so let's reopen the file handle and get latest blob 33 | hotfile = DriveApp.getFileById(newDriveFileId); 34 | return hotfile.getBlob(); 35 | 36 | } 37 | 38 | } 39 | 40 | function sheetParser(sheet, to_replace, replace_with) { 41 | //get the current data range values as an array 42 | var values = sheet.getDataRange().getValues(); 43 | 44 | //loop over the rows in the array 45 | for(var row in values){ 46 | 47 | //use Array.map to execute a replace call on each of the cells in the row. 48 | var replaced_values = values[row].map(function(original_value){ 49 | return original_value.toString().replace(to_replace,replace_with); 50 | }); 51 | 52 | //replace the original row values with the replaced values 53 | values[row] = replaced_values; 54 | } 55 | 56 | //write the updated values to the sheet 57 | sheet.getDataRange().setValues(values); 58 | } --------------------------------------------------------------------------------