According to the breeze api, when setDeleted is called, it will remove the entity from all the related entities. This is true, but the behavior is flawed when importing a deleted item using the entity manager's importEntities function. The deleted entity will have it's navigational properties emptied, but other entities that have navigational properties containing the deleted item will keep that item in their nav lists. If setDeleted is called on the entity, even though it is already deleted, it will fix the problem, but this should not be necessary.
I've created this test case for the DocCode tests.
/*********************************************************
* Create an EM with parent/child relationship data. Export the EM and import it into a new one, delete the child item in the exported EM
* export the 2nd EM into the first EM.
*********************************************************/
test("test imported deleted nav properties", 2, function () {
var em = newEm();
// create a new parent Customer
var parentCustomer = em.createEntity("Customer", {
CustomerID: dummyCustID,
CompanyName: 'TestCo'
});
// a new Order which is a child of the parent Customer
var newOrder = em.createEntity("Order", {
CustomerID: parentCustomer.CustomerID()
});
parentCustomer.entityAspect.setUnchanged();
newOrder.entityAspect.setUnchanged();
// clone the EM data
var expEntities = em.exportEntities(null, true);
//var newEm = newEm();
var newEM = new breeze.EntityManager();
newEM.importEntities(expEntities, { mergeStrategy: breeze.MergeStrategy.OverwriteChanges });
// delete the order
var newOrderCopy = newEM.getEntities("Order")[0];
newOrderCopy.entityAspect.setDeleted();
// export the cloned EM
var expEntitiesNew = newEM.exportEntities();
// merge to the original EM
em.importEntities(expEntitiesNew, { mergeStrategy: breeze.MergeStrategy.OverwriteChanges });
var deletedOrder = parentCustomer.Orders();
ok(newOrder.entityAspect.entityState.isDeleted(), "newOrder should be 'deleted'");
ok(deletedOrder.length === 0, "parentCustomer's 'Orders' should be empty");
});
The Breeze docu describes the concept of Sandbox Editing (http://www.getbreezenow.com/documentation/multiple-managers). I wanted to apply this concept on a modal dialog. Only when the user clicks the ok button her changes should be accepted in the main window. When she clicks the cancel button her changes should be dropped.
These are the steps:
Create a new EntityManager (Sandbox)
Fill the Sandbox EntityManager with entities that should be displayed or changed in the dialog.
Open the dialog and hand over the Sandbox EntityManager.
When the user clicks the ok button export all changed, added and deleted entities from the Sandbox EntityManager and import them into the main or parent EntityManager.
When the user clicks the cancel button leave the Sandbox EntityManager alone and let the garbage collector do the work.
While transfering back the deleted entities into the main or parent EntityManager I ran into the exact same problem which is described here.
var deletedEntities = sandboxEm.getEntities(breeze.EntityState.Deleted);
var deletedEntitiesExport = sandboxEm.exportEntities(deletedEntities, false);
mainEm.importEntities(deletedEntitiesExport).entities;
The imported entities are marked as deleted (EntityState is Deleted) but they are not removed for the navigation properties of related entities.
The workaround recommended by tsdude did not work for me:
var deletedEntities = sandboxEm.getEntities(breeze.EntityState.Deleted);
var deletedEntitiesExport = sandboxEm.exportEntities(deletedEntities, false);
var deleted = mainEm.importEntities(deletedEntitiesExport).entities;
// without effect :-(
deleted.forEach(function (e) {
e.entityAspect.setDeleted();
});
I was able to do it without importing the deleted entities. I iterate over the deleted entities and mark there counterparts in the main EntityManger explicitly as deleted:
sandboxEm.getEntities(breeze.EntityState.Deleted).forEach(function (deleted) {
var key = deleted.entityAspect.getKey();
var e = mainEm.getEntityByKey(key);
e.entityAspect.setDeleted();
});
Note: The Breeze version is 1.5.2.
Sorry for taking so long, this one slipped through.
It was a bug and is now fixed in the GitHub repo. It will also go out in the 1.5.3 release. ... and thanks for the repro.
Related
I'm working on a project where I should save data locally with Core Data.
Here is my workflow :
To start, I ask the user to fill a form, let's say his firstname and lastname.
He clicks on the submit button, then data is saved on the device using Core Data
User is redirected to the "last filled form" view controller.
I have a bar button item that when clicked can show the latest filled form.
I should test if the array of filled forms is empty, then the button should be disabled.
Otherwise, the button should be enabled ...
I tried this piece of code, where I fetch data from the database and affected to an array but the button seams not working at all and it never gets disabled ...
class ViewController: UIViewController {
var userIdentity: UserIDentity = UserIDentity(context: PersistanceService.context)
var identityArray = [UserIDentity]()
override func viewDidLoad() {
super.viewDidLoad()
self.fetchIdentityHistoryArray()
}
func fetchIdentityHistoryArray(){
let fetchRequest: NSFetchRequest<UserIDentity> = UserIDentity.fetchRequest()
do {
let identityArray = try PersistanceService.context.fetch(fetchRequest)
if identityArray.isEmpty {
self.identityHistoryButton.isEnabled = false
}
else {
self.identityHistoryButton.isEnabled = true
}
}
catch {
print("Error fetching sworn statement history !")
}
}
}
So I have 2 questions :
What's wrong with my code ?
How can I manage that when the user clicks on the "back button" for the first form filled ever, the "history button" can refresh itself and turn from disabled to enabled button ?
Any help is much appreciated. Thank you
You are initialising a core data object directly in your code
var userIdentity: UserIDentity = UserIDentity(context: PersistanceService.context)
This new object will exist in Core Data and will be included everytime you execute the fetch request. You must understand that Core Data is not a database layer it is an object mapping layer so even if you haven't called save() yet the object exists in the Core Data context.
Either change the declaration to
var userIdentity: UserIDentity?
or remove it completely if it isn't used.
this.grid = new Grid<>(Person.class);
this.grid.setItems(personList);
this.grid.setSelectionMode(SelectionMode.MULTI);
this.grid.removeAllColumns();
this.grid.setColumns("firstname");
this.editButton = new Button(null, ImageIcons.EDIT.create());
this.editButton.getStyle().set("color", "#000000");
this.grid.addComponentColumn(person -> this.editButton);
this.deleteButton = new Button(null, IronIcons.DELETE_FOREVER.create());
this.deleteButton.getStyle().set("color", "#000000");
this.grid.addComponentColumn(person -> this.deleteButton);
this.addComponentAsFirst(this.grid);
I have a personList with several entries. The grid shows all these entries with their first name. But it only shows the buttons in the last row. What is the problem?
You use the very same Button instance for each row. You should create a new Button within the componentRenderer, so each row will have its own Button.
Try it like this:
this.grid = new Grid<>(Person.class, false);
this.grid.setItems(personList);
this.grid.setSelectionMode(SelectionMode.MULTI);
this.grid.setColumns("firstname");
this.grid.addComponentColumn(person -> {
// edit: added click listener for inline-editing of the person. Editor must be configured for this to work. See https://vaadin.com/components/vaadin-grid/java-examples/grid-editor
// You don't have to use inline-editing if you don't want. you can also edit the item in a separate Layout with Input fields and a Binder.
Button editButton = new Button(ImageIcons.EDIT.create(), click -> {
this.grid.getEditor().editItem(person);
});
editButton.getStyle().set("color", "#000000");
return editButton;
});
this.grid.addComponentColumn(person -> {
// edit: added click listener for person removal
Button deleteButton = new Button(null, IronIcons.DELETE_FOREVER.create(), click -> {
this.personDao.remove(person);
// TODO: when using an in-memory dataprovider, fetch all items again from service/dao and set them with grid.setItems(this.personDao.findAll());
// but that is not necessary when using a callback dataprovider, which I belive OP is using
this.grid.getDataProvider().refresh();
});
deleteButton.getStyle().set("color", "#000000");
return deleteButton;
}
this.addComponentAsFirst(this.grid);
Edit: a minor thing but I still wanted to mention it - You do some unnecessary creation of columns, only to remove them all again later. Instead of this you could tell the grid not to create these columns in the first place, by passing false as second parameter of the Grid constructor.
this.grid = new Grid(Person.class, false);
// instead of
this.grid = new Grid(Person.class);
this.grid.removeAllColumns();
I'm trying to have a button to allow users to upload a file, into a specific folder. I tried to follow other advise, and add this hook to
onPickerInit:
var uploadView = new google.picker.DocsUploadView()
uploadView.setParent('XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'); // test folder
pickerBuilder.addView(uploadView);
I've set the MULTISELECT_ENABLED feature (without it, the destination folder is not respected), and I can in fact now upload the files where they belong. Yay!
HOWEVER: The picker widget now has two upload tabs. The first one just does the regular upload into the drive main folder, the second tab does the right thing. My guess is that appmaker contstructs the first upload tab behind the curtains and there is no feature to disable this.
This is obviously fairly quirky and hardly usable. My questions are:
1) are there (possibly undocumented) API calls in the pickerbuilder to remove the original upload view?
2) Is it possible to respect the destination folder even is the MULTISELECT feature is off ?
Many thanks in advance for any pragmatic solutions!
EDIT 07/28/2020
Due to the constant changes in drive picker, this code aims for a more permanent solution:
var folderId = "10fYS3l32R6gk79POOSS8X_Vbsz7vqzRWX"; //the desired folder id
var prps = [];
for(var prop in pickerBuilder){
var value = pickerBuilder[prop];
if(!!value){
if(typeof(value)==="object"){
var proto = value.__proto__; //jshint ignore: line
if(!!proto["addLabel"] && !!proto["addView"]){
prps.push(prop);
for(var key in value){
var target = value[key];
var type = Object.prototype.toString.call(target);
if(type === "[object Array]"){
prps.push(key);
for(var key in target[0]){
var value = target[0][key];
if(typeof(value)==="object"){
prps.push(key);
}
}
for(var key in target[0]){
var value = target[0][key];
if(typeof(value)==="string") {
prps.push(key);
}
}
}
}
}
}
}
}
var views = pickerBuilder[prps[0]][prps[1]];
for(var i=0; i<views.length; i++){
var view = views[i];
if(view[prps[3]] === "upload"){
view[prps[2]].parent = folderId;
}
}
EDIT 06/29/2020
There has been another change in the Drive picker API. To make this work please change what you have to:
var folderId = "10fYS3l32R6gk79POOSS8X_Vbsz7vqzRWX"; //the desired folder id
pickerBuilder.rw.kf["0"].Ta.parent = folderId;
EDIT 05/26/2020
There has been another change in the Drive picker API. To make this work please change what you have to:
var folderId = "10fYS3l32R6gk79POOSS8X_Vbsz7vqzRWX"; //the desired folder id
pickerBuilder.xw.jf["0"].Ta.parent = folderId;
EDIT 02/17/2020
There has been a change in the Drive picker API. To make this work please change what you have to:
var folderId = "10fYS3l32R6gk79POOSS8X_Vbsz7vqzRWX"; //the desired folder id
pickerBuilder.mw.$e["0"].Ra.parent = folderId;
To answer your questions directly:
1.) YES
2.) YES
Now, let's dig a little bit into what's happening under the hood. You are right:
My guess is that appmaker contstructs the first upload tab behind the curtains and there is no feature to disable this.
However, we can manipulate the object. So instead of creating a new picker view, let's simply configure the default one to upload the files to the folder you want. We can achieve that by doing the following:
1.) After you insert a Drive Picker into your UI, make sure the Drive Picker Properties are all empty:
2.) Next, go to the event handlers and click on the onPickerInit event handler. Type in this code:
var folderId = "10fYS3l32R6gk79POOSS8X_Vbsz7vqzRWX"; //the desired folder id
pickerBuilder.SW.Vq["0"].mc.parent = folderId;
In summary, I've come to the conclusion that the property SW contains the array of drive views, which are saved under the property Vq. Vq["0"] is the first view in the array of views and the mc property contains the features; hence parent = folderId.
How would you resolve the following memory leak?
I am trouble shooting memory leaks in an iOS app Ti SDK 6.2 without much success.
When opening and closing windows the xcode Instruments Allocations tool shows a number of TiUIxxxxProxy objects remain in memory.
To test/debug the issue I created a super simple classic appcelerator app (code shown below). Using the code below I open window1 from app.js, then open window2 from a button on window1.
You can see in the attached xcode Instruments Allocations images that after window2 is closed the proxy objects remain (window, table, etc). Worse yet, opening and closing window2 multiple times keeps adding additional proxy objects that use memory.
App.js
require('Window1').CreateWindow().open();
Window1.js
exports.CreateWindow = function(){
try{
var window1 = Ti.UI.createWindow({
title:'Window 1',backgroundColor:'yellow',
});
var button1 = Ti.UI.createButton({
top:'50dp', center:'50%',
borderWidth:'1dp',borderColor:'black',
title:' Open Window 2 '
});
window1.add(button1);
button1.addEventListener('click', function() {
var win2 = require('Window2').CreateWindow();
win2.open();
});
return window1;
}
catch(e){
alert('Window 1 Error: ' + e);
}
};
Window2.js
exports.CreateWindow = function(){
try{
var window2 = Ti.UI.createWindow({
title:'Window 2',layout:'vertical'
,top:'200dp',bottom:'200dp',width:'80%'
,backgroundColor:'orange'
});
var button2 = Ti.UI.createButton({
top:'50dp', center:'50%',
borderWidth:'1dp',borderColor:'black',
title:' Close Window 2 '
});
window2.add(button2);
button2.addEventListener('click', function() {
window2.close();
});
// create a table row
var tvRow1 = Ti.UI.createTableViewRow({ });
//create a label to display location name
var labelRow1 = Ti.UI.createLabel({
color:'#000000',
left:'15dp',
top:'10dp',
text:'Label in row 1'
});
tvRow1.add(labelRow1);
// define table section for this group of rows
var tableViewSection1 = Ti.UI.createTableViewSection({ });
// push row into table view section
tableViewSection1.add(tvRow1);
//create array to store table sections
var arrayTableSections = [];
//add table section to array
arrayTableSections.push(tableViewSection1);
// create table
var table2 = Titanium.UI.createTableView({
data: arrayTableSections,
top:'50dp',
left:'50dp',
right:'50dp',
height:Ti.UI.SIZE,
backgroundColor:'#ffffff' ,
borderColor:'black',borderWidth:'1dp'
});
// add table to window
window2.add(table2);
return window2;
}
catch(e){
alert('Window 2 Error: ' + e);
}
};
The specific problem you're having is that you're manually creating elements and not cleaning up. So, on the close of Window2 you should be removing all elements from their parents, and ultimately from Window2 so:
remove the label from the row
remove the row from the section
remove the section from the table
remove the table from the window
remove the button from the window
then
nullify the label, the row, the section, the table and the button
and most importantly:
clear the pointer to the array of sections / rows.
Once done, ensure you have a removeEvenlistener in close to remove the event handler too.
Finally, close the window and nullify it.
One last thing, don't create a window2 pointer in window1 if you're not going to need it later so:
require("window2").createWindow().open();
is better than:
var window2 = require("window2").createWindow();
window2.open();
Doing that I was able to open window2 8+ times, and it all cleaned up afterwards.
I have a page, which is used for building queries and running them against different entities (Kind of a query builder/generic search).
The results are displayed in JQGrid, so effectively the same grid will be used for rendering results from different entities.
This results grid has to support context menus, which will differ for each entity. So I need a way to change the context menu as per the entity. Each entity may have different number of menu items in context menu and each item may respond in a different manner (sometimes an alert, sometimes an action spawning in a different tab).
Rendering different menus (through li) is not an issue but attaching the methods to the li is proving to be a challenge. Any pointers will be highly appreciated.
I am using jquery.contextmenu-ui.js .
Following is from a sample that I picked from their (JQGrid) site
function initGrid() {
$("#EntityGrid").contextMenu('cMenu'
,{
bindings: { /* I would like to avoid this and pass all the actions to one method*/
'edit': function (t) {
editRow();
},
'add': function (t) {
addRow();
},
'del': function (t) {
delRow();
}
},
onContextMenu: function (event, menu) {
var rowId = $(event.target).parent("tr").attr("id")
var grid = $("#EntityGrid");
grid.setSelection(rowId);
return true;
}
}
);
}
Thanks,
Avinash
You can use onShowMenu callback of contextMenu instead of static binding using bindings. In the same way the menuId used as the first parameter of contextMenu could be the id of dynamically created div with empty <ul>. The onShowMenu has the form
onShowMenu: function (e, $menu) {
// here one can clear `<ul>` child of $menu
// and append it with "<li>" items
return $menu;
}
In the answer you will find an example of the code which build menu dynamically.