├── 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
├── addtrigger.png
├── ludicrousmode.pdf
└── addsecondtrigger.png
├── columndictionary.gs
├── getdocnamefromid.gs
├── LICENSE
├── parser.gs
├── email.gs
├── main.gs
└── README.md
/media/01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/northwestcoder/appsheet-drivemerge/HEAD/media/01.png
--------------------------------------------------------------------------------
/media/02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/northwestcoder/appsheet-drivemerge/HEAD/media/02.png
--------------------------------------------------------------------------------
/media/03.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/northwestcoder/appsheet-drivemerge/HEAD/media/03.png
--------------------------------------------------------------------------------
/media/04.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/northwestcoder/appsheet-drivemerge/HEAD/media/04.png
--------------------------------------------------------------------------------
/media/05.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/northwestcoder/appsheet-drivemerge/HEAD/media/05.png
--------------------------------------------------------------------------------
/media/06.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/northwestcoder/appsheet-drivemerge/HEAD/media/06.png
--------------------------------------------------------------------------------
/media/07.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/northwestcoder/appsheet-drivemerge/HEAD/media/07.png
--------------------------------------------------------------------------------
/media/08.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/northwestcoder/appsheet-drivemerge/HEAD/media/08.png
--------------------------------------------------------------------------------
/media/09.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/northwestcoder/appsheet-drivemerge/HEAD/media/09.png
--------------------------------------------------------------------------------
/media/10.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/northwestcoder/appsheet-drivemerge/HEAD/media/10.png
--------------------------------------------------------------------------------
/media/11.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/northwestcoder/appsheet-drivemerge/HEAD/media/11.png
--------------------------------------------------------------------------------
/media/12.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/northwestcoder/appsheet-drivemerge/HEAD/media/12.png
--------------------------------------------------------------------------------
/media/addtrigger.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/northwestcoder/appsheet-drivemerge/HEAD/media/addtrigger.png
--------------------------------------------------------------------------------
/media/ludicrousmode.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/northwestcoder/appsheet-drivemerge/HEAD/media/ludicrousmode.pdf
--------------------------------------------------------------------------------
/media/addsecondtrigger.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/northwestcoder/appsheet-drivemerge/HEAD/media/addsecondtrigger.png
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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.
| Name | 57 |Details | 58 |
| " + emaillinknames[i] + " | "; 64 | finallinktable += "" + emaillinkdetails[i] + " |
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 |
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 | 
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 | 
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 | 
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 |
--------------------------------------------------------------------------------