I am using dexie.js to interface with IndexedDB. I am wondering if it is possible to orderby or sortby using more than one index at once (eg. db.people.orderBy( index1, desc : index2, asc )...
If it is possible, what is the correct syntax?
Either use compound indexes, or use Collection.and().
If you can live with only targeting Chrome, Firefox or Opera, you can use compound indexes. If it must work on Safari, IndexedDBShim, Edge or IE, you cannot use compound indexes today. There's a shim that enables it for IE/Edge though, but it is still in beta, so I would recommend to instead use Collection.and() for those cases.
Let' say you have a form where users can fill in various attributes of friends:
<form>
<input name="name"/>
<input name="age"/>
<input name="shoeSize" />
</form>
Using Collection.and()
First, pick the most probably index to start your search on. In this case, "name" would be a perfect index that wouldn't match so many items, while age or shoeSize would probably match more friends.
Schema:
db.version(X).stores({
friends: "id, name, age, shoeSize"
});
Query:
function prepareQuery () {
// Pick a good index. The picked index will filter out with IndexedDB's built-in keyrange
var query;
if (form.name.value) {
query = db.friends.where('name').equals(form.name.value);
} else if (form.age.value) {
query = db.friends.where('age').equals(parseInt(form.age.value));
} else if (form.shoeSize.value) {
query = db.friends.where('shoeSize').equals(parseInt(form.shoeSize.value));
} else {
query = db.friends.toCollection();
}
// Then manually filter the result. May filter a field that the DB has already filtered out,
// but the time that takes is negligible.
return query.and (function (friend) {
return (
(!form.name.value || friend.name === form.name.value) &&
(!form.age.value || friend.age == form.age.value) &&
(!form.shoeSize.value || friend.shoeSize == form.shoeSize.value));
});
}
// Run the query:
form.onsubmit = function () {
prepareQuery() // Returns a Collection
.limit(25) // Optionally add a limit onto the Collection
.toArray(function (result) { // Execute query
alert (JSON.stringify(result, null, 4));
})
.catch (function (e) {
alert ("Oops: " + e);
});
}
Using compound indexes
As written above, compound indexes code will only work on mozilla- and chromium based browsers.
db.version(x).stores({
friends: "id, name, age, shoeSize," +
"[name+age+shoeSize]," +
"[name+shoeSize]," +
"[name+age]," +
"[age+shoeSize]"
});
The prepareQuery() function when using compound indexes:
function prepareQuery() {
var indexes = []; // Array of Array[index, key]
if (form.name.value)
indexes.push(["name", form.name.value]);
if (form.age.value)
indexes.push(["age", parseInt(form.age.value)]);
if (form.shoeSize.value)
indexes.push(["shoeSize", parseInt(form.shoeSize.value)]);
var index = indexes.map(x => x[0]).join('+'),
keys = indexes.map(x => x[1]);
if (indexes.length === 0) {
// No field filled in. Return unfiltered Collection
return db.friends.toCollection();
} else if (indexes.length === 1) {
// Single field filled in. Use simple index:
return db.friends.where(index).equals(keys[0]);
} else {
// Multiple fields filled in. Use compound index:
return db.friends.where("[" + index + "]").equals(keys);
}
}
// Run the query:
form.onsubmit = function () {
prepareQuery() // Returns a Collection
.limit(25) // Optionally add a limit onto the Collection
.toArray(function (result) { // Execute query
alert (JSON.stringify(result, null, 4));
})
.catch (function (e) {
alert ("Oops: " + e);
});
}
Using arrow functions here to make it more readable. Also, you're targeting chromium or firefox and they support it already.
Related
I want to create a table that represents the data from a model that i have loaded in the forge viewer.
i want the table to be in a docking panel, and show the propertys for elements in the model ( level, name, comment,Area, type name--> for each element [if it has the property])
i have tried to use the API reference, and create a DataTable, but i did not find examples of how to actually impliment it.
where and when do i need to set the datatable? ( after or before creating the docking pannel?)
what is the content of the arrays that i should pass in the constructor? ( according to the documentation : array of arrays for the rows, and array for the columns. is the row array, is simply an array that contains the columns arrays?)
this is my current code for the extension that shows the amount to instances in the model for each property that i want to adjust:
'''
class testTest extends Autodesk.Viewing.Extension {
constructor(viewer, options) {
super(viewer, options);
this._group = null;
this._button = null;
}
load() {
console.log('testTest has been loaded');
return true;
}
unload() {
// Clean our UI elements if we added any
if (this._group) {
this._group.removeControl(this._button);
if (this._group.getNumberOfControls() === 0) {
this.viewer.toolbar.removeControl(this._group);
}
}
console.log('testTest has been unloaded');
return true;
}
// The Viewer contains all elements on the model, including categories (e.g. families or part definition),
//so we need to enumerate the leaf nodes, meaning actual instances on the model.
getAllLeafComponents(callback) {
this.viewer.getObjectTree(function (tree) {
let leaves = [];// an empty 'leaf' list that we want to fill wiith the objects that has no mo children
//dbId== object id
// for each child that we enumerate from a root, call a code , and finally a true /false flag parameter that run the function recursivly for all the children of a child.
tree.enumNodeChildren(tree.getRootId(), function (dbId) {
if (tree.getChildCount(dbId) === 0) {
leaves.push(dbId);// if the object has no children--> add it to the list.
}
}, true);// the last argument we past ("true") will make sure that the function in the seccond argument ("function (dbId)(...)" ")will run recursively not only for the children of the roots,
//but for all the children and childrtn's children.
callback(leaves);//return the leaves
});
}
onToolbarCreated() {
// Create a new toolbar group if it doesn't exist
this._group = this.viewer.toolbar.getControl('allMyAwesomeExtensionsToolbar');//if there is no controller named "allMyAwesomeExtensionsToolbar" create one
if (!this._group) {
this._group = new Autodesk.Viewing.UI.ControlGroup('allMyAwesomeExtensionsToolbar');
this.viewer.toolbar.addControl(this._group);// add the control to tool bar
}
// Add a new button to the toolbar group
this._button = new Autodesk.Viewing.UI.Button('testTest');
this._button.onClick = (ev) => {
// Check if the panel is created or not
if (this._panel == null) {//check if there is an instance of our pannel. if not- create one
this._panel = new ModelSummaryPanel(this.viewer, this.viewer.container, 'modelSummaryPanel', 'Model Summary');
}
// Show/hide docking panel
this._panel.setVisible(!this._panel.isVisible());//cal a method from the parent to show/ hide the panel -->use this to toggle from visible to invisible
this._panel.set
// If panel is NOT visible, exit the function
if (!this._panel.isVisible())
return;
// First, the viewer contains all elements on the model, including
// categories (e.g. families or part definition), so we need to enumerate
// the leaf nodes, meaning actual instances of the model. The following
// getAllLeafComponents function is defined at the bottom
this.getAllLeafComponents((dbIds) => {// now we have the list of the Id's of all the leaves
// Now for leaf components, let's get some properties and count occurrences of each value
debugger;
const filteredProps = ['Level','Name','Comments','Area','Type Name'];
// Get only the properties we need for the leaf dbIds
this.viewer.model.getBulkProperties(dbIds,filteredProps , (items) => {
// Iterate through the elements we found
items.forEach((item) => {
// and iterate through each property
item.properties.forEach(function (prop) {
// Use the filteredProps to store the count as a subarray
if (filteredProps[prop.displayName] === undefined)
filteredProps[prop.displayName] = {};
// Start counting: if first time finding it, set as 1, else +1
if (filteredProps[prop.displayName][prop.displayValue] === undefined)
filteredProps[prop.displayName][prop.displayValue] = 1;
else
filteredProps[prop.displayName][prop.displayValue] += 1;
});
});
// Now ready to show!
// The PropertyPanel has the .addProperty that receives the name, value
// and category, that simple! So just iterate through the list and add them
filteredProps.forEach((prop) => {
if (filteredProps[prop] === undefined) return;
Object.keys(filteredProps[prop]).forEach((val) => {
this._panel.addProperty(val, filteredProps[prop][val], prop);
this.dt = new DataTabe(this._panel);
this.dt.setData()
});
});
});
});
};
this._button.setToolTip('Or Levis extenssion');
this._button.addClass('testTest');
this._group.addControl(this._button);
}
}
Autodesk.Viewing.theExtensionManager.registerExtension('testTest', testTest);'''
'''
We have a tutorial with step-by-step on how to do it.
Please, refer to Dashboard tutorial, specifically in Data Grid section.
In this case, the tutorial uses an external library (Tabulator) to show the data.
I want to be able to overwrite one of the fields in ListItem when a ListItem of the same Id comes in from my middleware. The issue is that if I create a custom merge function for any field it makes my pagination stop working. The new data is not properly merged in the list of results.
When I console.log in the resultPagination merge-function it seems to merge properly. But, in my interface where I am calling resultPagination the state is stuck on loading. So, in other words, I can fetch the first page of results, but when I call fetchMore I get stuck on loading.
If I remove the custom merge function in ListItem the pagination starts working properly again. If I don't define a merge for any specific fields in ListItem, but just define merge as true the pagination works properly, and the ListItem is updated, but the values in the ListItem are completely overwritten ( I just want to change the value of one field ).
How do I define a custom Merge function for ListItem while still allowing Pagination to work in the result Pagination?
Below I give an example of how my structure is looking right now:
export const typePolicies: TypePolicies = {
ListItem: {
fields: {
tags: {
merge: (existing: Tag[] = [], incoming: Tag[]) => {
return !!incoming ? incoming : existing;
}
}
}
},
Query: {
fields: {
resultPagination: {
keyArgs: ["query", "sortByDate", "specificFields"],
merge: (existing, incoming, args) => {
if (!incoming) return existing;
if (args.args.offset === 0) return incoming;
const existingListItemLists: ListItem[][] =
existing?.articleSummaries ?? [];
const incomingListItemLists: ListItem[][] =
incoming?.articleSummaries ?? [];
return {
...incoming,
results: [...existingListItemLists, ...incomingListItemLists]
};
}
}
}
}
};
I have attempted to extend this fiddle
https://jsfiddle.net/rn3ja9n4/
here
https://jsfiddle.net/thyfoubq/32/
but it is unclear what I should use to power the dc.selectmenu or if the it is even capable of working in the multi-key scenario.
Is this possible?
I have tried several variations of this but i can't seem to get the filter to work with the composite key or the single branch key
var storedim =ndx.dimension(function(d){ return [d.store]})
var storegroup =storedim.group()
storeselect
.dimension(storedim)
.group(storegroup)
.multiple(true)
.size(5)
.controlsUseVisibility(true)
.title(kv => kv.key);
----------------------- possible solution that appears to work---------
---change the group to remove empty bins
var suppliersSumGroup = remove_empty_bins(suppliersSumDistinct.group().reduceSum( d => d.earnings ));
function remove_empty_bins(source_group) {
return {
all:function () {
return source_group.all().filter(function(d) {
return d.value !=0; //appears to work borrowed from another SO post
//unsure which one but it seems logical and works for this example
});
}
};
}
In my user registration workflow, I ask the user to select a pre-defined list of classifications (which I use a series of checkboxes for). Currently, I update my model with those values (or a blank value if the box isn't checked) when they move to the next step. These are not currently structured as an array, rather as a separate and distinct checkbox.
My 2 questions are as follows:
1) I read that if I save the checkboxes as an array I won't be able to easily search the database for users with a particular classification.
2) If that's true, I'm fine with my current structure, but I need to properly validate (server-side) that at least one of the checkboxes is selected otherwise provide an error. I have tried the following, but it doesn't return anything and the database record is created with nothing in each column.
if ($request->all() === "")
{
return Request::json('you must select one');
}
Database Table Migration:
Schema::create('user_type', function (Blueprint $table) {
$table->increments('id');
$table->integer('user_id');
$table->string('games');
$table->string('art');
$table->string('music');
$table->string('building_stuff');
$table->string('educational');
$table->timestamps();
});
Javascript: (I submit as AJAX)
$('.modal-type-btn').click(function(e){
e.preventDefault();
if($('.checkbox-games').is(':checked'))
{
var games = "games";
} else {
var games = "";
}
if($('.checkbox-art').is(':checked'))
{
var art = "art";
} else {
var art = "";
}
if($('.checkbox-music').is(':checked'))
{
var music = music;
} else {
var music = "";
}
if($('.checkbox-building-stuff').is(':checked'))
{
var buildingStuff = "buildingstuff";
} else {
var buildingStuff = "";
}
if($('.checkbox-educational').is(':checked'))
{
var educational = "educational";
} else {
var educational = "";
}
$.ajax({
type: "POST",
url: "/profile/setup/1",
data: {games:games, art:art, music:music, buildingStuff:buildingStuff, educational:educational},
error: function(data){
console.log(data);
},
success: function(data){
console.log(data);
}
});
Thanks!
So, firstly you're much better of using them as an array and doing something like:
html:
<input type="checkbox" name="options[games]" value="1" />
that way, you can just get the form data and pass that into your $.ajax call:
...
data: $('form').serialize(),
....
and then in your controller, checking that the length of options is at least one:
$options = Input::get('options');
if(count($options) > 0){ ... }
You'll also probably want to define a list of "allowed" options and then use that to build your query:
private $_allowed_options = array('education', 'buildingStuff' ...);
public function update()
{
$options = Input::get('options');
foreach($this->_allowed_options as $allowed_options){
if(array_key_exists($allowed_option, $option)){
// add this option to our sql to add it to our user
// as they have selected it
}else{
// the user didn't select this one, so add it to be removed
// from the users
}
}
}
I'm displaying a table with multiple rows and columns. I'm using a JQUERY plugin called uiTableFilter which uses a text field input and filters (shows/hides) the table rows based on the input you provide. All you do is specify a column you want to filter on, and it will display only rows that have the text field input in that column. Simple and works fine.
I want to add a SECOND text input field that will help me narrow the results down even further. So, for instance if I had a PETS table and one column was petType and one was petColor -- I could type in CAT into the first text field, to show ALL cats, and then in the 2nd text field, I could type black, and the resulting table would display only rows where BLACK CATS were found. Basically, a subset.
Here is the JQUERY I'm using:
$("#typeFilter").live('keyup', function() {
if ($(this).val().length > 2 || $(this).val().length == 0)
{
var newTable = $('#pets');
$.uiTableFilter( theTable, this.value, "petType" );
}
}) // end typefilter
$("#colorFilter").live('keyup', function() {
if ($(this).val().length > 2 || $(this).val().length == 0)
{
var newTable = $('#pets');
$.uiTableFilter( newTable, this.value, "petColor" );
}
}) // end colorfilter
Problem is, I can use one filter, and it will display the correct subset of table rows, but when I provide input for the other filter, it doesn't seem to recognize the visible table rows that are remaining from the previous column, but instead it appears that it does an entirely new filtering of the original table. If 10 rows are returned after applying one filter, the 2nd filter should only apply to THOSE 10 rows. I've tried LIVE and BIND, but not working.
Can anyone shed some light on where I'm going wrong? Thanks!
The uiTableFilter plugin doesn't support what you're trying to do. A quick look at the source reveals this:
elems.each(function(){
var elem = jQuery(this);
jQuery.uiTableFilter.has_words(getText(elem), words, false)
? matches(elem)
: noMatch(elem);
});
and that expands to (essentially) this:
elems.each(function(){
var elem = jQuery(this);
jQuery.uiTableFilter.has_words(getText(elem), words, false)
? elem.show()
: elem.hide();
});
So all it does is spin through all the rows, .show() those that match, and .hide() those that don't; uiTableSorter doesn't pay attention to the current shown/hidden state of the rows and there's no way to tell it to filter on multiple columns.
If you really need your desired functionality then you can modify the plugin's behavior (the code is pretty small and simple) or just write your own. Here's a stripped down and simplified version that supports multiple filters and is a more conventional jQuery plugin than uiTableFilter:
(function($) {
$.fn.multiFilter = function(filters) {
var $table = $(this);
return $table.find('tbody > tr').each(function() {
var tr = $(this);
// Make it an array to avoid special cases later.
if(!$.isArray(filters))
filters = [ filters ];
howMany = 0;
for(i = 0, f = filters[0]; i < filters.length; f = filters[++i]) {
var index = 0;
$table.find('thead > tr > th').each(function(i) {
if($(this).text() == f.column) {
index = i;
return false;
}
});
var text = tr.find('td:eq(' + index + ')').text();
if(text.toLowerCase().indexOf(f.word.toLowerCase()) != -1)
++howMany;
}
if(howMany == filters.length)
tr.show();
else
tr.hide();
});
};
})(jQuery);
I'll leave error handling and performance as an exercise for the reader, this is just an illustrative example and I wouldn't want to get in the way of your learning. You could wire it up something like this:
$('#type').keyup(function() {
$('#leeLooDallas').multiFilter({ column: 'petType', word: this.value });
});
$('#color').keyup(function() {
$('#leeLooDallas').multiFilter([
{ column: 'petType', word: $('#type').val() },
{ column: 'petColor', word: this.value }
]);
});
And here's a live example (which assumes that you're going to enter something in "type" before "color"): http://jsfiddle.net/ambiguous/hdFDt/1/