Validate cell entry with Google Script - google-apps-script

I've built a form system using Google Sheet. I'm wondering if there is a function to valid a cell entry if the user still has the cell selected.
Right now, if the user click on the save button, the change made in cell B7:G7 won't be saved.
So I'm looking for a function to add to my save script. Is there such a thing ?
function save() {
var sheet = SpreadsheetApp.getActiveSheet()
var db = SpreadsheetApp.openById("1OsPjUQDlq_mrCbnJspoTLFOamuIgGVp2fCeJc-tRq20").getActiveSheet()
var lastRow = db.getLastRow() + 1
// formula to validate cell entry
var id = sheet.getRange("G4:I4").getValue()
var date = sheet.getRange("B7:G7").getValue()
var idCol = 1
var dateCol = 2
db.getRange(lastRow, idCol).setValue(id)
db.getRange(lastRow, dateCol).setValue(date)
}

Activate the active range.
Example:
function save(){
SpreadsheetApp.getActiveRange().activate();
// do the whatever else should be done
}
Note: Activating a different range, will make that the value entered will be wrote to the activated range.

Here's a simple example:
code:
function submitData() {
const ss = SpreadsheetApp.getActive();
const ui = SpreadsheetApp.getUi();
const sh = ss.getSheetByName("Sheet0");
const vs = sh.getRange(2, 2, sh.getLastRow() - 1).getValues().flat();
let valid = true;
vs.forEach((e, i) => {
if (!e) {
ui.alert(`No data at B${i + 2}`);
valid = false;
}
});
if (valid) {
ui.alert('Data was submitted');
} else {
ui.alert('Data not submitted');
}
}
Form:
Desc
Data
First
First
Last
Last
MI
MI
Demo:

Related

Searching a sheet for value stored in a cell fails

I'm an extreme novice in programming and am trying to work an assignment that keeps failing. My code searches a sheet(tab1) in Google sheets for a value that is in a cell in another sheet(tab2). For some reason, the search never finds the value. For simplicity I tested by changing the value that I'm looking for to a whole number, not a ".GetValue". I then had an if statement that would alert with message "Found" and "Not Found" if it wasnt found. The data is never found.
function editRecord(){
var myGoogleSheet=SpreadsheetApp.getActiveSpreadsheet();
var shUserForm=myGoogleSheet.getSheetByName("Form");
var datasheet=myGoogleSheet.getSheetByName("Records");
var ui=SpreadsheetApp.getUi();
var response=ui.alert("Submit", 'Do you want to submit your data?', ui.ButtonSet.YES_NO);
var str=shUserForm.getRange("F7").getValue();
var values=datasheet.getDataRange().getValues();
var valuesFound=false;
if(response==ui.Button.NO){
return;
}
for (var i=0;i<values.length;i++){
if (values[i][1]==str){
var iRow=i+1;
datasheet.getDataRange(iRow,5).setValue(shUserForm.getDataRange("F15").getValue());
datasheet.getDataRange(iRow,6).setValue(shUserForm.getDataRange("F17").getValue());
datasheet.getDataRange(iRow,7).setValue(shUserForm.getDataRange("F19").getValue());
ui.alert("Data updated.");
shUserForm.getDataRange("F15").clearContent;
shUserForm.getDataRange("F17").clearContent;
shUserForm.getDataRange("F19").clearContent;
shUserForm.getDataRange("F21").clearContent;
shUserForm.getDataRange("F23").clearContent;
shUserForm.getDataRange("F25").clearContent;
valuesFound=true;
return;
}
if (valuesFound==false){
ui.alert("Your ID was not found.");
return;
}
}
}
Modification points:
I think that in your script, by return of if (valuesFound==false){}, when values[i][1]==str is false, the for loop is finished. By this, for example, when values[i][1]==str is false at the 1st row of values, the for loop is finished. I thought that this might be the reason of your issue.
About getDataRange(iRow, 5) of datasheet.getDataRange(iRow, 5), unfortunately, getDataRange has no arguments. I think that an error also occurs at this part.
About clearContent of shUserForm.getDataRange("F15").clearContent, in this case, the method of clearContent is not run.
If you want to search the value, how about the following modification?
Modified script 1:
When your script is modified, it becomes as follows.
function editRecord() {
var myGoogleSheet = SpreadsheetApp.getActiveSpreadsheet();
var shUserForm = myGoogleSheet.getSheetByName("Form");
var datasheet = myGoogleSheet.getSheetByName("Records");
var ui = SpreadsheetApp.getUi();
var response = ui.alert("Submit", 'Do you want to submit your data?', ui.ButtonSet.YES_NO);
var str = shUserForm.getRange("F7").getValue();
var values = datasheet.getDataRange().getValues();
var valuesFound = false;
if (response == ui.Button.NO) {
return;
}
for (var i = 0; i < values.length; i++) {
if (values[i][1] == str) {
var iRow = i + 1;
datasheet.getRange(iRow, 5).setValue(shUserForm.getRange("F15").getValue());
datasheet.getRange(iRow, 6).setValue(shUserForm.getRange("F17").getValue());
datasheet.getRange(iRow, 7).setValue(shUserForm.getRange("F19").getValue());
ui.alert("Data updated.");
shUserForm.getRange("F15").clearContent();
shUserForm.getRange("F17").clearContent();
shUserForm.getRange("F19").clearContent();
shUserForm.getRange("F21").clearContent();
shUserForm.getRange("F23").clearContent();
shUserForm.getRange("F25").clearContent();
valuesFound = true;
return;
}
}
if (valuesFound == false) {
ui.alert("Your ID was not found.");
return;
}
}
Modified script 2:
I thought that in your script, getValue and setValue are used in the loop. In this case, the process cost will become high. So, as another modified script, how about the following modification?
unction editRecord() {
var myGoogleSheet = SpreadsheetApp.getActiveSpreadsheet();
var shUserForm = myGoogleSheet.getSheetByName("Form");
var datasheet = myGoogleSheet.getSheetByName("Records");
var ui = SpreadsheetApp.getUi();
var response = ui.alert("Submit", 'Do you want to submit your data?', ui.ButtonSet.YES_NO);
var str = shUserForm.getRange("F7").getValue();
if (response == ui.Button.NO) {
return;
}
var res = datasheet.getRange("B1:B" + datasheet.getLastRow()).createTextFinder(str).matchEntireCell(true).findNext();
if (!res) {
ui.alert("Your ID was not found.");
return;
}
var [[v1],,[v2],,[v3]] = shUserForm.getRange("F15:F19").getValues();
datasheet.getRange(res.getRow(), 5, 1, 3).setValues([[v1, v2, v3]]);
shUserForm.getRangeList(["F15","F17","F19","F21","F23","F25"]).clearContent();
SpreadsheetApp.flush();
ui.alert("Data updated.");
}
In this sample, the value is searched using TextFinder. And, the cells are cleared using the RangeList.
References:
getRange(row, column)
clearContent()
createTextFinder(findText)

How to clear only "Column A" with Google Apps Script before updating with latest content in the same column

I would like to clear the contents of "column A" before "Update" function fills the latest data in the same column. The idea is remove any redundant data that is not required.
Usecase: Update all the tabs in one Index sheet, if people delete a sheet - that should not reflect in this Index sheet. Here is the code I have used after some research. I am new to this so need some help.
EDIT: Also how to exclude certain "Sheets" from the "Update" function so it doesn't show up in the Index column?
function onOpen() {
var ui = SpreadsheetApp.getUi();
ui.createMenu('Index Menu')
.addItem('Create Index', 'createIndex')
.addItem('Update Index', 'updateIndex')
.addToUi();
}
// function to create the index
function createIndex() {
// Get all the different sheet IDs
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheets = ss.getSheets();
var namesArray = sheetNamesIds(sheets);
var indexSheetNames = namesArray[0];
var indexSheetIds = namesArray[1];
// check if sheet called sheet called already exists
// if no index sheet exists, create one
if (ss.getSheetByName('index') == null) {
var indexSheet = ss.insertSheet('Index',0);
}
// if sheet called index does exist, prompt user for a different name or option to cancel
else {
var indexNewName = Browser.inputBox('The name Index is already being used, please choose a different name:', 'Please choose another name', Browser.Buttons.OK_CANCEL);
if (indexNewName != 'cancel') {
var indexSheet = ss.insertSheet(indexNewName,0);
}
else {
Browser.msgBox('No index sheet created');
}
}
// add sheet title, sheet names and hyperlink formulas
if (indexSheet) {
printIndex(indexSheet,indexSheetNames,indexSheetIds);
}
}
// function to update the index, assumes index is the first sheet in the workbook
function updateIndex() {
// Get all the different sheet IDs
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheets = ss.getSheets();
var indexSheet = sheets[0];
var namesArray = sheetNamesIds(sheets);
var indexSheetNames = namesArray[0];
var indexSheetIds = namesArray[1];
printIndex(indexSheet,indexSheetNames,indexSheetIds);
}
// function to clear index
function clearContentsOnly() {
var range = SpreadsheetApp
.getActive()
.getSheetByName("Index")
.getRange(4,2,2,2);
range.clearContent();
}
// function to print out the index
function printIndex(sheet,names,formulas) {
sheet.getRange(1,1).setValue('Task Index').setFontWeight('bold');
sheet.getRange(7,1,formulas.length,1).setFormulas(formulas);
}
// function to create array of sheet names and sheet ids
function sheetNamesIds(sheets) {
var indexSheetNames = [];
var indexSheetIds = [];
// create array of sheet names and sheet gids
sheets.forEach(function(sheet){
indexSheetNames.push([sheet.getSheetName()]);
indexSheetIds.push(['=hyperlink("https://docs.google.com/spreadsheets/d/XXXX/edit#gid='
+ sheet.getSheetId()
+ '","'
+ sheet.getSheetName()
+ '")']);
});
return [indexSheetNames, indexSheetIds];
} ```
clear contents of column A
sheet.getRange(1,1,sheet.getLastRow()).clearContent();
if you have header rows and you specify the start row:
const sr = 2;
sheet.getRange( sr, 1, sheet.getLastRow() - sr + 1).clearContent();
function updateIndex() {
// Get all the different sheet IDs
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheets = ss.getSheets();
var indexSheet = sheets[0]; //better to use the name of the index sheet - as the sheet position may get changed easily by the user
indexSheet.GetRange("A2:A").clearContent(); //add this line
var namesArray = sheetNamesIds(sheets);
var indexSheetNames = namesArray[0];
var indexSheetIds = namesArray[1];
printIndex(indexSheet,indexSheetNames,indexSheetIds);
}
Make sure you create a Sheet with the name "Index" and then the following code will update column A with all Sheets and directly link to the respective sheet. It will add new sheets with the name and url below existing entries. And if a sheet has changed its name (but not its id), then it will update the sheet name.
function updateIndex(){
const ACTIVE = SpreadsheetApp.getActive()
const spreadsheetURL = ACTIVE.getUrl()
const currentIndexSheet = ACTIVE.getSheetByName("Index")
// Empty the sheet
currentIndexSheet.getRange("A1:A").clearContent()
// We will populate this with all rows
let outputRows = []
// List Sheets which should not be listed
let skipSheets = ["Index", "Another Sheet to Skip"]
// Add Header Row
outputRows.push(['="Linked Sheet"'])
// Get all Sheet and add them to the outputRows using a Hyperlink
ACTIVE.getSheets().forEach( sheet => {
// Skip certain sheets which are defined above
if( skipSheets.indexOf(sheet.getName()) != - 1) return
outputRows.push([`=HYPERLINK("${spreadsheetURL}#gid=${sheet.getSheetId()}", "${sheet.getName()}")`])
})
// Write everything to Index
currentIndexSheet.getRange(1,1, outputRows.length).setFormulas(outputRows)
}

How can I check if a numerical value is within a range of cells in google sheets?

I would like to find if a certain value is in a range using app scripts for google sheets.
var sheet = SpreadsheetApp.getActiveSheet();
var rangeBikeNumbers = sheet.getDataRange("A5:A5000");
var values = rangeBikeNumbers.getValues();
If I have my range rangeBikeNumbers, how can I check if the number "42" for example is in that range. I have searched for hours now and have beeb unable to find any answer to this. indexOf only seems to return -1, regardless of whether or not the value is in the range.
var indexDataNumber = values.indexOf(42); for example always ends up being -1
I believe your goal as follows.
You want to check whether the value of 42 is existing in the range of A5:A5000.
In this case, I would like to propose to use TextFinder. Because when TexiFinder is used, the process cost is low. Ref By the way, getDataRange has not arguments. From your script, I thought that you might want var rangeBikeNumbers = sheet.getRange("A5:A5000");.
When this is reflected to your script, it becomes as follows.
Modified script:
function myFunction() {
var sheet = SpreadsheetApp.getActiveSheet();
var rangeBikeNumbers = sheet.getRange("A5:A5000");
var find = rangeBikeNumbers.createTextFinder("42").matchEntireCell(true).findNext();
if (find) {
// In this case, the value of 42 is existing in the range.
} else {
// In this case, the value of 42 is NOT existing in the range.
}
}
Note:
About var indexDataNumber = values.indexOf(42); for example always ends up being -1, I think that the reason of this issue is due to that values is 2 dimensional array. If you want to use this, you can also use the following script.
function myFunction() {
var sheet = SpreadsheetApp.getActiveSheet();
var rangeBikeNumbers = sheet.getRange("A5:A5000");
var values = rangeBikeNumbers.getValues();
var find = values.map(([e]) => e).indexOf(42); // of values.flat().indexOf(42);
if (find > -1) {
// In this case, the value of 42 is existing in the range.
} else {
// In this case, the value of 42 is NOT existing in the range.
}
}
References:
Benchmark: Process Costs for Searching Values in Spreadsheet using Google Apps Script
getDataRange()
getRange(a1Notation)
createTextFinder(findText)
Select any active range that you wish to search and it will search for the seed in that at range. The seed is currently defaulted to 42 but you can change it.
function findSeedInRange(seed = 42) {
const ui = SpreadsheetApp.getUi();
const ss = SpreadsheetApp.getActive();
const sh = ss.getActiveSheet();
const rg = sh.getActiveRange();
const row = rg.getRow();
const col = rg.getColumn();
var found = false;
rg.getValues().forEach((r, i) => {
r.forEach((c, j) => {
if (c == seed) {
let r = sh.getRange(i + row, j + col).getA1Notation();
ui.alert(`Found ${seed} in ${r}`);
found = true;
}
})
})
if(!found) {
ui.alert(`Did not find ${seed}`);
} else {
ui.alert('That is all.')
}
}
Here's another approach:
function findSeedInRange() {
const ui = SpreadsheetApp.getUi();
const ss = SpreadsheetApp.getActive();
const sh = ss.getActiveSheet();
const rg = sh.getActiveRange();
const resp = ui.prompt('Enter Seed', 'Enter Seed', ui.ButtonSet.OK_CANCEL)
if (resp.getSelectedButton() == ui.Button.OK) {
var seed = parseInt(resp.getResponseText());
const row = rg.getRow();
const col = rg.getColumn();
var found = false;
rg.getValues().forEach((r, i) => {
r.forEach((c, j) => {
if (c == seed) {
let r = sh.getRange(i + row, j + col).getA1Notation();
ui.alert(`Found ${seed} in ${r}`);
found = true;
}
});
});
if (!found) {
ui.alert(`Did not find ${seed}`);
} else {
ui.alert('That is all.')
}
} else {
ui.alert('Operation cancelled.')
}
}

How to copy all data from source sheet to destination sheet?

I've made a Google Sheet that serves to keep track of lot numbers within a manufacturing setting. Right now, when the onEdit trigger is enabled, the data from the source sheet only doubles first row of data in the destination sheet. Here's the link to the Sheet. This is the code I have so far:
function onEdit(e) {
if (e.range.getA1Notation() == 'N2') {
if (/^\w+$/.test(e.value)) {
eval(e.value)();
e.range.clear();
}
}
}
function Reset() {
var s = SpreadsheetApp.getActiveSheet();
if( s.getName() == "Input" ) {
var dataRange = s.getRange('A5:V14');
var values = dataRange.clearContent();
s.getRange("C5:C14").setValue('-');
s.getRange("F5:F14").setValue('-');
s.getRange("I5:I14").setValue('-');
s.getRange("L5:L14").setValue('-');
}
}
function Submit() {
const ss = SpreadsheetApp.getActive();
const sh = ss.getSheetByName('Input');
const targetSheet = ss.getSheetByName('Database');
const data = sh.getRange(5,1,1,23).getValues(); // A5:W14
targetSheet.getRange(targetSheet.getLastRow()+1,3,data.length,data[0].length).setValues(data);
}
If I understand your question correctly you are worried that when you select Submit, there is a double insert of data into the Database sheet.
It can be assumed that this is due to the Submit function being run twice. The first time when checking /^\w+$/.test(e.value) and the second time directly when you run eval(e.value)().
I'm not sure if this is a bug or a feature :-)
Try this code, it works:
function onEdit(e) {
if (e.range.getA1Notation() == 'N2') {
var val = e.value;
switch(val) {
case 'Reset': Reset();
break;
case 'Submit': Submit();
break;
}
e.range.clear();
}
}
Upd.
The double insertion is due to the fact that you are using 2 onEdit() triggers. Have a look at Edith / Triggers of the current project. Delete it.
Upd2.
If you want only filled rows out of 10 to appear on the Database sheet then use this code. It filters the rows based on whether there is some text in column H, such as Adult or Pediatric.
function Submit() {
var ss = SpreadsheetApp.getActive();
var sh = ss.getSheetByName('Input');
var targetSheet = ss.getSheetByName('Database');
var data = sh.getRange(5,1,10,23).getValues();
Logger.log(data);
var fData = data.filter(data => (data[13] != ""));
Logger.log(fData);
targetSheet.getRange(targetSheet.getLastRow()+1,3,fData.length,fData[0].length).setValues(fData);
}

How to freeze a range of cells when a checkbox is clicked?

Screenshot for the sheet:
I just need someone to help me write simple code to freeze a range of cells when a certain checkbox is clicked.
I would like it so that when I click on the 'Complete' checkbox, all of the ones above it cannot be edited or changed anymore. Vise Versa when the 'Complete' checkbox is unchecked the ones above are editable. That simple.
The purpose of the sheet is to take attendance for a class. When I am done taking the attendance I don't want to be able to change it anymore (or risk clicking on the wrong checkbox). That's why the complete button is there.
Can anyone write the code for me, please?
(Freeze or seal or protect)
This code is not working (I am a beginner so sorry)
function onEdit() {
var sheet = SpreadsheetApp.getActive();;
var completedRow = sheet.getDataRange();
for (i = 2; i < 18; i++){
var isComplete = source.getRange(countRow, i).getValue();
if (isComplete === true){
source.getRange(2, i, countRow-1).protect();
}
}
}
Your code reflects the basic logic, though there are some syntax flaws. Hopefully this answer will help you understand and adapt that syntax.
The code doesn't doesn't take advantage of the Event Objects that are available to onEdit(e), which include the row, column and value of the edited cell. It's not compulsory to use the Event objects, but they certainly make life easier.
countRow isn't defined; and because you are working with a spreadsheet of finite length (20 rows); it is probably unnecessary. But it is a sensible idea to allow for bigger spreadsheets. Maybe something like var countRow = sheet.getLastRow(); would be a good alternative Doc Ref.
isComplete - we know that this is always on row 20; we also know that it will have a value of "true" or "false". So, you don't need a loop to define this row.
At some stage, you may want to "unprotect" a column; say at the start of a new term or year; so it's likely that checking row 20 for a value of "false" could be useful.
Your goal can probably be achieved in many ways. The following should be considered as just one option.
The main function is setup in an onEdit(e) simple trigger.
I also setup a custom menu (using onOpen) that gives you access to view all the protected columns, and to remove protection if you need to.
I've also left some Logger.log statements in the code that may enable you to check the value of certain fields at key stages of the code.
All-in-all, this code follows the same logic as your code, but with some more detail.
One last thing, this code is designed to work on a specific sheet by virtue of var sheet = ss.getSheetByName(sheetname); but you could just as easily change this to var sheet = SpreadsheetApp.getActiveSheet(); to make it work on multiple sheets in your spreadsheet.
function onEdit(e) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheetname = "Sheet1";
var sheet = ss.getSheetByName(sheetname);
// set variable for last column
//Logger.log(JSON.stringify(e))
// set variables for edited cells,
var edittedRow = e.range.rowStart;
var edittedColumn = e.range.columnStart;
var newValue = e.value;
var headerrange = sheet.getRange(1, edittedColumn);
var headervalue = headerrange.getDisplayValue();
//Logger.log("DEBUG: The header range is "+headerrange.getA1Notation()+", and the value is "+headervalue);
// test if edit row =20, and the checkbox was ticked
if (edittedRow === 20 && newValue === "TRUE") {
//Logger.log("DEBUG: The 'ON' leg applies");
//Logger.log("DEBUG: edittedRow = "+edittedRow+", Editted column = "+edittedColumn+", and value = "+newValue);
// define the range to protect
var protectRangeOn = sheet.getRange(1, edittedColumn, 19, 1);
// protect the range - warning only.
protectRangeOn.protect().setDescription(headervalue)
.setWarningOnly(true);
//Logger.log("DEBUG1: protection set for "+protectRangeOn.getA1Notation());
}
//test if edit row=20, and the checkbox was unticked
if (edittedRow === 20 && newValue === "FALSE") {
//Logger.log("DEBUG: The 'OFF' leg applies");
//Logger.log("DEBUG: edittedRow = "+edittedRow+", Editted column = "+edittedColumn+", and value = "+newValue);
// define the range to unprotect
var protectRangeOff = sheet.getRange(1, edittedColumn, 19, 1);
var protections = sheet.getProtections(SpreadsheetApp
.ProtectionType.RANGE)
for (var i = 0; i < protections.length; i++) {
Logger.log("protections range name = " + protections[i]
.getDescription() + " - Header value = " + headervalue);
if (protections[i].getDescription() === headervalue) {
//Logger.log("DEBUG: OFF matches")
protections[i].remove();
}
}
//Logger.log("DEBUG2: protection unset for "+protectRangeOff.getA1Notation());
}
}
// Add a custom menu to the active spreadsheet to access Utilities
function onOpen(e) {
SpreadsheetApp.getUi()
.createMenu('Protection Utilities')
.addItem('Show all protections', 'uigetprotections')
.addItem('Remove all protections', 'removeallprotections')
.addToUi();
}
function removeallprotections() {
// remove all protections
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheetname = "Sheet1";
var sheet = ss.getSheetByName(sheetname);
var protections = ss.getProtections(SpreadsheetApp.ProtectionType
.RANGE);
Logger.log(protections);
for (var i = 0; i < protections.length; i++) {
var protection = protections[i];
Logger.log(protection.getEditors())
if (protection.canEdit()) {
protection.remove();
}
}
// Display confirmation dialog
var ui = SpreadsheetApp.getUi();
var response = ui.alert('REMOVE ALL PROTECTION',
'Confirmed: Removed all protections', ui.ButtonSet.OK);
}
function uigetprotections() {
// generate a list of all RANGE protections
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheetname = "Sheet1";
var sheet = ss.getSheetByName(sheetname);
var protections = ss.getProtections(SpreadsheetApp.ProtectionType
.RANGE);
//Logger.log(protections);
var ui = SpreadsheetApp.getUi();
var protectioninfo = "";
if (protections.length != 0) {
for (var p = 0; p < protections.length; p++) {
//Logger.log("DEBUG: Date = "+protections[p].getDescription()+", Range = "+protections[p].getRange().getA1Notation());
protectioninfo = protectioninfo + "Date: " + protections[p]
.getDescription() + ", Range = " + protections[p].getRange()
.getA1Notation() + "\n";
}
var response = ui.alert('SHOW ALL PROTECTIONS', protectioninfo, ui
.ButtonSet.OK);
} else {
var response = ui.alert('SHOW ALL PROTECTIONS',
"There were no protected ranges", ui.ButtonSet.OK);
}
}