Relational Query - 2 degrees away - google-app-maker

I have three models:
Timesheets
Employee
Manager
I am looking for all timesheets that need to be approved by a manager (many timesheets per employee, one manager per employee).
I have tried creating datasources and prefetching both Employee and Employee.Manager, but I so far no success as of yet.
Is there a trick to this? Do I need to load the query and then do another load? Or create an intermediary datasource that holds both the Timesheet and Employee data or something else?

You can do it by applying a query filter to the datasource onDataLoad event or another event. For example, you could bind the value of a dropdown with Managers to:
#datasource.query.filters.Employee.Manager._equals
- assuming that the datasource of the widget is set to Timesheets.
If you are linking to the page from another page, you could also call a script instead of using a preset action. On the link click, invoke the script below, passing it the desired manager object from the linking page.
function loadPageTimesheets(manager){
app.showPage(app.pages.Timesheets);
app.pages.Timesheets.datasource.query.filters.Employee.Manager._equals = manager;
app.pages.Timesheets.datasource.load();
}

I would recommend to redesign your app a little bit to use full power of App Maker. You can go with Directory Model (Manager -> Employees) plus one table with data (Timesheets). In this case your timesheets query can look similar to this:
// Server side script
function getTimesheets(query) {
var managerEmail = query.parameters.ManagerEmail;
var dirQuery = app.models.Directory.newQuery();
dirQuery.filters.PrimaryEmail._equals = managerEmail;
dirQuery.prefetch.DirectReports._add();
var people = dirQuery.run();
if (people.length === 0) {
return [];
}
var manager = people[0];
// Subordinates lookup can look fancier if you need recursively
// include everybody down the hierarchy chart. In this case
// it also will make sense to update prefetch above to include
// reports of reports of reports...
var subortinatesEmails = manager.DirectReports.map(function(employee) {
return employee.PrimaryEmail;
});
var tsQuery = app.models.Timesheet.newQuery();
tsQuery.filters.EmployeeEmail._in = subortinatesEmails;
return tsQuery.run();
}

Related

Handling browser refresh with query filters from page parameters

I have two data sources
Assets
Locations
Assets has a One to Many relation with Locations
Assets has a query builder data source, AssetLocFiltered, that is set with the following:
ParentLocationKey =:ParentLocationKey
ParentLocationKey is the relation field for the locations table.
On the home page you select a location and then click a button that passes the location ID to a parameter on the ShopPageDemo page. with the following code
app.pages.ShopPageDemo.properties.ParentLocationKey =
widget.datasource.item.Id;
console.log(widget.datasource.item.Id);
app.showPage(app.pages.ShopPageDemo);
One the ShopPageDemo Page there is a table view of the AssetLocFiltered that sets the query parameter in the ondataload event with the following code:
widget.root.datasource.query.parameters.ParentLocationKey =
widget.root.properties.ParentLocationKey;
widget.root.datasource.load();
This works great. The problem is when i hit the browser refresh it seems like it clears the property in ShopPageDemo. How do I handle this browser refresh issue? I am not really sure where to start.
There is more than one way to do this; However, the approach I usually take involves deep linking. In your case, you'll need to do something like this:
1.) On the button that takes you to the next page, add this code:
var params = {
key: widget.datasource.item.Id
};
var page = app.pages.ShopPageDemo;
app.showPage(page);
google.script.history.replace(null, params, page.name);
2.) On the ShopPageDemo onAttach event handler, add the following:
google.script.url.getLocation(function(location) {
widget.root.properties.ParentLocationKey = location.parameter.key;
var ds = widget.datasource;
ds.query.parameters.ParentLocationKey = widget.root.properties.ParentLocationKey;
ds.load();
});
For better performance, I would set the ShopPageDemo datasource to NOT load automatically and also to unload its data on the onDetach event handler like this: widget.datasource.unload().
Reference:
1. https://developers.google.com/apps-script/guides/html/reference/history
2. https://developers.google.com/apps-script/guides/html/reference/url

Changelog method based on project tracker template

Based on the project tracker I have integrated a changelog into my app that relates my UserSettings model to a UserHistory model. The latter contains the fields FieldName, CreatedBy, CreatedDate, OldValue, NewValue.
The relation between both models works fine. Whenever a record is modified, I can see the changes in a changelog table. I now want add an "undo"-button to the table that allows the admin to undo a change he clicks on. I have therefore created a method that is handled by the widget that holds the changelog record:
function undoChangesToUserRecord(changelog) {
if (!isAdmin()) {
return;
}
var fieldName = changelog.datasource.item.FieldName;
var record = changelog.datasource.item.UserSettings;
record[fieldName] = changelog.datasource.item.OldValue;
}
In theory method goes the connection between UserHistory and UserSettings up to the field and rewrites its value. But when I click on the button, I get a "Failed due to circular reference" error. What am I doing wrong?
I was able to repro the issue with this bit of code:
google.script.run.ServerFunction(app.currentPage.descendants.SomeWidget);
It is kinda expected behavior, because all App Maker objects are pretty much complex and Apps Script RPC has some limitations.
App Maker way to implement it would look like this:
// Server side script
function undoChangesToUserRecord(key) {
if (!isAdmin()) {
return;
}
var history = app.models.UserHistory.getRecord(key);
if (history !== null) {
var fieldName = history.FieldName;
var settings = history.UserSettings;
settings[fieldName] = history.OldValue;
}
}
// Client side script
function onUndoClick(button) {
var history = widget.datasource.item;
google.script.run
.undoChangesToUserRecord(history._key);
}

Is it possible to create multiple draft items on createdatasource?

I am building an application that will have the ability to create agenda items to discuss in a meeting. The agenda item might include one or more attachments to discuss so there is a one to many relation between the AgendaItems and the AgendaDocs models. So far, I have an insert form that looks like this:
The "Select File" button is a drive picker and the code I have inside the onDocumentSelect event is the following:
var docs = result.docs;
var createDataSource = app.datasources.AgendaDocs.modes.create;
for(var i=0; i<docs.length-1; i++){
var uniqueDraft = createDataSource.item;
createDataSource.items.push(uniqueDraft);
}
for(var i=0; i<createDataSource.items.length-1; i++){
var draft = createDataSource.item;
createDataSource.items[i].DocTitle = docs[i].name;
createDataSource.items[i].DocURL = docs[i].url;
createDataSource.items[i].DriveID = docs[i].id;
}
console.log(createDataSource.items);
The code is supposed to fill out the the List widget below the "Select File" button, but as how you see, the three items are the same. The datasource of the List widget is "AgendaDocs.modes.create" and the datasource of the insert form is "AgendaItems.modes.create".
Reading the official documentation from appmaker, makes me think it is possible since the properties of "CreateDataSource" includes "items". I need help from an expert here. Is this possible? Am I using the wrong approach?
First things first, it seems that you are trying to create records from different models and relationship between them in a one call... at this time App Maker is not that smart to digest such a complex meal. Most likely you'll need to break your flow into multiple steps:
Create (persist) Agenda Item
Create AgendaDocs records and relation with AgendaItem
Similar flow is implemented in Travel Approval template app, but it is not exactly the same as yours, since it doesn't create associations in batches.
Going back to the original question. Yep, it is possible to have multiple drafts, but not with the Create Datasource. You are looking for Manual Save Mode. Somewhere in perfect world your code would look similar to this:
// AgendaItems in Manual Save mode
var agendaDs = app.datasources.AgendaItems;
// this line will create item on client and automatically push it
// to ds.items and set ds.item to it.
agendaDs.createItem();
var agendaDraft = agendaDs.item;
// Field values can be populated from UI via bindings...
agendaDraft.Type = 'X';
agendaDraft.Description = 'Y';
// onDocumentSelect Drive Picker's event handler
var docsDs = agendaDs.relations.AgendaDocs;
result.docs.forEach(function(doc) {
// this line will create item on client and automatically push it
// to ds.items and set ds.item to it...however it will throw an exception
// with this message:
// Cannot save a foreign key association for the 'AgendaItem'
// relation because the target record has not been persisted
// to the server. To fix this, call saveChanges()
// on the data source for that record's model: AgendaItem
docsDs.createItem();
var docDraft = docsDs.item;
docDraft.DocTitle = doc.name;
docDraft.DocURL = doc.url;
docDraft.DriveID = doc.id;
});
// submit button click
agendaDraft.saveChanges();

Trying To Filter Only Rows That Meet Two Criteria

I promise I have read through the Query information page, but obviously I am missing/misunderstanding something.
I have a Table that has the statuses for multiple departments (the fields are Strings). When a user loads that table I want App Maker to hide jobs that have been finished.
The way we categorize a job as finishes is when:
The Inventory Status = Complete and when the The Delivery Status = Delivered.
Both these conditions need to be met.
Example:
Inventory (Complete) + Delivery (Delivered) = hide
Inventory (In Progress) + Delivery (Delivered) = don't hide
Inventory (Complete) + Delivery (Scheduled) = don't hide
I tried the following, however it hides all the example listed above, not just the first one.
var datasource = app.datasources.SystemOrders;
var inventory = ['Complete'];
var delivery = ['Delivered'];
datasource.query.filters.InventoryStatus._notIn = inventory;
datasource.query.filters.DeliveryStatus._notIn = delivery;
datasource.load();
I have also tried this:
var datasource = app.datasources.SystemOrders;
datasource.query.filters.InventoryStatus._notIn = 'Complete';
datasource.query.filters.DeliveryStatus._notIn = 'Delivered';
datasource.load();
But I get this error:
Type mismatch: Cannot set type String for property _notIn. Type List is expected. at SystemOrders.ToolBar.Button2.onClick:2:46
Any help would be greatly appreciated.
Filters are using AND operator. Please consider switching the Datasource Query Builder and applying the following query:
"InventoryStatus != :CompleteStatus OR DeliveryStatus != :DeliveredStatus"
Set CompleteStatus variable to Complete
Set DeliveredStatus variable to Delivered
Explanation:
Filter you want to apply is "NOT(InventoryStatus = Complete AND DeliveryStatus = Delivered)" which is equivalent to "InventoryStatus != Complete OR DeliveryStatus != Delivered".
Vasyl answer my question perfectly, but I wanted to add a few details in case anyone needs to do the same thing and aren't familiar with using the Datasource Query Builder.
All I did was click the Database I was using and then clicked the Datasources section at the top.
I clicked Add Datasource, named it a new name and pasted Vasyl's code into the Query Builder Expression box.
Two new boxes appear below it allowing me to enter the desired statuses that I wanted to filter out.
Lastly I went back to my Table and changed its datasource to my newly created datasource.
Since you are changing your datasource, if you have any extra code on there it may need to be updated to point to the new datasource.
Example:
I had some buttons that would filter entries for the various departments.
So this:
widget.datasource.query.clearFilters();
var datasource = app.datasources.SystemOrders;
var statuses = ['Complete'];
datasource.query.filters.WarehouseStatus._notIn = statuses;
datasource.load();
had to change to this:
widget.datasource.query.clearFilters();
var datasource = app.datasources.SystemOrders_HideComplete;
var statuses = ['Complete'];
datasource.query.filters.WarehouseStatus._notIn = statuses;
datasource.load();
You can use multiple run and then concatenate their results something like following
/**
* Retrieves records for ActionItems datasource.
* #param {RecordQuery} query - query object of the datasource;
* #return {Array<ActionItems>} user's rating as an array.
*/
function getActionItemsForUser_(query) {
var userRoles = app.getActiveUserRoles();
query.filters.Owner._contains = Session.getActiveUser().getEmail();
var ownerRecords = query.run();
query.clearFilters();
query.filters.AddedBy._contains = Session.getActiveUser().getEmail();
var addedByRecords = query.run();
return addedByRecords.concat(ownerRecords);
}

writing a library class properly

This is my very first library class that i am writing and i feel like i need to load up on that topic but cannot find the best sources. I have a web forms project that uploads a pdf and creates a qrcode for it and places it in the document. I need to create a library but don't know where to start or the exact structure. Every method it's own subclass in the library class? or can i have them all in one and what is a professional way of going about this.
This is party of my web forms application that i need to create a library for:
void UpdateStudentSubmissionGrid()
{
var usr = StudentListStep2.SelectedItem as User;
var lib = AssignmentListStep2.SelectedItem as Library;
if (usr == null || lib == null) return;
using (var dc = new DocMgmtDataContext())
{
var subs =
(from doc in dc.Documents
where doc.OwnedByUserID == usr.ID && doc.LibraryID == lib.ID
select new {DocID = doc.ID, Assignment = doc.Library.Name, Submitted = doc.UploadDT})
.OrderByDescending(c => c.Submitted)
.ToList();
StudentSubmissionGrid.DataSource = subs;
}
}
How do i start with this method?
By the looks of things you are using this function for a single webpage. You can call that function from any event i.e. user hits submit button. On the. Click the button and it will create a onclick event. Call the code from inside there UpdateStudentSubmissionGrid(); Just make sure the function is not nested inside another event or function. Webforms is already a class, you are just placing a function within the class.

Resources