Openui5: wrong sort order with sap.ui.model.Sorter - sapui5

It appears sap.ui.model.Sorter doesn't sort ISO-8859-1 characters correctly.
Below is an example where we create a list with one item pr character in the Norwegian alfabet. The output of this is not in the correct order, instead the order is "AÅÆBCDEFGHIJKLMNOØPQRSTUVWXYZ".
The expected results is the same order as when the alfabet variable is declared: "ABCDEFGHIJKLMOPQRSTUVWXYZÆØÅ"
How can we sort the model correctly?
JSBIN: https://jsbin.com/xuyafu/
var alfabet = "ABCDEFGHIJKLMOPQRSTUVWXYZÆØÅ"
var data = [];
for(var i=0; i< alfabet.length; i++){
data.push ({value:alfabet.charAt(i)});
}
var modelList = new sap.ui.model.json.JSONModel(data);
sap.ui.getCore().setModel(modelList);
var oSorter = new sap.ui.model.Sorter("value", null, null);
// Simple List in a Page
new sap.m.App({
pages: [
new sap.m.Page({
title: "Sorting with norwegian characters",
content: [
new sap.m.List("list", {
items: {
path: '/',
template: new sap.m.StandardListItem({
title: '{value}'
}),
sorter: oSorter
}
})
]
})
]
}).placeAt("content");

Based on the input from the comments on the question, it is straight forward to override the sorting function fnCompare to get the right order
var oSorter = new sap.ui.model.Sorter("value", null, null);
oSorter.fnCompare = function (a, b) {
if (a == b) {
return 0;
}
if (b == null) {
return -1;
}
if (a == null) {
return 1;
}
if (typeof a == "string" && typeof b == "string") {
return a.localeCompare(b, "nb");
}
if (a < b) {
return -1;
}
if (a > b) {
return 1;
}
return 0;
}
Here "nb" is the locale the sort is done with

Related

Updating data doesnt expand the data tree inside material-table

Im trying to build a table with nested tree folder inside.
When trying to add nested data into the datasource data the structure will not updated and will not toggle anymore.
Code below:
https://stackblitz.com/edit/angular-table-tree-example-k2zqmt?file=app%2Ftable-basic-example.ts&file=app%2Ftable-basic-example.html,app%2Ftable-basic-example.ts
Environment
Angular:
Material Table
Material tree system
These are the things that are happening when logNode method is called
The item is getting added but the treeControl.toggle method does not work anymore.
When you are assigning a new dataset to the dataSource all the nodes get reset and the tree closes, so this.treeControl.toggle is trying to toggle a node that does not exist.
You need to find the node to be toggled from the list you get from treeControl.dataNodes
I would suggest having the toggle code in a separate method and adding a node code in a separate method, and a separate button to add the node.
The below code should work for your scenario, also remove this line from your HTML, (click)="treeControl.toggle(data)"
interface ExampleFlatNode {
expandable: boolean;
RoleName: string;
Access: boolean;
level: number;
CatId: number;
}
private transformer = (node: FoodNode, level: number) => {
return {
expandable:
!!node.CategoryPermissions && node.CategoryPermissions.length > 0,
RoleName: node.RoleName,
Access: node.Access,
level: level,
CatId: node.CatId,
};
};
tempNodes = []
constructor() {
this.dataSource.data = TREE_DATA;
}
logNode(clickedNode) {
this.tempNodes = [];
this.treeControl.dataNodes.forEach((node) =>
this.tempNodes.push({
...node,
expanded: this.treeControl.isExpanded(node),
})
);
if (!this.treeControl.isExpanded(clickedNode)) {
const temp = {
Access: true,
RoleName: 'test 1 2',
CatId: 113,
};
const clickedNodeIdx = this.treeControl.dataNodes.findIndex(
(node: any) =>
node.CatId === clickedNode.CatId &&
node.RoleName === clickedNode.RoleName &&
node.level === clickedNode.level
);
const childIdx = 1;
let child;
if (clickedNode.level === 0) {
child =
this.dataSource.data[clickedNodeIdx].CategoryPermissions[childIdx];
} else {
this.dataSource.data.forEach(
(item) => (child = this.findDataSource(item, clickedNode))
);
}
child.CategoryPermissions.push(temp);
this.dataSource.data = this.dataSource.data;
const addedNode = this.treeControl.dataNodes.find(
(node: any) =>
node.CatId === temp.CatId && node.RoleName === temp.RoleName
);
this.expandParent(addedNode);
this.setPreviousState();
} else {
this.treeControl.collapse(clickedNode);
}
}
findDataSource(item, node) {
if (item.RoleName === node.RoleName) {
return item;
} else if (item.CategoryPermissions) {
let matchedItem;
item.CategoryPermissions.forEach((e) => {
const temp = this.findDataSource(e, node);
if (temp) {
matchedItem = temp;
}
});
return matchedItem;
}
}
setPreviousState() {
for (let i = 0, j = 0; i < this.treeControl.dataNodes.length; i++) {
if (
this.tempNodes[j] &&
this.treeControl.dataNodes[i].RoleName === this.tempNodes[j].RoleName &&
this.treeControl.dataNodes[i].CatId === this.tempNodes[j].CatId &&
this.treeControl.dataNodes[i].level === this.tempNodes[j].level
) {
if (this.tempNodes[j].expanded) {
this.treeControl.expand(this.treeControl.dataNodes[i]);
}
j++;
}
}
}
expandParent(node: ExampleFlatNode) {
const { treeControl } = this;
const currentLevel = treeControl.getLevel(node);
const index = treeControl.dataNodes.indexOf(node) - 1;
for (let i = index; i >= 0; i--) {
const currentNode = treeControl.dataNodes[i];
if (currentLevel === 0) {
this.treeControl.expand(currentNode);
return null;
}
if (treeControl.getLevel(currentNode) < currentLevel) {
this.treeControl.expand(currentNode);
this.expandParent(currentNode);
break;
}
}
}

w2ui filter option "contains not" possible?

I am using w2ui (1.5) and I would be interested in whether it is possible to use a filter that only delivers negative results
That means only records/keys which not fullfilling a certain criteria.
Like a condition "contains not" or "is not" in addition to
http://w2ui.com/web/docs/1.5/w2grid.textSearch.
Thanks!
Gordon
okay, a possible solution is
w2ui['grid'].search([{ field: var1, value: var2, operator: 'not in'}], 'OR');
I coded my own solution to this problem. This adds a way to use "not" for string and "!=" for number searches.
This function does the search and it is also used to store the grid advanced search popup in history state.
I'm sure this can be even more optimized, so please use this more like a guideline. Hope this helps somebody.
function searchExtend(event, grid) {
// if event is null, we do just the local search
var searchObj;
if (event == null) {
searchObj = grid;
} else {
searchObj = event;
}
// make a copy of old data
const oldSearchData = structuredClone(searchObj.searchData);
const oldSearchLogic = structuredClone(searchObj.searchLogic);
var searchData = searchObj.searchData;
var invertedSdata = [];
var toSplice = [];
// check operator if it's "not" or "!="
for (var i = 0; i < searchData.length; i++) {
var sdata = searchData[i];
// invert the condition
if (sdata.operator == "not") {
toSplice.push(i);
invertedSdata.push({
field: sdata.field,
type: sdata.type,
operator: "contains",
value: sdata.value
});
}
if (sdata.operator == "!=") {
toSplice.push(i);
invertedSdata.push({
field: sdata.field,
type: sdata.type,
operator: "=",
value: sdata.value
});
}
}
// remove all "not" and "!=" from searchData
for (var i in toSplice) {
searchData.splice(i, 1);
}
var foundIds = [];
// use inverted criteria to search
if (invertedSdata.length > 0) {
grid.searchData = invertedSdata;
grid.searchLogic = "OR";
grid.localSearch();
grid.searchLogic = oldSearchLogic;
// store found ids
foundIds = structuredClone(grid.last.searchIds);
}
if (foundIds.length > 0) {
// perform a search with original criteria - spliced "not" and "!="
grid.searchData = searchData;
grid.localSearch();
var allRecIds = structuredClone(grid.last.searchIds);
// if there's not any results, push push all recIds
if (grid.last.searchIds.length == 0) {
for (let i = 0; i < grid.records.length; i++) {
allRecIds.push(i);
}
}
// remove all ids found with inverted criteria from results. This way we do the "not" search
for (const id of foundIds) {
allRecIds.splice(allRecIds.indexOf(id), 1);
}
if (event != null) {
// let the search finish, then refresh grid
event.onComplete = function() {
refreshGrid(grid, allRecIds, oldSearchData);
setSearchState(grid);
}
} else {
// refresh the grid
refreshGrid(grid, allRecIds, oldSearchData);
setSearchState(grid);
}
return;
}
if (event != null) {
event.onComplete = function() {
setSearchState(grid); // store state
}
} else {
// refresh whole grid
refreshGrid(grid, allRecIds, oldSearchData);
setSearchState(grid);
}
}
function refreshGrid(grid, allRecIds, oldSearchData) {
grid.last.searchIds = allRecIds;
grid.total = grid.last.searchIds.length;
grid.searchData = oldSearchData;
grid.refresh();
}
function setSearchState(grid) {
history.replaceState(JSON.stringify(grid.searchData), "Search");
}
To use this, you have to call it from the grid's onSearch:
onSearch: function(event) {
searchExtend(event, w2ui["grid"]);
}
Also, if you want to use the history.state feature, it needs to be called from onLoad function:
onLoad: function(event) {
event.onComplete = function() {
console.log("History state: " + history.state);
if (history.state != null) {
w2ui["grid"].searchData = JSON.parse(history.state);
searchExtend(null, w2ui["grid"]);
}
}
To add operators, please use this reference.
This is my solution to the problem:
operators: {
'text': ['is', 'begins', 'contains', 'ends', 'not'], // could have "in" and "not in"
'number': ['=', 'between', '>', '<', '>=', '<=', '!='],
'date': ['is', 'between', {
oper: 'less',
text: 'before'
}, {
oper: 'more',
text: 'after'
}],
'list': ['is'],
'hex': ['is', 'between'],
'color': ['is', 'begins', 'contains', 'ends'],
'enum': ['in', 'not in']
}

Find and Replace Header/Footer text Office JS

I'm having trouble with office js and processing a list of items with lookup codes and replacement values for the header and footer. I've got the body working just not the header/footer. I'm getting this error:0x800a139e - JavaScript runtime error: The property 'items' is not available. Before reading the property's value, call the load method on the containing object and call "context.sync()" on the associated request context. As you can see I do call load and sync before trying to access the results.
function mergeHeader(documentFieldKeys) {
if (documentFieldKeys.length > 0)
Word.run(function(context) {
var key = documentFieldKeys.shift();
var mySections = context.document.sections;
context.load(mySections, 'body/style');
return context.sync().then(function() {
for (var i = 0; i < mySections.items.length; i++ ) {
findAndReplace(key, context, mySections.items[i].getHeader("primary"));
}
return context.sync().then(function() {
return mergeHeader(documentFieldKeys);
})
.then(context.sync);
});
});
}
function findAndReplace(key, context, body) {
var results = body.search(key.Code, { matchWholeWord: false, matchCase: false });
context.load(results);
return context.sync().then(function() {
if (results.items.length > 0 && key.Value === "") {
missingFields.push(key.Description);
} else {
for (var i = 0; i < results.items.length; i++) {
results.items[i].insertText(key.Value, "replace");
}
}
})
.then(context.sync);
}
Any help would be appreciated.
Add
context.load(mySections, 'items');
or
mySections.load('items');

How to count objects inside object (without count var index)

I have the following model stored in mongodb:
{
"_id": (MongoId)
"user": "X",
"field-name-1":{
"obj-1": 12345,
"obj-2": 54321,
"obj-3": 67890,
(...)
},
"field-name-2:{
"obj-1": {"foo":"bar", "bar":"foo"},
"obj-2": {"foo":"bar2", "bar":"foo2"},
(...)
}
}
How do I find the ocurrences that contains more than 5 objects inside the "field-name-1"? Is it the same for "field-name-2"?
I can't use a variable to count the amount of objects because I upsert values inside the "field-name-1"/"field-name-2" constantly.
Thanks.
Already tried (without success):
db.summoners.find({$where: "this.['field-name-1'].length > 5"})
db.summoners.find({$where: "['field-name-1'].length > 5"})
$where should be qualified in this case:
function findMyDocument(key, value) {
var f = this[key];
if (typeof f != "object" || typeof value != "number") {
return false;
}
var count = 0;
for (var x in f) {
if (++count > value) {
return true;
}
}
return false;
}
// store above function to database for invoking
db.system.js.insert({_id:"findMyDocument", value: findMyDocument});
// apply here
db.summoners.find({$where: "findMyDocument.call(this, 'field-name-1', 5);"});
db.summoners.find({$where: "findMyDocument.call(this, 'field-name-2', 5);"});

How to dynamic create custom controls

I am trying to add a customized mymenubutton, the menu's items are based on another dropdown's selected value, which returns a json array with bunch items.
So I use the example http://fiddle.tinymce.com/gaaaab which could create mymenubutton for the first time, but when the drop down list changes, how should I re-init this control and rebind json array to mymenubutton?
function generateTokensList(result) {
tinymce.create('tinymce.plugins.ExamplePlugin', {
createControl: function (n, cm) {
switch (n) {
case 'mysplitbutton':
var c = cm.createSplitButton('mysplitbutton', {
title: 'My split button',
image: 'some.gif',
onclick: function () {
alert('Button was clicked.');
}
});
c.onRenderMenu.add(function (c, m) {
m.add({ title: 'Tokens', 'class': 'mceMenuItemTitle' }).setDisabled(1);
var insertVar = function (val) {
return function () { tinyMCE.activeEditor.execCommand('mceInsertContent', false, val); }
};
for (var i = 0; i < result.length; i++) {
var field = result[i].field;
var variable = insertVar(result[i].field);
m.add({ title: result[i].name, onclick: variable });
}
});
// Return the new splitbutton instance
return c;
}
return null;
}
});
tinymce.PluginManager.add('example', tinymce.plugins.ExamplePlugin);
}
Solved this by myself. Bind the data to a variable and each time just call var.
c.onRenderMenu.add(function (c, m) {
m.add({ title: 'Tokens', 'class': 'mceMenuItemTitle' }).setDisabled(1);
var insertVar = function (val) {
return function () { tinyMCE.activeEditor.execCommand('mceInsertContent', false, val); }
};
for (var i = 0; i < tokens.length; i++) {
var field = tokens[i].field;
var variable = insertVar( '[['+tokens[i].name+']]');
m.add({ title: '[['+tokens[i].name+']]', onclick: variable });
}
});