Google Sheets - Read only column that i can copy as well - google-apps-script

I have a Google Sheet that I gave editor permissions for a few people that can edit the data.
inside this sheet, I have a column (column B, WEEKDAY) that I want to display as a read-only value according to the value from another column (column A, holding a date value) of the same row.
for example if value of column A is Feb 14th, 2022 then automatically I see value of "Monday" on column B.
in order to prevent mistakes by my editors, I set column B as read-only using the protected range feature.
but, my problem - once I set this protection, they can't copy the entire row to another row, or delete the entire row, as column B is protected.
What is the correct way to solve my need?
using Apps script is possible as well for me.
meanwhile, I changed the protected range from error to warning as I don't have a better solution for now.
I am using ARRAYFORMULA in cell B1, but users can ignore it and manually override the value of B7 (as an example) instead of the formula value. if they do such manual override they get an error on B1 "Array result was not expanded because it would overwrite data in B7.". this is the reason I was looking to set column B as read-only so users can not manually override the formula value.

Issue:
You are using an ARRAYFORMULA on B1 which populates several cells in column B.
You want users to be able to copy and delete rows.
You want users to be unable to edit values in column B (since that would mess with the ARRAYFORMULA).
Solution:
Use an onEdit trigger which does the following every time a user edit a spreadsheet cell:
Check whether the user has permission to edit column B, using Session.getActiveUser() (the list of users who have permissions is defined in the script itself, and it may well be empty).
If the user has permission, check whether the edited cell is in the protected range (B2:B), using the event object.
If the edited cell is in that range, remove any values in that range, using Range.clearContent().
Regarding the cell B1, which contains the formula, I'd protect it conventionally via Sheets editor (ref).
Code sample:
const EDITORS = ["your_editor#domain.com"]; // Users who can edit column B
const SHEET_NAME = "Sheet1"; // Change accordingly
const PROTECTED_COL = 2; // Column B
const FIRST_ROW = 2; // Header row is protected conventionally
function onEdit(e) {
const userEmail = Session.getActiveUser().getEmail();
if (!EDITORS.includes(userEmail)) {
const range = e.range;
const sheet = range.getSheet();
if (sheet.getName() === SHEET_NAME && range.getColumn() === PROTECTED_COL && range.getRow() >= FIRST_ROW) {
sheet.getRange(FIRST_ROW, PROTECTED_COL, sheet.getLastRow()-FIRST_ROW+1).clearContent();
}
}
}
Note:
Make sure you modify EDITORS and SHEET_NAME according to your preferences.

Related

Google Sheet Copy Column Data On Edit

I had entered a formula in column B of the Google Sheet (link given below) which will generate number if I enter data in Column C.
I want to design a script which will run when data is entered in column C and the script will copy the number generated in column B in column A.
https://docs.google.com/spreadsheets/d/1kfa9pkItGQGaWWD_L8opR9w-MZnWxA2qfBTmNYp0wmg/edit?usp=sharing
Any help on above will be appreciated.
Issue:
If I understand you correctly, you want a script to do the following:
Fire every time column C is edited.
On the edited row, copy the value from column B to column A.
Solution:
In that case, you can use a simple onEdit trigger and the corresponding event object, like this (check inline comments):
function onEdit(e) {
const range = e.range; // Edited range, from event object
if (range.getColumn() === 3) { // Check edited column is C
const b = range.offset(0,-1).getValue(); // Get value from B
if (b !== "") { // Check B is not empty
range.offset(0,-2).setValue(b); // Set value in A
}
}
}

Protect range (not entire rows, but multi height and partial width) based on another cell value

NOTE: my question is NOT answered by Protecting/unprotecting range based on another cell value
My question asks about a range, with only part of the rows selected, NOT entire rows as in that question. I have extensively searched the site and there isn't anything that answers my question.
Please read my query properly prior to linking it to something else that does not answer it.
QUESTION
I have created a shared test Google Sheets workbook (TestWBook) to illustrate what I am trying to do. The 'Outgoing' sheet is where I need the restrictions to be applied; it is used to store order data.
The whole 'Outgoing' sheet (A1:AS), except range A2:M882, is currently protected so that only the owner and two other editors can make changes to it (let's call them editor1#email.com and editor2#email.com).
Other editors enter data in A2:M882 to specify new order requirements (which is why this range started by not being protected).
I now need to protect each row between columns A and M(active_row) from all editors but the owner and two other named editors (editor1#email.com and editor2#email.com) whenever a date is entered in column O (Date Sent) (or if that cell is not blank, either would work).
I don't want to protect an entire single row every time the script runs, as I don't want to end up with hundreds of protected ranges.
I want the protection to be applied dynamically to the range from A2 to M(active_row), so that effectively that one protected range is updated every time a date/value is entered in column O.
I have been looking through scripts for the last two days and cannot find a way to specify this dynamic range (sorry, I can adapt scripts but cannot write them up from scratch!)
Can anyone help?
Try
function onEdit(event) {
var sh = event.source.getActiveSheet();
var rng = event.source.getActiveRange();
if (sh.getName() != 'Outgoing') return;
if (rng.getColumn() != 15) return;
// save the protection parameters
var p1 = sh.getProtections(SpreadsheetApp.ProtectionType.SHEET)[0];
// delete actual protection
let protection = sh.getProtections(SpreadsheetApp.ProtectionType.SHEET)[0];
if (protection) { if (protection.canEdit()) { protection.remove(); } }
// rebuild protection
var p2 = sh.protect();
p2.setDescription(p1.getDescription());
p2.setWarningOnly(p1.isWarningOnly());
if (!p1.isWarningOnly()) {
p2.removeEditors(p2.getEditors());
p2.addEditors(p1.getEditors());
}
// define new uprotected area
p2.setUnprotectedRanges([sh.getRange('A' + rng.getRow() + ':M882')]);
}

Google Apps Script - How to get clipboard column number

Goal: When a user pastes a value to a cell, I want the script to check whether or not the copied value is from the same column or not.
Problem: Oftentimes, the user copies a cell from a different column and pastes in a cell that's under another column. This causes some problems for this particular sheet, so I want to be able to have the script check if the clipboard’s copied value's column number (or letter) is the same with the column of where the user is trying to paste it.
Example:
Scenario A: User copied a cell from A1 and attempts to paste it to B2 (either by mistake or intentionally)
If statement check: Check if the user’s clipboard copied value’s column number (or letter) is equal to 2 (if by letter: B).
Result: Since the copied column number is 1 and editing column number is 2, the script will show an error message to refuse the editing.
Scenario B: User copied a cell from A1 and attempts to paste it to A2
If statement check: Check if the user’s clipboard copied value’s column number (or letter) is equal to 1 (if by letter: A).
Result: Since the copied column number is 1 and editing column number is also 1, the script will allow the user to pates the value to A2.
Here's my attempt: I thought it would make sense to use the onEdit function to compare
function onEdit(e) {
const range = e.range;
const source = e.source;
const sheetName = source.getActiveSheet().getName();
const row = range.getRow();
const column = range.getColumn();
const editedRangeValue = range.getValue();
// Call the copyPrevention function
copyPrevention(sheetName, range, row, column, editedRangeValue);
}
function copyPrevention(sheetName, range, row, column, editedRangeValue) {
const headerRowNum = 12;
if (sheetName == 'Status' && row > headerRowNum && editedRangeValue != '') {
// row variable: This is to ensure the editing will only apply if the user tries to edit after row 12 (headerRow).
if (column == "") { // The "" is just a placeholder. If this is even possible, I would like to compare user's editing column number & their clipboard column number. And if the column numbers are the same, then I'd like the script to continue the execution without throwing any errors.
range.setValue(editedRangeValue);
} else if (column != "") { // The "" is just a placeholder. I would like to compare user's editing column number & their clipboard column number. And if the column numbers are not the same, then I'd like the script to throw an error and not let the user edit the cell.
const statusSheetUi = SpreadsheetApp.getUi();
statusSheetUi.alert("You're pasting in the wrong column!");
}
}
}
I have a feeling that this isn't possible, however wanted to check.
Thank you!

Google Sheets looking for a google script like VLOOKUP but combines the found values and joins them into a specific cell

I have a spreadsheet where I'm trying to add an automatic search function whenever a cell on the Sheet "List" Column 1 gets selected.
It's sort of like using a VLOOKUP function of the sheets but I've been unsuccessful trying to come up with an effective code for it.
Test Spreadsheet link
I made a simple test spreadsheet hopefully for people to easily understand how the sheet should work.
In the 'Database' sheet, there's a list of people with the fruits they like or dislike.
In the 'List' sheet, the list is reversed where the fruits are now the main list but here, when a person clicks on the cells with the fruits, the top cells should automatically update with the combined names of people who like or dislike that certain fruit.
The function should go like this:
If I click a cell on column 1 of the 'List' sheet,
A1 cell updates it's value with the active cell value
B1 cell updates it's value with the combined values based on who likes it from the 'Database' sheet Column 2.
B2 cell updates it's value similar as above but from Dislikes in Column 3
I've attached the code I currently have but I'm not able to get the last 2 steps working.
There is also a guide in the sheets on what the correct answers should look like for each fruit.
I got stuck looking for a way to make the steps 2 and 3 work. I was able to find a code here but it stops after finding one match. stackoverflow.com/questions/10838294/… My current problem is: Since the cells [B2:B] to filter from on the Database sheet has multiple values, is it possible to find all cells with at least a partial match, get the values of the cells on the left of that then list them into the B1 cell on the "List" sheet?
Test Spreadsheet link
Code.gs
function onSelectionChange(e){
var currentsheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
var currentcell = currentsheet.getActiveCell();
var activerow = currentcell.getRow();
var activecol = currentcell.getColumn();
var sname = currentsheet.getName();
// Check if current sheet name is correct
if (
sname == 'List'
)
{
// Check if this on first column
if (activecol == 1)
{
// ACTION
var name = currentcell.getValue(); //Get current cell for searching
var prefsheet = SpreadsheetApp.getActive().getSheetByName("Database");
var last = prefsheet.getLastRow();
var data = prefsheet.getRange(1,1,last,2).getValues(); // create an array of data from columns A and B
for(nn=0;nn<data.length;++nn){
if (data[nn][1]==name){break} ;
SpreadsheetApp.getActiveSheet().getRange('A1').setValue('Fruit: ' + name);
SpreadsheetApp.getActiveSheet().getRange('B1').setValue('Like: ' + data[nn][0]);
}
}
};
};
I was able to find a workaround for this and it turns out I didn't have to use a Google Script code to keep updating the formula on List!B1.
I'm new with scripting and sheets but I'll be sharing this formula here in case someone needs something like this in the future.
This is what I used on List!B1
=JOIN(", ",query(filter(Database!A2:B,REGEXMATCH(Database!B2:B,A1)),"Select Col1"))
Regexmatch does the search for partial value and returns as "TRUE"
Filter will then list these rows and query "Select Col1" will only keep the first column of the results and remove the second column. Lastly, Join formula will concatenate them into one cell and add separators.
Which is working very nicely for my purpose. If anyone has any suggestions I'd love to hear about it too.

Google Apps Script - Spreadsheet run script onEdit?

I have a script that I use in my spreadsheet. It is at cell B2 and it takes an argument like so =myfunction(A2:A12). Internally the function gets info from a large range of cells.
Nothing seems to work. I tried adding it to
Scripts > Resources > Current Project Triggers > On edit
Scripts > Resources > Current Project Triggers > On open
How can I have this function update the result with every document edit?
When you are making calls to Google Apps services inside your custom function (like getRange and getValues etc), unfortunately there is no way of updating such custom functions with each edit, other than passing all of the cells that you are "watching" for editing.
And, perhaps even more frustratingly, the workaround of passing say a single cell that references all of your "watched" cells with a formula doesn't trigger an update - it seems that one needs to reference the "watched" cells directly.
You could pass GoogleClock() as an argument which will at least update the function output every minute.
But the advice from many members on this forum (who have much more knowledge about this stuff than me) would simply be: "don't use custom functions".
I am not sure if this exact code will work but you can try something like this...
function onEdit(event){
var activeSheet = event.source.getActiveSheet();
if(activeSheet.getName() == "ActiveSheetName") {
var targetSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("TargetSheetName");
targetSheet.getRange(2,2).setValue("=myfunction(A2:A12)");
}
}
Assuming that B2 cell in on the sheet "TargetSheetName" and assuming that the edited cell is on the sheet "ActiveSheetName", the function onEdit will trigger when you edited any cell in any sheet. Since there is an if statement to check if that edited cell is on the sheet "ActiveSheetName" it will trigger only if the edited cell is on that sheet and it will set the B" cell to the value =myfunction(A2:A12), forcing it to update (i guess).
hope that i am correct and that i was helpful
I had a similar issue, for me I wanted to "watch" one particular cell to trigger my function.
I did the following (pretending A1 is the cell i am watching)
IF(LEN(A1) < 1, " ", customFunction() )
This successfully triggered if I ever edited that cell. However:
"Custom functions return values, but they cannot set values outside
the cells they are in. In most circumstances, a custom function in
cell A1 cannot modify cell A5. However, if a custom function returns a
double array, the results overflow the cell containing the function
and fill the cells below and to the right of the cell containing the
custom function. You can test this with a custom function containing
return [[1,2],[3,4]];."
from: https://developers.google.com/apps-script/execution_custom_functions
which makes it almost useless, but it might work for your case?
If the custom function is assigned to a project trigger it has more power so personally I ended adding it to "Scripts > Resources > Current Project Triggers > On edit"
and basically "watched a column" so it only did things if the current cell was within the "edit range". This is a bit of a bodge and requires some hidden cells, but it works for me at the moment
var rowIndex = thisspreadsheet.getActiveCell().getRowIndex();
var colIndex = thisspreadsheet.getActiveCell().getColumn();
//clamp to custom range
if(rowIndex < 3 && colIndex != 5)
{
return 0;
}
//check against copy
var name = SpreadsheetApp.getActiveSheet().getRange(rowIndex, 5).getValue();
var copy = SpreadsheetApp.getActiveSheet().getRange(rowIndex, 6).getValue();
if(name != copy )
{
///value has been changed, do stuff
SpreadsheetApp.getActiveSheet().getRange(rowIndex, 6).setValue(name);
}