I need help creating Install trigger to replace onEdit() - google-apps-script

I have created a spreadsheet that receives form input, fiddles about with the data and then I want to view one row of data by selecting a record based on an id field.
I read and watch a lot of vids and found this brilliant video that showed me how to write a script for it.
I made a simplified spreadsheet SimplifiedDB2023
and a script
//DummyDatabse2023
//const ss = SpreadsheetApp.openById("11Fw0OmFpvkkfh11c95gBd7qEWifGJO6eUv38UwKRxNU")
//SimplifiedDB2023
//const ss = SpreadsheetApp.openById("1c-y_XUuK0L86qUUX471-GdBT-CN5GNTk4EGK7G57_94")
const ss = SpreadsheetApp.getActiveSpreadsheet()
const updateWS = ss.getSheetByName ("Form")
const mergeddataWS = ss.getSheetByName ("mergeddata")
const fieldRange = ["P5","G7","C24","C25","D7","C27","J7","I9","J9","H9","G9","F9","D9","D11","G11","G12","C38","E14","C40","C41","C42","C43","H5","R7","R12","P10","C48","C49","C50","C51","C52","C53","C54","C55","C56","P6","P12","P14","P16","P8","R8","D5"]
const searchCell = updateWS.getRange("E2")
function search() {
const searchValue = searchCell.getValue()
const data = mergeddataWS.getRange("A2:AQ").getValues()
const recordsFound = data.filter(r => r[42] == searchValue)
if(recordsFound.length === 0) return
fieldRange.forEach((f,i) => updateWS.getRange(f).setValue(recordsFound[0][i]))
}
which worlked beautifully. When I edit cell E2. (Basically choosing from a list), using the onEdit function,
SimplifiedScript2023
it selects all the information from that row using a filter in the search() function..
When I followed the exact same procedure for my more complicated and involved spreadsheet,
DUMMYDB2023
I ran into issue. The problem seems to be that the second script I wrote
( which I basically copied and pasted ) doesn't seem to be "bound" to the sheet, so I cannot use : const ss = SpreadsheetApp.getActiveSpreadsheet().
as I get an error :
TypeError: Cannot read properties of null (reading 'getSheetByName')
I looked into this and then when I changed
const ss = SpreadsheetApp.openById("11Fw0OmFpvkkfh11c95gBd7qEWifGJO6eUv38UwKRxNU")
It worked, but I am not able to use the onEdit function, which is vital, because I want any edits to cell E2, to trigger the function (search).
So my questions are - Why does the second version (- DummyDB2023) not work - I suspect this is because this more developed sheet has links to external sources like the forms, but even so, I am the owner of the spreadsheet and I followed the exact same procedure to create the script so it 'should' be bound, but apparently is not.
The second question is, if I must use the openById, I need to create an installable trigger. I thought this would be quite simple, but I cannot find any comprehensible resource that shows how to create an installable trigger for when a single cell is edited.
Can anyone help me write a script for a trigger that runs my search function when cell E2, on sheet Form! is edited?
As always, thanks for your time and help.
I created a stripped down version of my spreadsheet and the procedure worked perfectly.
When I applied it to my full spreadsheet, it wouldn'T run.

Related

Running a similar App Scripts code on Multiple Sheets

Just started to use App Scripts recently (Beginner programmer here).
I'm trying to automate the creation of a column + transposition of data from column B to newly created column C.
It's working fine, but when I try to replicate the script on multiple sheets, only one of the script works.
Below is my code (I tried to duplicate my constants to replicate the first script but then the first script does not work anymore. I guess I'm doing it the wrong way) :
// First script working fine.
const ss_1 = SpreadsheetApp.getActiveSpreadsheet();
const sheet_1 = ss_1.getSheetByName('Marketing Report');
function insertColumn() {
const range_1 = sheet_1.getRange('B1:B300').getValues();
const newrange_1 = sheet_1.getRange('C1:C300');
const tmp_1 = new Date();
const yesterday_1 = new Date(tmp_1.setDate(tmp_1.getDate()-1));
sheet_1.insertColumnAfter(2);
newrange_1.setValues(range_1);
sheet_1.getRange('B2').setValue(yesterday_1).setNumberFormat("MMMM");
}
// Second script, I tried to duplicate my constants to replicate the first script but then the first script does not work anymore. I guess I'm doing it the wrong way
const ss_2 = SpreadsheetApp.getActiveSpreadsheet();
const sheet_2 = ss_2.getSheetByName('Sales Report');
function insertColumn() {
const range_2 = sheet_2.getRange('C1:C150').getValues();
const newrange_2 = sheet_2.getRange('D1:D150');
const tmp_2 = new Date();
const yesterday_2 = new Date(tmp_2.setDate(tmp_2.getDate()-1));
sheet_2.insertColumnAfter(3);
newrange_2.setValues(range_2);
sheet_2.getRange('C2').setValue(yesterday_2).setNumberFormat("MMMM");
}
Any help would be highly appreciated !
First of all, try to rename one of the functions in your script since they both have the same name. Don't forget to hit save. Please take note that Google Apps Script can run one function at a time (not including triggers). You can toggle through your functions through the dropdown menu (between the "Debug" and "Execution Log" buttons) to change to the function you would like to run. Make sure that both functions have different names.

Can I duplicate a trigger with my new spreadsheet?

I have a new client template that I want to keep from being filled out prior to being duplicated. I've set a trigger to run a duplication and renaming function for the file when the file is opened.
function newRecord(){
var sheet = SpreadsheetApp.getActiveSpreadsheet();
var scheck = sheet.getName();
if (scheck=='#New Client Record'){
var file = DriveApp.getFilesByName('#New Client Record').next();
file.setName('New Client')
file.makeCopy('#New Client Record');
}
}
This checks to see that the file is the correct file before proceeding, changes the name of the template and then duplicates the file and renames it the old template name. I configured it in this way so that when we start filling out the data right away while we're talking on the phone with a client, we aren't sullying our original template which remains pristine. It works nicely except that the trigger from file #1 doesn't transfer to file #2 which takes on the new identity of the original template.
So my question is - can I duplicate the trigger as well? Or is there script that can open the duplicate file and close the template file to protect my form?
EDIT: I threw a bit more at this last night and tried to add an install trigger with limited effect (I probably sound like a boomer talking about smoking the drugs with this - I'm not a programmer and have only a rudimentary vocabulary for script). I added the following function, which is transferring, but does not seem consistent in its functionality (excusing the pun).
function createSpreadsheetOpenTrigger() {
var ss = SpreadsheetApp.getActive();
ScriptApp.newTrigger('newRecord')
.forSpreadsheet(ss)
.onOpen()
.create();
}
Thanks for any assistance any of you can provide on this. I've stumbled my way this far on my own with the archives, but I've finally gotten myself stuck.
How to get a form to create a new sheet with values from a template
Here is a quick example of how to carry this out. What you need to see this example in action:
A template sheet that looks like this:
A form that will have 3 "Short answers" that are required.
Client Name
Phone
Email
A standalone script (Drive Web app > New > More > Google Apps Script).
A folder into which all the newly created sheets will go.
At the top of the script (outside any function) you can define 3 global variables with the appropriate IDs:
const FORM_ID = ...
const TEMPLATE_ID = ...
const CLIENT_FOLDER_ID = ...
First, to set up an onFormSubmit trigger, you can do this by running the following function only once.
function createTrigger(){
const form = FormApp.openById(FORM_ID)
ScriptApp.newTrigger("formSubmitReceiver")
.forForm(form)
.onFormSubmit()
.create()
}
After which comes the main function that will:
Receive the formSubmit event and create a range of values from it.
In this example, it will generate a followUpDate that will be 7 days from the submission of the form.
Create a copy of the template file.
Fill the range B1:B4 with the relevant info.
Resulting in a new spreadsheet:
function formSubmitReceiver(e){
const itemResponses = e.response.getItemResponses()
const values = itemResponses.map( itemResponse => [itemResponse.getResponse()] )
const followUpDate = new Date()
followUpDate.setDate(followUpDate.getDate() + 7)
values.push([followUpDate])
const newFile = DriveApp.getFileById(TEMPLATE_ID).makeCopy()
const parentFolder = DriveApp.getFolderById(CLIENT_FOLDER_ID)
newFile.moveTo(parentFolder)
newFile.setName(values[0][0])
const newId = newFile.getId()
const newSpreadsheet = SpreadsheetApp.openById(newId)
const sheet = newSpreadsheet.getSheetByName("Sheet1");
const range = sheet.getRange("B1:B4")
range.setValues(values)
}
Depending on how complex you make your form, and what type of questions you choose (i.e multiple choice etc) this can get more complicated but hopefully it will give you a good idea of how to get something like this working. The simplest way is just to use "Short Answers" as this will just return a string. Also remember to make the questions "Required" if you don't want to handle empty values. Again, this all depends on how exactly you want to implement it and the complexity of the information involved!
References and Further Reading
FormResponse
getItemResponses()
ItemResponse
getFileById(id)
makeCopy()

Creating a backup of data entered into a sheet via Google App Scripts

I have a spreadsheet where users can enter data and then execute a function when clicking on a button. When the button is clicked it logs the time and entered data in a new row on another sheet in that spreadsheet.
To make sure that sheet is not accidentally edited by the users I want to create a non-shared backup of that data.
I import the range to another spreadsheet, but just importing the range means that if the original sheet is edited/erased that data will also be edited/erased, so I wrote the following script to log the changes as they come in.
function onEdit(event){
var ss = SpreadsheetApp.getActiveSpreadsheet();
var incomingSheet = ss.getSheetByName('Incoming');
var lastRow = incomingSheet.getLastRow();
var incomingData = incomingSheet.getRange(lastRow,1,1,7);
var permanentSheet = ss.getSheetByName('PermanentLog')
var newdataRow = permanentSheet.getLastRow();
incomingData.copyTo(permanentSheet.getRange(newdataRow+1,1));
}
This works when Run from the Apps Script Editor, however, when I enter new data and click the button on the original spreadsheet, it logs the data to the log sheet there, and the range is imported to the 'Incoming' sheet of the new Spreadsheet, but the data is not copied over to the 'Permanent Log' sheet (unless I Run it manually from within the Apps Script Editor). It also works if I remove the ImportRange function from the first sheet and then just manually enter data in on the 'Incoming' sheet.
So does this mean new rows from an Imported Range do not trigger onEdit? What would be the solution? I don't want to run this on a timed trigger, I want to permanently capture each new row of data as it comes in.
Also, am I overlooking a more elegant and simple solution to this whole problem?
Thank you for your time.
This function will copy the data to a new Spreadsheet whenever you edit column 7 which I assume is the last column in your data. It only does it for the sheets that you specify in the names array. Note: you cannot run this from the script editor without getting an error unless you provide the event object which replaces the e. I used an installable onEdit trigger.
The function also appends a timestamp and a row number to the beginning of the archive data row
function onMyEdit(e) {
e.source.toast('entry');//just a toast showing that the function is working for debug purposes
const sh = e.range.getSheet();//active sheet name
const names = ['Sheet1', 'Sheet2'];//sheetname this function operates in
if (~names.indexOf(sh.getName()) && e.range.columnStart == 7) {
const ass = SpreadsheetApp.openById('ssid');//archive spreadsheet
const ash = ass.getSheetByName('PermanentLog');//archive sheet
let row = sh.getRange(e.range.rowStart, 1, 1, 7).getValues()[0];
let ts = Utilities.formatDate(new Date(), Session.getScriptTimeZone(), "yyyy/MM/dd HH:mm:ss");//timestamp
row.unshift(ts, e.range.rowStart);//add timestamp and row number to beginning
Logger.log(row);//logs the row for debug purposes
ash.appendRow(row);//appends row to bottom of data with ts and row
}
Logger.log(JSON.stringify(e));
}
Restrictions
Script executions and API requests do not cause triggers to run. For example, calling Range.setValue() to edit a cell does not cause the spreadsheet's onEdit trigger to run.
https://developers.google.com/apps-script/guides/triggers
So yeah, as far as I understand you it can't be done that way.

Script that adds new row to sheets in selected AND another Google Spreadsheet

I'm working on a Spreadsheet to keep track of team member's project hours. I've created a Spreadsheet per team member for them to fill out weekly, and a project overview Spreadsheet that takes in all data through IMPORTRANGE.
To be able to quickly add a new project I want a macro to insert a new row in the Project overview + the separate Spreadsheets per team member. However I can't figure out how to write the correct code for the separate team member Spreadsheets. What's going wrong here?
If possible I'd also like to make a macro to DELETE a row in the project overview + team member spreadsheets, and one to HIDE a row...
Project overview
Team member Kate
Team member David
My current code:
function InsertRow() {
var ss = SpreadsheetApp.getActive();
var allsheets = ss.getSheets();
var row = SpreadsheetApp.getActiveRange().getRow();
// Array holding the names of the sheets to exclude from the execution
// I only managed to make it work when I exclude the sheet that I actually want to affect instead of the other way around?
var exclude = (["PROJECTS"] ||
SpreadsheetApp.openById("1xjR3lx5_KAA9nqiD3YsjZnulQaMyWGPQqgYsjtzQ0xI").getSheets() ||
SpreadsheetApp.openById("1Q5gtZlqf41of1Zwi8pvZbDx4NN5LcDh5SxfwasLUDMU").getSheets())
for(var s in allsheets){
var sheet = allsheets[s];
// Stop iteration execution if the condition is meet.
if(exclude.indexOf(sheet.getName())==-1) continue;
sheet.insertRowBefore(row);
}
}
As I see it you have a couple of options, which I'll be listing here as A, B, and C. Please note that you might need two different .GS files as you are linking to two sheets
A
Try code found on google app script documentation
I found the google apps script documentation for this command found here, so you might want to check that for this questions and others , but here is the exact code included
// The code below opens a spreadsheet using its ID and logs the name for it.
// Note that the spreadsheet is NOT physically opened on the client side.
// It is opened on the server only (for modification by the script).
var ss = SpreadsheetApp.openById("abc1234567");
Logger.log(ss.getName());
B
use open by url instead of open by id
Your issue might be that your current id isn't correct, I have no way of knowing, so here is some alternate code here (link to documentation here)
// The code below opens a spreadsheet using its id and logs the name for it.
// Note that the spreadsheet is NOT physically opened on the client side.
// It is opened on the server only (for modification by the script).
var ss = SpreadsheetApp.openByUrl(
'https://docs.google.com/spreadsheets/d/abc1234567/edit');
Logger.log(ss.getName());
C
Tie the google script to one sheet
This last option doesn't require any code, just an explanation. Instead of trying to link your script to two separate sheets, you might be able to automatically link it to a single google sheet and create two pages in the sheets file that you treat as two different sheets but are one thing. This might not be what you want, but I included it anyways. You link the sheet to the code automatically by:
1 opening your sheet
2 going to "tools"
3 clicking script editor
4 copy and paste your code (except for the "open by id" part)
5 success!
Your exclude variable doesn't contain what you think it does. You're using an "or" operator (||), which will take the first "truthy" value and skip the rest.
console.log((["PROJECTS"] || 'something else')); // ["PROJECTS"]
Moreover, you don't have a good way of telling which spreadsheet belongs to which team member. To solve that problem, you can create an object.
const teamSpreadsheetIds = {
'DAVID': 'ABC',
'KATE': '123',
};
console.log(teamSpreadsheetIds['DAVID']); // ABC
With the teamSpreadsheetIds object, you can now go about updating your team member sheets locally as well as their individual spreadsheets. The "PROJECTS" sheet is unique, so there's only one check for it.
function InsertRow() {
const ss = SpreadsheetApp.getActive();
const allSheets = ss.getSheets();
const row = SpreadsheetApp.getActiveRange().getRow();
const teamSpreadsheetIds = {
'DAVID': '1Q5gtZlqf41of1Zwi8pvZbDx4NN5LcDh5SxfwasLUDMU',
'KATE': '1xjR3lx5_KAA9nqiD3YsjZnulQaMyWGPQqgYsjtzQ0xI',
};
for (let sheet of allSheets) {
const sheetName = sheet.getName();
const memberSpreadsheetId = teamSpreadsheetIds[sheetName];
const isSkippable = memberSpreadsheetId === undefined && sheetName !== 'PROJECTS';
if (isSkippable) { continue };
// Insert a row in the local sheet
sheet.insertRowBefore(row);
// Get the member sheet and insert a row
if (memberSpreadsheetId) {
const memberSpreadsheet = SpreadsheetApp.openById(memberSpreadsheetId);
const memberSheet = memberSpreadsheet.getSheets()[0]; // Assumes the first sheet is the one to modify
memberSheet.insertRowBefore(row);
}
}
}

Script to copy and sort form submission data

I'm using Google forms to create a spreadsheet that I want sorted automatically by datestamp Z-A. The sorting will be triggered whenever anyone fills out a form.
I think the way to do it is:
ask if there is a "copy of Form responses" on spreadsheet...
if yes, clear all contents...
else...
copy spreadsheet to "copy of form responses"...
sort according to timestamp
Below is what I've cobbled so far. It works only the first time a response is recorded. I'm not a coder so any help is appreciated. If someone could also point me to a list of commands with basic syntax I'd be grateful.
function CopySheet() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var msheet = ss.getSheetByName("Form Responses");
msheet.copyTo(ss);
var CopySheet = ss.getSheetByName("Copy of Form Responses");
CopySheet.sort(1, false); // here 1 is for column no. 1 that
// is "Column A" and true is for ascending, make it
// false if you want descending.
};
You can accomplish this without a script, by using QUERY() in the copy sheet. For instance, if you put this function in cell A1 of your copy sheet, and substitute the key for your form response spreadsheet, you'll end up with a reverse-timestamp-sorted copy of the responses:
=Query(ImportRange(spreadsheet_key,"Form Responses!A:Z"), "select * order by Col1 desc")
This data will be refreshed periodically (~5 mins), so it will reflect new form submissions, but not in real-time.
Note: When using ImportRange() as source data for Query, you need to refer to columns in the Query string using ColN notation.
Alternatively, you could produce a form submission trigger function in the spreadsheet receiving the form submissions, and have it copy the sorted form responses to your copy sheet. The following function does that. You need to set it up as a trigger function for Spreadsheet Form Submission Events. For information on how to test such a function, see How can I test a trigger function in GAS?.
function copyFormSubmissions(e) {
var sourceSheet = e.range.getSheet();
var data = sourceSheet.getDataRange().getValues();
var headers = data.splice(0,1)[0]; // remove headers from data
data.sort(reverseTimestampOrder); // Sort 2d array
data.splice(0,0,headers); // replace headers
var destId = "--copy-sheet-ID--";
var destSheet = SpreadsheetApp.openById(destId).getSheetByName('Sheet1');
destSheet.clear();
destSheet.getRange(1,1,data.length,data[0].length).setValues(data);
};
function reverseTimestampOrder(a,b) {
// Timestamp is in first (zero-th) column
return (b[0]-a[0]);
}
If someone could also point me to a list of commands with basic syntax I'd be grateful.
The Google Apps Script API classes and methods reference is here. If you're learning, try the tutorials (same place), and I recommend you get familiar with Javascript through some form of e-learning - CodeAcademy.com is a good place to start, since it introduces all the language constructs without focusing on web page development, making it very relevant for Googls Apps Script.