Google Apps Script, Google Sheets, Javascript
Have you ever wanted to have a Google Sheet available for only those users who need to edit and then once they are done, take their edit permissions away to maintain the integrity of the sheet?
I know that I have come across these conditions a number of times in the past. Maybe you have a task list that you want to send your team each time a task is allocated to them. Once they let you know that they are done, by say, entering a set of values or checking a task complete box on the row they need to work on, you want to be able to remove their edit permissions from your sheet.
In this post, we have created a Google Apps Script that will add and remove editors to a Google Sheet based on the spreadsheet’s data. More specifically, this script will:
- Grab the users name and email in each row along with whether or not they have complete the task or if the Google Sheet has been shared and sent to the user.
- Share the assigned user to the Google Sheet.
- Send an email to the user. A separate Google Sheet tab is added to the sheet so an administrator can add their custom email message.
- Automatically check a reference column of checkboxes indicating that the assigned user has been shared as an editor on the Google Sheet and an email has been sent to them.
- Once the task has been complete the user check the “Edit Complete” checkbox in their assigned row.
- Either automatically each day or when the Google Sheets administrator clicks the button, each user who has completed all assigned tasks is removed from having edit permissions to the Google Sheet.
The best way to probably understand this script is through an example…
Note! As always take what you need from this and skip the rest. The content below will help you to skip to what you want to read.
[EZ-TOC]
The Example: Assigning famous editors, editorial tasks, by giving them edit permission
*Ah… the things I do to amuse myself. 😁🤓
I have a fictional editorial company and I am using a Google Sheet to assign editing tasks to my editors on my Google Sheet named, Awaiting Edit. I only want to give my editors access to the Google Sheet if they have a task and once they are done, I want to be able to either manually remove all editors who have currently completed the task or they will be removed automatically at the end of each day.
Let’s take a look a the main Edit sheet tab of our Awaiting Edit Google Sheet.
Awaiting Edit: Edit tab
Take a look at our example sheet below:
In this tab, we have a list of documents to be added, their reference numbers and then the assigned editor’s name and email. We will use the name and email (columns C and D) to send personalised emails and assign or remove edit permissions to our sheet.
Next, we have a list of two columns of checkboxes:
- Edit Complete: Assigned editors check this box to indicate that they have completed the task.
- Email Sent Admin Only: This box will be checked when the Google Sheets administrator/s clicks the green Add & Email Editors button.
Add & Email Editors button
In our example, the administrator either by hand, from a form or programmatically, enters new tasks to the sheet and assigns and editor. Once they have assigned new tasks and editors to the sheet for that session, they then click the Add & Email Editors button.
This runs the Google Apps Script Automation that will grab all the data from the editor’s name, their email and the Email Sent checkboxes. Any row that contains an email and checkbox that is not checked will be:
- Added as an editor if they are not already and editor.
- Sent a single email even if the editor has multiple tasks.
- The Email Sent checkbox is checked to indicate that the automation is complete.
Error handling
The main area where an error might occur is when an email is entered incorrectly. The automation will throw an alert on the Google Sheet but will continue to assign editors to the sheet and send them an email.
If all emails were correct, then the follow alter will appear to indicate to the administrator that there are not problems:
More details on how editors are added to sheets and the error handling can be found here.
Preparing the Emails Message
On the Awaiting Edit Google Sheet, there is a separate Sheet Tab called Email.
Here the assigned range is blue, in our case A4:A9 contains a space for the Google Sheet administrator/s to create an email template. Luckily for them, the Subject line, salutation and link to this Google Sheet is added automatically for them.
Removing the Editor
Removing editors can be done manually by clicking the button, Remove Editors or with a time trigger to be done on a daily basis.
Editors are only removed if:
- They are not also assigned administrators of the Google Sheet.
- They have complete all assigned tasks and check the corresponding Task Complete check box.
Quick-Use Guide
There are a few steps you need to take to implement this automation for your own project. Don’t worry, you won’t need to know any code and I will walk you through each step.
Let’s get started:
Setup Your Google Sheet
Requirements
You can incorporate this Google Apps Script automation into your own project. However, your data must contain a column with the name of your editor and another column with the editors corresponding email. In our example, this is columns C and D.
For your own project, names and emails can be in any column. I often hide the email column or put it at the end so it isn’t in the way for the user. You can change the header for these to any text or language you like.
Adding Some maintenance checkboxes
This automation uses two columns of checkboxes. It’s probably easier to talk about them in reverse order.
In our example, we have an Email Sent Admin Only column in grey in column F. This column will be checked automatically at the end of the automation process. Not only do the checked checkboxes of this column indicate to your Google Sheet admins which user has received an email and has been given edit permissions, but it is also used in the Google Sheet automation to help find what users need to be added as editors and sent an email.
It is recommended that you as the developer protect this column for all users except for the administrators of this Google Sheet.
In our example, we also have a Task Complete column of checkboxes. Assigned editors update this checkbox once they have completed their assigned task. This helps the admin of this Google Sheet to know that the task is complete and they can use the remove button to remove the user. Alternatively, this column is referenced when the Remove Editor automation is run daily on a time trigger.
In both of these checkbox columns, you can place them anywhere in your sheet adjacent to the rest of your data. You can also rename the headers to whatever you desire.
Adding an “email” tab to your Google Sheet
We want our Google Sheet admin to be able to update the email template that the automation will send to the users.
In your Google Sheet, create a new sheet tab and name it whatever you want; just make sure you take note of the name of this new sheet tab. In our example, we named our sheet tab “Email”.
You will assign a single column for your admin to be able to insert their email template message. You can make it any length of rows. In our example, we made it 6 rows.
It is probably a good idea to leave a set of instructions for the admin and let them know that the personalised salutation and link to this Google Sheet will be handled by the Google Apps Script automation.
Oh, and also make sure they protect the sheet from other users and hide it when they are done.
Setup Your Code
Now that you have your Google Sheet all set up it is time to add your Google Apps Script code. If you have never worked with Google Apps Script before the tutorial below is a good primer, but not essential:
To access the script editor in your Google Sheet, go to Extensions > App Script
Paste the code
Once you are in the Script Editor, delete out the demonstration function in the Code.gs file and paste in the following code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 |
/*Add and remove multiple editors from a Google Sheet. * * See tutorial on Yagisanatode.com for a "Quick Guide" to setting up. * https://wp.me/p9ksIb-5O2 */ //#### GLOBALS ##### //Update these values on the right of the colon (:) for your own project. const MY_DATA = { DATA_SHEET_NAME : "Edit",// Change: this to the sheet with your data you want to draw your email addresses from. FIRST_ROW : 2, //Change: The col the user's name is on. COLS:{ NAME : 3,//Change: The col the user's name is on. EMAIL : 4, //Change: The col the email is on. TASK_COMPLETE : 5, //Change: The col the Task Complete Checkbox is on. SHARED_AND_SENT : 6 //Change: the Col that is checked when user is shared and email sent. }, EMAIL_SHEET_NAME : "Email", // Change: the is the sheet tab you have your email message in. EMAIL_TEXT_COL_RANGE: "A4:A9", //Change: single column of email message data. Each cell is a paragraph. PERMANANT_EDITORS:["yagisanatode@gmail.com"]//Change: Add any permant editors whose permission should never be removed. }; //#### END GLOBALS #### /*################################################# * Gives users edit access, then send them an email. * Finally, it updates the checkboxes to indicate that * the task is complete. */ function shareAndSend() { //Variables drawn from GLOBALS const SS = SpreadsheetApp.getActiveSpreadsheet(); const DATA_SHEET = SS.getSheetByName(MY_DATA.DATA_SHEET_NAME); const FIRST_ROW = MY_DATA.FIRST_ROW; const COLS = { name:MY_DATA.COLS.NAME, email:MY_DATA.COLS.EMAIL, sharedAndSent:MY_DATA.COLS.SHARED_AND_SENT }; const EMAIL_SHEET = SS.getSheetByName(MY_DATA.EMAIL_SHEET_NAME); const EMAIL_TEXT_RANGE = MY_DATA.EMAIL_TEXT_COL_RANGE //Get start and end columns let start_col = 100, end_col = 0; Object.keys(COLS).forEach(key =>{ start_col = (COLS[key] < start_col) ? COLS[key] : start_col; end_col = (COLS[key] > end_col) ? COLS[key] : end_col; }); let col_width = end_col - start_col + 1; //Get range of data const data_range = DATA_SHEET.getRange(FIRST_ROW,start_col,DATA_SHEET.getLastRow()-FIRST_ROW,col_width); const data_vals = data_range.getValues(); //Get new col locations of data_range let newCols = { name:COLS.name - start_col, email:COLS.email - start_col, taskComplete: COLS.taskComplete - start_col, sharedAndSent:COLS.sharedAndSent - start_col } /*Filter range to only include data that is * A) does not have a checked Share and Send col and * B) doesn't have an empty name col. */ let data_vals_trimmed = data_vals.filter(row =>{ if(row[newCols.name] !== "" && row[newCols.sharedAndSent] === false) return row; }); //Add users as editors. let allEmails = data_vals_trimmed.map(row => row[newCols.email]); let emails =[...new Set(allEmails)]; //Get a unique list of emails. assignMultipleEditorsWithValidation_(emails); //Send users email. let emailsAndNames = addEmailsAndNames_(emails, data_vals_trimmed,newCols); sendEmail_(emailsAndNames, EMAIL_SHEET, EMAIL_TEXT_RANGE); //Tick Share and send boxes // Loops trough entire original range of data_vals add 1 to the first unchecked row. // This will provided the row of the first unchecked range. let firstUnchecked = FIRST_ROW; data_vals.forEach(row=> { if(row[newCols.sharedAndSent])firstUnchecked++; }) //Get range of new data and check corresponding boxes. const checkRange = DATA_SHEET.getRange(firstUnchecked,COLS.sharedAndSent,data_vals_trimmed.length) checkRange.check(); }; /*################################################# * Adds names to unique list of emails * * Private funciton * * @param {array} emails - an array of unique email addresses. * @param {array} data - 2d array of trimmed data from Google Sheet containing emails and names. * @param {object} newCols - Contains object of key-value pairs of the new column numbers of data. */ function addEmailsAndNames_(emails, data,newCols){ //Searches for each email in 'data' and adds corresponding name. return emails.map(email =>{ let name = ""; for(let i = 0; i < data.length; i++){ let row = data[i]; if(email === row[newCols.email]){ name = row[newCols.name]; }; }; return [email, name]; }); }; /*################################################# * Send email to editors informing them that they have a task * and sharing them the link to the Google Sheet. * * Private funciton * * @param {array} editors - a 2d array of email addresses and names. * @param {object} sheet - sheet object generated from ss.getSheetByName(name) * @param {string} A1range - String containg the range of the email text. */ function sendEmail_(editors, sheet, A1range){ //Grabs the email data from the 'email' template Google Sheet const messageArray = sheet.getRange(A1range).getValues(); //Reduces 2d array to 1d. Filters out empty rows and joins each array element // into a single string. const message = messageArray .flat() .filter(paragraph => paragraph.length >0) .join("<br>") // Creates link to shared google Sheet. This will appear at the bottom of email. const ss = SpreadsheetApp.getActiveSpreadsheet() const ssName = ss.getName(); const ssLink = `<br> Link: <a href=${ss.getUrl()}>${ssName}</a>`; //Loop through each editor and send email. editors.forEach(editor =>{ let recipient = editor[0]; let subject = `${ssName} Google Sheet` //Email subjec is: title of sheet + 'Google Sheet' let body = `Hi ${editor[1]},<br>${message}<br><br>${ssLink}`; //Combines salutation and message. //Sends email with body as HTML. try{ MailApp.sendEmail({ to: recipient, subject: subject, htmlBody: body }); } catch(e){ } //error already handled when adding editor. }) }; /*################################################# * Add multiple users to a Google Sheet as an Editor * with validation to ensure all available emails are sent * and all erroneous emails are reported in an alert for * user. * * Private function * * @param {array} editors - an array of email addresses. */ function assignMultipleEditorsWithValidation_(editors){ //Variables const SS = SpreadsheetApp.getActiveSpreadsheet(); let badEmails = [];//Empty array to be used to load any bad emails //Attempt to add editors from list. editors.forEach(editor => { try{ SS.addEditor(editor) } catch(e){ badEmails.push(e); } }); //After task is complete send Alert message. let message; if(badEmails.length > 0){ message = `The follow users could not be added as editors due to errors: \n ${badEmails.join(" \n")}`; }else{ message = `All users assigned.` } const ui = SpreadsheetApp.getUi(); ui.alert("Adding Users Complete!",message, ui.ButtonSet.OK); }; //################################################# /*################################################# * Removes any editor that is: * 1) Complete all assigned tasks only. * 2) Is not an identified permanant editor found in global var * MY_DATA.PERMANANT_EDITORS * */ function removeEditors(){ // Main variables drawn from Globals const SS = SpreadsheetApp.getActiveSpreadsheet(); const DATA_SHEET = SS.getSheetByName(MY_DATA.DATA_SHEET_NAME); const START_ROW = MY_DATA.FIRST_ROW; const COL_EMAIL = MY_DATA.COLS.EMAIL; const COL_TASK_COMPLETE = MY_DATA.COLS.TASK_COMPLETE; const PERMA_EDITORS = MY_DATA.PERMANANT_EDITORS; // Find start column. If the email col is less than the 'Task complete' col then it will be start const startCol = (COL_EMAIL < COL_TASK_COMPLETE) ? COL_EMAIL : COL_TASK_COMPLETE; // Gets how many columns between the 'email' and 'task compelte' cols const colWidth = Math.abs(COL_EMAIL - COL_TASK_COMPLETE) + 1; // Gets the column range and values of select cells from 'Email' col to 'task compelte' col. const range = DATA_SHEET.getRange(START_ROW, startCol,DATA_SHEET.getLastRow(),colWidth); const vals =range.getValues(); //The 'vals' variable starts at zero now so respective columns need to be recalculated. const newEmailCol = COL_EMAIL - startCol; const newTaskComp = COL_TASK_COMPLETE - startCol; //Filters down those rows that have an email address and the task complete is marked false. // then creates a new array mapping only the email row. const editorIncomplete = vals.filter(row => { if(row[newEmailCol] !== "" && row[newTaskComp] === false){ return row; }; }).map(row => row[newEmailCol]); //Removes duplicates from editor incomplete and adds the permanant editors lists. const uniqueEditorIncomplete = [...new Set(editorIncomplete),...PERMA_EDITORS]; // Get list of curren editors const editors = SS.getEditors(); let editorsEmails = editors.map(editor => editor.getEmail()) // Iterates through current editor emails and if the current emails does not exists in the // uniqueEditorIncomplete array that editor will be removed. editorsEmails.forEach(editor => { if(!uniqueEditorIncomplete.includes(editor)){ SS.removeEditor(editor); }; }); // Alert popup to inform user that task is complete. const ui = SpreadsheetApp.getUi(); const message = `All non-permanant users who have complete all tasks have been removed`; ui.alert("Users Removed!",message, ui.ButtonSet.OK); }; |
If you hover over the code above there will be a little header menu at the top that appears. One of the options will be a copy button.
Once you have pasted in the code, save your project by clicking the save button on the header menu. You will be asked to name your Google Apps Script project. I often name mine to the same title as the connected sheet.
Update Global object with your own data
You will need to update a few things in the code for it to work for your own project. Fortunately, everything you need to update can be found at the top of the Code.gs file within the MY_DATA object with instruction on what you should change.
As you can see, all column locations are referenced as a number. So column A is 1, B is 2, C is 3, etc.
The final object on line 19 is the list of permanent editors, these are more than likely going to be your administrators of the Google Sheet. To add more than one editor separate them by a comma, for example:
1 2 3 |
... PERMANANT_EDITORS:["yagisanatode@gmail.com","turtlemcnozzlemouth@gmail.com", "mrfluffykins@gmail.com"] ... |
create buttons and time triggers
Now that we have updated our code it’s time to add buttons and a time trigger.
Buttons
Buttons in Google Sheets are just images with the shape of a button that you then add a textbox with text to. To draw a button, click on the cell you want to add it to and then go to Insert > Drawing. You will find the button in the shapes menu.
We labelled our buttons Add & email editors in green and Remove Editors in grey.
Once you have created your button, click Save and Close in the Drawing frame and the button will appear. Go ahead and resize them to fit for your own preference.
Next, you will need to assign your Google Apps Script function to each of your buttons. You can do this by right-clicking the button, clicking on the ellipses that appear in the top right corner and then select Assign a script. Add the following functions on the right of the button name below to each button (remember, if you changed the names, it will need to go into a new button).
- Add & email editors –
shareAndSend
- Remove editors –
removeEditors
That’s it done. Your buttons are ready to go. If you want to learn more about creating buttons, you can check out this post:
Google Apps Script: How to Connect a Button to a Function in Google Sheets
Note, the first time you run the code you and your users with access to the buttons will face a warning and will ask you to authenticate the code. More on this here:
Running Google Apps Script for the First Time: What’s with all the Warnings!
Set a Time Trigger to Remove Editors Daily (optional)
If you don’t want the hassle of manually going into your Google Sheet each day or week to remove editors who have completed their task you can add a time trigger to the removeEditors()
function.
To do this, go back into the script editor. In the side, menu select triggers. This will open a separate browser tab into a triggers page for your Google Apps Script trigger.
Down the bottom right of the page click the blue box called Add trigger. A popup window with some options will appear. The first option will ask you to assign a function. Go ahead and assigned the function removeEditors
.
Ensure Choose which deployment should run is set to Head.
In the following option, Select event source as Time driven. This will change the menu options below it. Under Select type of time based trigger select, Day timer. Then under Select time of day, choose, Midnight to 1am.
When you are all done, hit Save down the bottom right. The popup menu will close and you will see a summary of your trigger in your projects Triggers menu. You can close the triggers menu now.
You can access the example Google Sheet here:
So that is all there is to implementing this automation into your own project. I would love to hear how you applied this code to your own projects in the comments below.
Need help with Google Workspace development?
Go something to solve bigger than Chat GPT?
I can help you with all of your Google Workspace development needs, from custom app development to integrations and security. I have a proven track record of success in helping businesses of all sizes get the most out of Google Workspace.
Schedule a free consultation today to discuss your needs and get started or learn more about our services here.
A brief code breakdown
If you have been following along from the previous post on adding and removing editors from a Google Sheet with Google Apps Script, then you would have seen a familiar function in the code. If not, I recommend that you take at that tutorial:
Google Apps Script: How to Add and Remove Editors to A Google Sheet with Code
I’ve taken pains to comment on all the code and ensured that the functions and variable names are readable.
In this section, I’ll just highlight a few interesting parts of the code and what each function does with links to relevant documentation.
Before I go on, you’ll note that some of the functions end with an underscore. Google Apps Script recognises these as private functions and it will not display these functions in your script run menu.
I’ve also added a MY_DATA
object as a global variable here to make it easier for non-coders to customise the variables for their own project.
shareAndSend()
This is the main driving function that is called when the Add & email editors button is clicked.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
/*################################################# * Gives users edit access, then send them an email. * Finally, it updates the checkboxes to indicate that * the task is complete. */ function shareAndSend() { //Variables drawn from GLOBALS const SS = SpreadsheetApp.getActiveSpreadsheet(); const DATA_SHEET = SS.getSheetByName(MY_DATA.DATA_SHEET_NAME); const FIRST_ROW = MY_DATA.FIRST_ROW; const COLS = { name:MY_DATA.COLS.NAME, email:MY_DATA.COLS.EMAIL, sharedAndSent:MY_DATA.COLS.SHARED_AND_SENT }; const EMAIL_SHEET = SS.getSheetByName(MY_DATA.EMAIL_SHEET_NAME); const EMAIL_TEXT_RANGE = MY_DATA.EMAIL_TEXT_COL_RANGE //Get start and end columns let start_col = 100, end_col = 0; Object.keys(COLS).forEach(key =>{ start_col = (COLS[key] < start_col) ? COLS[key] : start_col; end_col = (COLS[key] > end_col) ? COLS[key] : end_col; }); let col_width = end_col - start_col + 1; //Get range of data const data_range = DATA_SHEET.getRange(FIRST_ROW,start_col,DATA_SHEET.getLastRow()-FIRST_ROW,col_width); const data_vals = data_range.getValues(); //Get new col locations of data_range let newCols = { name:COLS.name - start_col, email:COLS.email - start_col, taskComplete: COLS.taskComplete - start_col, sharedAndSent:COLS.sharedAndSent - start_col } /*Filter range to only include data that is * A) does not have a checked Share and Send col and * B) doesn't have an empty name col. */ let data_vals_trimmed = data_vals.filter(row =>{ if(row[newCols.name] !== "" && row[newCols.sharedAndSent] === false) return row; }); //Add users as editors. let allEmails = data_vals_trimmed.map(row => row[newCols.email]); let emails =[...new Set(allEmails)]; //Get a unique list of emails. assignMultipleEditorsWithValidation_(emails); //Send users email. let emailsAndNames = addEmailsAndNames_(emails, data_vals_trimmed,newCols); sendEmail_(emailsAndNames, EMAIL_SHEET, EMAIL_TEXT_RANGE); //Tick Share and send boxes // Loops trough entire original range of data_vals add 1 to the first unchecked row. // This will provided the row of the first unchecked range. let firstUnchecked = FIRST_ROW; data_vals.forEach(row=> { if(row[newCols.sharedAndSent])firstUnchecked++; }) //Get range of new data and check corresponding boxes. const checkRange = DATA_SHEET.getRange(firstUnchecked,COLS.sharedAndSent,data_vals_trimmed.length) checkRange.check(); }; |
From lines 7-15 we update our function with our global variable. During this process, we call the current spreadsheet (line 9) with the SpreadsheetApp Class and the getActiveSpreadsheet() method. We then prepare both the Editors and Email sheet tab with the getSheetByName() method.
Our next task is to get the range we will use to find our editor’s details. One of the trickiest things is to find out where the columns start and end based on the locations of the name, email and, share and sent columns. To do this we use the Object.keys() function on the forEach() method to iterate over the object of COLS
found on lines 11 to 15.
We then use a start and end column ternary operator that is basically an inline if-statement that updates the start_col
if the COLS object’s value is less than the current value of the start_column
and updates the end_col
if the value is greater than the current end_col
. Lines 22-28
On lines 31 and 32 we first get the range of data from the DATA_SHEET
sheet using the getRange method. There are multiple options to invoke this method, but for us, we will use the 4-argument approach. This approach takes the start row, the start column, the row depth, and the column width. To get the row depth we use the getLastRow() method. Then we subtract the start row from it.
Once the range is collected we use the getValues() method to grab the data in this range as a 2d array of values.
Lines 34 – 40 reestablish the locations of the columns on the data_vals
variable.
Then, on lines 42-48 our goal is to reduce the data_vals
array to exclude all previously sent emails. We do this with a filter method, which includes only those rows that contain a name and an unchecked Share and Send box.
Next, on lines 50-53 we prepare our data and send it off to the assignMultipleEditorsWithValidation_(emails);
for users to be added as editors. This function only takes a 1d array of emails. To create this we use the map method to create an array of only emails. We know that some of these emails may be duplicates if the editor has more than one task. These are removed by using new Set() in a spread operator.
On lines 55 to 57, we send our new editors an email to let them know they have a task. First, we need to readd names back to our list of unique emails. We invoke a small function called addEmailsAndNames_
which takes three arguments, the unique email list, the trimmed set of data values and the new column number. This function will return a 2d array of unique emails with their assigned names called emailsAndNames
.
Once complete, we send emailsAndNames
to our sendEmail_
function along with the email_sheet
and text range the message template is in on that sheet.
When the emails have been sent, we then need to check all the corresponding boxes in the Email Sent column. Lines 61-65 finds the first unchecked row of the Email Sent column by counting all the checkboxes in the row from the start of the data. On line 68, this is added to the getRange method. This time we only invoke 3 arguments for this method. The start row, column and row depth. We get the row depth by counting the length of the data_vals_trimmed
2d array. Once we have the range, we set all checkboxes to checked on line 69.
addEmailsAndNames_
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
/*################################################# * Adds names to unique list of emails * * Private funciton * * @param {array} emails - an array of unique email addresses. * @param {array} data - 2d array of trimmed data from Google Sheet containing emails and names. * @param {object} newCols - Contains object of key-value pairs of the new column numbers of data. */ function addEmailsAndNames_(emails, data,newCols){ //Searches for each email in 'data' and adds corresponding name. return emails.map(email =>{ let name = ""; for(let i = 0; i < data.length; i++){ let row = data[i]; if(email === row[newCols.email]){ name = row[newCols.name]; }; }; return [email, name]; }); }; |
This short function takes 3 parameters, the email array, the trimmed 2d array of data and the object of new column positions. The function then maps through the email array. Within each iteration of the map, it searches the data by referencing the email and if it finds a match, it adds the name to the email.
The function then returns a 2d array of unique emails and their names.
sendEmail_
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
/*################################################# * Send email to editors informing them that they have a task * and sharing them the link to the Google Sheet. * * Private funciton * * @param {array} editors - a 2d array of email addresses and names. * @param {object} sheet - sheet object generated from ss.getSheetByName(name) * @param {string} A1range - String containg the range of the email text. */ function sendEmail_(editors, sheet, A1range){ //Grabs the email data from the 'email' template Google Sheet const messageArray = sheet.getRange(A1range).getValues(); //Reduces 2d array to 1d. Filters out empty rows and joins each array element // into a single string. const message = messageArray .flat() .filter(paragraph => paragraph.length >0) .join("<br>") // Creates link to shared google Sheet. This will appear at the bottom of email. const ss = SpreadsheetApp.getActiveSpreadsheet() const ssName = ss.getName(); const ssLink = `<br> Link: <a href=${ss.getUrl()}>${ssName}</a>`; //Loop through each editor and send email. editors.forEach(editor =>{ let recipient = editor[0]; let subject = `${ssName} Google Sheet` //Email subjec is: title of sheet + 'Google Sheet' let body = `Hi ${editor[1]},<br>${message}<br><br>${ssLink}`; //Combines salutation and message. //Sends email with body as HTML. try{ MailApp.sendEmail({ to: recipient, subject: subject, htmlBody: body }); } catch(e){ } //error already handled when adding editor. }) }; |
The sendEmail_
function takes 3 parameters:
- editors – the 2d array of emails and names we created earlier.
- sheet – the email sheet object
- A1range – the range the message is in.
The first task for this function is to create the message from the Email Google Sheet Tab. We collect the values using another approach to getRange, applying an A1notation value which is the start cell, found by the column letter and the row number and the end cell found the same way.
We then use getValues
to get the data from each cell in the range. On lines 16-19 we stack a bunch of methods to transform the message into a string. First, we use flat to transform the 2d array of values into a single dimensional array. Then we filter out any element of the array that does not have text. Finally, we use join to convert the array to a string that is separated by an HTML break (<br>
).
The next task is to grab both the name of the Google Sheet document and its URL. We get the name by referencing the active spreadsheet method and then called the getName() method. Then we use the getURL() method to grab the URL. Thanks for making things so easy Google Apps Script. For the last part, we create a link to the Google Sheet that we will put at the end of our email. Lines 22-24
Lastly, we loop through the editor array from lines 27 to 41. During each iteration, we grab the email of the recipient and create a subject line that includes the name of the document. We then combine the body of the email by adding the salutation that includes the name of the editor, the message and the link to the Google Sheet at the end. Here we are using template literals to create our text and insert our variables inline.
Our final task is then to attempt to send the email using the mailApp class using the sendEmail method. This method can take a number of approaches but the one we are using will take an object containing the keys: to
, subject
and HTMLbody
. This will allow us to add an HTML link and use the break HTML tag to return each line. We run this inside a try...catch
statement just in case there is a bad email.
assignMultipleEditorsWithValidation_
We have already covered this in our previous tutorial below:
Google Apps Script: How to Add and Remove Editors to A Google Sheet with Code
removeEditors
This function is initialised when either the time trigger is activated or the Remove Editors button is clicked.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
function removeEditors(){ // Main variables drawn from Globals const SS = SpreadsheetApp.getActiveSpreadsheet(); const DATA_SHEET = SS.getSheetByName(MY_DATA.DATA_SHEET_NAME); const START_ROW = MY_DATA.FIRST_ROW; const COL_EMAIL = MY_DATA.COLS.EMAIL; const COL_TASK_COMPLETE = MY_DATA.COLS.TASK_COMPLETE; const PERMA_EDITORS = MY_DATA.PERMANANT_EDITORS; // Find start column. If the email col is less than the 'Task complete' col then it will be start const startCol = (COL_EMAIL < COL_TASK_COMPLETE) ? COL_EMAIL : COL_TASK_COMPLETE; // Gets how many columns between the 'email' and 'task compelte' cols const colWidth = Math.abs(COL_EMAIL - COL_TASK_COMPLETE) + 1; // Gets the column range and values of select cells from 'Email' col to 'task compelte' col. const range = DATA_SHEET.getRange(START_ROW, startCol,DATA_SHEET.getLastRow(),colWidth); const vals =range.getValues(); //The 'vals' variable starts at zero now so respective columns need to be recalculated. const newEmailCol = COL_EMAIL - startCol; const newTaskComp = COL_TASK_COMPLETE - startCol; //Filters down those rows that have an email address and the task complete is marked false. // then creates a new array mapping only the email row. const editorIncomplete = vals.filter(row => { if(row[newEmailCol] !== "" && row[newTaskComp] === false){ return row; }; }).map(row => row[newEmailCol]); //Removes duplicates from editor incomplete and adds the permanant editors lists. const uniqueEditorIncomplete = [...new Set(editorIncomplete),...PERMA_EDITORS]; // Get list of curren editors const editors = SS.getEditors(); let editorsEmails = editors.map(editor => editor.getEmail()) // Iterates through current editor emails and if the current emails does not exists in the // uniqueEditorIncomplete array that editor will be removed. editorsEmails.forEach(editor => { if(!uniqueEditorIncomplete.includes(editor)){ SS.removeEditor(editor); }; }); // Alert popup to inform user that task is complete. const ui = SpreadsheetApp.getUi(); const message = `All non-permanant users who have complete all tasks have been removed`; ui.alert("Users Removed!",message, ui.ButtonSet.OK); }; |
First up, we bring in all our globals and gather our needed sheet data on lines 3-8.
Next, we need to find our start column again. This time we are only referencing the email column and the task complete column we use a ternary operator to determine which column is the smallest and that becomes the start column (Line 11). Once we have that we need to determine the column width by subtracting the email column by the task complete column. We use the Math.abs to ensure that the end result is a positive number and then add 1 (Line 13).
Now that we have the start col and col width we can get the range of our data and their values. Lines 16 & 17.
Lines 20 and 21 update the column locations of the email and task complete with the new 2d array, vals
.
We need to determine which rows and editors who have not completed their tasks because we should not remove them. We do this on lines 25 to 27 with the filter method by first checking the row contains an email and then checking that the Task Complete box has not been checked. Then we create a map of all the emails of those whose task is incomplete.
Back in our global object, we made a list of all the permanent editors who should not be removed from the Google Sheet. Using the spread syntax again, we first use new Set on our incomplete email list and then add our permanent editor list to create a brand new array on line 32.
Next, we want to compare the currently available editors on our Google Sheet with our list of editors we want to remove. On line 35 we use the Google Apps Script getEditors() method to get a list of editors. Within this method, we need to call getEmails() as we map through each editor to create a list of emails of the editors. Lines 35-37
To crosscheck our list of editors with our list of incomplete and required editors we use forEach
to loop through our editor list and if that email is not included (using the includes method) in our list of uniqueEditorIncomplete
we remove the email from the list with the removeEditor
method. Lines 41-45
Our last task is to send the user an alert to let them know that the task is complete. Lines 48-50.
Conclusion
Towards the end of writing this tutorial, I discovered that I wrote a similar tutorial on this last year 🤦♂️:
Google Apps Script: Send Bulk Emails and Share a Google Sheet with One Button Click
It was actually quite interesting the change in my coding style towards a more functional approach using methods like map and filter now. Also compared to the previous tutorial, I have more cross-checking conditions that I learned over time were more necessary. This current tutorial also makes it easier for a non-coding admin of the Google Sheet to update the email and modify the global variable object for their own project. Finally, I added in a ‘remove editor’ process to more fully help out with automation.
I can honestly say that writing this version, took a lot less time than the first version last year.
Anyway, I hope you found this useful and you found some good snippets for your own projects.
If you would like to see more of these types of coding projects hit the like button so I know that the big write up was worthwhile. If you are interested in a regular dose of all things Google Workspace, Google Apps Script and JS, please subscribe (top right or just below the comments section).
~Yagi
Yagi, my noggin is already thinking about how I can tweak this project into for other needs. Thank you for continuing to blog and building my skills.
Great to hear. Would love to hear how you modified it for your own projects.
Hi, I’m a newbie in this terrain and your blog is helping me a lot. I was a VBA macro programmer, now I’m moving here, baby steps.
I need to give and remove editor privileges to a lot of users to a lot of documents.
If I loop thru each document and use this approach it will work, but it does not feel right. Is there a way to assign documents to any kind of security group and only add/remove users from this group?
Thanks for sharing you work!
Hi Horacuix,
My preference is to build groups in the admin console and then assign those groups to shared documents and folders.
It makes it much easier to manage and staff with owner permissions in these folders get the hang of it quickly too.
Is it possible to run this in one workbook but share a different workbook?
I’ve got the same question as Dave. I have a folder with multiple workbooks, and the workbooks are shared with various users rather than sheets within the same book. I’d be grateful if anyone has suggestions for how to modify this code to allow maintaining the sharing settings for a master list of workbooks and editors.
assign a separate sheet for giving them access instead of the google sheet itself with the scripts attached
separate add & email editor button
if remove editor runs, edit complete checkbox should be uncheck
i have had the timer running on this script for quite some time, and when all the emails are sent, i get a recurring (daily email report) error to my email that “Cannot call SpreadsheetApp.getUi() from this context”. Perhaps there is a fix for this?
Yes, this tutorial was designed for use when the script is run from the Sheet and activated by a button.
You might have noticed when running the sample script prior to implementing it as a time trigger, that you would have received some alerts in your when certain tasks are complete.
These alerts will fail to run if the Sheet is closed.
Try finding the locations in the script where this occurs. You can then test the removal of these parts of code by turning them into comments (
//
) or ” comment out”.Then test your code by running it from the Apps Script IDE, this will simulate what would happen with your time trigger. If you don’t get any errors and everything works, then you can safely delete the commented out code.
~Yagi