How to move subfolder based on values in Google Sheet column? - javascript

I have a google sheet which I use to Index a list of Property, with an App Script setup on Edit to create a Property SubFolder inside a Draft Folder using the values in column A and B when 'Draft' is Selected in Col C.
Ie. REF + LOCATION
I'm currently trying to get the Script to move the Property SubFolder between 'Draft Folder', 'Live Folder', 'Archive Folder' depending on Listing Status in Column C. (These folders exist inside a main parent and I would like to avoid manually moving the property subfolder between Draft, Live and Archive.
However, I am having issues, in terms of Property Sub-Folder not moving. The Folder IDs are correct and the Folder Names match exactly, but when I select 'Live' or 'Archive' in Col C, the Property Sub-Folder doesn't move from draft into selection folder and nothing happens.
Could anyone please advise accordingly where I'm going wrong, and how to move the propertyFolder with selection at Col C?
The App Script is straightforward in regard to it checks if the folder already exists, and IF not creates the subfolder in Draft and ELSE checks if folder needs to be moved.
Here is the ELSE IF to Set folder where to move the Property SubFolder
} else if ((status == 'Live' && statusFoldername != 'Live Folder') || (status == 'Archived' && statusFoldername != 'Archive Folder')) {
if (statusFoldername != 'Live Folder') {
var moveToFolder = liveFolder;
} else {
var moveToFolder = archveFolder;
}
propertyFolder.moveTo(moveToFolder);
}
Here is the full verison..
function createFolderSubFolders(e) {
// Get Parent folder
var parentFolder = DriveApp.getFolderById('ID');
var draftFolder = DriveApp.getFolderById('ID');
var liveFolder = DriveApp.getFolderById('ID');
var archveFolder = DriveApp.getFolderById('ID');
// Get current spreadsheet
var ss = SpreadsheetApp.openById('ID');
var i = e.range.getRow();
// Process if Col B is not empty and Col C equals 'Draft'
if(ss.getRange('C' + i).getValue() == 'Draft') {
//Set intended folder name
var ref = ss.getRange('A' + i).getDisplayValue();
var location = ss.getRange('B' + i).getValue();
var status = ss.getRange('C' + i).getValue();
var propertyFolderName = ref + ' - ' + location;
///Check if folder already exists
var findTest = false;
//Get FolderIterator for looking into status folders
var statusFolders = parentFolder.getFolders();
// Checks if Parent Folder already has Folder with given name
while (statusFolders.hasNext() && findTest == false) {
var statusFolder = statusFolders.next();
//Property Listings in current iterated status folder.
var propertyFolders = statusFolder.getFolders();
while (propertyFolders.hasNext() && findTest == false) {
var propertyFolder = propertyFolders.next();
if (propertyFolderName == propertyFolder.getName()) {
//Check where property folder lies currently.
var statusFoldername = statusFolder.getName();
findTest = true;
break;
}
}
}
// Create new property folder in draftFolder if not already created.
if (findTest == false) {
var propertyFolder = draftFolder.createFolder(propertyFolderName);
//Otherwise check if folder needs to be moved to other section.
} else if ((status == 'Live' && statusFoldername != 'Live Folder') || (status == 'Archived' && statusFoldername != 'Archive Folder')) {
**//Set folder where to move the propertyFolder
if (statusFoldername != 'Live Folder') {
var moveToFolder = liveFolder;
} else {
var moveToFolder = archveFolder;
}
propertyFolder.moveTo(moveToFolder);
}**
}
}

I believe your goal is as follows.
You have a sheet including the columns "A" to "C". When the dropdown list of column "C" is changed, you want to create a folder name using columns "A" and "B" and you want to search the folder name from 3 folders of "Live", "Archived", "Draft". When the folder name is not found and the value of column "C" is "Draft", you want to create a new folder using the folder name to "Draft" folder. When the folder name is found, you want to move the folder using the value of column "C".
Modification points:
When I saw your script, each folder ID is a unique ID. So, in this case, it is not required to scan all folders from each folder. So, I thought that the folder can be directly searched using the folder name from each folder.
And, in order to retrieve the values from columns "A" to "C", this can be achieved by one call.
By modifying these points, the script might be simple and the process cost of the script might be able to be reduced. When these points are reflected in your script, how about the following modification?
Modified script 1:
Please copy and paste the following script to the script editor of Spreadsheet and save the script. And, in this case, in order to run the script by changing the dropdown list of column "C", please install the OnEdit trigger to the function createFolderSubFolders.
function createFolderSubFolders(e) {
// I prepared this object from your script.
// Please set your folder IDs.
var obj = {
parentFolder: "###",
searchFolders: [
{ name: "Draft", id: "###" },
{ name: "Live", id: "###" },
{ name: "Archived", id: "###" },
]
};
var sheetName = "Sheet1"; // Please set your sheet name.
var { range, value } = e;
var sheet = range.getSheet();
if (sheet.getSheetName() != sheetName || range.columnStart != 3 || !["Live", "Archived", "Draft"].includes(value)) return;
var [ref, location, status] = sheet.getRange(range.rowStart, 1, 1, 3).getDisplayValues()[0];
if (!ref && !location) return; // <--- Added. Or, if (!ref || !location) return;
var propertyFolderName = ref + ' - ' + location;
var check = obj.searchFolders.reduce((o, { name, id }) => {
var f = DriveApp.getFolderById(id).getFoldersByName(propertyFolderName);
if (f.hasNext()) {
o = { name, f: f.next() };
}
return o;
}, null);
if (!check && status == "Draft") {
DriveApp.getFolderById(obj.searchFolders[0].id).createFolder(propertyFolderName);
} else if (check && status != "Draft" && check.name != status) {
check.f.moveTo(DriveApp.getFolderById(obj.searchFolders.find(({ name }) => name == status).id));
}
}
In this modified script, I thought that the above goal is achieved.
Sample script 2:
I thought that the following modified script might be able to be used. In this modification, the filename of propertyFolderName is searched by one call.
function createFolderSubFolders(e) {
// I prepared this object from your script.
// Please set your folder IDs.
var obj = {
parentFolder: "###",
searchFolders: [
{ name: "Draft", id: "###" },
{ name: "Live", id: "###" },
{ name: "Archived", id: "###" },
]
};
var sheetName = "Sheet1"; // Please set your sheet name.
var { range, value } = e;
var sheet = range.getSheet();
if (sheet.getSheetName() != sheetName || range.columnStart != 3 || !["Live", "Archived", "Draft"].includes(value)) return;
var [ref, location, status] = sheet.getRange(range.rowStart, 1, 1, 3).getDisplayValues()[0];
if (!ref && !location) return; // <--- Added. Or, if (!ref || !location) return;
var propertyFolderName = ref + ' - ' + location;
var q = `title='${propertyFolderName}' and mimeType='${MimeType.FOLDER}' and (` + obj.searchFolders.map(({ id }) => `'${id}' in parents`).join(" or ") + ") and trashed=false";
var f = DriveApp.searchFolders(q);
var check = f.hasNext();
if (!check && status == "Draft") {
DriveApp.getFolderById(obj.searchFolders[0].id).createFolder(propertyFolderName);
} else if (check && status != "Draft") {
var fol = f.next();
var pid = fol.getParents().next().getId();
var pname = obj.searchFolders.find(({ id }) => id == pid).name;
if (pname != status) {
fol.moveTo(DriveApp.getFolderById(obj.searchFolders.find(({ name }) => name == status).id));
}
}
}
References:
reduce()
find()

Related

Managing resources in lookup filter

My client wants to be able to filter jobplans with the selected asset. To be able to do that, I have developped a function that filters the results based on the custom resource jpassetsplink:
filterJobPlansForLookup: function(eventContext){
var workOrderSet = CommonHandler._getAdditionalResource(eventContext,"workOrder");
var jobPlanSet = CommonHandler._getAdditionalResource(eventContext,"jobPlanResource");
jobPlanSet._lookupFilter = null;
var assetSet = null;
var assetnum = null;
var itemnum = null;
var jpAssetSpLinkSet = null;
//CommonHandler._clearFilterForResource(this,jobPlanSet);
var siteid = workOrderSet.getCurrentRecord().get("siteid");
if(siteid == null){
siteid = UserManager.getInfo("defsite");
}
if(workOrderSet.getCurrentRecord() != null){
assetnum = workOrderSet.getCurrentRecord().get("asset");
assetSet = CommonHandler._getAdditionalResource(eventContext,"additionalasset");
CommonHandler._clearFilterForResource(eventContext, assetSet);
assetSet = assetSet.clearFilterAndSort().filter('siteid == $1', siteid)
if (assetnum != null){
var asset = assetSet.find('assetnum == $1', assetnum);
if (asset && asset.length>0){
itemnum = asset[0].get('itemnum');
}
}
}
var filter = [{siteid: siteid, status: "ACTIF"}];
if (assetnum != null){
jpAssetSpLinkSet = CommonHandler._getAdditionalResource(eventContext,"jpassetsplinkResource");
jpAssetSpLinkSet._lookupFilter = null;
CommonHandler._clearFilterForResource(eventContext, jpAssetSpLinkSet);
var filteredJpAssets = null;
if (itemnum == null){
filteredJpAssets = jpAssetSpLinkSet.clearFilterAndSort().filter('assetnum == $1', assetnum);
} else {
filteredJpAssets = jpAssetSpLinkSet.clearFilterAndSort().filter('itemnum == $1', itemnum);
}
Logger.trace("[WODetailExtensionHandler] Found " + filteredJpAssets.data.length + " links out of " + jpAssetSpLinkSet.count() );
if(filteredJpAssets && filteredJpAssets.data.length>0){
filter = [];
filteredJpAssets.data.forEach(function(jpAsset){
filter.push({jpnum: jpAsset.get("jpnum"), siteid: siteid, status: "ACTIF"});
});
}
}
jobPlanSet.lookupFilter = filter;
}
With the right circumstances this code works. There are multiple problems with it though:
1- When searching for an asset, the resulting filter is applied to the resource and cannot seem to be removed. If I search an asset in the asset lookup, when the execution gets to this function, the resource data is still filtered and calls to CommonHandler._clearFilterForResource, assetSet.clearFilterAndSort() or directly changing the _lookupFilter property does not work. This sometimes results in the impossibility to find the selected asset in the asset resource, thus the filtering ends up failing.
2- Not searching and directly inputing the desired asset leads to another problem. Since there is no filter on the resource, only the number of entries specified by pageSize is loaded. In my case, pageSize is set at 2000 for the asset resource. That means if the selected asset is not in the 2000 first entries, it is not found by the function, thus the filtering fails.
3- If the filter manages to work, it seems to block further filtering by jpnum or description in the jobplan lookup.
To conclude, here's my question: Is there a way to manage resources so that these problems do not occur ? Any tip is appreciated.
Thanks,

Spreadsheet tracker

Im having trouble with an script that posts the outcome twice, I have reviewed the script but I cant find the issue.
The script gets the "Timestamp", "Cell address", "Column label" and "Value entered" and post it on the sheet named "Tracker" but it gets posted twice
function onEdit() {
var sheetsToWatch = ['Responses'];
// name of the sheet where the changelog is stored
var changelogSheetName = "Tracker";
var timestamp = new Date();
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getActiveSheet();
var cell = sheet.getActiveCell();
var sheetName = sheet.getName();
// if it is the changelog sheet that is being edited, do not record the change
if (sheetName == changelogSheetName) return;
// if the sheet name does not appear in sheetsToWatch, do not record the change
var matchFound = false;
for (var i = 0; i < sheetsToWatch.length; i++) {
if (sheetName.match(sheetsToWatch[i])) matchFound = true;
}
if (!matchFound) return;
var columnLabel = sheet.getRange(/* row 1 */ 1, cell.getColumn()).getValue();
var rowLabel = sheet.getRange(cell.getRow(), /* column A */ 1).getValue();
var changelogSheet = ss.getSheetByName(changelogSheetName);
if (!changelogSheet) {
// no changelog sheet found, create it as the last sheet in the spreadsheet
changelogSheet = ss.insertSheet(changelogSheetName, ss.getNumSheets());
// Utilities.sleep(2000); // give time for the new sheet to render before going back
// ss.setActiveSheet(sheet);
changelogSheet.appendRow(["Timestamp", "Cell address", "Column label", "Value entered"]);
changelogSheet.setFrozenRows(1);
}
changelogSheet.appendRow([timestamp, cell.getA1Notation(), columnLabel, cell.getValue()]);
}
Google's method documentation makes it seem like you can programmatically check for the existence of some other user's trigger with a function like this:
function triggerLogger() {
// Read installed triggers for the project.
var triggers = ScriptApp.getProjectTriggers();
var installedReport = {};
triggers.forEach(function (t) { installedReport[t.getUniqueId()] = {
event: t.getEventType(),
calledFunction: t.getHandlerFunction(),
source: t.getTriggerSource(),
source_id: t.getTriggerSourceId() || "Time-based triggers have no source id."
}});
// Read "simple" triggers for the project by checking for globals that start with "on".
var simpleReport = {};
for (var thing in this)
if (thing.indexOf("on") === 0 && thing.length > 2)
simpleReport[String(thing)] = {def: this[thing]};
var possibleSimple = Object.keys(simpleReport).length,
message = "Trigger report: " + triggers.length + " installed";
if (possibleSimple) message += ", " + possibleSimple + " possible simple triggers";
message += ".";
// Log to Stackdriver (so the report can be viewed sensibly).
console.log({
message: message,
installed: Object.keys(installedReport).length ?
installedReport : "No detected installed triggers.",
simple: possibleSimple ?
simpleReport : "No simple triggers used",
reportRunAs: Session.getActiveUser().getEmail()
});
}
But the getProjectTriggers() method, despite claiming to get all of the current project's installed triggers, will only obtain your installed triggers for the document, even if you are the owner of the document.
Note that this behavior is accepted as a bug (meaning someone, someday will fix it). If you would like to feel that you have done your part to accelerate that timeline, please star that issue:

Grabbing GeoJSON data in Openlayers

What I'm trying to do:
Figure out how to reference/grab geoJSON data from a server.
In this case I'm just using an example on the openLayers doc.
Ideally I'd just be able to print out a features ID/type, but I cannot get it to work.
What's happening:
var selectElement = document.getElementById('type');
var source = vector.getSource();
var feature = source.getFeatures()[0];
var changeInteraction = function() {
if (select !== null) {
map.removeInteraction(select);
}
var value = selectElement.value;
if (value == 'singleclick') {
select = selectSingleClick;
} else if (value == 'click') {
select = selectClick;
} else if (value == 'pointermove') {
select = selectPointerMove;
} else if (value == 'altclick') {
select = selectAltClick;
} else {
select = null;
}
if (select !== null) {
map.addInteraction(select);
select.on('select', function(e) {
document.getElementById('status').innerHTML = feature.getGeometry().getType();
});
console.log(feature);
}
};
I was hoping my innerHTML would display "Polygon" in this case, but no such luck. I've tried various combinations, and been looking over the documentation, can't see what I'm doing wrong.
The server I'm trying to grab the info from is,
https://openlayers.org/en/v4.6.4/examples/data/geojson/countries.geojson
Any help would be appreciated.
(I can attach full code if helpful)
I was able to replicate your program and find the solution for retrieving the Country's name for a selected feature, as mentioned in your comments.
First, remove the following lines. You don't want the first feature of the source file but the first selected feature instead.
var source = vector.getSource();
var feature = source.getFeatures()[0];
Second, define the feature inside the callback function(e) for the select Event. Also, since getFeatures() will return a Collection of features the getArray() method is necessary.
The get(key) method will return a value for a determined key, "name" in this case.
if (select !== null) {
map.addInteraction(select);
select.on('select', function(e) {
var feature = e.target.getFeatures().getArray()[0];
document.getElementById('status').innerHTML = ' ' +
feature.get("name") + ' ' + feature.getId();
});
}

Meteor subscribe callback running when subscription contains previous subscribe result

I am fairly new to meteor, and I am running into a strange issue with subscribe callbacks. I have a database containing courses and reviews. I'm using a publish/subscribe model on the reviews to return reviews that are only relevant to a selected class, and I want this to change each time a new class is clicked on. I want to print all the reviews and compile some metrics about the reviews (average quality, difficulty rating). Using the following code, with a subscribe that updates the reviews sent to the client, the printed reviews (which are grabbed from a helper) return correctly, but the metrics (which are grabbed on an onReady callback to the helper) are inaccurate. When the onReady function is run, the current result of the local reviews collection contains the union of the clicked class and the previously clicked class, even though the reviews themselves print correctly.
I've also tried using autoTracker, but I got the same results. Is there a way to clear previous subscribe results before updating them?
publish:
Meteor.publish('reviews', function validReviews(courseId, visiblity) {
console.log(courseId);
console.log(visiblity);
var ret = null
//show valid reviews for this course
if (courseId != undefined && courseId != "" && visiblity == 1) {
console.log("checked reviews for a class");
ret = Reviews.find({class : courseId, visible : 1}, {limit: 700});
} else if (courseId != undefined && courseId != "" && visiblity == 0) { //invalidated reviews for a class
console.log("unchecked reviews for a class");
ret = Reviews.find({class : courseId, visible : 0},
{limit: 700});
} else if (visiblity == 0) { //all invalidated reviews
console.log("all unchecked reviews");
ret = Reviews.find({visible : 0}, {limit: 700});
} else { //no reviews
console.log("no reviews");
//will always be empty because visible is 0 or 1. allows meteor to still send the ready
//flag when a new publication is sent
ret = Reviews.find({visible : 10});
}
//console.log(ret.fetch())
return ret
});
subscribe:
this.helpers({
reviews() {
return Reviews.find({});
}
});
and subscribe call, in constructor with the helpers:
constructor($scope) {
$scope.viewModel(this);
//when a new class is selected, update the reviews that are returned by the database and update the gauges
this.subscribe('reviews', () => [(this.getReactively('selectedClass'))._id, 1], {
//callback function, should only run once the reveiws collection updates, BUT ISNT
//seems to be combining the previously clicked class's reviews into the collection
onReady: function() {
console.log("class is: ", this.selectedClass);
if (this.isClassSelected == true) { //will later need to check that the side window is open
//create initial variables
var countGrade = 0;
var countDiff = 0;
var countQual = 0;
var count = 0;
//table to translate grades from numerical value
var gradeTranslation = ["C-", "C", "C+", "B-", "B", "B-", "A-", "A", "A+"];
//get all current reviews, which will now have only this class's reviews because of the subscribe.
var allReviews = Reviews.find({});
console.log(allReviews.fetch());
console.log("len is " + allReviews.fetch().length)
if (allReviews.fetch().length != 0) {
console.log("exist")
allReviews.forEach(function(review) {
count++;
countGrade = countGrade + Number(review["grade"]);
countDiff = countDiff + review["difficulty"];
countQual = countQual + review["quality"];
});
this.qual = (countQual/count).toFixed(1);
this.diff = (countDiff/count).toFixed(1);
this.grade = gradeTranslation[Math.floor(countGrade/count) - 1];
} else {
console.log("first else");
this.qual = 0;
this.diff = 0;
this.grade = "-";
}
} else {
console.log("second else");
this.qual = 0;
this.diff = 0;
this.grade = "-";
}
}
})
When using pub-sub the minimongo database on the client will contain the union of subscriptions unless they are explicitly cleared. For that reason you want to repeat the query that's in the publication on the client side so that you filter and sort the same way. Minimongo is very fast on the client and you typically have much less data there so don't worry about performance.
In your constructor you have:
var allReviews = Reviews.find({});
instead use:
var allReviews = Reviews.find(
{
class : (this.getReactively('selectedClass'))._id,
visible : 1
},
{limit: 700}
);
Another side tip: javascript is quite clever about truthy and falsy values.
if (courseId != undefined && courseId != "" && visibility == 1)
can be simplified to:
if (courseId && visibility)
assuming you're using visibility == 1 to denote true and visibility == 0 to denote false

How to Delete List Item in a Custom Page?

I have a custom Dashboard page (Dashboard.aspx) that I made - it is not located inside the list, rather inside Pages folder.
I created this Dashboard to replace SharePoint's default "AllItems.aspx".
However, I could not replicate the onClick event of "Delete Item" from the SharePoint default list view.
Can anyone provide code snippets of how to delete a list item from a custom page?
P.S.: My custom page already has the ID and List Name. I appreciate your responses!
The function provided by Microsoft is this one:
function DeleteListItem() {
ULSrLq: ;
if (!IsContextSet()) return;
var b = currentCtx,
e = currentItemID,
g = currentItemFSObjType,
c = L_STSRecycleConfirm_Text;
if (!b.RecycleBinEnabled || b.ExternalDataList) c = L_STSDelConfirm_Text;
if (b.HasRelatedCascadeLists && b.CascadeDeleteWarningMessage != null) c = b.CascadeDeleteWarningMessage + c;
if (confirm(c)) {
var h = L_Notification_Delete,
f = addNotification(h, true),
a = b.clvp;
if (b.ExternalDataList && a != null) {
a.DeleteItemCore(e, g, false);
a.pendingItems = [];
a.cctx.executeQueryAsync(function () {
ULSrLq: ;
if (typeof a.rgehs != "undefined") {
if (a.rgehs.length == 1 && a.rgehs[0].get_serverErrorCode() == SP.ClientErrorCodes.redirect) {
GoToPage(a.rgehs[0].get_serverErrorValue());
return
}
removeNotification(f);
a.ShowErrorDialog(RefreshOnDialogClose)
} else RefreshPage(SP.UI.DialogResult.OK)
}, function () {
ULSrLq: ;
removeNotification(f);
typeof a.rgehs != "undefined" && a.ShowErrorDialog()
})
} else {
var d = b.HttpPath + "&Cmd=Delete&List=" + b.listName + "&ID=" + e + "&NextUsing=" + GetSource();
if (null != currentItemContentTypeId) d += "&ContentTypeId=" + currentItemContentTypeId;
SubmitFormPost(d)
}
}
}
With that you should be able to find what you need for your case.
If you use some jQuery/JavaScript in your page, you may also want to check SharepointPlus that provides some useful functions (like to get data from a list or to delete an item).
I figured it out!
I have a JS library called "SPAPI_Lists", which is from SharePoint Services, I believe.
It provides a function called quickDeleteListItem(listName, listItemId).
Code looks like this:
var urlThatContainsList = 'http://www.samplesite.com/sample';
var listName = 'Sample List';
var listItemId = 3;
new SPAPI_Lists(urlThatContainsList).quickDeleteListItem(listName, listItemId);

Categories

Resources