diff --git a/client/claim/src/action/index.html b/client/claim/src/action/index.html
index 7a1a1515f0..7f4917c2d8 100644
--- a/client/claim/src/action/index.html
+++ b/client/claim/src/action/index.html
@@ -37,7 +37,7 @@
id="claimDestinationFk"
field="saleClaimed.claimDestinationFk"
url="/claim/api/ClaimDestinations"
- select-fields="['id','description']"
+ fields="['id','description']"
value-field="id"
show-field="description"
on-change="$ctrl.setClaimDestination(saleClaimed.id, value)">
diff --git a/client/claim/src/basic-data/index.html b/client/claim/src/basic-data/index.html
index 774ebe3154..5c7c12701f 100644
--- a/client/claim/src/basic-data/index.html
+++ b/client/claim/src/basic-data/index.html
@@ -24,7 +24,7 @@
disabled="true"
field="$ctrl.claim.workerFk"
url="/client/api/Workers"
- select-fields="['firstName', 'name']"
+ fields="['firstName', 'name']"
value-field="id"
label="Worker">
{{firstName}} {{name}}
diff --git a/client/claim/src/development/index.html b/client/claim/src/development/index.html
index 0d462256f8..d0b3d5ae91 100644
--- a/client/claim/src/development/index.html
+++ b/client/claim/src/development/index.html
@@ -49,7 +49,7 @@
id="claimReason"
field="claimDevelopment.claimReasonFk"
data="claimReasons"
- select-fields="['id','description']"
+ fields="['id', 'description']"
show-field="description"
vn-acl="salesAssistant">
@@ -59,7 +59,7 @@
id="claimResult"
field="claimDevelopment.claimResultFk"
data="claimResults"
- select-fields="['id','description']"
+ fields="['id', 'description']"
show-field="description"
vn-acl="salesAssistant">
@@ -69,7 +69,7 @@
id="Responsible"
field="claimDevelopment.claimResponsibleFk"
data="claimResponsibles"
- select-fields="['id','description']"
+ fields="['id', 'description']"
show-field="description"
vn-acl="salesAssistant">
@@ -88,7 +88,7 @@
id="redelivery"
field="claimDevelopment.claimRedeliveryFk"
data="claimRedeliveries"
- select-fields="['id','description']"
+ fields="['id', 'description']"
show-field="description"
vn-acl="salesAssistant">
diff --git a/client/client/src/billing-data/index.html b/client/client/src/billing-data/index.html
index 86a7ef4e4b..c42bb722df 100644
--- a/client/client/src/billing-data/index.html
+++ b/client/client/src/billing-data/index.html
@@ -15,7 +15,7 @@
vn-acl="administrative, salesAssistant"
field="$ctrl.client.payMethodFk"
url="/client/api/PayMethods"
- select-fields="ibanRequired"
+ fields="['ibanRequired']"
initial-data="$ctrl.client.payMethod">
this.sortFunc(a, b, orderComp));
+ } else if (typeof order === 'function')
+ data.sort(order);
+
+ this.skip = skip;
+
+ if (filter.limit) {
+ let end = skip + filter.limit;
+ this.moreRows = end < data.length;
+ data = data.slice(this.skip, end);
+ } else
+ this.moreRows = false;
+
+ this.currentFilter = filter;
+ return data;
+ }
+
+ applyFilter(userFilter, userParams) {
+ this.userFilter = userFilter;
+ this.userParams = userParams;
+ return this.refresh();
+ }
+
+ addFilter(userFilter, userParams) {
+ this.userFilter = this.mergeFilters(userFilter, this.userFilter);
+ Object.assign(this.userParams, userParams);
+ return this.refresh();
+ }
+
+ removeFilter() {
+ return applyFilter(null, null);
+ }
+
+ /**
+ * When limit is enabled, loads the next set of rows.
+ *
+ * @return {Promise} The request promise
+ */
+ loadMore() {
+ if (!this.moreRows)
+ return this.$q.resolve();
+
+ let data = this.proccessData(this.skip + this.currentFilter.limit);
+ this.data = this.data.concat(data);
+ return this.$q.resolve();
+ }
+
+ /**
+ * Clears the model, removing all it's data.
+ */
+ clear() {
+ this.data = null;
+ this.userFilter = null;
+ this.moreRows = false;
+ this.skip = 0;
+ }
+
+ /**
+ * Saves current changes on the server.
+ *
+ * @return {Promise} The save request promise
+ */
+ save() {
+ if (this.getChanges())
+ this.orgData = this.data;
+
+ return this.$q.resolve();
+ }
+
+ sortFunc(a, b, order) {
+ for (let i of order) {
+ let compRes = this.compareFunc(a[i.field], b[i.field]) * i.way;
+ if (compRes !== 0)
+ return compRes;
+ }
+
+ return 0;
+ }
+
+ compareFunc(a, b) {
+ if (a === b)
+ return 0;
+
+ let aType = typeof a;
+
+ if (aType === typeof b) {
+ switch (aType) {
+ case 'string':
+ return a.localeCompare(b);
+ case 'number':
+ return a - b;
+ case 'boolean':
+ return a ? 1 : -1;
+ case 'object':
+ if (a instanceof Date && b instanceof Date)
+ return a.getTime() - b.getTime();
+ }
+ }
+
+ if (a === undefined)
+ return -1;
+ if (b === undefined)
+ return 1;
+ if (a === null)
+ return -1;
+ if (b === null)
+ return 1;
+
+ return a > b ? 1 : -1;
+ }
+
+ mergeFilters(src, dst) {
+ let mergedWhere = [];
+ let wheres = [dst.where, src.where];
+
+ for (let where of wheres) {
+ if (Array.isArray(where))
+ mergedWhere = mergedWhere.concat(where);
+ else if (where)
+ mergedWhere.push(where);
+ }
+
+ switch (mergedWhere.length) {
+ case 0:
+ mergedWhere = undefined;
+ break;
+ case 1:
+ mergedWhere = mergedWhere[0];
+ break;
+ }
+
+ return {
+ where: mergedWhere,
+ order: src.order || dst.order,
+ limit: src.limit || dst.limit
+ };
+ }
+
+ undoChanges() {
+ super.undoChanges();
+ this.refresh();
+ }
+}
+ArrayModel.$inject = ['$q'];
+
+ngModule.component('vnArrayModel', {
+ controller: ArrayModel,
+ bindings: {
+ orgData: '',
+ data: '=?',
+ link: '',
+ order: '@?',
+ limit: '',
+ autoLoad: ''
+ }
+});
diff --git a/client/core/src/components/autocomplete/autocomplete.html b/client/core/src/components/autocomplete/autocomplete.html
index 7e6580d781..4f02fef80e 100755
--- a/client/core/src/components/autocomplete/autocomplete.html
+++ b/client/core/src/components/autocomplete/autocomplete.html
@@ -20,5 +20,6 @@
+ on-select="$ctrl.onDropDownSelect(value)"
+ on-data-ready="$ctrl.onDataReady()">
\ No newline at end of file
diff --git a/client/core/src/components/autocomplete/autocomplete.js b/client/core/src/components/autocomplete/autocomplete.js
index ffa353ea24..2d4e5facc1 100755
--- a/client/core/src/components/autocomplete/autocomplete.js
+++ b/client/core/src/components/autocomplete/autocomplete.js
@@ -1,6 +1,6 @@
import ngModule from '../../module';
import Input from '../../lib/input';
-import asignProps from '../../lib/asign-props';
+import assignProps from '../../lib/assign-props';
import './style.scss';
/**
@@ -11,6 +11,8 @@ import './style.scss';
* @property {Array} data Static data for the autocomplete
* @property {Object} intialData A initial data to avoid the server request used to get the selection
* @property {Boolean} multiple Wether to allow multiple selection
+ *
+ * @event change Thrown when value is changed
*/
export default class Autocomplete extends Input {
constructor($element, $scope, $http, $transclude) {
@@ -20,9 +22,6 @@ export default class Autocomplete extends Input {
this._field = undefined;
this._selection = null;
- this.valueField = 'id';
- this.showField = 'name';
- this._multiField = [];
this.readonly = true;
this.form = null;
this.input = this.element.querySelector('.mdl-textfield__input');
@@ -31,15 +30,41 @@ export default class Autocomplete extends Input {
this.element.querySelector('.mdl-textfield'));
}
- set url(value) {
- this._url = value;
+ $postLink() {
+ this.assignDropdownProps();
+ this.showField = this.$.dropDown.showField;
+ this.valueField = this.$.dropDown.valueField;
+ this.linked = true;
this.refreshSelection();
}
+ get model() {
+ return this._model;
+ }
+
+ set model(value) {
+ this._model = value;
+ this.assignDropdownProps();
+ }
+
+ get data() {
+ return this._data;
+ }
+
+ set data(value) {
+ this._data = value;
+ this.assignDropdownProps();
+ }
+
get url() {
return this._url;
}
+ set url(value) {
+ this._url = value;
+ this.assignDropdownProps();
+ }
+
/**
* @type {any} The autocomplete value.
*/
@@ -53,9 +78,7 @@ export default class Autocomplete extends Input {
this._field = value;
this.refreshSelection();
-
- if (this.onChange)
- this.onChange({value});
+ this.emit('change', {value});
}
/**
@@ -71,15 +94,6 @@ export default class Autocomplete extends Input {
this.refreshDisplayed();
}
- set data(value) {
- this._data = value;
- this.refreshSelection();
- }
-
- get data() {
- return this._data;
- }
-
selectionIsValid(selection) {
return selection
&& selection[this.valueField] == this._field
@@ -92,33 +106,32 @@ export default class Autocomplete extends Input {
let value = this._field;
- if (value && this.valueField && this.showField) {
+ if (value && this.linked) {
if (this.selectionIsValid(this.initialData)) {
this.selection = this.initialData;
return;
}
- let data = this.data;
+ if (this.$.dropDown) {
+ let data;
+ if (this.$.dropDown.model)
+ data = this.$.dropDown.model.orgData;
- if (!data && this.$.dropDown)
- data = this.$.dropDown.$.model.data;
+ if (data)
+ for (let i = 0; i < data.length; i++)
+ if (data[i][this.valueField] === value) {
+ this.selection = data[i];
+ return;
+ }
- if (data)
- for (let i = 0; i < data.length; i++)
- if (data[i][this.valueField] === value) {
- this.selection = data[i];
- return;
- }
-
- if (this.url) {
this.requestSelection(value);
- return;
}
} else
this.selection = null;
}
requestSelection(value) {
+ if (!this.url) return;
let where = {};
if (this.multiple)
@@ -127,7 +140,7 @@ export default class Autocomplete extends Input {
where[this.valueField] = value;
let filter = {
- fields: this.getFields(),
+ fields: this.$.dropDown.getFields(),
where: where
};
@@ -170,18 +183,6 @@ export default class Autocomplete extends Input {
this.mdlUpdate();
}
- getFields() {
- let fields = [];
- fields.push(this.valueField);
- fields.push(this.showField);
-
- if (this.selectFields)
- for (let field of this.selectFields)
- fields.push(field);
-
- return fields;
- }
-
mdlUpdate() {
let field = this.element.querySelector('.mdl-textfield');
let mdlField = field.MaterialTextfield;
@@ -227,26 +228,34 @@ export default class Autocomplete extends Input {
this.showDropDown();
}
- showDropDown(search) {
- Object.assign(this.$.dropDown.$.model, {
- url: this.url,
- staticData: this._data
- });
+ onDataReady() {
+ this.refreshSelection();
+ }
- asignProps(this, this.$.dropDown, [
+ assignDropdownProps() {
+ if (!this.$.dropDown) return;
+ assignProps(this, this.$.dropDown, [
'valueField',
'showField',
+ 'showFilter',
+ 'multiple',
+ '$transclude',
+ 'translateFields',
+ 'model',
+ 'data',
+ 'url',
+ 'fields',
+ 'include',
'where',
'order',
'limit',
- 'showFilter',
- 'multiple',
- '$transclude'
+ 'searchFunction'
]);
+ }
- this.$.dropDown.selectFields = this.getFields();
- this.$.dropDown.parent = this.input;
- this.$.dropDown.show(search);
+ showDropDown(search) {
+ this.assignDropdownProps();
+ this.$.dropDown.show(this.input, search);
}
}
Autocomplete.$inject = ['$element', '$scope', '$http', '$transclude'];
@@ -255,22 +264,23 @@ ngModule.component('vnAutocomplete', {
template: require('./autocomplete.html'),
controller: Autocomplete,
bindings: {
- url: '@?',
- data: '',
+ label: '@',
+ field: '=?',
+ disabled: '',
showField: '@?',
valueField: '@?',
- selectFields: '',
- disabled: '',
- where: '@?',
- order: '@?',
- label: '@',
initialData: '',
- field: '=?',
- limit: '',
showFilter: '',
selection: '',
multiple: '',
- onChange: '&?'
+ data: '',
+ url: '@?',
+ fields: '',
+ include: '',
+ where: '',
+ order: '@?',
+ limit: '',
+ searchFunction: '&?'
},
transclude: {
tplItem: '?tplItem'
diff --git a/client/core/src/components/rest-model/crud-model.js b/client/core/src/components/crud-model/crud-model.js
similarity index 65%
rename from client/core/src/components/rest-model/crud-model.js
rename to client/core/src/components/crud-model/crud-model.js
index d4ae1c0cd4..a823e989af 100644
--- a/client/core/src/components/rest-model/crud-model.js
+++ b/client/core/src/components/crud-model/crud-model.js
@@ -1,8 +1,9 @@
import ngModule from '../../module';
import ModelProxy from '../model-proxy/model-proxy';
+import {mergeWhere, mergeFilters} from 'vn-loopback/common/filter.js';
export default class CrudModel extends ModelProxy {
- constructor($http, $q) {
+ constructor($q, $http) {
super();
this.$http = $http;
this.$q = $q;
@@ -10,6 +11,10 @@ export default class CrudModel extends ModelProxy {
this.autoLoad = true;
}
+ $onInit() {
+ this.autoRefresh();
+ }
+
/**
* Whether the model is loading.
*/
@@ -17,9 +22,21 @@ export default class CrudModel extends ModelProxy {
return this.canceler != null;
}
- $onInit() {
+ set url(url) {
+ if (this._url === url) return;
+ this._url = url;
+ this.clear();
+ this.autoRefresh();
+ }
+
+ get url() {
+ return this._url;
+ }
+
+ autoRefresh() {
if (this.autoLoad)
- this.refresh();
+ return this.refresh();
+ return this.$q.resolve();
}
buildFilter() {
@@ -48,7 +65,8 @@ export default class CrudModel extends ModelProxy {
* @return {Promise} The request promise
*/
refresh() {
- if (!this.url) return;
+ if (!this._url)
+ return this.$q.resolve();
return this.sendRequest(this.buildFilter());
}
@@ -84,9 +102,7 @@ export default class CrudModel extends ModelProxy {
* @return {Promise} The request promise
*/
removeFilter() {
- this.userFilter = null;
- this.userParams = null;
- return this.refresh();
+ return applyFilter(null, null);
}
/**
@@ -101,12 +117,23 @@ export default class CrudModel extends ModelProxy {
/**
* When limit is enabled, loads the next set of rows.
+ *
+ * @return {Promise} The request promise
*/
loadMore() {
- if (!this.moreRows) return;
+ if (!this.moreRows)
+ return this.$q.resolve();
+
let filter = Object.assign({}, this.currentFilter);
filter.skip = this.orgData ? this.orgData.length : 0;
- this.sendRequest(filter, true);
+ return this.sendRequest(filter, true);
+ }
+
+ /**
+ * Clears the model, removing all it's data.
+ */
+ clear() {
+ this.orgData = null;
}
/**
@@ -115,28 +142,31 @@ export default class CrudModel extends ModelProxy {
* @return {Object} The current changes
*/
getChanges() {
+ if (!this.isChanged)
+ return null;
+
let create = [];
let update = [];
let remove = [];
for (let row of this.removed)
- remove.push(row.$data[this.primaryKey]);
+ remove.push(row[this.primaryKey]);
for (let row of this._data) {
- if (row.$isNew)
- create.push(row.$data);
- else if (row.$oldData)
- update.push(row.$data);
+ if (row.$isNew) {
+ let data = {};
+ for (let prop in row)
+ if (prop.charAt(0) !== '$')
+ data[prop] = row[prop];
+ create.push(data);
+ } else if (row.$oldData) {
+ let data = {};
+ for (let prop in row.$oldData)
+ data[prop] = row[prop];
+ update.push(data);
+ }
}
- let isChanged =
- create.length > 0 ||
- update.length > 0 ||
- remove.length > 0;
-
- if (!isChanged)
- return null;
-
let changes = {
create: create,
update: update,
@@ -157,9 +187,9 @@ export default class CrudModel extends ModelProxy {
if (!changes)
return this.$q.resolve();
- let url = this.saveUrl ? this.saveUrl : `${this.url}/crud`;
+ let url = this.saveUrl ? this.saveUrl : `${this._url}/crud`;
return this.$http.post(url, changes)
- .then(() => this.resetChanges());
+ .then(() => this.applyChanges());
}
buildParams() {
@@ -186,7 +216,7 @@ export default class CrudModel extends ModelProxy {
params: params
};
- return this.$http.get(this.url, options).then(
+ return this.$http.get(this._url, options).then(
json => this.onRemoteDone(json, filter, append),
json => this.onRemoteError(json)
);
@@ -202,9 +232,9 @@ export default class CrudModel extends ModelProxy {
this.currentFilter = filter;
}
+ this.data = this.proxiedData.slice();
this.moreRows = filter.limit && data.length == filter.limit;
this.onRequestEnd();
- this.dataChanged();
}
onRemoteError(err) {
@@ -215,8 +245,13 @@ export default class CrudModel extends ModelProxy {
onRequestEnd() {
this.canceler = null;
}
+
+ undoChanges() {
+ super.undoChanges();
+ this.data = this.proxiedData.slice();
+ }
}
-CrudModel.$inject = ['$http', '$q'];
+CrudModel.$inject = ['$q', '$http'];
ngModule.component('vnCrudModel', {
controller: CrudModel,
@@ -224,12 +259,12 @@ ngModule.component('vnCrudModel', {
orgData: '',
data: '=?',
onDataChange: '&?',
- fields: '',
link: '',
url: '@?',
saveUrl: '@?',
- where: '',
+ fields: '',
include: '',
+ where: '',
order: '@?',
limit: '',
filter: '',
@@ -240,80 +275,3 @@ ngModule.component('vnCrudModel', {
autoLoad: ''
}
});
-
-/**
- * Passes a loopback fields filter to an object.
- *
- * @param {Object} fields The fields object or array
- * @return {Object} The fields as object
- */
-function fieldsToObject(fields) {
- let fieldsObj = {};
-
- if (Array.isArray(fields))
- for (let field of fields)
- fieldsObj[field] = true;
- else if (typeof fields == 'object')
- for (let field in fields)
- if (fields[field])
- fieldsObj[field] = true;
-
- return fieldsObj;
-}
-
-/**
- * Merges two loopback fields filters.
- *
- * @param {Object|Array} src The source fields
- * @param {Object|Array} dst The destination fields
- * @return {Array} The merged fields as an array
- */
-function mergeFields(src, dst) {
- let fields = {};
- Object.assign(fields,
- fieldsToObject(src),
- fieldsToObject(dst)
- );
- return Object.keys(fields);
-}
-
-/**
- * Merges two loopback where filters.
- *
- * @param {Object|Array} src The source where
- * @param {Object|Array} dst The destination where
- * @return {Array} The merged wheres
- */
-function mergeWhere(src, dst) {
- let and = [];
- if (src) and.push(src);
- if (dst) and.push(dst);
- return and.length > 1 ? {and} : and[0];
-}
-
-/**
- * Merges two loopback filters returning the merged filter.
- *
- * @param {Object} src The source filter
- * @param {Object} dst The destination filter
- * @return {Object} The result filter
- */
-function mergeFilters(src, dst) {
- let res = Object.assign({}, dst);
-
- if (!src)
- return res;
-
- if (src.fields)
- res.fields = mergeFields(src.fields, res.fields);
- if (src.where)
- res.where = mergeWhere(res.where, src.where);
- if (src.include)
- res.include = src.include;
- if (src.order)
- res.order = src.order;
- if (src.limit)
- res.limit = src.limit;
-
- return res;
-}
diff --git a/client/core/src/components/drop-down/drop-down.html b/client/core/src/components/drop-down/drop-down.html
index ce8d23a4f6..9ec18044a4 100755
--- a/client/core/src/components/drop-down/drop-down.html
+++ b/client/core/src/components/drop-down/drop-down.html
@@ -1,7 +1,3 @@
-
-
{
- this.refreshModel();
- this.searchTimeout = null;
- }, 350);
+
+ if (this.model) {
+ this.model.clear();
+ this.searchTimeout = this.$timeout(() => {
+ this.refreshModel();
+ this.searchTimeout = null;
+ }, 350);
+ }
this.buildList();
}
get statusText() {
- let model = this.$.model;
- let data = model.data;
- let statusText = null;
+ let model = this.model;
+ let data = this.modelData;
+ if (!model)
+ return 'No data';
if (model.isLoading || this.searchTimeout)
- statusText = 'Loading...';
- else if (data == null)
- statusText = 'No data';
- else if (model.moreRows)
- statusText = 'Load More';
- else if (data.length === 0)
- statusText = 'No results found';
+ return 'Loading...';
+ if (data == null)
+ return 'No data';
+ if (model.moreRows)
+ return 'Load More';
+ if (data.length === 0)
+ return 'No results found';
- return statusText;
- }
-
- get model() {
- return this.$.model;
+ return null;
}
get activeOption() {
@@ -86,14 +100,14 @@ export default class DropDown extends Component {
* Shows the drop-down. If a parent is specified it is shown in a visible
* relative position to it.
*
+ * @param {HTMLElement} parent The parent element to show drop down relative to
* @param {String} search The initial search term or %null
*/
- show(search) {
+ show(parent, search) {
this._activeOption = -1;
this.search = search;
this.buildList();
- this.$.popover.parent = this.parent;
- this.$.popover.show();
+ this.$.popover.show(parent || this.parent);
}
/**
@@ -135,7 +149,7 @@ export default class DropDown extends Component {
if (this.activeLi)
this.activeLi.className = '';
- let data = this.$.model.data;
+ let data = this.modelData;
if (option >= 0 && data && option < data.length) {
this.activeLi = this.ul.children[option];
@@ -149,7 +163,7 @@ export default class DropDown extends Component {
* @param {Number} option The option index
*/
selectOption(option) {
- let data = this.$.model.data;
+ let data = this.modelData;
let item = option != -1 && data ? data[option] : null;
if (item) {
@@ -168,47 +182,13 @@ export default class DropDown extends Component {
this.field = value;
}
- if (this.onSelect)
- this.onSelect({value: value});
+ this.emit('select', {value: value});
}
if (!this.multiple)
this.$.popover.hide();
}
- refreshModel() {
- this.$.model.filter = {
- fields: this.selectFields,
- where: this.getWhere(this._search),
- order: this.getOrder(),
- limit: this.limit || 8
- };
- this.$.model.refresh(this._search);
- }
-
- getWhere(search) {
- if (search == '' || search == null)
- return undefined;
-
- if (this.where) {
- let jsonFilter = this.where.replace(/search/g, search);
- return this.$.$eval(jsonFilter);
- }
-
- let where = {};
- where[this.showField] = {like: `%${search}%`};
- return where;
- }
-
- getOrder() {
- if (this.order)
- return this.order;
- else if (this.showField)
- return `${this.showField} ASC`;
-
- return undefined;
- }
-
onOpen() {
this.document.addEventListener('keydown', this.docKeyDownHandler);
this.list.scrollTop = 0;
@@ -227,15 +207,15 @@ export default class DropDown extends Component {
let list = this.list;
let shouldLoad =
list.scrollTop + list.clientHeight >= (list.scrollHeight - 40)
- && !this.$.model.isLoading;
+ && !this.model.isLoading;
if (shouldLoad)
- this.$.model.loadMore();
+ this.model.loadMore();
}
onLoadMoreClick(event) {
event.preventDefault();
- this.$.model.loadMore();
+ this.model.loadMore();
}
onContainerClick(event) {
@@ -244,14 +224,10 @@ export default class DropDown extends Component {
if (index != -1) this.selectOption(index);
}
- onModelDataChange() {
- this.buildList();
- }
-
onDocKeyDown(event) {
if (event.defaultPrevented) return;
- let data = this.$.model.data;
+ let data = this.modelData;
let option = this.activeOption;
let nOpts = data ? data.length - 1 : 0;
@@ -283,11 +259,12 @@ export default class DropDown extends Component {
}
buildList() {
+ if (!this.shown) return;
this.destroyList();
let hasTemplate = this.$transclude && this.$transclude.isSlotFilled('tplItem');
let fragment = this.document.createDocumentFragment();
- let data = this.$.model.data;
+ let data = this.modelData;
if (data) {
for (let i = 0; i < data.length; i++) {
@@ -339,11 +316,109 @@ export default class DropDown extends Component {
this.scopes = [];
}
+ getFields() {
+ let fields = [];
+ fields.push(this.valueField);
+ fields.push(this.showField);
+
+ if (this.fields)
+ for (let field of this.fields)
+ fields.push(field);
+
+ return fields;
+ }
+
$onDestroy() {
this.destroyList();
}
+
+ // Model related code
+
+ onDataChange() {
+ if (this.model.orgData)
+ this.emit('dataReady');
+ this.buildList();
+ }
+
+ get modelData() {
+ return this._model ? this._model.data : null;
+ }
+
+ get model() {
+ return this._model;
+ }
+
+ set model(value) {
+ this.linkEvents({_model: value}, {dataChange: this.onDataChange});
+ this.onDataChange();
+ }
+
+ get url() {
+ return this._url;
+ }
+
+ set url(value) {
+ this._url = value;
+ if (value) {
+ this.model = new CrudModel(this.$q, this.$http);
+ this.model.autoLoad = false;
+ this.model.url = value;
+ }
+ }
+
+ get data() {
+ return this._data;
+ }
+
+ set data(value) {
+ this._data = value;
+ if (value) {
+ this.model = new ArrayModel(this.$q, this.$filter);
+ this.model.autoLoad = false;
+ this.model.orgData = value;
+ }
+ }
+
+ refreshModel() {
+ let model = this.model;
+
+ let order;
+ if (this.order)
+ order = this.order;
+ else if (this.showField)
+ order = `${this.showField} ASC`;
+
+ let filter = {
+ order,
+ limit: this.limit || 8
+ };
+
+ if (model instanceof CrudModel) {
+ let searchExpr = this._search == null
+ ? null
+ : this.searchFunction({$search: this._search});
+
+ Object.assign(filter, {
+ fields: this.getFields(),
+ include: this.include,
+ where: mergeWhere(this.where, searchExpr)
+ });
+ } else if (model instanceof ArrayModel) {
+ if (this._search != null)
+ filter.where = this.searchFunction({$search: this._search});
+ }
+
+ return this.model.applyFilter(filter);
+ }
+
+ searchFunction(scope) {
+ if (this.model instanceof CrudModel)
+ return {[this.showField]: {like: `%${scope.$search}%`}};
+ if (this.model instanceof ArrayModel)
+ return {[this.showField]: scope.$search};
+ }
}
-DropDown.$inject = ['$element', '$scope', '$transclude', '$timeout', '$translate'];
+DropDown.$inject = ['$element', '$scope', '$transclude', '$timeout', '$translate', '$http', '$q', '$filter'];
/**
* Gets the position of an event element relative to a parent.
@@ -374,15 +449,21 @@ ngModule.component('vnDropDown', {
controller: DropDown,
bindings: {
field: '=?',
- data: '',
selection: '=?',
search: '',
- limit: '',
showFilter: '',
parent: '',
multiple: '',
onSelect: '&?',
- translateFields: ''
+ translateFields: '',
+ data: '',
+ url: '@?',
+ fields: '',
+ include: '',
+ where: '',
+ order: '@?',
+ limit: '',
+ searchFunction: '&?'
},
transclude: {
tplItem: '?tplItem'
diff --git a/client/core/src/components/icon-menu/icon-menu.js b/client/core/src/components/icon-menu/icon-menu.js
index 120898150c..bf140bb0b7 100644
--- a/client/core/src/components/icon-menu/icon-menu.js
+++ b/client/core/src/components/icon-menu/icon-menu.js
@@ -1,6 +1,6 @@
import ngModule from '../../module';
import Input from '../../lib/input';
-import asignProps from '../../lib/asign-props';
+import assignProps from '../../lib/assign-props';
import './style.scss';
export default class IconMenu extends Input {
@@ -8,20 +8,6 @@ export default class IconMenu extends Input {
super($element, $scope);
this.$transclude = $transclude;
this.input = this.element.querySelector('.mdl-button');
- this.valueField = 'id';
- this.showField = 'name';
- }
-
- getFields() {
- let fields = [];
- fields.push(this.valueField);
- fields.push(this.showField);
-
- if (this.selectFields)
- for (let field of this.selectFields)
- fields.push(field);
-
- return fields;
}
onClick(event) {
@@ -38,26 +24,24 @@ export default class IconMenu extends Input {
}
showDropDown() {
- Object.assign(this.$.dropDown.$.model, {
- url: this.url,
- staticData: this.data
- });
-
- asignProps(this, this.$.dropDown, [
+ assignProps(this, this.$.dropDown, [
'valueField',
'showField',
- 'where',
- 'order',
'showFilter',
'multiple',
- 'limit',
'$transclude',
- 'translateFields'
+ 'translateFields',
+ 'model',
+ 'data',
+ 'url',
+ 'fields',
+ 'include',
+ 'where',
+ 'order',
+ 'limit',
+ 'searchFunction'
]);
-
- this.$.dropDown.selectFields = this.getFields();
- this.$.dropDown.parent = this.input;
- this.$.dropDown.show();
+ this.$.dropDown.show(this.input);
}
}
IconMenu.$inject = ['$element', '$scope', '$transclude'];
@@ -65,20 +49,20 @@ IconMenu.$inject = ['$element', '$scope', '$transclude'];
ngModule.component('vnIconMenu', {
template: require('./icon-menu.html'),
bindings: {
- url: '@?',
- data: '',
+ label: '@',
showField: '@?',
+ selection: '',
valueField: '@?',
selectFields: '',
disabled: '',
+ initialData: '',
+ showFilter: '',
+ field: '=?',
+ url: '@?',
+ data: '',
where: '@?',
order: '@?',
- label: '@',
- initialData: '',
- field: '=?',
limit: '',
- showFilter: '',
- selection: '',
multiple: '',
onChange: '&?',
icon: '@?',
diff --git a/client/core/src/components/index.js b/client/core/src/components/index.js
index 6ca4fd0e51..46739b88cd 100644
--- a/client/core/src/components/index.js
+++ b/client/core/src/components/index.js
@@ -1,6 +1,5 @@
import './model-proxy/model-proxy';
-import './rest-model/crud-model';
-import './rest-model/rest-model';
+import './crud-model/crud-model';
import './watcher/watcher';
import './textfield/textfield';
import './icon/icon';
diff --git a/client/core/src/components/model-proxy/model-proxy.js b/client/core/src/components/model-proxy/model-proxy.js
index d0fdff9392..f692877cf7 100644
--- a/client/core/src/components/model-proxy/model-proxy.js
+++ b/client/core/src/components/model-proxy/model-proxy.js
@@ -1,8 +1,66 @@
import ngModule from '../../module';
+import EventEmitter from '../../lib/event-emitter';
-export default class ModelProxy {
+/**
+ * A filtrable model.
+ *
+ * @property {Function} filter The filter function
+ */
+export class Filtrable {
+ applyFilter(userFilter, userParams) {}
+ addFilter(userFilter, userParams) {}
+ removeFilter() {}
+}
+
+/**
+ * A sortable model.
+ *
+ * @property {String|Array|Function} order The sort specification
+ */
+export class Sortable {
+}
+
+/**
+ * Paginable model.
+ *
+ * @property {Number} limit The page size
+ */
+export class Paginable {
+ get isLoading() {}
+ get moreRows() {}
+ loadMore() {}
+}
+
+/**
+ * A data model.
+ *
+ * @event dataChange Emitted when data property changes
+ */
+export class DataModel extends EventEmitter {
+ get data() {}
+ refresh() {}
+ clear() {}
+}
+
+ngModule.component('vnDataModel', {
+ controller: DataModel,
+ bindings: {
+ data: '=?',
+ autoLoad: '',
+ autoSync: '',
+ order: '@?',
+ limit: ''
+ }
+});
+
+/**
+ * A data model that monitorizes changes to the data.
+ *
+ * @event dataChange Emitted when data property changes
+ */
+export default class ModelProxy extends DataModel {
constructor() {
- this._data = [];
+ super();
this.resetChanges();
}
@@ -12,18 +70,16 @@ export default class ModelProxy {
set orgData(value) {
this._orgData = value;
- // this._data.splice(0, this._data.length);
- if (this.Row) {
- this._data = [];
+ if (value) {
+ this.proxiedData = new Array(value.length);
- for (let i = 0; i < value.length; i++) {
- let row = new this.Row(value[i], i);
- this._data.push(row);
- }
+ for (let i = 0; i < value.length; i++)
+ this.proxiedData[i] = this.createRow(Object.assign({}, value[i]), i);
} else
- this._data = value;
+ this.proxiedData = null;
+ this.data = null;
this.resetChanges();
}
@@ -31,105 +87,129 @@ export default class ModelProxy {
return this._data;
}
- set data(value) {}
-
- get fields() {
- return this._fields;
- }
-
- set fields(value) {
- this._fields = value;
-
- let Row = function(data, index) {
- this.$data = data;
- this.$index = index;
- this.$oldData = null;
- this.$isNew = false;
- };
-
- for (let prop of value) {
- Object.defineProperty(Row.prototype, prop, {
- enumerable: true,
- configurable: false,
- set: function(value) {
- if (!this.$isNew) {
- if (!this.$oldData)
- this.$oldData = {};
- if (!this.$oldData[prop])
- this.$oldData[prop] = this.$data[prop];
- }
-
- this.$data[prop] = value;
- },
- get: function() {
- return this.$data[prop];
- }
- });
- }
-
- this.Row = Row;
+ set data(value) {
+ this._data = value;
+ this.emit('dataChange');
}
remove(index) {
- let data = this._data;
+ let [item] = this.data.splice(index, 1);
- let item;
- [item] = data.splice(index, 1);
-
- for (let i = index; i < data.length; i++)
- data[i].$index = i;
+ let proxiedIndex = this.proxiedData.indexOf(item);
+ this.proxiedData.splice(proxiedIndex, 1);
if (!item.$isNew)
this.removed.push(item);
+
+ this.isChanged = true;
+ this.emit('rowRemove', index);
}
insert(data) {
data = Object.assign(data || {}, this.link);
- let newRow = new this.Row(data, this._data.length);
+ let newRow = this.createRow(data, null);
newRow.$isNew = true;
- return this._data.push(newRow);
+ let index = this.proxiedData.push(newRow) - 1;
+
+ if (this.data)
+ this.data.push(newRow);
+
+ this.isChanged = true;
+ this.emit('rowInsert', index);
+ return index;
+ }
+
+ createRow(obj, index) {
+ let proxy = new Proxy(obj, {
+ set: (obj, prop, value) => {
+ if (prop.charAt(0) !== '$' && value !== obj[prop] && !obj.$isNew) {
+ if (!obj.$oldData)
+ obj.$oldData = {};
+ if (!obj.$oldData[prop])
+ obj.$oldData[prop] = value;
+ this.isChanged = true;
+ }
+ return Reflect.set(obj, prop, value);
+ }
+ });
+ Object.assign(proxy, {
+ $orgIndex: index,
+ $oldData: null,
+ $isNew: false
+ });
+ return proxy;
}
resetChanges() {
this.removed = [];
+ this.isChanged = false;
- for (let row of this._data) {
- row.$oldData = null;
- row.$isNew = false;
+ let data = this.proxiedData;
+ if (data)
+ for (let row of data)
+ row.$oldData = null;
+ }
+
+ applyChanges() {
+ let data = this.proxiedData;
+ let orgData = this.orgData;
+ if (!data) return;
+
+ for (let row of this.removed) {
+ if (row.$isNew) {
+ let data = {};
+ for (let prop in row)
+ if (prop.charAt(0) !== '$')
+ data[prop] = row[prop];
+ row.$orgIndex = orgData.push(data) - 1;
+ row.$isNew = false;
+ } else if (row.$oldData)
+ for (let prop in row.$oldData)
+ orgData[row.$orgIndex][prop] = row[prop];
}
+
+ let removed = this.removed;
+
+ if (removed) {
+ removed = removed.sort((a, b) => b.$orgIndex - a.$orgIndex);
+
+ for (let row of this.removed)
+ orgData.splice(row.$orgIndex, 1);
+ }
+
+ this.resetChanges();
}
undoChanges() {
- let data = this._data;
+ let data = this.proxiedData;
+ if (!data) return;
for (let i = 0; i < data.length; i++) {
let row = data[i];
if (row.$oldData)
- Object.assign(row.$data, row.$oldData);
+ Object.assign(row, row.$oldData);
if (row.$isNew)
data.splice(i--, 1);
}
- for (let row of this.removed)
- data.splice(row.$index, 0, row);
+ let removed = this.removed;
+
+ if (removed) {
+ removed = removed.sort((a, b) => a.$orgIndex - b.$orgIndex);
+
+ for (let row of this.removed)
+ data.splice(row.$orgIndex, 0, row);
+ }
this.resetChanges();
}
-
- dataChanged() {
- if (this.onDataChange)
- this.onDataChange();
- }
}
ngModule.component('vnModelProxy', {
controller: ModelProxy,
bindings: {
orgData: '',
- data: '=?',
- fields: '',
- link: '',
- onDataChange: '&?'
+ data: '=?'
}
});
diff --git a/client/core/src/components/popover/popover.js b/client/core/src/components/popover/popover.js
index 2a8760e6aa..6207e7f7fd 100644
--- a/client/core/src/components/popover/popover.js
+++ b/client/core/src/components/popover/popover.js
@@ -4,6 +4,11 @@ import './style.scss';
/**
* A simple popover.
+ *
+ * @property {HTMLElement} parent The parent element to show drop down relative to
+ *
+ * @event open Thrown when popover is displayed
+ * @event close Thrown when popover is hidden
*/
export default class Popover extends Component {
constructor($element, $scope, $timeout, $transitions) {
@@ -61,10 +66,13 @@ export default class Popover extends Component {
/**
* Shows the popover. If a parent is specified it is shown in a visible
* relative position to it.
+ *
+ * @param {HTMLElement} parent Overrides the parent property
*/
- show() {
+ show(parent) {
if (this._shown) return;
+ if (parent) this.parent = parent;
this._shown = true;
this.element.style.display = 'block';
this.$timeout.cancel(this.showTimeout);
@@ -78,9 +86,7 @@ export default class Popover extends Component {
this.deregisterCallback = this.$transitions.onStart({}, () => this.hide());
this.relocate();
-
- if (this.onOpen)
- this.onOpen();
+ this.emit('open');
}
/**
@@ -95,9 +101,7 @@ export default class Popover extends Component {
this.showTimeout = this.$timeout(() => {
this.element.style.display = 'none';
this.showTimeout = null;
-
- if (this.onClose)
- this.onClose();
+ this.emit('close');
}, 250);
this.document.removeEventListener('keydown', this.docKeyDownHandler);
@@ -187,9 +191,5 @@ Popover.$inject = ['$element', '$scope', '$timeout', '$transitions'];
ngModule.component('vnPopover', {
template: require('./popover.html'),
controller: Popover,
- transclude: true,
- bindings: {
- onOpen: '&?',
- onClose: '&?'
- }
+ transclude: true
});
diff --git a/client/core/src/components/rest-model/rest-model.js b/client/core/src/components/rest-model/rest-model.js
deleted file mode 100644
index f667bc69f9..0000000000
--- a/client/core/src/components/rest-model/rest-model.js
+++ /dev/null
@@ -1,127 +0,0 @@
-import ngModule from '../../module';
-
-export default class RestModel {
- constructor($http, $q, $filter) {
- this.$http = $http;
- this.$q = $q;
- this.$filter = $filter;
- this.filter = null;
- this.clear();
- }
-
- set staticData(value) {
- this._staticData = value;
- this.refresh();
- }
-
- get staticData() {
- return this._staticData;
- }
-
- get isLoading() {
- return this.canceler != null;
- }
-
- set url(url) {
- if (this._url != url) {
- this._url = url;
- this.clear();
- }
- }
-
- get url() {
- return this._url;
- }
-
- loadMore() {
- if (this.moreRows) {
- let filter = Object.assign({}, this.myFilter);
- filter.skip += filter.limit;
- this.sendRequest(filter, true);
- }
- }
-
- clear() {
- this.cancelRequest();
- this.data = null;
- this.moreRows = false;
- this.dataChanged();
- }
-
- refresh(search) {
- if (this.url) {
- let filter = Object.assign({}, this.filter);
-
- if (filter.limit)
- filter.skip = 0;
-
- this.clear();
- this.sendRequest(filter);
- } else if (this._staticData) {
- if (search)
- this.data = this.$filter('filter')(this._staticData, search);
- else
- this.data = this._staticData;
- this.dataChanged();
- }
- }
-
- cancelRequest() {
- if (this.canceler) {
- this.canceler.resolve();
- this.canceler = null;
- this.request = null;
- }
- }
-
- sendRequest(filter, append) {
- this.cancelRequest();
- this.canceler = this.$q.defer();
- let options = {timeout: this.canceler.promise};
- let json = encodeURIComponent(JSON.stringify(filter));
- this.request = this.$http.get(`${this.url}?filter=${json}`, options).then(
- json => this.onRemoteDone(json, filter, append),
- json => this.onRemoteError(json)
- );
- }
-
- onRemoteDone(json, filter, append) {
- let data = json.data;
-
- if (append)
- this.data = this.data.concat(data);
- else
- this.data = data;
-
- this.myFilter = filter;
- this.moreRows = filter.limit && data.length == filter.limit;
- this.onRequestEnd();
- this.dataChanged();
- }
-
- onRemoteError(json) {
- this.onRequestEnd();
- }
-
- onRequestEnd() {
- this.request = null;
- this.canceler = null;
- }
-
- dataChanged() {
- if (this.onDataChange)
- this.onDataChange();
- }
-}
-RestModel.$inject = ['$http', '$q', '$filter'];
-
-ngModule.component('vnRestModel', {
- controller: RestModel,
- bindings: {
- url: '@?',
- staticData: '',
- data: '=?',
- limit: '',
- onDataChange: '&?'
- }
-});
diff --git a/client/core/src/components/rest-model/rest-model.spec.js b/client/core/src/components/rest-model/rest-model.spec.js
deleted file mode 100644
index afb28163fa..0000000000
--- a/client/core/src/components/rest-model/rest-model.spec.js
+++ /dev/null
@@ -1,34 +0,0 @@
-import './rest-model.js';
-
-describe('Component vnRestModel', () => {
- let $componentController;
- let $httpBackend;
- let controller;
-
- beforeEach(() => {
- angular.mock.module('client');
- });
-
- beforeEach(angular.mock.inject((_$componentController_, _$httpBackend_) => {
- $componentController = _$componentController_;
- controller = $componentController('vnRestModel', {$httpBackend});
- }));
-
- describe('set url', () => {
- it(`should call clear function when the controller _url is undefined`, () => {
- spyOn(controller, 'clear');
- controller.url = 'localhost';
-
- expect(controller._url).toEqual('localhost');
- expect(controller.clear).toHaveBeenCalledWith();
- });
-
- it(`should do nothing when the url is matching`, () => {
- controller._url = 'localhost';
- spyOn(controller, 'clear');
- controller.url = 'localhost';
-
- expect(controller.clear).not.toHaveBeenCalledWith();
- });
- });
-});
diff --git a/client/core/src/lib/asign-props.js b/client/core/src/lib/assign-props.js
similarity index 100%
rename from client/core/src/lib/asign-props.js
rename to client/core/src/lib/assign-props.js
diff --git a/client/core/src/lib/component.js b/client/core/src/lib/component.js
index 36380a5ed6..66ae63f703 100644
--- a/client/core/src/lib/component.js
+++ b/client/core/src/lib/component.js
@@ -1,7 +1,22 @@
+import EventEmitter from './event-emitter';
+
/**
* Base class for component controllers.
*/
-export default class Component {
+export default class Component extends EventEmitter {
+ /**
+ * Contructor.
+ *
+ * @param {HTMLElement} $element The main component element
+ * @param {$rootScope.Scope} $scope The element scope
+ */
+ constructor($element, $scope) {
+ super($element, $scope);
+ this.element = $element[0];
+ this.element.$ctrl = this;
+ this.$element = $element;
+ this.$ = $scope;
+ }
/**
* The component owner window.
*/
@@ -14,17 +29,5 @@ export default class Component {
get document() {
return this.element.ownerDocument;
}
- /**
- * Contructor.
- *
- * @param {HTMLElement} $element The main component element
- * @param {$rootScope.Scope} $scope The element scope
- */
- constructor($element, $scope) {
- this.element = $element[0];
- this.element.$ctrl = this;
- this.$element = $element;
- this.$ = $scope;
- }
}
Component.$inject = ['$element', '$scope'];
diff --git a/client/core/src/lib/event-emitter.js b/client/core/src/lib/event-emitter.js
new file mode 100644
index 0000000000..0876ddbc77
--- /dev/null
+++ b/client/core/src/lib/event-emitter.js
@@ -0,0 +1,91 @@
+import {kebabToCamel} from './string';
+
+export default class EventEmitter {
+ constructor($element, $scope) {
+ if (!$element) return;
+ let attrs = $element[0].attributes;
+ for (let attr of attrs) {
+ if (attr.name.substr(0, 2) !== 'on') continue;
+ let eventName = kebabToCamel(attr.name.substr(3));
+ let callback = locals => $scope.$parent.$eval(attr.nodeValue, locals);
+ this.on(eventName, callback);
+ }
+ }
+
+ /**
+ * Connects to an object event.
+ *
+ * @param {String} eventName The event name
+ * @param {Function} callback The callback function
+ * @param {Object} thisArg The scope for the callback or %null
+ */
+ on(eventName, callback, thisArg) {
+ if (!this.$events)
+ this.$events = {};
+ if (!this.$events[eventName])
+ this.$events[eventName] = [];
+ this.$events[eventName].push({callback, thisArg});
+ }
+
+ /**
+ * Disconnects all handlers for callback.
+ *
+ * @param {Function} callback The callback function
+ */
+ off(callback) {
+ if (!this.$events) return;
+ for (let event in this.$events)
+ for (let i = 0; i < event.length; i++)
+ if (event[i].callback === callback)
+ event.splice(i--, 1);
+ }
+
+ /**
+ * Disconnects all instance callbacks.
+ *
+ * @param {Object} thisArg The callbacks instance
+ */
+ disconnect(thisArg) {
+ if (!this.$events) return;
+ for (let event in this.$events)
+ for (let i = 0; i < event.length; i++)
+ if (event[i].thisArg === thisArg)
+ event.splice(i--, 1);
+ }
+
+ /**
+ * Emits an event.
+ *
+ * @param {String} eventName The event name
+ * @param {...*} args Arguments to pass to the callbacks
+ */
+ emit(eventName) {
+ if (!this.$events || !this.$events[eventName])
+ return;
+
+ let args = Array.prototype.slice.call(arguments, 1);
+
+ let callbacks = this.$events[eventName];
+ for (let callback of callbacks)
+ callback.callback.apply(callback.thisArg, args);
+ }
+
+ /**
+ * Links the object with another object events.
+ *
+ * @param {Object} propValue The property and the new value
+ * @param {Object} handlers The event handlers
+ */
+ linkEvents(propValue, handlers) {
+ for (let prop in propValue) {
+ let value = propValue[prop];
+ if (this[prop])
+ this[prop].disconnect(this);
+ this[prop] = value;
+ if (value)
+ for (let event in handlers)
+ value.on(event, handlers[event], this);
+ }
+ }
+}
+EventEmitter.$inject = ['$element', '$scope'];
diff --git a/client/item/src/create/index.html b/client/item/src/create/index.html
index 895d120eee..822b03ca82 100644
--- a/client/item/src/create/index.html
+++ b/client/item/src/create/index.html
@@ -16,10 +16,10 @@
+ search-function="{or: [{code: {regexp: $search}}, {name: {regexp: $search}}]}">
{{::code}}
{{::name}}
@@ -31,7 +31,7 @@
show-field="description"
value-field="id"
field="$ctrl.item.intrastatFk"
- where="{or: [{id: {regexp: 'search'}}, {description: {regexp: 'search'}}]}">
+ search-function="{or: [{id: {regexp: $search}}, {description: {regexp: $search}}]}">
{{::id}}
{{::description}}
diff --git a/client/item/src/data/index.html b/client/item/src/data/index.html
index 7e939e8112..c72a13dc37 100644
--- a/client/item/src/data/index.html
+++ b/client/item/src/data/index.html
@@ -30,7 +30,7 @@
show-field="description"
value-field="id"
field="$ctrl.item.intrastatFk"
- where="{or: [{id: {regexp: 'search'}}, {description: {regexp: 'search'}}]}"
+ search-function="{or: [{id: {regexp: $search}}, {description: {regexp: $search}}]}"
initial-data="$ctrl.item.intrastat">
{{::id}}
diff --git a/client/item/src/tags/index.html b/client/item/src/tags/index.html
index 05a98203eb..5afd154e60 100644
--- a/client/item/src/tags/index.html
+++ b/client/item/src/tags/index.html
@@ -3,7 +3,7 @@
url="/item/api/ItemTags"
fields="['id', 'itemFk', 'tagFk', 'value', 'priority']"
link="{itemFk: $ctrl.$stateParams.id}"
- filter="{include: {relation: 'tag'}}"
+ include="$ctrl.include"
order="priority ASC"
data="itemTags">
@@ -17,17 +17,16 @@
Tags
+ vn-focus>
{{itemFk}} : {{name}}
diff --git a/services/loopback/common/filter.js b/services/loopback/common/filter.js
new file mode 100644
index 0000000000..9f23b5330e
--- /dev/null
+++ b/services/loopback/common/filter.js
@@ -0,0 +1,92 @@
+
+/**
+ * Passes a loopback fields filter to an object.
+ *
+ * @param {Object} fields The fields object or array
+ * @return {Object} The fields as object
+ */
+function fieldsToObject(fields) {
+ let fieldsObj = {};
+
+ if (Array.isArray(fields))
+ for (let field of fields)
+ fieldsObj[field] = true;
+ else if (typeof fields == 'object')
+ for (let field in fields)
+ if (fields[field])
+ fieldsObj[field] = true;
+
+ return fieldsObj;
+}
+
+/**
+ * Merges two loopback fields filters.
+ *
+ * @param {Object|Array} src The source fields
+ * @param {Object|Array} dst The destination fields
+ * @return {Array} The merged fields as an array
+ */
+function mergeFields(src, dst) {
+ let fields = {};
+ Object.assign(fields,
+ fieldsToObject(src),
+ fieldsToObject(dst)
+ );
+ return Object.keys(fields);
+}
+
+/**
+ * Merges two loopback where filters.
+ *
+ * @param {Object|Array} src The source where
+ * @param {Object|Array} dst The destination where
+ * @return {Array} The merged wheres
+ */
+function mergeWhere(src, dst) {
+ let and = [];
+ if (src) and.push(src);
+ if (dst) and.push(dst);
+
+ switch (and.length) {
+ case 0:
+ return undefined;
+ case 1:
+ return and[0];
+ default:
+ return {and};
+ }
+}
+
+/**
+ * Merges two loopback filters returning the merged filter.
+ *
+ * @param {Object} src The source filter
+ * @param {Object} dst The destination filter
+ * @return {Object} The result filter
+ */
+function mergeFilters(src, dst) {
+ let res = Object.assign({}, dst);
+
+ if (!src)
+ return res;
+
+ if (src.fields)
+ res.fields = mergeFields(src.fields, res.fields);
+ if (src.where)
+ res.where = mergeWhere(res.where, src.where);
+ if (src.include)
+ res.include = src.include;
+ if (src.order)
+ res.order = src.order;
+ if (src.limit)
+ res.limit = src.limit;
+
+ return res;
+}
+
+module.exports = {
+ fieldsToObject: fieldsToObject,
+ mergeFields: mergeFields,
+ mergeWhere: mergeWhere,
+ mergeFilters: mergeFilters
+};