Google Sheets - Replace sheet without breaking references to that sheet - google-apps-script

We are building a google sheets database where each user has their own spreadsheet that accesses a central sheet for information using apps script.
This means that with 50 employees, we have 50 spreadsheets to maintain. I am trying to find a way to push updates to all 50 spreadsheets without having to update each one manually. I have all the apps script code in a library that each user's sheet references, so I have the coding maintenance figured out. But keeping each users actual spreadsheet up to date with the latest features is proving difficult.
One way I'm figuring to do that is have a "Template" user sheet that gets updated with the changes/new features. Then when each user opens their spreadsheet, it cross references all of its sheets to the template sheet, and checks if it needs to replace it's sheet with the latest sheet based on time that it was updated in the template sheet. For example, when the sheet "Project Report" in the template is newer than the "Project Report" sheet in the user's spreadsheet, the user SS deletes it's current "Project Report" and copies the template "Project Report" sheet to it's own via the copyTo() method.
I have this all working with apps script, but the issue now is that when the user's local sheet is deleted and replaced with the new updated seet, all formula references to that sheet in other sheets break and replace the reference with #REF. I had planned on overcoming this by using only Named Ranges, but even the named ranges break when the sheet is replaced to the point where even the apps script can no longer find the named range because the named range it is looking for was automatically renamed when the new version of the sheet was imported (aka, "CustomNamedRange" in the template SS was renamed to "'SheetName'!CustomNamedRange" in the user SS).
The only way I know to overcome this issue at this point is to create a centralized "Range Index" spreadsheet that has all the named ranges with their destination sheet and range. I would have to create a custom function that filters through the range index and finds the address it needs based on the name given. For example, instead of calling "CustomNamedRange" in a sheet formula, I would call custom function: getNamedRange("CustomNamedRange"), and apps script would return the range found in the range index. And when a sheet is replaced with the newer version, no references would break because all references go through the apps script filter function.
The only problem with this is that I can foresee this method (calling every range needed in the script through a custom function) slowing down my spreadsheet A LOT because every time a range is called for, it will have to go search through the range index to find it and return it.
Does anyone have any other ideas on how to accomplish what I'm looking for? As in keeping 50+ individual spreadsheets updated with new features without having to do it manually and without breaking all the references?
Sorry for the long post, but I appreciate any ideas!

I had a similar problem and was able to resolve it by using SheetAPI to replace text. I have a template called Sheet1_Template and its hidden. I delete Sheet1, copy Sheet1_Template, show it and then replace all occurances of "Sheet1" in formulas to "Sheet1". Sheet API has to be enabled in the Resources and Google API Console.
function copyTemplate() {
try {
var spread = SpreadsheetApp.getActiveSpreadsheet();
var sheet = spread.getSheetByName("Sheet1");
if( sheet !== null ) spread.deleteSheet(sheet);
sheet = spread.getSheetByName("Sheet1_Template");
sheet = sheet.copyTo(spread);
sheet.setName("Sheet1");
sheet.showSheet();
sheet.activate();
spread.moveActiveSheet(0);
var requests = {"requests":[{"findReplace":{"allSheets":true,"find":"Sheet1","replacement":"Sheet1","includeFormulas":true}}]};
Sheets.Spreadsheets.batchUpdate(requests, spread.getId());
}
catch(err) {
Logger.log("error in copyTemplate: "+err);
}
}

I haven't been able to test implementation of it yet, but I believe the answer above is what I was originally looking for.
I haven't spent any time messing with the API yet, so in the meantime I have found another solution:
Google Sheets recently added macros to it's feature set. The beauty of this is that You can see and edit the macro code after you've recorded your actions in the sheet. For now, I plan on recording a macro when I make updates to the template sheet, then copying the script for that macro into a custom function in my library that will run every time a user opens their spreadsheet. When they open their SS, apps script will check to see if the library's macro function has a later date than the last time the sheet was opened. If it does have a new date, then it will run the macro script, and that user's SS should get updated to the same state as the template.

Also if you are seeing that you cannot run the query from #TheWizEd
It may be due to "Sheets API" not being enabled at Advanced Google services. Please enable>
In the script editor, select Resources > Advanced Google services In the dialog that appears, click the on/off switch for Google Sheets API v4. Please turn on. Click OK button.

Thank you so much to TheWizEd for getting me started (please vote for that post too).
This is what I needed:
function replaceFormulasInSheet(sheet, searchFor, replaceWith) {
// https://stackoverflow.com/a/67151030/470749
// First you need to do this to enable the feature: https://developers.google.com/apps-script/guides/services/advanced#enabling_advanced_services
// https://developers.google.com/sheets/api/quickstart/apps-script
// https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#findreplacerequest
// https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/batchUpdate
const spread = SpreadsheetApp.getActiveSpreadsheet();
const requests = {
"requests": [
{
"findReplace": {
// "allSheets": true, Omitting this property and instead setting the sheetId property is the only way to effectively set allSheets as false.
"sheetId": sheet.getSheetId(),
"find": searchFor,
"replacement": replaceWith,
"includeFormulas": true
}
}
]
};
return Sheets.Spreadsheets.batchUpdate(requests, spread.getId());
}
Also note that it does not work for sheets with hyphens in their names. If you need hyphens in their names, remove the hyphens beforehand and re-add them after.

Related

ActiveSheet().getName() returns different results. Could somebody explain why? [duplicate]

I guess this question is the most similar to my current question. But basically, I have a google sheets file that has several sheets along the bottom. I would like for the user to execute this script on whatever sheet they have open at the time.
And like the previous question said according to this documentation, the active sheet is the one that is being displayed in the spreadsheet UI. I assume this means if I have the google sheets file open in another tab, that the sheet currently selected and viewable by me is intended to be the active sheet.
However, the actual active sheet returned is always the leftmost sheet along the bottom tabs. I've confirmed it by switching the order of the sheets. So no matter what sheet I actually have open and visible in the UI, it always gets the leftmost sheet. Here is the line that gets it.
var Sheet = SpreadsheetApp.openById("redactedid").getActiveSheet()
Is this a known error with App Script? Or is the documentation wrong? If it is, is there a workaround for getting the currently open sheet in the UI? I don't want anyone to have to hardcode a sheetname in the code as the file is constantly changing and sheets are being added.
Notes:
To use getActiveSheet() or any active object, the calling
Script must be bound to the Spreadsheet
Script function should be invoked from menu or button or macro keyboard shortcuts and NOT from webapp or API( Script editor also works, but not preferred).
Spreadsheet must be open/visible in the user interface/browser preferably in the active/currently selected window.
All previous calls to get objects must have used active. For eg, To get active range, You should have first got active spreadsheet => active sheet => active range(the chain of active objects). If any of the calls is not to the active object(say if you used getSheetByName() instead of getActiveSheet() in the middle step), the final call is likely to default to the default object(First sheet A1).
Solution:
Here, To get Spreadsheet, use SpreadsheetApp.getActive() instead of SpreadsheetApp.openById(), So that getActiveSheet() will be under the chain of active objects.
Snippet:
const sheet = SpreadsheetApp.getActive().getActiveSheet();
It's known that the getActiveSheet method returns the first sheet when used with an spreadsheet that is not the active spreadsheet.
The active spreadsheet is retrieved by using getActiveSpreadsheet. I only works on scripts bounded to an spreadsheet and in G Suite Editors add-on for spreadsheets.
In the the same way, getCurrentCell and getActiveRange returns A1 when corresponding sheet is not an active sheet (meaning it's not a sheet from an active spreadsheet)
Resources
https://developers.google.com/apps-script/guides/sheets
Related
Google Apps Script, select a sheet - Sets values in first sheet tab - not a specific sheet tab
All the points in the accepted solution are all true, but sometimes your spreadsheet looses some connection to something on Google's back end or the cache gets corrupted, who knows. The fix is to close the all tabs for the worksheet, editor, triggers, executions -- whichever ones you have open for that spreadsheet, close them. Reopen the spreadsheet. Start again. It's fine now.

Google Sheets custom function not working when downloading as published CSV

I've created a custom function in a Google Spreadsheet that populates a cell with a pipe-separated list of file paths based on a loop, a prefix, postfix and some cell references. This works great on the sheet itself.
But I've also published this sheet as a CSV using the "Publish to the web..." functionality. This also works, as long as I have the sheet itself open in my browser.
But once I close that browser window, after about 5 minutes (the publish refresh window I believe), those cells are populated with "#NAME?" in the downloaded CSV. It would seem the custom functions cannot be run in the "headless" mode of a published CSV file.
Is there a way around this?
Custom functions do not provide values to the published view as they run under the scope of the person accessing. For the published view, that is an anonymous visitor with no access to the script resources (hence the #NAME error).
The solution is to implement static value assignment from script functions. Depending on your spreadsheet usage (you provide no definitive information), an on edit or a time-based trigger may be used to invoke the customized calculations.
I suspect you will want a function that is triggered when the cells containing the file paths are edited, and writes to a given static range.
function onEdit(e) {
// Ensure the edited cell was the correct sheet and column.
var s = e.range.getSheet();
if(s.getName() == ... && e.range.getColumn() == ... ) {
// Your code called from here
}
}

Duplicate Form responses to two Google Sheets programmatically

I need to save my Google Form in two different sheets.
The first sheet will be the one for history and the other one will be exploited by the logistic services (who may delete some rows when the clients receive the shipped stuff).
I really need to keep all the responses on the first sheet whatever the logistic services do on the second.
The fact is; I was using formRat, but is not working anymore and I don't see any complementary module that does exactly the same thing. I'm not good enough in programming to write the script by myself.
I tried to write this in the second sheet:
=ArrayFormula('first_sheet_name'!A:W)
But when I try to delete a row on the second sheet, it reappears a few seconds later because Google Sheets recalculates it.
A form submission trigger script attached to the Form Response spreadsheet can easily copy responses to the second sheet, as they arrive. Any modifications made later on the second sheet will survive.
Here is a very simple example of such an Installable Trigger Function. You need to declare ss2ID with the Sheet ID of spreadsheet 2. The script assumes that the responses are to be copied to the first sheet in spreadsheet 2, and that all form answers are populated.
function copyResponse( event ) {
fixFormEvent( event ); // From https://stackoverflow.com/a/26975968/1677912
var ss2Id = "---sheet-id---";
var sheet2 = SpreadsheetApp.openById( secondSheetId ).getSheets()[0];
sheet2.appendRow( [event.values] );
}
This function uses fixFormEvent( event ) from e.values in google forms skips empty answers, is there a workaround? to ensure the columns in the new sheet align with the original questions.

script to force republish of google spreadsheet

I have created a form that pushes data to a Google Spreadsheet. The data is latitude, longitude, location, and other identifying data. The spreadsheet is then published as a .CSV file and imported into ARC GIS to be displayed on an interactive map. It works exactly as I wanted and I set it to republish after each change.
The problem is that when the spreadsheet has rows appended by the script, it is not seeing it as a change and republishing. In order to get the updated data imported to the map, I need to go in and manually republish. Is there anyway through the Google Apps Script that I could make a few lines of code to force a republish? I could then add that to the "on form submit" script I have or another time based one that already runs at 3 am everyday.
I have looked through the Google Apps Script documents and not found anything. When searching for help on the web, the overwhelming majority of responses are for how to publish your script as a template for other.
My testing sheet was republished after the following function was executed by either a menu entry or a time-based trigger.
function ChangeIt() {
var sheet = SpreadsheetApp.getActiveSpreadsheet()
var t = new Date()
var x = 'upd: ' + t
var range = sheet.getRange('a3')
range.setValue(x)
}
If I were in your shoes, I'd add an extra column to the end of the sheet with some benign constant data that a script can change without affecting the systems consuming the data. If an extra column isn't an option, try modifying my sample to read in a current value, change it, and immediately change it back.
Also, I'd see if the spreadsheet onEdit() trigger fires when the form submit adds a new row. If so, tie your GAS function to it to force the republish. If not, setup a timed trigger to execute the GAS function.
A quick workaround for this issue that doesn't require scripting is to simply make an array copy of the data.
For example, I made a new tab and in A1 put this: =ArrayFormula('Form Responses 1'!A1:Z1000)
While the main Form responses tab will insert rows and not play nice with formulas this new tab stay nice and constant and updates automatically when new data is added.

Some spreadsheet cells not updated before trigger function sends email

I have a spreadsheet which extracts and accumulates all the required data from other 6 spreadsheets using Vmerge and Query formulas and all the consolidated data will be converted to pdf and emaild to mail ids using trigger event.
Here begins the problem every time the attachment mail posted consist all the headers and other format, but the data which is extracted does not appear. It seems to appear like - to open the same spreadsheet, after a while all the - (hyphens) are replaced by the data / Hope all the data updates after a while of opening the spreadsheet.
link for sheet
Can anyone direct me to make this issue sorted out.
For this solution should be -> all the data should be updated and then the email script should work; or either it should update before emailing script starts.
Or any other better ideas are appreciated.
Some spreadsheet formulas, like ImportRange and ImportXml (and also Apps Script custom formulas) are only evaluated when there's someone logged in the spreadsheet. It's like these functions need an account to be evaluated from, for example, in importRange the account logged in must have access to the range being imported, if you share this spreadsheet with someone but not the importRange source, when this person is viewing this spreadsheet, the importRange function will not work (well, unless you're also in the spreadsheet and the formulas have already been evaluated).
Bottom line is, you can't have this formulas and use a script triggered on time-driven (or other trigger that does not require someone logged in) and expect the script to be able to read this data.
The workaround though is quite simple. Do what the importRange function does inside your script! e.g.
var source = SpreadsheetApp.openById('source-spreadsheet-key');
var data = source.getSheetByName('List').getRange('I6:AT500').getValues();
//then save it somewhere
var s = Spreadsheet.getActive().getSheetByName('hidden-import');
s.getRange('I6:AT500').setValues(data);
SpreadsheetApp.flush(); //force the data to be written
//so all the other formulas on your spreadsheet get updated with the new data
All your "logic" formulas, like query and vmerge, which are difficult for the script to mimic, can be left on the spreadsheet, but reference this "hidden-import" sheet I just invented instead of nesting importRange directly.
[edit]
To copy only non-empty rows do like this:
var data = SpreadsheetApp.openById('source-spreadsheet-key').
getSheetByName('List').getDataRange().getValues();
Spreadsheet.getActive().getSheetByName('hidden-import').
getRange(1,1,data.length,data[0].length).setValues(data);