Merge pull request 'fixes #5418 Buscador lateral en item/fixed-price' (!1412) from 5418-buscador-lateral-fixed-price into dev
gitea/salix/pipeline/head This commit looks good Details

Reviewed-on: #1412
Reviewed-by: Juan Ferrer <juan@verdnatura.es>
This commit is contained in:
Alexandre Riera 2023-04-18 08:49:53 +00:00
commit 11b905138f
10 changed files with 465 additions and 166 deletions

View File

@ -8,13 +8,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [2316.01] - 2023-05-04 ## [2316.01] - 2023-05-04
### Added ### Added
-
### Changed
- -
### Changed
- (Artículo -> Precio fijado) Modificado el buscador superior por uno lateral
### Fixed ### Fixed
- -
## [2314.01] - 2023-04-20 ## [2314.01] - 2023-04-20

View File

@ -414,7 +414,7 @@ export default {
saveFieldsButton: '.vn-popover.shown vn-button[label="Save"] > button' saveFieldsButton: '.vn-popover.shown vn-button[label="Save"] > button'
}, },
itemFixedPrice: { itemFixedPrice: {
add: 'vn-fixed-price vn-icon-button[icon="add_circle"]', add: 'vn-fixed-price vn-icon-button[vn-tooltip="Add fixed price"]',
firstItemID: 'vn-fixed-price tr:nth-child(2) vn-autocomplete[ng-model="price.itemFk"]', firstItemID: 'vn-fixed-price tr:nth-child(2) vn-autocomplete[ng-model="price.itemFk"]',
fourthFixedPrice: 'vn-fixed-price tr:nth-child(5)', fourthFixedPrice: 'vn-fixed-price tr:nth-child(5)',
fourthItemID: 'vn-fixed-price tr:nth-child(5) vn-autocomplete[ng-model="price.itemFk"]', fourthItemID: 'vn-fixed-price tr:nth-child(5) vn-autocomplete[ng-model="price.itemFk"]',
@ -427,7 +427,18 @@ export default {
fourthEnded: 'vn-fixed-price tr:nth-child(5) vn-date-picker[ng-model="price.ended"]', fourthEnded: 'vn-fixed-price tr:nth-child(5) vn-date-picker[ng-model="price.ended"]',
fourthDeleteIcon: 'vn-fixed-price tr:nth-child(5) > td:nth-child(9) > vn-icon-button[icon="delete"]', fourthDeleteIcon: 'vn-fixed-price tr:nth-child(5) > td:nth-child(9) > vn-icon-button[icon="delete"]',
orderColumnId: 'vn-fixed-price th[field="itemFk"]', orderColumnId: 'vn-fixed-price th[field="itemFk"]',
removeWarehouseFilter: 'vn-searchbar > form > vn-textfield > div.container > div.prepend > prepend > div > span:nth-child(1) > vn-icon > i' removeWarehouseFilter: 'vn-searchbar > form > vn-textfield > div.container > div.prepend > prepend > div > span:nth-child(1) > vn-icon > i',
generalSearchFilter: 'vn-fixed-price-search-panel vn-textfield[ng-model="$ctrl.filter.search"]',
reignFilter: 'vn-fixed-price-search-panel vn-horizontal.item-category vn-one',
typeFilter: 'vn-fixed-price-search-panel vn-autocomplete[ng-model="$ctrl.filter.typeFk"]',
buyerFilter: 'vn-fixed-price-search-panel vn-autocomplete[ng-model="$ctrl.filter.buyerFk"]',
warehouseFilter: 'vn-fixed-price-search-panel vn-autocomplete[ng-model="$ctrl.filter.warehouseFk"]',
mineFilter: 'vn-fixed-price-search-panel vn-check[ng-model="$ctrl.filter.mine"]',
hasMinPriceFilter: 'vn-fixed-price-search-panel vn-check[ng-model="$ctrl.filter.hasMinPrice"]',
addTag: 'vn-fixed-price-search-panel vn-icon-button[icon="add_circle"]',
tagFilter: 'vn-fixed-price-search-panel vn-autocomplete[ng-model="itemTag.tagFk"]',
tagValueFilter: 'vn-fixed-price-search-panel vn-autocomplete[ng-model="itemTag.value"]',
chip: 'vn-fixed-price-search-panel vn-chip > vn-icon'
}, },
itemCreateView: { itemCreateView: {
temporalName: 'vn-item-create vn-textfield[ng-model="$ctrl.item.provisionalName"]', temporalName: 'vn-item-create vn-textfield[ng-model="$ctrl.item.provisionalName"]',

View File

@ -4,20 +4,69 @@ import getBrowser from '../../helpers/puppeteer';
describe('Item fixed prices path', () => { describe('Item fixed prices path', () => {
let browser; let browser;
let page; let page;
let httpRequest;
beforeAll(async() => { beforeAll(async() => {
browser = await getBrowser(); browser = await getBrowser();
page = browser.page; page = browser.page;
await page.loginAndModule('buyer', 'item'); await page.loginAndModule('buyer', 'item');
await page.accessToSection('item.fixedPrice'); await page.accessToSection('item.fixedPrice');
page.on('request', req => {
if (req.url().includes(`FixedPrices/filter`))
httpRequest = req.url();
});
}); });
afterAll(async() => { afterAll(async() => {
await browser.close(); await browser.close();
}); });
it('should filter using all the fields', async() => {
await page.write(selectors.itemFixedPrice.generalSearchFilter, 'item');
await page.keyboard.press('Enter');
expect(httpRequest).toContain('search=item');
await page.click(selectors.itemFixedPrice.chip);
await page.click(selectors.itemFixedPrice.reignFilter);
expect(httpRequest).toContain('categoryFk');
await page.autocompleteSearch(selectors.itemFixedPrice.typeFilter, 'Alstroemeria');
expect(httpRequest).toContain('typeFk');
await page.click(selectors.itemFixedPrice.chip);
await page.autocompleteSearch(selectors.itemFixedPrice.buyerFilter, 'buyerNick');
expect(httpRequest).toContain('buyerFk');
await page.click(selectors.itemFixedPrice.chip);
await page.autocompleteSearch(selectors.itemFixedPrice.warehouseFilter, 'Algemesi');
expect(httpRequest).toContain('warehouseFk');
await page.click(selectors.itemFixedPrice.chip);
await page.click(selectors.itemFixedPrice.mineFilter);
expect(httpRequest).toContain('mine=true');
await page.click(selectors.itemFixedPrice.chip);
await page.click(selectors.itemFixedPrice.hasMinPriceFilter);
expect(httpRequest).toContain('hasMinPrice=true');
await page.click(selectors.itemFixedPrice.chip);
await page.click(selectors.itemFixedPrice.addTag);
await page.autocompleteSearch(selectors.itemFixedPrice.tagFilter, 'Color');
await page.autocompleteSearch(selectors.itemFixedPrice.tagValueFilter, 'Brown');
expect(httpRequest).toContain('tags');
await page.click(selectors.itemFixedPrice.chip);
});
it('should click on the add new fixed price button', async() => { it('should click on the add new fixed price button', async() => {
await page.waitToClick(selectors.itemFixedPrice.removeWarehouseFilter);
await page.waitForSpinnerLoad();
await page.waitToClick(selectors.itemFixedPrice.add); await page.waitToClick(selectors.itemFixedPrice.add);
await page.waitForSelector(selectors.itemFixedPrice.fourthFixedPrice); await page.waitForSelector(selectors.itemFixedPrice.fourthFixedPrice);
}); });
@ -36,10 +85,7 @@ describe('Item fixed prices path', () => {
}); });
it('should reload the section and check the created price has the expected ID', async() => { it('should reload the section and check the created price has the expected ID', async() => {
await page.accessToSection('item.index'); await page.goto(`http://localhost:5000/#!/item/fixed-price`);
await page.accessToSection('item.fixedPrice');
await page.waitToClick(selectors.itemFixedPrice.removeWarehouseFilter);
await page.waitForSpinnerLoad();
const result = await page.waitToGetProperty(selectors.itemFixedPrice.fourthItemID, 'value'); const result = await page.waitToGetProperty(selectors.itemFixedPrice.fourthItemID, 'value');

View File

@ -20,9 +20,9 @@
}, },
"created": { "created": {
"type": "date" "type": "date"
}, },
"isChecked": { "isChecked": {
"type": "boolean" "type": "boolean"
} }
}, },
"relations": { "relations": {

View File

@ -1,136 +1,215 @@
<vn-crud-model <vn-crud-model url="Tags" fields="['id','name','isFree']" data="$ctrl.tags" auto-load="true" />
url="Tags" <vn-crud-model url="ItemCategories" data="categories" auto-load="true" />
fields="['id','name','isFree', 'sourceTable']" <vn-side-menu side="right">
data="tags" <vn-horizontal class="input">
auto-load="true"> <vn-textfield
</vn-crud-model> label="General search"
<div class="search-panel"> ng-model="$ctrl.filter.search"
<form class="vn-pa-lg" ng-submit="$ctrl.onSearch()"> info="Search items by id, name or barcode"
<vn-horizontal> vn-focus
<vn-textfield ng-keydown="$ctrl.onKeyPress($event)">
vn-one </vn-textfield>
label="General search" </vn-horizontal>
ng-model="filter.search" <vn-horizontal class="item-category">
vn-focus> <vn-autocomplete
</vn-textfield> vn-id="category"
</vn-horizontal> data="categories"
<vn-horizontal> ng-model="$ctrl.filter.categoryFk"
<vn-autocomplete show-field="name"
vn-one value-field="id"
url="ItemCategories" label="Category">
label="Category" </vn-autocomplete>
show-field="name" <vn-one ng-repeat="category in categories">
value-field="id" <vn-icon
ng-model="filter.categoryFk"> ng-class="{'active': $ctrl.filter.categoryFk == category.id}"
</vn-autocomplete> icon="{{::category.icon}}"
<vn-autocomplete vn-one vn-tooltip="{{::category.name}}"
url="ItemTypes" ng-click="$ctrl.changeCategory(category.id)">
label="Type" </vn-icon>
where="{categoryFk: filter.categoryFk}" </vn-one>
show-field="name" </vn-horizontal>
value-field="id" <vn-vertical class="input">
ng-model="filter.typeFk" <vn-autocomplete
fields="['categoryFk']" vn-id="type"
include="'category'"> disabled="!$ctrl.filter.categoryFk"
<tpl-item> url="ItemTypes"
<div>{{name}}</div> label="Type"
<div class="text-caption text-secondary"> where="{categoryFk: $ctrl.filter.categoryFk}"
{{category.name}} show-field="name"
</div> value-field="id"
</tpl-item> ng-model="$ctrl.filter.typeFk"
</vn-autocomplete> fields="['categoryFk']"
</vn-horizontal> include="'category'"
<vn-horizontal> on-change="$ctrl.addFilters()">
<vn-autocomplete <tpl-item>
vn-one <div>{{name}}</div>
ng-model="filter.buyerFk" <div class="text-caption text-secondary">
url="Workers/activeWithRole" {{category.name}}
show-field="nickname" </div>
search-function="{firstName: $search}" </tpl-item>
value-field="id" </vn-autocomplete>
where="{role: {inq: ['logistic', 'buyer']}}" </vn-vertical>
label="Buyer"> <vn-horizontal class="input horizontal">
</vn-autocomplete> <vn-autocomplete
<vn-autocomplete vn-id="buyer"
vn-one disabled="false"
label="Warehouse" ng-model="$ctrl.filter.buyerFk"
ng-model="filter.warehouseFk" url="Workers/activeWithRole"
url="Warehouses"> show-field="nickname"
</vn-autocomplete> search-function="{firstName: $search}"
</vn-horizontal> value-field="id"
<vn-horizontal> where="{role: {inq: ['logistic', 'buyer']}}"
<vn-date-picker label="Buyer"
vn-one on-change="$ctrl.addFilters()">
label="Started" </vn-autocomplete>
ng-model="filter.started"> <vn-autocomplete
</vn-date-picker> vn-id="warehouse"
<vn-date-picker label="Warehouse"
vn-one ng-model="$ctrl.filter.warehouseFk"
label="Ended" url="Warehouses"
ng-model="filter.ended"> show-field="name"
</vn-date-picker> value-field="id"
</vn-horizontal> on-change="$ctrl.addFilters()">
<vn-horizontal> </vn-autocomplete>
<vn-check vn-one </vn-horizontal>
triple-state="true" <vn-horizontal class="input horizontal">
label="For me" <vn-date-picker
ng-model="filter.mine"> label="From"
</vn-check> ng-model="$ctrl.filter.started"
<vn-check vn-one on-change="$ctrl.addFilters()">
triple-state="true" </vn-date-picker>
label="Minimum price" <vn-date-picker
ng-model="filter.hasMinPrice"> label="To"
</vn-check> ng-model="$ctrl.filter.ended"
</vn-horizontal> on-change="$ctrl.addFilters()">
<vn-horizontal class="vn-pt-sm"> </vn-date-picker>
<vn-one class="text-subtitle1" translate> </vn-horizontal>
Tags <vn-horizontal class="input horizontal">
</vn-one> <vn-check
<vn-icon-button label="For me"
vn-none ng-model="$ctrl.filter.mine"
vn-bind="+" triple-state="true"
vn-tooltip="Add tag" ng-click="$ctrl.addFilters()">
icon="add_circle" </vn-check>
ng-click="filter.tags.push({})"> <vn-check
</vn-icon-button> label="Minimum price"
</vn-horizontal> ng-model="$ctrl.filter.hasMinPrice"
<vn-horizontal ng-repeat="itemTag in filter.tags"> triple-state="true"
<vn-autocomplete vn-two vn-id="tag" ng-click="$ctrl.addFilters()">
label="Tag" </vn-check>
initial-data="itemTag.tag" </vn-horizontal>
ng-model="itemTag.tagFk" <vn-horizontal class="tags">
data="tags" <vn-one class="text-subtitle1" translate> Tags </vn-one>
show-field="name" <vn-icon-button
on-change="itemTag.value = null" vn-none
rule> vn-tooltip="Add tag"
</vn-autocomplete> icon="add_circle"
<vn-textfield vn-three ng-click="$ctrl.filter.tags.push({})">
ng-show="tag.selection.isFree || tag.selection.isFree == undefined" </vn-icon-button>
vn-id="text" </vn-horizontal>
label="Value" <vn-horizontal class="tags horizontal" ng-repeat="itemTag in $ctrl.filter.tags">
ng-model="itemTag.value" <vn-autocomplete
rule> vn-id="tag"
</vn-textfield> data="$ctrl.tags"
<vn-autocomplete vn-three ng-model="itemTag.tagFk"
ng-show="tag.selection.isFree === false" show-field="name"
url="{{'Tags/' + itemTag.tagFk + '/filterValue'}}" label="Tag"
search-function="{value: $search}" on-change="itemTag.value = null">
label="Value" </vn-autocomplete>
ng-model="itemTag.value" <vn-textfield
show-field="value" ng-show="tag.selection.isFree || tag.selection.isFree == undefined"
value-field="value" label="Value"
rule> ng-model="itemTag.value"
</vn-autocomplete> ng-keydown="$ctrl.onKeyPress($event)">
<vn-icon-button </vn-textfield>
vn-none <vn-autocomplete
vn-tooltip="Remove tag" ng-show="tag.selection.isFree === false"
icon="delete" url="{{'Tags/' + itemTag.tagFk + '/filterValue'}}"
ng-click="filter.tags.splice($index, 1)" search-function="{value: $search}"
tabindex="-1"> label="Value"
</vn-icon-button> ng-model="itemTag.value"
</vn-horizontal> show-field="value"
<vn-horizontal class="vn-mt-lg"> value-field="value"
<vn-submit label="Search"></vn-submit> on-change="$ctrl.addFilters()">
</vn-horizontal> </vn-autocomplete>
</form> <vn-icon-button
</div> vn-none
vn-tooltip="Remove tag"
icon="delete"
ng-click="$ctrl.removeTag(itemTag)">
</vn-icon-button>
</vn-horizontal>
<div class="chips">
<vn-chip
ng-if="$ctrl.filter.search"
removable="true"
on-remove="$ctrl.removeItemFilter('search')"
class="colored">
<span>Id/{{$ctrl.$t('Name')}}: {{$ctrl.filter.search}}</span>
</vn-chip>
<vn-chip
ng-if="category.selection"
removable="true"
on-remove="$ctrl.removeItemFilter('categoryFk')"
class="colored">
<span>{{$ctrl.$t('Category')}}: {{category.selection.name}}</span>
</vn-chip>
<vn-chip
ng-if="type.selection"
removable="true"
on-remove="$ctrl.removeItemFilter('typeFk')"
class="colored">
<span>{{$ctrl.$t('Type')}}: {{type.selection.name}}</span>
</vn-chip>
<vn-chip
ng-if="buyer.selection"
removable="true"
on-remove="$ctrl.removeItemFilter('buyerFk')"
class="colored">
<span>{{$ctrl.$t('Buyer')}}: {{buyer.selection.nickname}}</span>
</vn-chip>
<vn-chip
ng-if="warehouse.selection"
removable="true"
on-remove="$ctrl.removeItemFilter('warehouseFk')"
class="colored">
<span>{{$ctrl.$t('Warehouse')}}: {{warehouse.selection.name}}</span>
</vn-chip>
<vn-chip
ng-if="$ctrl.filter.started"
removable="true"
on-remove="$ctrl.removeItemFilter('started')"
class="colored">
<span>{{$ctrl.$t('Started')}}: {{$ctrl.filter.started | date:'dd/MM/yyyy'}}</span>
</vn-chip>
<vn-chip
ng-if="$ctrl.filter.ended"
removable="true"
on-remove="$ctrl.removeItemFilter('ended')"
class="colored">
<span>{{$ctrl.$t('Ended')}}: {{$ctrl.filter.ended | date:'dd/MM/yyyy'}}</span>
</vn-chip>
<vn-chip
ng-if="$ctrl.filter.mine != null"
removable="true"
on-remove="$ctrl.removeItemFilter('mine')"
class="colored">
<span>{{$ctrl.$t('For me')}}: {{$ctrl.filter.mine ? '✓' : '✗'}}</span>
</vn-chip>
<vn-chip
ng-if="$ctrl.filter.hasMinPrice != null"
removable="true"
on-remove="$ctrl.removeItemFilter('hasMinPrice')"
class="colored">
<span>{{$ctrl.$t('Minimum price')}}: {{$ctrl.filter.hasMinPrice ? '✓' : '✗'}}</span>
</vn-chip>
<vn-chip
ng-repeat="chipTag in $ctrl.filter.tags"
removable="true"
on-remove="$ctrl.removeTag(chipTag)"
class="colored"
ng-if="chipTag.value">
<span>{{$ctrl.showTagInfo(chipTag)}}</span>
</vn-chip>
</div>
</vn-side-menu>

View File

@ -1,19 +1,60 @@
import ngModule from '../module'; import ngModule from '../module';
import SearchPanel from 'core/components/searchbar/search-panel'; import SearchPanel from 'core/components/searchbar/search-panel';
import './style.scss';
class Controller extends SearchPanel { class Controller extends SearchPanel {
get filter() { constructor($element, $) {
return this.$.filter; super($element, $);
} }
set filter(value = {}) { $onInit() {
if (!value.tags) value.tags = [{}]; this.filter = {
tags: []
};
}
this.$.filter = value; changeCategory(id) {
if (this.filter.categoryFk != id) {
this.filter.categoryFk = id;
this.addFilters();
}
}
removeItemFilter(param) {
this.filter[param] = null;
if (param == 'categoryFk') this.filter['typeFk'] = null;
this.addFilters();
}
removeTag(tag) {
const index = this.filter.tags.indexOf(tag);
if (index > -1) this.filter.tags.splice(index, 1);
this.addFilters();
}
onKeyPress($event) {
if ($event.key === 'Enter')
this.addFilters();
}
addFilters() {
for (let i = 0; i < this.filter.tags.length; i++) {
if (!this.filter.tags[i].value)
this.filter.tags.splice(i, 1);
}
return this.model.addFilter({}, this.filter);
}
showTagInfo(itemTag) {
if (!itemTag.tagFk) return itemTag.value;
return `${this.tags.find(tag => tag.id == itemTag.tagFk).name}: ${itemTag.value}`;
} }
} }
ngModule.vnComponent('vnFixedPriceSearchPanel', { ngModule.vnComponent('vnFixedPriceSearchPanel', {
template: require('./index.html'), template: require('./index.html'),
controller: Controller controller: Controller,
bindings: {
model: '<'
}
}); });

View File

@ -0,0 +1,56 @@
import './index.js';
describe('Item', () => {
describe('Component vnFixedPriceSearchPanel', () => {
let $element;
let controller;
beforeEach(ngModule('item'));
beforeEach(angular.mock.inject($componentController => {
$element = angular.element(`<vn-fixed-price-search-panel></vn-fixed-price-search-panel>`);
controller = $componentController('vnFixedPriceSearchPanel', {$element});
controller.model = {addFilter: () => {}};
}));
describe('removeItemFilter()', () => {
it(`should remove param from filter`, () => {
controller.filter = {tags: [], categoryFk: 1, typeFk: 1};
const expectFilter = {tags: [], categoryFk: null, typeFk: null};
controller.removeItemFilter('categoryFk');
expect(controller.filter).toEqual(expectFilter);
});
});
describe('removeTag()', () => {
it(`should remove tag from filter`, () => {
const tag = {tagFk: 1, value: 'Value'};
controller.filter = {tags: [tag]};
const expectFilter = {tags: []};
controller.removeTag(tag);
expect(controller.filter).toEqual(expectFilter);
});
});
describe('showTagInfo()', () => {
it(`should show tag value`, () => {
const tag = {value: 'Value'};
const result = controller.showTagInfo(tag);
expect(result).toEqual('Value');
});
it(`should show tag name and value`, () => {
const tag = {tagFk: 1, value: 'Value'};
controller.tags = [{id: 1, name: 'tagName'}];
const result = controller.showTagInfo(tag);
expect(result).toEqual('tagName: Value');
});
});
});
});

View File

@ -0,0 +1,71 @@
@import "variables";
vn-fixed-price-search-panel vn-side-menu {
.menu {
min-width: $right-menu-width;
}
& > div {
.input {
padding-left: $spacing-md;
padding-right: $spacing-md;
border-color: $color-spacer;
border-bottom: $border-thin;
}
.horizontal {
padding-left: $spacing-md;
padding-right: $spacing-md;
grid-auto-flow: column;
grid-column-gap: $spacing-sm;
align-items: center;
}
.tags {
padding: $spacing-md;
padding-bottom: 0%;
padding-top: 0%;
align-items: center;
}
.chips {
display: flex;
flex-wrap: wrap;
padding: $spacing-md;
overflow: hidden;
max-width: 100%;
border-color: $color-spacer;
border-top: $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: 2.6rem;
width: 16px;
height: 16px;
}
}
}
}
}
}

View File

@ -14,16 +14,10 @@
order="name"> order="name">
</vn-crud-model> </vn-crud-model>
<vn-portal slot="topbar"> <vn-portal slot="topbar">
<vn-searchbar
auto-state="false"
panel="vn-fixed-price-search-panel"
info="Search prices by item ID or code"
suggested-filter="$ctrl.filterParams"
filter="$ctrl.filterParams"
placeholder="Search fixed prices"
model="model">
</vn-searchbar>
</vn-portal> </vn-portal>
<vn-fixed-price-search-panel
model="model">
</vn-fixed-price-search-panel>
<div class="vn-w-xl"> <div class="vn-w-xl">
<vn-card> <vn-card>
<smart-table <smart-table