refactor
gitea/salix/2064-catalog_refactor This commit looks good Details

This commit is contained in:
Joan Sanchez 2020-01-30 13:53:14 +01:00
parent 31349b2ebf
commit 8f851d5c15
14 changed files with 629 additions and 654 deletions

View File

@ -604,14 +604,14 @@ export default {
orderByAutocomplete: 'vn-autocomplete[label="Order by"]', orderByAutocomplete: 'vn-autocomplete[label="Order by"]',
plantRealmButton: 'vn-order-catalog > vn-side-menu vn-icon[icon="icon-plant"]', plantRealmButton: 'vn-order-catalog > vn-side-menu vn-icon[icon="icon-plant"]',
typeAutocomplete: 'vn-autocomplete[data="$ctrl.itemTypes"]', typeAutocomplete: 'vn-autocomplete[data="$ctrl.itemTypes"]',
itemIdInput: 'vn-catalog-filter vn-textfield[ng-model="$ctrl.itemId"]', itemIdInput: 'vn-order-catalog > vn-side-menu vn-textfield[ng-model="$ctrl.itemId"]',
itemTagValueInput: 'vn-catalog-filter vn-textfield[ng-model="$ctrl.value"]', itemTagValueInput: 'vn-order-catalog > vn-side-menu vn-textfield[ng-model="$ctrl.value"]',
openTagSearch: 'vn-catalog-filter > div > vn-vertical > vn-textfield[ng-model="$ctrl.value"] .append i', openTagSearch: 'vn-order-catalog > vn-side-menu > div > vn-vertical > vn-textfield[ng-model="$ctrl.value"] .append i',
tagAutocomplete: 'vn-order-catalog-search-panel vn-autocomplete[ng-model="filter.tagFk"]', tagAutocomplete: 'vn-order-catalog-search-panel vn-autocomplete[ng-model="filter.tagFk"]',
tagValueInput: 'vn-order-catalog-search-panel [ng-model="filter.value"]', tagValueInput: 'vn-order-catalog-search-panel [ng-model="filter.value"]',
searchTagButton: 'vn-order-catalog-search-panel button[type=submit]', searchTagButton: 'vn-order-catalog-search-panel button[type=submit]',
thirdFilterRemoveButton: 'vn-catalog-filter .chips > vn-chip:nth-child(3) vn-icon[icon=cancel]', thirdFilterRemoveButton: 'vn-order-catalog > vn-side-menu .chips > vn-chip:nth-child(3) vn-icon[icon=cancel]',
fourthFilterRemoveButton: 'vn-catalog-filter .chips > vn-chip:nth-child(4) vn-icon[icon=cancel]', fourthFilterRemoveButton: 'vn-order-catalog > vn-side-menu .chips > vn-chip:nth-child(4) vn-icon[icon=cancel]',
}, },
orderBasicData: { orderBasicData: {
clientAutocomplete: 'vn-autocomplete[label="Client"]', clientAutocomplete: 'vn-autocomplete[label="Client"]',

View File

@ -0,0 +1,61 @@
<vn-data-viewer
model="$ctrl.model">
<vn-horizontal class="catalog-list">
<section class="product" ng-repeat="item in $ctrl.model.data">
<vn-card>
<div class="image">
<img
ng-src="//verdnatura.es/vn-image-data/catalog/200x200/{{::item.image}}"
zoom-image="//verdnatura.es/vn-image-data/catalog/1600x900/{{::item.image}}"
on-error-src/>
</div>
<div class="description">
<h3>
{{::item.name}}
</h3>
<h4 class="ellipsize">
<span translate-attr="::{title: item.subName}">{{::item.subName}}</span>
</h4>
<div class="tags">
<vn-label-value
ng-if="::item.value5"
label="{{::item.tag5}}"
value="{{::item.value5}}">
</vn-label-value>
<vn-label-value
ng-if="::item.value6"
label="{{::item.tag6}}"
value="{{::item.value6}}">
</vn-label-value>
<vn-label-value
ng-if="::item.value7"
label="{{::item.tag7}}"
value="{{::item.value7}}">
</vn-label-value>
</div>
<div class="footer">
<div class="price">
<vn-one>
<span>{{::item.available}}</span>
<span translate>from</span>
<span>{{::item.price | currency:'EUR':2}}</span>
</vn-one>
<vn-icon-button vn-none
icon="add_circle"
ng-click="$ctrl.preview($event, item)"
vn-tooltip="Add">
</vn-icon-button>
</div>
<div class="priceKg" ng-show="::item.priceKg">
<span>Precio por kilo {{::item.priceKg | currency: 'EUR'}}</span>
</div>
</div>
</div>
</vn-card>
</section>
</vn-horizontal>
</vn-data-viewer>
<vn-order-prices-popover
vn-id="pricesPopover"
order="$ctrl.order">
</vn-order-prices-popover>

View File

@ -0,0 +1,24 @@
import ngModule from '../module';
import Component from 'core/lib/component';
import './style.scss';
class Controller extends Component {
preview(event, item) {
event.preventDefault();
this.$.pricesPopover.show(event, item);
}
onDescriptorLoad() {
this.$.popover.relocate();
}
}
ngModule.component('vnOrderCatalogView', {
template: require('./index.html'),
controller: Controller,
bindings: {
order: '<',
model: '<'
}
});

View File

@ -0,0 +1,31 @@
@import "variables";
vn-order-catalog {
.catalog-header {
border-bottom: $border-thin;
padding: $spacing-md;
align-items: center;
& > vn-one {
display: flex;
flex: 1;
span {
color: $color-font-secondary
}
}
& > vn-auto {
width: 28em;
display: flex;
overflow: hidden;
& > * {
padding-left: $spacing-md;
}
}
}
.catalog-list {
padding-top: $spacing-sm;
}
}

View File

@ -1,72 +1,140 @@
<vn-crud-model
url="ItemCategories"
data="categories"
auto-load="true">
</vn-crud-model>
<vn-crud-model <vn-crud-model
vn-id="model" vn-id="model"
url="Orders/CatalogFilter" url="Orders/CatalogFilter"
params="{orderFk: $ctrl.$state.params.id}" params="{orderFk: $ctrl.$state.params.id}"
limit="50" limit="50"
data="items" data="$ctrl.items">
on-data-change="$ctrl.onDataChange()">
</vn-crud-model> </vn-crud-model>
<vn-data-viewer
model="model"> <vn-order-catalog-view model="model"
<vn-horizontal class="catalog-list">
<section class="product" ng-repeat="item in items">
<vn-card>
<div class="image">
<img
ng-src="//verdnatura.es/vn-image-data/catalog/200x200/{{::item.image}}"
zoom-image="//verdnatura.es/vn-image-data/catalog/1600x900/{{::item.image}}"
on-error-src/>
</div>
<div class="description">
<h3>
{{::item.name}}
</h3>
<h4 class="ellipsize">
<span translate-attr="::{title: item.subName}">{{::item.subName}}</span>
</h4>
<div class="tags">
<vn-label-value
ng-if="::item.value5"
label="{{::item.tag5}}"
value="{{::item.value5}}">
</vn-label-value>
<vn-label-value
ng-if="::item.value6"
label="{{::item.tag6}}"
value="{{::item.value6}}">
</vn-label-value>
<vn-label-value
ng-if="::item.value7"
label="{{::item.tag7}}"
value="{{::item.value7}}">
</vn-label-value>
</div>
<div class="footer">
<div class="price">
<vn-one>
<span>{{::item.available}}</span>
<span translate>from</span>
<span>{{::item.price | currency:'EUR':2}}</span>
</vn-one>
<vn-icon-button vn-none
icon="add_circle"
ng-click="$ctrl.preview($event, item)"
vn-tooltip="Add">
</vn-icon-button>
</div>
<div class="priceKg" ng-show="::item.priceKg">
<span>Precio por kilo {{::item.priceKg | currency: 'EUR'}}</span>
</div>
</div>
</div>
</vn-card>
</section>
</vn-horizontal>
</vn-data-viewer>
<vn-side-menu side="right">
<vn-catalog-filter order="$ctrl.order"></vn-catalog-filter>
</vn-side-menu>
<vn-order-prices-popover
vn-id="pricesPopover"
order="$ctrl.order"> order="$ctrl.order">
</vn-order-prices-popover> </vn-order-catalog-view>
<vn-side-menu side="right">
<vn-horizontal class="item-category">
<vn-autocomplete vn-id="category"
data="categories"
ng-model="$ctrl.categoryId"
show-field="name"
value-field="id"
label="Category">
</vn-autocomplete>
<vn-one ng-repeat="category in categories">
<vn-icon
ng-class="{'active': $ctrl.categoryId == category.id}"
icon="{{::category.icon}}"
vn-tooltip="{{::category.name}}"
ng-click="$ctrl.categoryId = category.id">
</vn-icon>
</vn-one>
</vn-horizontal>
<vn-vertical class="input">
<vn-autocomplete vn-id="type"
data="$ctrl.itemTypes"
ng-model="$ctrl.typeId"
show-field="name"
value-field="id"
label="Type"
fields="['categoryFk']"
include="'category'">
<tpl-item>
<div>{{name}}</div>
<div class="text-caption text-secondary">
{{categoryName}}
</div>
</tpl-item>
</vn-autocomplete>
</vn-vertical>
<vn-vertical class="input vn-pt-md">
<vn-autocomplete
vn-id="field"
data="$ctrl.orderFields"
ng-model="$ctrl.orderField"
selection="$ctrl.orderSelection"
translate-fields="['name']"
order="name"
show-field="name"
value-field="field"
label="Order by"
disabled="!model.data">
</vn-autocomplete>
<vn-autocomplete
data="$ctrl.orderWays"
ng-model="$ctrl.orderWay"
translate-fields="['name']"
show-field="name"
value-field="way"
label="Order"
disabled="!model.data">
</vn-autocomplete>
<div ng-if="false && model.moreRows">
<span translate>More than</span> {{model.limit}} <span translate>results</span>
</div>
</vn-vertical>
<vn-vertical class="input vn-pt-md">
<vn-textfield
ng-keyUp="$ctrl.onSearchById($event)"
label="Item id"
ng-model="$ctrl.itemId">
<prepend>
<vn-icon icon="icon-item"></vn-icon>
</prepend>
</vn-textfield>
<vn-textfield
vn-one
vn-id="search"
ng-keyUp="$ctrl.onSearchByTag($event)"
label="Search tag"
ng-model="$ctrl.value">
<prepend>
<vn-icon icon="search"></vn-icon>
</prepend>
<append>
<vn-icon
icon="keyboard_arrow_down"
ng-click="$ctrl.openPanel($event)"
style="cursor: pointer;">
</vn-icon>
</append>
</vn-textfield>
</vn-vertical>
<vn-popover
vn-id="popover"
on-close="$ctrl.onPopoverClose()">
<vn-order-catalog-search-panel
filter="panelFilter"
on-submit="$ctrl.onPanelSubmit($filter)">
</vn-order-catalog-search-panel>
</vn-popover>
<div class="chips">
<vn-chip
ng-if="category.selection"
removable="true"
translate-attr="{title: 'Category'}"
on-remove="$ctrl.categoryId = null"
class="colored">
<span translate>{{category.selection.name}}</span>
</vn-chip>
<vn-chip
ng-if="type.selection"
removable="true"
translate-attr="{title: 'Type'}"
on-remove="$ctrl.typeId = null"
class="colored">
<span translate>{{type.selection.name}}</span>
</vn-chip>
<vn-chip
ng-repeat="tag in $ctrl.tags"
removable="true"
translate-attr="{title: 'Tag'}"
on-remove="$ctrl.remove($index)"
class="colored">
<span translate>{{::tag.value}}</span>
</vn-chip>
</div>
</vn-side-menu>

View File

@ -2,10 +2,15 @@ import ngModule from '../module';
import './style.scss'; import './style.scss';
class Controller { class Controller {
constructor($scope, $state) { constructor($http, $scope, $state, $compile, $transitions) {
this.$scope = $scope; this.$http = $http;
this.$ = $scope;
this.$state = $state; this.$state = $state;
this.$stateParams = $state.params; this.$stateParams = $state.params;
this.$compile = $compile;
this.$transitions = $transitions;
this.itemTypes = [];
this.tags = [];
// Static autocomplete data // Static autocomplete data
this.orderWays = [ this.orderWays = [
@ -23,16 +28,49 @@ class Controller {
this.orderField = this.orderFields[0].field; this.orderField = this.orderFields[0].field;
} }
$onChanges() {
if (this.order && this.order.isConfirmed)
this.$state.go('order.card.line');
}
/** /**
* Fills order autocomplete with tags * Fills order autocomplete with tags
* obtained from last filtered * obtained from last filtered
*/ */
onDataChange() { get order() {
const items = this.$scope.model.data; return this._order;
const newFilterList = []; }
if (!items) return;
items.forEach(item => { /**
* Sets filter values from state params
*
* @param {Object} value - Order data
*/
set order(value) {
this._order = value;
if (!value) return;
this.$.$applyAsync(() => {
if (this.$stateParams.categoryId)
this.categoryId = this.$stateParams.categoryId;
if (this.$stateParams.typeId)
this.typeId = this.$stateParams.typeId;
});
}
get items() {
return this._items;
}
set items(value) {
this._items = value;
if (!value) return;
const newFilterList = [];
value.forEach(item => {
// Add new tag filters // Add new tag filters
item.tags.forEach(itemTag => { item.tags.forEach(itemTag => {
const alreadyAdded = newFilterList.findIndex(filter => { const alreadyAdded = newFilterList.findIndex(filter => {
@ -64,6 +102,40 @@ class Controller {
this.orderFields = newFilterList; this.orderFields = newFilterList;
} }
get categoryId() {
return this._categoryId;
}
set categoryId(value) {
if (!value || (this.categoryId == value))
value = null;
this._categoryId = value;
this.itemTypes = [];
this.typeId = null;
this.updateStateParams();
if (this.tags.length > 0)
this.applyFilters();
if (value)
this.updateItemTypes();
}
get typeId() {
return this._typeId;
}
set typeId(value) {
this._typeId = value;
this.updateStateParams();
if (value || this.tags.length > 0)
this.applyFilters();
}
/** /**
* Get order way ASC/DESC * Get order way ASC/DESC
*/ */
@ -76,6 +148,9 @@ class Controller {
if (value) this.applyOrder(); if (value) this.applyOrder();
} }
/**
* Returns the order way selection
*/
get orderSelection() { get orderSelection() {
return this._orderSelection; return this._orderSelection;
} }
@ -86,6 +161,14 @@ class Controller {
if (value) this.applyOrder(); if (value) this.applyOrder();
} }
/**
* Apply order to model
*/
applyOrder() {
if (this.typeId || this.tags.length > 0)
this.$.model.addFilter(null, {orderBy: this.getOrderBy()});
}
/** /**
* Returns order param * Returns order param
* *
@ -101,33 +184,101 @@ class Controller {
} }
/** /**
* Apply order to model * Refreshes item type dropdown data
*/ */
applyOrder() { updateItemTypes() {
this.$scope.model.addFilter(null, {orderBy: this.getOrderBy()}); let params = {
itemCategoryId: this.categoryId
};
const query = `Orders/${this.order.id}/getItemTypeAvailable`;
this.$http.get(query, {params}).then(res =>
this.itemTypes = res.data);
} }
preview(event, item) { onSearchById(event) {
const hasValue = this.tags.length > 0 || this.itemId || this.typeId;
if (event.key === 'Enter' && hasValue)
this.applyFilters();
}
onSearchByTag(event) {
if (event.key !== 'Enter' || !this.value) return;
this.tags.push({
value: this.value,
});
this.$.search.value = null;
this.applyFilters();
}
remove(index) {
this.tags.splice(index, 1);
if (this.tags.length >= 0 || this.itemId || this.typeId)
this.applyFilters();
}
applyFilters() {
let newParams = {};
let newFilter = {};
const model = this.$.model;
if (this.categoryId)
newFilter.categoryFk = this.categoryId;
if (this.typeId)
newFilter.typeFk = this.typeId;
if (this.itemId)
newFilter = {'i.id': this.itemId};
newParams = {
orderFk: this.order.id,
orderBy: this.getOrderBy(),
tags: this.tags,
};
model.applyFilter({where: newFilter}, newParams);
}
openPanel(event) {
if (event.defaultPrevented) return;
event.preventDefault(); event.preventDefault();
this.$scope.pricesPopover.show(event, item);
this.panelFilter = {};
this.$.popover.show(this.$.search.element);
} }
onDescriptorLoad() { onPanelSubmit(filter) {
this.$scope.popover.relocate(); this.$.popover.hide();
this.tags.push(filter);
this.applyFilters();
} }
$onChanges() { /**
if (this.order && this.order.isConfirmed) * Updates url state params from filter values
this.$state.go('order.card.line'); */
updateStateParams() {
const params = {};
if (this.categoryId)
params.categoryId = this.categoryId;
else params.categoryId = undefined;
if (this.typeId)
params.typeId = this.typeId;
else params.typeId = undefined;
this.$state.go(this.$state.current.name, params);
} }
} }
Controller.$inject = ['$scope', '$state']; Controller.$inject = ['$http', '$scope', '$state', '$compile', '$transitions'];
ngModule.component('vnOrderCatalog', { ngModule.component('vnOrderCatalog', {
template: require('./index.html'), template: require('./index.html'),
controller: Controller, controller: Controller,
bindings: { bindings: {
order: '<', order: '<'
} }
}); });

View File

@ -1,29 +1,47 @@
import './index.js'; import './index.js';
import crudModel from 'core/mocks/crud-model'; import crudModel from 'core/mocks/crud-model';
describe('Order', () => { fdescribe('Order', () => {
describe('Component vnOrderCatalog', () => { describe('Component vnOrderCatalog', () => {
let $scope; let $scope;
let $state;
let controller; let controller;
let $httpBackend;
beforeEach(ngModule('order')); beforeEach(ngModule('order'));
beforeEach(angular.mock.inject(($componentController, $rootScope) => { beforeEach(angular.mock.inject(($componentController, _$state_, _$httpBackend_, $rootScope) => {
$httpBackend = _$httpBackend_;
$scope = $rootScope.$new(); $scope = $rootScope.$new();
$scope.model = crudModel; $scope.model = crudModel;
$scope.field = {}; $scope.search = {};
controller = $componentController('vnOrderCatalog', {$scope: $scope}); $state = _$state_;
$state.params.categoryId = 1;
$state.params.typeId = 2;
$state.current.name = 'my.current.state';
controller = $componentController('vnOrderCatalog', {$scope, $state});
})); }));
describe('onDataChange()', () => { describe('order() setter', () => {
it(`should call scope $applyAsync() method and apply filters from state params`, () => {
$httpBackend.expect('GET', `Orders/4/getItemTypeAvailable?itemCategoryId=1`).respond();
controller.order = {id: 4};
$scope.$apply();
expect(controller.categoryId).toEqual(1);
expect(controller.typeId).toEqual(2);
});
});
describe('items() setter', () => {
it(`should return an object with order params`, () => { it(`should return an object with order params`, () => {
$scope.model.data = [{id: 1, name: 'My Item', tags: [ let expectedResult = [{field: 'showOrder, price', name: 'Color'}];
let unexpectedResult = [{tagFk: 5, name: 'Color'}];
controller.items = [{id: 1, name: 'My Item', tags: [
{tagFk: 4, name: 'Length'}, {tagFk: 4, name: 'Length'},
{tagFk: 5, name: 'Color'} {tagFk: 5, name: 'Color'}
]}]; ]}];
let expectedResult = [{field: 'showOrder, price', name: 'Color'}];
let unexpectedResult = [{tagFk: 5, name: 'Color'}];
controller.onDataChange();
expect(controller.orderFields.length).toEqual(5); expect(controller.orderFields.length).toEqual(5);
expect(controller.orderFields).toEqual(jasmine.arrayContaining(expectedResult)); expect(controller.orderFields).toEqual(jasmine.arrayContaining(expectedResult));
@ -31,6 +49,134 @@ describe('Order', () => {
}); });
}); });
describe('categoryId() setter', () => {
it(`should set category property to null, call updateStateParams() method and not call applyFilters()`, () => {
spyOn(controller, 'updateStateParams');
controller.categoryId = null;
expect(controller.updateStateParams).toHaveBeenCalledWith();
});
it(`should set category property and then call updateStateParams() and applyFilters() methods`, () => {
spyOn(controller, 'updateStateParams');
controller._order = {id: 4};
controller.categoryId = 2;
expect(controller.updateStateParams).toHaveBeenCalledWith();
});
});
describe('typeId() setter', () => {
it(`should set type property to null, call updateStateParams() method and not call applyFilters()`, () => {
spyOn(controller, 'updateStateParams');
spyOn(controller, 'applyFilters');
controller.typeId = null;
expect(controller.updateStateParams).toHaveBeenCalledWith();
expect(controller.applyFilters).not.toHaveBeenCalledWith();
});
it(`should set category property and then call updateStateParams() and applyFilters() methods`, () => {
spyOn(controller, 'updateStateParams');
spyOn(controller, 'applyFilters');
controller.typeId = 2;
expect(controller.updateStateParams).toHaveBeenCalledWith();
expect(controller.applyFilters).toHaveBeenCalledWith();
});
});
describe('onSearchByTag()', () => {
it(`should not add a new tag if the event key code doesn't equals to 'Enter'`, () => {
spyOn(controller, 'applyFilters');
controller.order = {id: 4};
controller.value = 'Color';
controller.onSearchByTag({key: 'Tab'});
expect(controller.applyFilters).not.toHaveBeenCalledWith();
});
it(`should add a new tag if the event key code equals to 'Enter' an then call applyFilters()`, () => {
spyOn(controller, 'applyFilters');
controller.order = {id: 4};
controller.value = 'Color';
controller.onSearchByTag({key: 'Enter'});
expect(controller.applyFilters).toHaveBeenCalledWith();
});
});
describe('onSearchById()', () => {
it(`should not filter by id if the event key code doesn't equals to 'Enter'`, () => {
spyOn(controller, 'applyFilters');
controller.itemId = 1;
controller.onSearchById({key: 'Tab'});
expect(controller.applyFilters).not.toHaveBeenCalledWith();
});
it(`should filter by id if the event key code equals to 'Enter' an then call applyFilters()`, () => {
spyOn(controller, 'applyFilters');
controller.itemId = 1;
controller.onSearchById({key: 'Enter'});
expect(controller.applyFilters).toHaveBeenCalledWith();
});
});
describe('applyFilters()', () => {
it(`should call model applyFilter() method with a new filter`, () => {
let model = controller.$.model;
spyOn(model, 'applyFilter');
controller._categoryId = 2;
controller._typeId = 4;
controller._order = {id: 4};
controller.applyFilters();
expect(model.applyFilter).toHaveBeenCalledWith(
{where: {categoryFk: 2, typeFk: 4}},
{orderFk: 4, orderBy: controller.getOrderBy(), tags: []});
});
});
describe('remove()', () => {
it(`should remove a tag from tags property`, () => {
spyOn(controller, 'applyFilters');
controller.tags = [{tagFk: 1, value: 'Blue'}, {tagFk: 2, value: '70'}];
controller.remove(0);
expect(controller.tags.length).toEqual(1);
expect(controller.tags[0].tagFk).toEqual(2);
expect(controller.applyFilters).toHaveBeenCalledWith();
});
it(`should remove a tag from tags property and call applyFilters() if there's no more tags`, () => {
spyOn(controller, 'applyFilters');
controller._categoryId = 1;
controller._typeId = 1;
controller.tags = [{tagFk: 1, value: 'Blue'}];
controller.remove(0);
expect(controller.tags.length).toEqual(0);
expect(controller.applyFilters).toHaveBeenCalledWith();
});
});
describe('updateStateParams()', () => {
it(`should call state go() method passing category and type state params`, () => {
spyOn(controller.$state, 'go');
controller._categoryId = 2;
controller._typeId = 4;
let result = {categoryId: 2, typeId: 4};
controller.updateStateParams();
expect(controller.$state.go).toHaveBeenCalledWith('my.current.state', result);
});
});
describe('getOrderBy()', () => { describe('getOrderBy()', () => {
it(`should return an object with order params`, () => { it(`should return an object with order params`, () => {
controller.orderField = 'relevancy DESC, name'; controller.orderField = 'relevancy DESC, name';
@ -50,13 +196,15 @@ describe('Order', () => {
it(`should apply order param to model calling getOrderBy()`, () => { it(`should apply order param to model calling getOrderBy()`, () => {
controller.field = 'relevancy DESC, name'; controller.field = 'relevancy DESC, name';
controller.way = 'ASC'; controller.way = 'ASC';
controller._categoryId = 1;
controller._typeId = 1;
let expectedOrder = {orderBy: controller.getOrderBy()}; let expectedOrder = {orderBy: controller.getOrderBy()};
spyOn(controller, 'getOrderBy').and.callThrough(); spyOn(controller, 'getOrderBy').and.callThrough();
spyOn(controller.$scope.model, 'addFilter'); spyOn(controller.$.model, 'addFilter');
controller.applyOrder(); controller.applyOrder();
expect(controller.getOrderBy).toHaveBeenCalledWith(); expect(controller.getOrderBy).toHaveBeenCalledWith();
expect(controller.$scope.model.addFilter).toHaveBeenCalledWith(null, expectedOrder); expect(controller.$.model.addFilter).toHaveBeenCalledWith(null, expectedOrder);
}); });
}); });
}); });

View File

@ -1,31 +1,54 @@
@import "variables"; @import "variables";
vn-order-catalog { vn-order-catalog vn-side-menu div {
.catalog-header { & > .input {
padding-left: $spacing-md;
padding-right: $spacing-md;
border-color: $color-spacer;
border-bottom: $border-thin; border-bottom: $border-thin;
padding: $spacing-md; }
align-items: center; .item-category {
padding: $spacing-sm;
justify-content: flex-start;
align-items: flex-start;
flex-wrap: wrap;
vn-autocomplete[vn-id="category"] {
display: none
}
& > vn-one { & > vn-one {
display: flex; padding: $spacing-sm;
flex: 1; min-width: 33.33%;
text-align: center;
box-sizing: border-box;
span { & > vn-icon {
color: $color-font-secondary padding: $spacing-sm;
background-color: $color-font-secondary;
border-radius: 50%;
cursor: pointer;
&.active {
background-color: $color-main;
color: #FFF
}
& > i:before {
font-size: 32pt;
width: 1em;
height: 1em;
} }
} }
& > vn-auto { }
width: 28em; }
.chips {
display: flex; display: flex;
flex-wrap: wrap;
padding: $spacing-md;
overflow: hidden; overflow: hidden;
max-width: 100%;
& > * { }
padding-left: $spacing-md; vn-autocomplete[vn-id="type"] .list {
max-height: 20em
} }
} }
}
.catalog-list {
padding-top: $spacing-sm;
}
}

View File

@ -1,129 +0,0 @@
<vn-crud-model
vn-id="model"
url="ItemCategories"
data="categories"
auto-load="true">
</vn-crud-model>
<div>
<vn-horizontal class="item-category">
<vn-autocomplete vn-id="category"
data="categories"
ng-model="$ctrl.categoryId"
show-field="name"
value-field="id"
label="Category">
</vn-autocomplete>
<vn-one ng-repeat="category in categories">
<vn-icon
ng-class="{'active': $ctrl.categoryId == category.id}"
icon="{{::category.icon}}"
vn-tooltip="{{::category.name}}"
ng-click="$ctrl.categoryId = category.id">
</vn-icon>
</vn-one>
</vn-horizontal>
<vn-vertical class="input">
<vn-autocomplete vn-id="type"
data="$ctrl.itemTypes"
ng-model="$ctrl.typeId"
show-field="name"
value-field="id"
label="Type"
fields="['categoryFk']"
include="'category'">
<tpl-item>
<div>{{name}}</div>
<div class="text-caption text-secondary">
{{categoryName}}
</div>
</tpl-item>
</vn-autocomplete>
</vn-vertical>
<vn-vertical class="input vn-pt-md">
<vn-autocomplete
vn-id="field"
data="$ctrl.catalog.orderFields"
ng-model="$ctrl.catalog.orderField"
selection="$ctrl.catalog.orderSelection"
translate-fields="['name']"
order="name"
show-field="name"
value-field="field"
label="Order by"
disabled="!model.data">
</vn-autocomplete>
<vn-autocomplete
data="$ctrl.catalog.orderWays"
ng-model="$ctrl.catalog.orderWay"
translate-fields="['name']"
show-field="name"
value-field="way"
label="Order"
disabled="!model.data">
</vn-autocomplete>
<div ng-if="false && model.moreRows">
<span translate>More than</span> {{model.limit}} <span translate>results</span>
</div>
</vn-vertical>
<vn-vertical class="input vn-pt-md">
<vn-textfield
ng-keyUp="$ctrl.onSearchById($event)"
label="Item id"
ng-model="$ctrl.itemId">
<prepend>
<vn-icon icon="icon-item"></vn-icon>
</prepend>
</vn-textfield>
<vn-textfield
vn-one
vn-id="search"
ng-keyUp="$ctrl.onSearchByTag($event)"
label="Search tag"
ng-model="$ctrl.value">
<prepend>
<vn-icon icon="search"></vn-icon>
</prepend>
<append>
<vn-icon
icon="keyboard_arrow_down"
ng-click="$ctrl.openPanel($event)"
style="cursor: pointer;">
</vn-icon>
</append>
</vn-textfield>
</vn-vertical>
<vn-popover
vn-id="popover"
on-close="$ctrl.onPopoverClose()">
<vn-order-catalog-search-panel
filter="panelFilter"
on-submit="$ctrl.onPanelSubmit($filter)">
</vn-order-catalog-search-panel>
</vn-popover>
<div class="chips">
<vn-chip
ng-if="category.selection"
removable="true"
translate-attr="{title: 'Category'}"
on-remove="$ctrl.categoryId = null"
class="colored">
<span translate>{{category.selection.name}}</span>
</vn-chip>
<vn-chip
ng-if="type.selection"
removable="true"
translate-attr="{title: 'Type'}"
on-remove="$ctrl.typeId = null"
class="colored">
<span translate>{{type.selection.name}}</span>
</vn-chip>
<vn-chip
ng-repeat="tag in $ctrl.tags"
removable="true"
translate-attr="{title: 'Tag'}"
on-remove="$ctrl.remove($index)"
class="colored">
<span translate>{{::tag.value}}</span>
</vn-chip>
</div>
</div>

View File

@ -1,175 +0,0 @@
import ngModule from '../module';
import './style.scss';
class Controller {
constructor($element, $http, $scope, $state, $compile, $transitions) {
this.$element = $element;
this.$http = $http;
this.$ = $scope;
this.$state = $state;
this.$stateParams = $state.params;
this.$compile = $compile;
this.$transitions = $transitions;
this.itemTypes = [];
this.tags = [];
}
get order() {
return this._order;
}
/**
* Sets filter values from state params
*
* @param {Object} value - Order data
*/
set order(value) {
this._order = value;
if (!value) return;
this.$.$applyAsync(() => {
if (this.$stateParams.categoryId)
this.categoryId = this.$stateParams.categoryId;
if (this.$stateParams.typeId)
this.typeId = this.$stateParams.typeId;
});
}
get categoryId() {
return this._categoryId;
}
set categoryId(value) {
if (!value || (this.categoryId == value))
value = null;
this._categoryId = value;
this.itemTypes = [];
this.typeId = null;
this.updateStateParams();
if (this.tags.length > 0)
this.applyFilters();
if (value)
this.updateItemTypes();
}
get typeId() {
return this._typeId;
}
set typeId(value) {
this._typeId = value;
this.updateStateParams();
if ((value) || this.tags.length > 0)
this.applyFilters();
}
/**
* Refreshes item type dropdown data
*/
updateItemTypes() {
let params = {
itemCategoryId: this.categoryId
};
const query = `Orders/${this.order.id}/getItemTypeAvailable`;
this.$http.get(query, {params}).then(res =>
this.itemTypes = res.data);
}
onSearchById(event) {
const hasValue = this.tags.length > 0 || this.itemId || this.typeId;
if (event.key === 'Enter' && hasValue)
this.applyFilters();
}
onSearchByTag(event) {
if (event.key !== 'Enter' || !this.value) return;
this.tags.push({
value: this.value,
});
this.$.search.value = null;
this.applyFilters();
}
remove(index) {
this.tags.splice(index, 1);
if (this.tags.length >= 0 || this.itemId || this.typeId)
this.applyFilters();
}
applyFilters() {
let newParams = {};
let newFilter = {};
const model = this.catalog.$scope.model;
if (this.categoryId)
newFilter.categoryFk = this.categoryId;
if (this.typeId)
newFilter.typeFk = this.typeId;
if (this.itemId)
newFilter = {'i.id': this.itemId};
newParams = {
orderFk: this.order.id,
orderBy: this.catalog.getOrderBy(),
tags: this.tags,
};
model.applyFilter({where: newFilter}, newParams);
}
openPanel(event) {
if (event.defaultPrevented) return;
event.preventDefault();
this.panelFilter = {};
this.$.popover.show(this.$.search.element);
}
onPanelSubmit(filter) {
this.$.popover.hide();
this.tags.push(filter);
this.applyFilters();
}
/**
* Updates url state params from filter values
*/
updateStateParams() {
const params = {};
if (this.categoryId)
params.categoryId = this.categoryId;
else params.categoryId = undefined;
if (this.typeId)
params.typeId = this.typeId;
else params.typeId = undefined;
this.$state.go(this.$state.current.name, params);
}
}
Controller.$inject = ['$element', '$http', '$scope', '$state', '$compile', '$transitions'];
ngModule.component('vnCatalogFilter', {
template: require('./index.html'),
controller: Controller,
require: {
catalog: '^vnOrderCatalog'
},
bindings: {
order: '<'
}
});

View File

@ -1,172 +0,0 @@
import './index.js';
import crudModel from 'core/mocks/crud-model';
describe('Order', () => {
describe('Component vnCatalogFilter', () => {
let $scope;
let $state;
let controller;
let $httpBackend;
beforeEach(ngModule('order'));
beforeEach(angular.mock.inject(($componentController, _$state_, _$httpBackend_, $rootScope) => {
$httpBackend = _$httpBackend_;
$scope = $rootScope.$new();
$scope.model = crudModel;
$scope.search = {};
$state = _$state_;
$state.params.categoryId = 1;
$state.params.typeId = 2;
$state.current.name = 'my.current.state';
controller = $componentController('vnCatalogFilter', {$element: null, $scope, $state});
controller.catalog = {
$scope: $scope,
getOrderBy: () => {
return {field: 'relevancy DESC, name', way: 'DESC'};
}
};
}));
describe('order() setter', () => {
it(`should call scope $applyAsync() method and apply filters from state params`, () => {
$httpBackend.expect('GET', `Orders/4/getItemTypeAvailable?itemCategoryId=1`).respond();
controller.order = {id: 4};
$scope.$apply();
expect(controller.categoryId).toEqual(1);
expect(controller.typeId).toEqual(2);
});
});
describe('categoryId() setter', () => {
it(`should set category property to null, call updateStateParams() method and not call applyFilters()`, () => {
spyOn(controller, 'updateStateParams');
controller.categoryId = null;
expect(controller.updateStateParams).toHaveBeenCalledWith();
});
it(`should set category property and then call updateStateParams() and applyFilters() methods`, () => {
spyOn(controller, 'updateStateParams');
controller._order = {id: 4};
controller.categoryId = 2;
expect(controller.updateStateParams).toHaveBeenCalledWith();
});
});
describe('typeId() setter', () => {
it(`should set type property to null, call updateStateParams() method and not call applyFilters()`, () => {
spyOn(controller, 'updateStateParams');
spyOn(controller, 'applyFilters');
controller.typeId = null;
expect(controller.updateStateParams).toHaveBeenCalledWith();
expect(controller.applyFilters).not.toHaveBeenCalledWith();
});
it(`should set category property and then call updateStateParams() and applyFilters() methods`, () => {
spyOn(controller, 'updateStateParams');
spyOn(controller, 'applyFilters');
controller.typeId = 2;
expect(controller.updateStateParams).toHaveBeenCalledWith();
expect(controller.applyFilters).toHaveBeenCalledWith();
});
});
describe('onSearchByTag()', () => {
it(`should not add a new tag if the event key code doesn't equals to 'Enter'`, () => {
spyOn(controller, 'applyFilters');
controller.order = {id: 4};
controller.value = 'Color';
controller.onSearchByTag({key: 'Tab'});
expect(controller.applyFilters).not.toHaveBeenCalledWith();
});
it(`should add a new tag if the event key code equals to 'Enter' an then call applyFilters()`, () => {
spyOn(controller, 'applyFilters');
controller.order = {id: 4};
controller.value = 'Color';
controller.onSearchByTag({key: 'Enter'});
expect(controller.applyFilters).toHaveBeenCalledWith();
});
});
describe('onSearchById()', () => {
it(`should not filter by id if the event key code doesn't equals to 'Enter'`, () => {
spyOn(controller, 'applyFilters');
controller.itemId = 1;
controller.onSearchById({key: 'Tab'});
expect(controller.applyFilters).not.toHaveBeenCalledWith();
});
it(`should filter by id if the event key code equals to 'Enter' an then call applyFilters()`, () => {
spyOn(controller, 'applyFilters');
controller.itemId = 1;
controller.onSearchById({key: 'Enter'});
expect(controller.applyFilters).toHaveBeenCalledWith();
});
});
describe('applyFilters()', () => {
it(`should call model applyFilter() method with a new filter`, () => {
let model = controller.catalog.$scope.model;
spyOn(model, 'applyFilter');
controller._categoryId = 2;
controller._typeId = 4;
controller._order = {id: 4};
controller.applyFilters();
expect(model.applyFilter).toHaveBeenCalledWith(
{where: {categoryFk: 2, typeFk: 4}},
{orderFk: 4, orderBy: controller.catalog.getOrderBy(), tags: []});
});
});
describe('remove()', () => {
it(`should remove a tag from tags property`, () => {
spyOn(controller, 'applyFilters');
controller.tags = [{tagFk: 1, value: 'Blue'}, {tagFk: 2, value: '70'}];
controller.remove(0);
expect(controller.tags.length).toEqual(1);
expect(controller.tags[0].tagFk).toEqual(2);
expect(controller.applyFilters).toHaveBeenCalledWith();
});
it(`should remove a tag from tags property and call applyFilters() if there's no more tags`, () => {
spyOn(controller, 'applyFilters');
controller._categoryId = 1;
controller._typeId = 1;
controller.tags = [{tagFk: 1, value: 'Blue'}];
controller.remove(0);
expect(controller.tags.length).toEqual(0);
expect(controller.applyFilters).toHaveBeenCalledWith();
});
});
describe('updateStateParams()', () => {
it(`should call state go() method passing category and type state params`, () => {
spyOn(controller.$state, 'go');
controller._categoryId = 2;
controller._typeId = 4;
let result = {categoryId: 2, typeId: 4};
controller.updateStateParams();
expect(controller.$state.go).toHaveBeenCalledWith('my.current.state', result);
});
});
});
});

View File

@ -1,55 +0,0 @@
@import "variables";
@import "variables";
vn-catalog-filter > div {
& > .input {
padding-left: $spacing-md;
padding-right: $spacing-md;
border-color: $color-spacer;
border-bottom: $border-thin;
}
.item-category {
padding: $spacing-sm;
justify-content: flex-start;
align-items: flex-start;
flex-wrap: wrap;
vn-autocomplete[vn-id="category"] {
display: none
}
& > vn-one {
padding: $spacing-sm;
min-width: 33.33%;
text-align: center;
box-sizing: border-box;
& > vn-icon {
padding: $spacing-sm;
background-color: $color-font-secondary;
border-radius: 50%;
cursor: pointer;
&.active {
background-color: $color-main;
color: #FFF
}
& > i:before {
font-size: 32pt;
width: 1em;
height: 1em;
}
}
}
}
.chips {
display: flex;
flex-wrap: wrap;
padding: $spacing-md;
overflow: hidden;
max-width: 100%;
}
vn-autocomplete[vn-id="type"] .list {
max-height: 20em
}
}

View File

@ -6,9 +6,9 @@ import './card';
import './descriptor'; import './descriptor';
import './search-panel'; import './search-panel';
import './catalog-search-panel'; import './catalog-search-panel';
import './filter'; import './catalog-view';
import './summary';
import './catalog'; import './catalog';
import './summary';
import './line'; import './line';
import './prices-popover'; import './prices-popover';
import './volume'; import './volume';