Merge pull request '4159-tableQ_filter' (!1069) from 4159-tableQ_filter into dev
gitea/salix/pipeline/head This commit looks good
Details
gitea/salix/pipeline/head This commit looks good
Details
fixes #4159
This commit is contained in:
commit
aa467dd565
|
@ -394,11 +394,18 @@ export default {
|
||||||
intrastadCheckbox: '.vn-popover.shown vn-horizontal:nth-child(3) > vn-check[label="Intrastat"]',
|
intrastadCheckbox: '.vn-popover.shown vn-horizontal:nth-child(3) > vn-check[label="Intrastat"]',
|
||||||
originCheckbox: '.vn-popover.shown vn-horizontal:nth-child(3) > vn-check[label="Origin"]',
|
originCheckbox: '.vn-popover.shown vn-horizontal:nth-child(3) > vn-check[label="Origin"]',
|
||||||
buyerCheckbox: '.vn-popover.shown vn-horizontal:nth-child(3) > vn-check[label="Buyer"]',
|
buyerCheckbox: '.vn-popover.shown vn-horizontal:nth-child(3) > vn-check[label="Buyer"]',
|
||||||
|
densityCheckbox: '.vn-popover.shown vn-horizontal:nth-child(3) > vn-check[label="Density"]',
|
||||||
|
openAdvancedSearchButton: 'vn-searchbar .append vn-icon[icon="arrow_drop_down"]',
|
||||||
|
advancedSearchItemType: 'vn-item-search-panel vn-autocomplete[ng-model="filter.typeFk"]',
|
||||||
|
advancedSearchButton: 'vn-item-search-panel button[type=submit]',
|
||||||
|
advancedSmartTableButton: 'vn-item-index vn-button[icon="search"]',
|
||||||
|
advancedSmartTableGrouping: 'vn-item-index vn-textfield[name=grouping]',
|
||||||
weightByPieceCheckbox: '.vn-popover.shown vn-horizontal:nth-child(3) > vn-check[label="Weight/Piece"]',
|
weightByPieceCheckbox: '.vn-popover.shown vn-horizontal:nth-child(3) > vn-check[label="Weight/Piece"]',
|
||||||
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[icon="add_circle"]',
|
||||||
|
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"]',
|
||||||
fourthWarehouse: 'vn-fixed-price tr:nth-child(5) vn-autocomplete[ng-model="price.warehouseFk"]',
|
fourthWarehouse: 'vn-fixed-price tr:nth-child(5) vn-autocomplete[ng-model="price.warehouseFk"]',
|
||||||
|
@ -408,7 +415,8 @@ export default {
|
||||||
fourthMinPrice: 'vn-fixed-price tr:nth-child(5) > td:nth-child(6) > vn-input-number[ng-model="price.minPrice"]',
|
fourthMinPrice: 'vn-fixed-price tr:nth-child(5) > td:nth-child(6) > vn-input-number[ng-model="price.minPrice"]',
|
||||||
fourthStarted: 'vn-fixed-price tr:nth-child(5) vn-date-picker[ng-model="price.started"]',
|
fourthStarted: 'vn-fixed-price tr:nth-child(5) vn-date-picker[ng-model="price.started"]',
|
||||||
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"]'
|
||||||
},
|
},
|
||||||
itemCreateView: {
|
itemCreateView: {
|
||||||
temporalName: 'vn-item-create vn-textfield[ng-model="$ctrl.item.provisionalName"]',
|
temporalName: 'vn-item-create vn-textfield[ng-model="$ctrl.item.provisionalName"]',
|
||||||
|
|
|
@ -0,0 +1,77 @@
|
||||||
|
import selectors from '../../helpers/selectors.js';
|
||||||
|
import getBrowser from '../../helpers/puppeteer';
|
||||||
|
|
||||||
|
describe('SmartTable SearchBar integration', () => {
|
||||||
|
let browser;
|
||||||
|
let page;
|
||||||
|
beforeAll(async() => {
|
||||||
|
browser = await getBrowser();
|
||||||
|
page = browser.page;
|
||||||
|
await page.loginAndModule('salesPerson', 'item');
|
||||||
|
await page.waitToClick(selectors.globalItems.searchButton);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async() => {
|
||||||
|
await browser.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('as filters', () => {
|
||||||
|
it('should search by type in searchBar', async() => {
|
||||||
|
await page.waitToClick(selectors.itemsIndex.openAdvancedSearchButton);
|
||||||
|
await page.autocompleteSearch(selectors.itemsIndex.advancedSearchItemType, 'Anthurium');
|
||||||
|
await page.waitToClick(selectors.itemsIndex.advancedSearchButton);
|
||||||
|
await page.waitForNumberOfElements(selectors.itemsIndex.searchResult, 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reload page and have same results', async() => {
|
||||||
|
await page.reload({
|
||||||
|
waitUntil: 'networkidle2'
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.waitForNumberOfElements(selectors.itemsIndex.searchResult, 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should search by grouping in smartTable', async() => {
|
||||||
|
await page.waitToClick(selectors.itemsIndex.advancedSmartTableButton);
|
||||||
|
await page.write(selectors.itemsIndex.advancedSmartTableGrouping, '1');
|
||||||
|
await page.keyboard.press('Enter');
|
||||||
|
await page.waitForNumberOfElements(selectors.itemsIndex.searchResult, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should now reload page and have same results', async() => {
|
||||||
|
await page.reload({
|
||||||
|
waitUntil: 'networkidle2'
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.waitForNumberOfElements(selectors.itemsIndex.searchResult, 2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('as orders', () => {
|
||||||
|
it('should order by first id', async() => {
|
||||||
|
await page.loginAndModule('developer', 'item');
|
||||||
|
await page.accessToSection('item.fixedPrice');
|
||||||
|
await page.doSearch();
|
||||||
|
|
||||||
|
const result = await page.waitToGetProperty(selectors.itemFixedPrice.firstItemID, 'value');
|
||||||
|
|
||||||
|
expect(result).toEqual('1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should order by last id', async() => {
|
||||||
|
await page.waitToClick(selectors.itemFixedPrice.orderColumnId);
|
||||||
|
const result = await page.waitToGetProperty(selectors.itemFixedPrice.firstItemID, 'value');
|
||||||
|
|
||||||
|
expect(result).toEqual('13');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reload page and have same order', async() => {
|
||||||
|
await page.reload({
|
||||||
|
waitUntil: 'networkidle2'
|
||||||
|
});
|
||||||
|
const result = await page.waitToGetProperty(selectors.itemFixedPrice.firstItemID, 'value');
|
||||||
|
|
||||||
|
expect(result).toEqual('13');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -99,6 +99,18 @@ export default class CrudModel extends ModelProxy {
|
||||||
return this.refresh();
|
return this.refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies a new filter to the model.
|
||||||
|
*
|
||||||
|
* @param {Object} params Custom parameters
|
||||||
|
* @return {Promise} The request promise
|
||||||
|
*/
|
||||||
|
|
||||||
|
applyParams(params) {
|
||||||
|
this.userParams = params;
|
||||||
|
return this.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
removeFilter() {
|
removeFilter() {
|
||||||
return this.applyFilter(null, null);
|
return this.applyFilter(null, null);
|
||||||
}
|
}
|
||||||
|
|
|
@ -139,8 +139,12 @@ export default class Searchbar extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
removeParam(index) {
|
removeParam(index) {
|
||||||
|
const field = this.params[index].key;
|
||||||
|
this.filterSanitizer(field);
|
||||||
|
|
||||||
this.params.splice(index, 1);
|
this.params.splice(index, 1);
|
||||||
this.doSearch(this.fromBar(), 'bar');
|
this.toRemove = field;
|
||||||
|
this.doSearch(this.fromBar(), 'removeBar');
|
||||||
}
|
}
|
||||||
|
|
||||||
fromBar() {
|
fromBar() {
|
||||||
|
@ -163,7 +167,7 @@ export default class Searchbar extends Component {
|
||||||
|
|
||||||
let keys = Object.keys(filter);
|
let keys = Object.keys(filter);
|
||||||
keys.forEach(key => {
|
keys.forEach(key => {
|
||||||
if (key == 'search') return;
|
if (key == 'search' || key == 'tableQ' || key == 'tableOrder') return;
|
||||||
let value = filter[key];
|
let value = filter[key];
|
||||||
let chip;
|
let chip;
|
||||||
|
|
||||||
|
@ -198,6 +202,7 @@ export default class Searchbar extends Component {
|
||||||
let promise = this.onSearch({$params: filter});
|
let promise = this.onSearch({$params: filter});
|
||||||
promise = promise || this.$q.resolve();
|
promise = promise || this.$q.resolve();
|
||||||
promise.then(data => this.onFilter(filter, source, data));
|
promise.then(data => this.onFilter(filter, source, data));
|
||||||
|
this.toBar(filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
onFilter(filter, source, data) {
|
onFilter(filter, source, data) {
|
||||||
|
@ -238,8 +243,11 @@ export default class Searchbar extends Component {
|
||||||
} else {
|
} else {
|
||||||
state = this.searchState;
|
state = this.searchState;
|
||||||
|
|
||||||
if (filter)
|
if (filter) {
|
||||||
|
if (this.tableQ)
|
||||||
|
filter.tableQ = this.tableQ;
|
||||||
params = {q: JSON.stringify(filter)};
|
params = {q: JSON.stringify(filter)};
|
||||||
|
}
|
||||||
if (this.$state.is(state))
|
if (this.$state.is(state))
|
||||||
opts = {location: 'replace'};
|
opts = {location: 'replace'};
|
||||||
}
|
}
|
||||||
|
@ -247,6 +255,12 @@ export default class Searchbar extends Component {
|
||||||
|
|
||||||
this.filter = filter;
|
this.filter = filter;
|
||||||
|
|
||||||
|
if (source == 'removeBar') {
|
||||||
|
delete params[this.toRemove];
|
||||||
|
delete this.model.userParams[this.toRemove];
|
||||||
|
this.model.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
if (!filter && this.model)
|
if (!filter && this.model)
|
||||||
this.model.clear();
|
this.model.clear();
|
||||||
if (source != 'state')
|
if (source != 'state')
|
||||||
|
@ -269,9 +283,14 @@ export default class Searchbar extends Component {
|
||||||
this.model.clear();
|
this.model.clear();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (Object.keys(filter).length === 0) {
|
||||||
|
this.filterSanitizer('search');
|
||||||
|
if (this.model.userParams)
|
||||||
|
delete this.model.userParams['search'];
|
||||||
|
}
|
||||||
|
|
||||||
let where = null;
|
let where = null;
|
||||||
let params = null;
|
let params = {};
|
||||||
|
|
||||||
if (this.exprBuilder) {
|
if (this.exprBuilder) {
|
||||||
where = buildFilter(filter,
|
where = buildFilter(filter,
|
||||||
|
@ -283,9 +302,89 @@ export default class Searchbar extends Component {
|
||||||
params = this.fetchParams({$params: params});
|
params = this.fetchParams({$params: params});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.tableQ = null;
|
||||||
|
|
||||||
|
const hasParams = this.$params.q && Object.keys(JSON.parse(this.$params.q)).length;
|
||||||
|
if (hasParams) {
|
||||||
|
const stateFilter = JSON.parse(this.$params.q);
|
||||||
|
for (let param in stateFilter) {
|
||||||
|
if (param != 'tableQ' && param != 'orderQ')
|
||||||
|
this.filterSanitizer(param);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let param in this.suggestedFilter) {
|
||||||
|
this.filterSanitizer(param);
|
||||||
|
delete stateFilter[param];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.tableQ = stateFilter.tableQ;
|
||||||
|
for (let param in stateFilter.tableQ)
|
||||||
|
params[param] = stateFilter.tableQ[param];
|
||||||
|
|
||||||
|
Object.assign(stateFilter, params);
|
||||||
|
return this.model.applyParams(params)
|
||||||
|
.then(() => this.model.data);
|
||||||
|
}
|
||||||
|
|
||||||
return this.model.applyFilter(where ? {where} : null, params)
|
return this.model.applyFilter(where ? {where} : null, params)
|
||||||
.then(() => this.model.data);
|
.then(() => this.model.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
filterSanitizer(field) {
|
||||||
|
if (!field) return;
|
||||||
|
const userFilter = this.model.userFilter;
|
||||||
|
const userParams = this.model.userParams;
|
||||||
|
const where = userFilter && userFilter.where;
|
||||||
|
|
||||||
|
if (this.model.userParams)
|
||||||
|
delete this.model.userParams[field];
|
||||||
|
|
||||||
|
if (this.exprBuilder) {
|
||||||
|
const param = this.exprBuilder({
|
||||||
|
param: field,
|
||||||
|
value: null
|
||||||
|
});
|
||||||
|
if (param) [field] = Object.keys(param);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!where) return;
|
||||||
|
|
||||||
|
const whereKeys = Object.keys(where);
|
||||||
|
for (let key of whereKeys) {
|
||||||
|
removeProp(where, field, key);
|
||||||
|
|
||||||
|
if (Object.keys(where).length == 0)
|
||||||
|
delete userFilter.where;
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeProp(obj, targetProp, prop) {
|
||||||
|
if (prop == targetProp)
|
||||||
|
delete obj[prop];
|
||||||
|
|
||||||
|
if (prop === 'and' || prop === 'or' && obj[prop]) {
|
||||||
|
const arrayCopy = obj[prop].slice();
|
||||||
|
for (let param of arrayCopy) {
|
||||||
|
const [key] = Object.keys(param);
|
||||||
|
const index = obj[prop].findIndex(param => {
|
||||||
|
return Object.keys(param)[0] == key;
|
||||||
|
});
|
||||||
|
if (key == targetProp)
|
||||||
|
obj[prop].splice(index, 1);
|
||||||
|
|
||||||
|
if (param[key] instanceof Array)
|
||||||
|
removeProp(param, field, key);
|
||||||
|
|
||||||
|
if (Object.keys(param).length == 0)
|
||||||
|
obj[prop].splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj[prop].length == 0)
|
||||||
|
delete obj[prop];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {userFilter, userParams};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ngModule.vnComponent('vnSearchbar', {
|
ngModule.vnComponent('vnSearchbar', {
|
||||||
|
|
|
@ -6,7 +6,7 @@ describe('Component vnSearchbar', () => {
|
||||||
let $state;
|
let $state;
|
||||||
let $params;
|
let $params;
|
||||||
let $scope;
|
let $scope;
|
||||||
let filter = {id: 1, search: 'needle'};
|
const filter = {id: 1, search: 'needle'};
|
||||||
|
|
||||||
beforeEach(ngModule('vnCore', $stateProvider => {
|
beforeEach(ngModule('vnCore', $stateProvider => {
|
||||||
$stateProvider
|
$stateProvider
|
||||||
|
@ -70,8 +70,8 @@ describe('Component vnSearchbar', () => {
|
||||||
|
|
||||||
describe('filter() setter', () => {
|
describe('filter() setter', () => {
|
||||||
it(`should update the bar params and search`, () => {
|
it(`should update the bar params and search`, () => {
|
||||||
let withoutHours = new Date(2000, 1, 1);
|
const withoutHours = new Date(2000, 1, 1);
|
||||||
let withHours = new Date(withoutHours.getTime());
|
const withHours = new Date(withoutHours.getTime());
|
||||||
withHours.setHours(12, 30, 15, 10);
|
withHours.setHours(12, 30, 15, 10);
|
||||||
|
|
||||||
controller.filter = {
|
controller.filter = {
|
||||||
|
@ -83,8 +83,8 @@ describe('Component vnSearchbar', () => {
|
||||||
myObjectProp: {myProp: 1}
|
myObjectProp: {myProp: 1}
|
||||||
};
|
};
|
||||||
|
|
||||||
let chips = {};
|
const chips = {};
|
||||||
for (let param of controller.params || [])
|
for (const param of controller.params || [])
|
||||||
chips[param.key] = param.chip;
|
chips[param.key] = param.chip;
|
||||||
|
|
||||||
expect(controller.searchString).toBe('needle');
|
expect(controller.searchString).toBe('needle');
|
||||||
|
@ -172,13 +172,22 @@ describe('Component vnSearchbar', () => {
|
||||||
describe('removeParam()', () => {
|
describe('removeParam()', () => {
|
||||||
it(`should remove the parameter from the filter`, () => {
|
it(`should remove the parameter from the filter`, () => {
|
||||||
jest.spyOn(controller, 'doSearch');
|
jest.spyOn(controller, 'doSearch');
|
||||||
|
controller.model = {
|
||||||
|
refresh: jest.fn(),
|
||||||
|
userParams: {
|
||||||
|
id: 1
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
controller.model.applyParams = jest.fn().mockReturnValue(Promise.resolve());
|
||||||
|
jest.spyOn(controller.model, 'applyParams');
|
||||||
|
|
||||||
controller.filter = filter;
|
controller.filter = filter;
|
||||||
controller.removeParam(0);
|
controller.removeParam(0);
|
||||||
|
|
||||||
expect(controller.doSearch).toHaveBeenCalledWith({
|
expect(controller.doSearch).toHaveBeenCalledWith({
|
||||||
search: 'needle'
|
search: 'needle'
|
||||||
}, 'bar');
|
}, 'removeBar');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -199,7 +208,7 @@ describe('Component vnSearchbar', () => {
|
||||||
it(`should go to the summary state when one result`, () => {
|
it(`should go to the summary state when one result`, () => {
|
||||||
jest.spyOn($state, 'go');
|
jest.spyOn($state, 'go');
|
||||||
|
|
||||||
let data = [{id: 1}];
|
const data = [{id: 1}];
|
||||||
|
|
||||||
controller.baseState = 'foo';
|
controller.baseState = 'foo';
|
||||||
controller.onFilter(filter, 'any', data);
|
controller.onFilter(filter, 'any', data);
|
||||||
|
@ -214,7 +223,7 @@ describe('Component vnSearchbar', () => {
|
||||||
$scope.$apply();
|
$scope.$apply();
|
||||||
|
|
||||||
jest.spyOn($state, 'go');
|
jest.spyOn($state, 'go');
|
||||||
let data = [{id: 1}];
|
const data = [{id: 1}];
|
||||||
|
|
||||||
controller.baseState = 'foo';
|
controller.baseState = 'foo';
|
||||||
controller.onFilter(filter, 'any', data);
|
controller.onFilter(filter, 'any', data);
|
||||||
|
@ -229,7 +238,7 @@ describe('Component vnSearchbar', () => {
|
||||||
$scope.$apply();
|
$scope.$apply();
|
||||||
|
|
||||||
jest.spyOn($state, 'go');
|
jest.spyOn($state, 'go');
|
||||||
let data = [{id: 1}];
|
const data = [{id: 1}];
|
||||||
|
|
||||||
controller.baseState = 'foo';
|
controller.baseState = 'foo';
|
||||||
controller.onFilter(filter, 'any', data);
|
controller.onFilter(filter, 'any', data);
|
||||||
|
@ -247,7 +256,7 @@ describe('Component vnSearchbar', () => {
|
||||||
controller.onFilter(filter, 'any');
|
controller.onFilter(filter, 'any');
|
||||||
$scope.$apply();
|
$scope.$apply();
|
||||||
|
|
||||||
let queryParams = {q: JSON.stringify(filter)};
|
const queryParams = {q: JSON.stringify(filter)};
|
||||||
|
|
||||||
expect($state.go).toHaveBeenCalledWith('search.state', queryParams, undefined);
|
expect($state.go).toHaveBeenCalledWith('search.state', queryParams, undefined);
|
||||||
expect(controller.filter).toEqual(filter);
|
expect(controller.filter).toEqual(filter);
|
||||||
|
|
|
@ -103,3 +103,4 @@
|
||||||
</div>
|
</div>
|
||||||
</tpl-body>
|
</tpl-body>
|
||||||
</vn-popover>
|
</vn-popover>
|
||||||
|
|
||||||
|
|
|
@ -15,9 +15,17 @@ export default class SmartTable extends Component {
|
||||||
this.$inputsScope;
|
this.$inputsScope;
|
||||||
this.columns = [];
|
this.columns = [];
|
||||||
this.autoSave = false;
|
this.autoSave = false;
|
||||||
|
this.autoState = true;
|
||||||
this.transclude();
|
this.transclude();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$onChanges() {
|
||||||
|
if (this.model) {
|
||||||
|
this.defaultFilter();
|
||||||
|
this.defaultOrder();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$onDestroy() {
|
$onDestroy() {
|
||||||
const styleElement = document.querySelector('style[id="smart-table"]');
|
const styleElement = document.querySelector('style[id="smart-table"]');
|
||||||
if (this.$.css && styleElement)
|
if (this.$.css && styleElement)
|
||||||
|
@ -47,10 +55,8 @@ export default class SmartTable extends Component {
|
||||||
|
|
||||||
set model(value) {
|
set model(value) {
|
||||||
this._model = value;
|
this._model = value;
|
||||||
if (value) {
|
if (value)
|
||||||
this.$.model = value;
|
this.$.model = value;
|
||||||
this.defaultOrder();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getDefaultViewConfig() {
|
getDefaultViewConfig() {
|
||||||
|
@ -160,8 +166,36 @@ export default class SmartTable extends Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defaultFilter() {
|
||||||
|
if (this.disabledTableFilter || !this.$params.q) return;
|
||||||
|
|
||||||
|
const stateFilter = JSON.parse(this.$params.q).tableQ;
|
||||||
|
if (!stateFilter || !this.exprBuilder) return;
|
||||||
|
|
||||||
|
const columns = this.columns.map(column => column.field);
|
||||||
|
|
||||||
|
this.displaySearch();
|
||||||
|
if (!this.$inputsScope.searchProps)
|
||||||
|
this.$inputsScope.searchProps = {};
|
||||||
|
|
||||||
|
for (let param in stateFilter) {
|
||||||
|
if (columns.includes(param)) {
|
||||||
|
const whereParams = {[param]: stateFilter[param]};
|
||||||
|
Object.assign(this.$inputsScope.searchProps, whereParams);
|
||||||
|
this.addFilter(param, stateFilter[param]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
defaultOrder() {
|
defaultOrder() {
|
||||||
const order = this.model.order;
|
if (this.disabledTableOrder) return;
|
||||||
|
|
||||||
|
let stateOrder;
|
||||||
|
if (this.$params.q)
|
||||||
|
stateOrder = JSON.parse(this.$params.q).tableOrder;
|
||||||
|
|
||||||
|
const order = stateOrder ? stateOrder : this.model.order;
|
||||||
|
|
||||||
if (!order) return;
|
if (!order) return;
|
||||||
|
|
||||||
const orderFields = order.split(', ');
|
const orderFields = order.split(', ');
|
||||||
|
@ -195,6 +229,9 @@ export default class SmartTable extends Component {
|
||||||
this.setPriority(column.element, priority);
|
this.setPriority(column.element, priority);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.model.order = order;
|
||||||
|
this.refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
registerColumns() {
|
registerColumns() {
|
||||||
|
@ -395,30 +432,56 @@ export default class SmartTable extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
searchByColumn(field) {
|
searchByColumn(field) {
|
||||||
const searchCriteria = this.$inputsScope.searchProps[field];
|
|
||||||
const emptySearch = searchCriteria === '' || searchCriteria == null;
|
|
||||||
|
|
||||||
const filters = this.filterSanitizer(field);
|
const filters = this.filterSanitizer(field);
|
||||||
|
|
||||||
if (filters && filters.userFilter)
|
if (filters && filters.userFilter)
|
||||||
this.model.userFilter = filters.userFilter;
|
this.model.userFilter = filters.userFilter;
|
||||||
if (!emptySearch)
|
|
||||||
this.addFilter(field, this.$inputsScope.searchProps[field]);
|
this.addFilter(field, this.$inputsScope.searchProps[field]);
|
||||||
else this.model.refresh();
|
}
|
||||||
|
|
||||||
|
searchPropsSanitizer() {
|
||||||
|
if (!this.$inputsScope || !this.$inputsScope.searchProps) return null;
|
||||||
|
let searchProps = this.$inputsScope.searchProps;
|
||||||
|
const searchPropsArray = Object.entries(searchProps);
|
||||||
|
searchProps = searchPropsArray.filter(
|
||||||
|
([key, value]) => value && value != ''
|
||||||
|
);
|
||||||
|
|
||||||
|
return Object.fromEntries(searchProps);
|
||||||
}
|
}
|
||||||
|
|
||||||
addFilter(field, value) {
|
addFilter(field, value) {
|
||||||
let where = {[field]: value};
|
if (value == '') value = null;
|
||||||
|
|
||||||
|
let stateFilter = {tableQ: {}};
|
||||||
|
if (this.$params.q) {
|
||||||
|
stateFilter = JSON.parse(this.$params.q);
|
||||||
|
if (!stateFilter.tableQ)
|
||||||
|
stateFilter.tableQ = {};
|
||||||
|
delete stateFilter.tableQ[field];
|
||||||
|
}
|
||||||
|
|
||||||
|
const whereParams = {[field]: value};
|
||||||
|
if (value) {
|
||||||
|
let where = {[field]: value};
|
||||||
if (this.exprBuilder) {
|
if (this.exprBuilder) {
|
||||||
where = buildFilter(where, (param, value) =>
|
where = buildFilter(whereParams, (param, value) =>
|
||||||
this.exprBuilder({param, value})
|
this.exprBuilder({param, value})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.model.addFilter({where});
|
this.model.addFilter({where});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const searchProps = this.searchPropsSanitizer();
|
||||||
|
|
||||||
|
Object.assign(stateFilter.tableQ, searchProps);
|
||||||
|
|
||||||
|
const params = {q: JSON.stringify(stateFilter)};
|
||||||
|
|
||||||
|
this.$state.go(this.$state.current.name, params, {location: 'replace'});
|
||||||
|
this.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
applySort() {
|
applySort() {
|
||||||
let order = this.sortCriteria.map(criteria => `${criteria.field} ${criteria.sortType}`);
|
let order = this.sortCriteria.map(criteria => `${criteria.field} ${criteria.sortType}`);
|
||||||
order = order.join(', ');
|
order = order.join(', ');
|
||||||
|
@ -426,7 +489,18 @@ export default class SmartTable extends Component {
|
||||||
if (order)
|
if (order)
|
||||||
this.model.order = order;
|
this.model.order = order;
|
||||||
|
|
||||||
this.model.refresh();
|
let stateFilter = {tableOrder: {}};
|
||||||
|
if (this.$params.q) {
|
||||||
|
stateFilter = JSON.parse(this.$params.q);
|
||||||
|
if (!stateFilter.tableOrder)
|
||||||
|
stateFilter.tableOrder = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
stateFilter.tableOrder = order;
|
||||||
|
|
||||||
|
const params = {q: JSON.stringify(stateFilter)};
|
||||||
|
this.$state.go(this.$state.current.name, params, {location: 'replace'});
|
||||||
|
this.refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
filterSanitizer(field) {
|
filterSanitizer(field) {
|
||||||
|
@ -535,6 +609,8 @@ ngModule.vnComponent('smartTable', {
|
||||||
autoSave: '<?',
|
autoSave: '<?',
|
||||||
exprBuilder: '&?',
|
exprBuilder: '&?',
|
||||||
defaultNewData: '&?',
|
defaultNewData: '&?',
|
||||||
options: '<?'
|
options: '<?',
|
||||||
|
disabledTableFilter: '<?',
|
||||||
|
disabledTableOrder: '<?',
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -9,6 +9,11 @@ describe('Component smartTable', () => {
|
||||||
$httpBackend = _$httpBackend_;
|
$httpBackend = _$httpBackend_;
|
||||||
$element = $compile(`<smart-table></smart-table>`)($rootScope);
|
$element = $compile(`<smart-table></smart-table>`)($rootScope);
|
||||||
controller = $element.controller('smartTable');
|
controller = $element.controller('smartTable');
|
||||||
|
controller.model = {
|
||||||
|
refresh: jest.fn().mockReturnValue(new Promise(resolve => resolve())),
|
||||||
|
addFilter: jest.fn(),
|
||||||
|
userParams: {}
|
||||||
|
};
|
||||||
}));
|
}));
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
@ -83,7 +88,7 @@ describe('Component smartTable', () => {
|
||||||
describe('defaultOrder', () => {
|
describe('defaultOrder', () => {
|
||||||
it('should insert a new object to the controller sortCriteria with a sortType value of "ASC"', () => {
|
it('should insert a new object to the controller sortCriteria with a sortType value of "ASC"', () => {
|
||||||
const element = document.createElement('div');
|
const element = document.createElement('div');
|
||||||
controller.model = {order: 'id'};
|
controller.model.order = 'id';
|
||||||
controller.columns = [
|
controller.columns = [
|
||||||
{field: 'id', element: element},
|
{field: 'id', element: element},
|
||||||
{field: 'test1', element: element},
|
{field: 'test1', element: element},
|
||||||
|
@ -101,7 +106,8 @@ describe('Component smartTable', () => {
|
||||||
|
|
||||||
it('should add new entries to the controller sortCriteria with a sortType values of "ASC" and "DESC"', () => {
|
it('should add new entries to the controller sortCriteria with a sortType values of "ASC" and "DESC"', () => {
|
||||||
const element = document.createElement('div');
|
const element = document.createElement('div');
|
||||||
controller.model = {order: 'test1, id DESC'};
|
controller.model.order = 'test1, id DESC';
|
||||||
|
|
||||||
controller.columns = [
|
controller.columns = [
|
||||||
{field: 'id', element: element},
|
{field: 'id', element: element},
|
||||||
{field: 'test1', element: element},
|
{field: 'test1', element: element},
|
||||||
|
@ -125,8 +131,6 @@ describe('Component smartTable', () => {
|
||||||
|
|
||||||
describe('addFilter()', () => {
|
describe('addFilter()', () => {
|
||||||
it('should call the model addFilter() with a basic where filter if exprBuilder() was not received', () => {
|
it('should call the model addFilter() with a basic where filter if exprBuilder() was not received', () => {
|
||||||
controller.model = {addFilter: jest.fn()};
|
|
||||||
|
|
||||||
controller.addFilter('myField', 'myValue');
|
controller.addFilter('myField', 'myValue');
|
||||||
|
|
||||||
const expectedFilter = {
|
const expectedFilter = {
|
||||||
|
@ -140,7 +144,6 @@ describe('Component smartTable', () => {
|
||||||
|
|
||||||
it('should call the model addFilter() with a built where filter resultant of exprBuilder()', () => {
|
it('should call the model addFilter() with a built where filter resultant of exprBuilder()', () => {
|
||||||
controller.exprBuilder = jest.fn().mockReturnValue({builtField: 'builtValue'});
|
controller.exprBuilder = jest.fn().mockReturnValue({builtField: 'builtValue'});
|
||||||
controller.model = {addFilter: jest.fn()};
|
|
||||||
|
|
||||||
controller.addFilter('myField', 'myValue');
|
controller.addFilter('myField', 'myValue');
|
||||||
|
|
||||||
|
@ -155,35 +158,48 @@ describe('Component smartTable', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('applySort()', () => {
|
describe('applySort()', () => {
|
||||||
it('should call the model refresh() without making changes on the model order', () => {
|
it('should call the $state go and model refresh without making changes on the model order', () => {
|
||||||
controller.model = {refresh: jest.fn()};
|
controller.$state = {
|
||||||
|
go: jest.fn(),
|
||||||
|
current: {
|
||||||
|
name: 'section'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
jest.spyOn(controller, 'refresh');
|
||||||
|
|
||||||
controller.applySort();
|
controller.applySort();
|
||||||
|
|
||||||
expect(controller.model.order).toBeUndefined();
|
expect(controller.model.order).toBeUndefined();
|
||||||
expect(controller.model.refresh).toHaveBeenCalled();
|
expect(controller.$state.go).toHaveBeenCalled();
|
||||||
|
expect(controller.refresh).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call the model.refresh() after setting model order according to the controller sortCriteria', () => {
|
it('should call the $state go and model refresh after setting model order according to the controller sortCriteria', () => {
|
||||||
controller.model = {refresh: jest.fn()};
|
|
||||||
const orderBy = {field: 'myField', sortType: 'ASC'};
|
const orderBy = {field: 'myField', sortType: 'ASC'};
|
||||||
|
controller.$state = {
|
||||||
|
go: jest.fn(),
|
||||||
|
current: {
|
||||||
|
name: 'section'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
jest.spyOn(controller, 'refresh');
|
||||||
|
|
||||||
controller.sortCriteria = [orderBy];
|
controller.sortCriteria = [orderBy];
|
||||||
|
|
||||||
controller.applySort();
|
controller.applySort();
|
||||||
|
|
||||||
expect(controller.model.order).toEqual(`${orderBy.field} ${orderBy.sortType}`);
|
expect(controller.model.order).toEqual(`${orderBy.field} ${orderBy.sortType}`);
|
||||||
expect(controller.model.refresh).toHaveBeenCalled();
|
expect(controller.$state.go).toHaveBeenCalled();
|
||||||
|
expect(controller.refresh).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('filterSanitizer()', () => {
|
describe('filterSanitizer()', () => {
|
||||||
it('should remove the where filter after leaving no fields in it', () => {
|
it('should remove the where filter after leaving no fields in it', () => {
|
||||||
controller.model = {
|
controller.model.userFilter = {
|
||||||
userFilter: {
|
|
||||||
where: {fieldToRemove: 'valueToRemove'}
|
where: {fieldToRemove: 'valueToRemove'}
|
||||||
},
|
|
||||||
userParams: {}
|
|
||||||
};
|
};
|
||||||
|
controller.model.userParams = {};
|
||||||
|
|
||||||
const result = controller.filterSanitizer('fieldToRemove');
|
const result = controller.filterSanitizer('fieldToRemove');
|
||||||
|
|
||||||
|
@ -193,8 +209,7 @@ describe('Component smartTable', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should remove the where filter after leaving no fields and "empty ands/ors" in it', () => {
|
it('should remove the where filter after leaving no fields and "empty ands/ors" in it', () => {
|
||||||
controller.model = {
|
controller.model.userFilter = {
|
||||||
userFilter: {
|
|
||||||
where: {
|
where: {
|
||||||
and: [
|
and: [
|
||||||
{aFieldToRemove: 'aValueToRemove'},
|
{aFieldToRemove: 'aValueToRemove'},
|
||||||
|
@ -208,8 +223,7 @@ describe('Component smartTable', () => {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
userParams: {}
|
controller.model.userParams = {};
|
||||||
};
|
|
||||||
|
|
||||||
const result = controller.filterSanitizer('aFieldToRemove');
|
const result = controller.filterSanitizer('aFieldToRemove');
|
||||||
|
|
||||||
|
@ -219,8 +233,7 @@ describe('Component smartTable', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not remove the where filter after leaving no empty "ands/ors" in it', () => {
|
it('should not remove the where filter after leaving no empty "ands/ors" in it', () => {
|
||||||
controller.model = {
|
controller.model.userFilter = {
|
||||||
userFilter: {
|
|
||||||
where: {
|
where: {
|
||||||
and: [
|
and: [
|
||||||
{aFieldToRemove: 'aValueToRemove'},
|
{aFieldToRemove: 'aValueToRemove'},
|
||||||
|
@ -234,9 +247,8 @@ describe('Component smartTable', () => {
|
||||||
],
|
],
|
||||||
or: [{dontKillMe: 'thanks'}]
|
or: [{dontKillMe: 'thanks'}]
|
||||||
}
|
}
|
||||||
},
|
|
||||||
userParams: {}
|
|
||||||
};
|
};
|
||||||
|
controller.model.userParams = {};
|
||||||
|
|
||||||
const result = controller.filterSanitizer('aFieldToRemove');
|
const result = controller.filterSanitizer('aFieldToRemove');
|
||||||
|
|
||||||
|
@ -249,7 +261,7 @@ describe('Component smartTable', () => {
|
||||||
describe('saveAll()', () => {
|
describe('saveAll()', () => {
|
||||||
it('should throw an error if there are no changes to save in the model', () => {
|
it('should throw an error if there are no changes to save in the model', () => {
|
||||||
jest.spyOn(controller.vnApp, 'showError');
|
jest.spyOn(controller.vnApp, 'showError');
|
||||||
controller.model = {isChanged: false};
|
controller.model.isChanged = false;
|
||||||
controller.saveAll();
|
controller.saveAll();
|
||||||
|
|
||||||
expect(controller.vnApp.showError).toHaveBeenCalledWith('No changes to save');
|
expect(controller.vnApp.showError).toHaveBeenCalledWith('No changes to save');
|
||||||
|
@ -258,10 +270,8 @@ describe('Component smartTable', () => {
|
||||||
it('should call the showSuccess() if there are changes to save in the model', done => {
|
it('should call the showSuccess() if there are changes to save in the model', done => {
|
||||||
jest.spyOn(controller.vnApp, 'showSuccess');
|
jest.spyOn(controller.vnApp, 'showSuccess');
|
||||||
|
|
||||||
controller.model = {
|
controller.model.save = jest.fn().mockReturnValue(Promise.resolve());
|
||||||
save: jest.fn().mockReturnValue(Promise.resolve()),
|
controller.model.isChanged = true;
|
||||||
isChanged: true
|
|
||||||
};
|
|
||||||
|
|
||||||
controller.saveAll().then(() => {
|
controller.saveAll().then(() => {
|
||||||
expect(controller.vnApp.showSuccess).toHaveBeenCalledWith('Data saved!');
|
expect(controller.vnApp.showSuccess).toHaveBeenCalledWith('Data saved!');
|
||||||
|
@ -269,4 +279,43 @@ describe('Component smartTable', () => {
|
||||||
}).catch(done.fail);
|
}).catch(done.fail);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('defaultFilter()', () => {
|
||||||
|
it('should call model refresh and model addFilter with filter', () => {
|
||||||
|
controller.exprBuilder = jest.fn().mockReturnValue({builtField: 'builtValue'});
|
||||||
|
|
||||||
|
controller.$params = {
|
||||||
|
q: '{"tableQ": {"fieldName":"value"}}'
|
||||||
|
};
|
||||||
|
controller.columns = [
|
||||||
|
{field: 'fieldName'}
|
||||||
|
];
|
||||||
|
controller.$inputsScope = {
|
||||||
|
searchProps: {}
|
||||||
|
};
|
||||||
|
jest.spyOn(controller, 'refresh');
|
||||||
|
|
||||||
|
controller.defaultFilter();
|
||||||
|
|
||||||
|
expect(controller.model.addFilter).toHaveBeenCalled();
|
||||||
|
expect(controller.refresh).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('searchPropsSanitizer()', () => {
|
||||||
|
it('should searchProps sanitize', () => {
|
||||||
|
controller.$inputsScope = {
|
||||||
|
searchProps: {
|
||||||
|
filterOne: '1',
|
||||||
|
filterTwo: ''
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const searchPropsExpected = {
|
||||||
|
filterOne: '1'
|
||||||
|
};
|
||||||
|
const newSearchProps = controller.searchPropsSanitizer();
|
||||||
|
|
||||||
|
expect(newSearchProps).toEqual(searchPropsExpected);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -105,7 +105,7 @@
|
||||||
"acl": ["claimManager"]
|
"acl": ["claimManager"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"url": "/action",
|
"url": "/action?q",
|
||||||
"state": "claim.card.action",
|
"state": "claim.card.action",
|
||||||
"component": "vn-claim-action",
|
"component": "vn-claim-action",
|
||||||
"description": "Action",
|
"description": "Action",
|
||||||
|
|
|
@ -406,13 +406,13 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"url": "/defaulter",
|
"url": "/defaulter?q",
|
||||||
"state": "client.defaulter",
|
"state": "client.defaulter",
|
||||||
"component": "vn-client-defaulter",
|
"component": "vn-client-defaulter",
|
||||||
"description": "Defaulter"
|
"description": "Defaulter"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"url" : "/notification",
|
"url" : "/notification?q",
|
||||||
"state": "client.notification",
|
"state": "client.notification",
|
||||||
"component": "vn-client-notification",
|
"component": "vn-client-notification",
|
||||||
"description": "Notifications"
|
"description": "Notifications"
|
||||||
|
@ -424,7 +424,7 @@
|
||||||
"description": "Unpaid"
|
"description": "Unpaid"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"url": "/extended-list",
|
"url": "/extended-list?q",
|
||||||
"state": "client.extendedList",
|
"state": "client.extendedList",
|
||||||
"component": "vn-client-extended-list",
|
"component": "vn-client-extended-list",
|
||||||
"description": "Extended list"
|
"description": "Extended list"
|
||||||
|
|
|
@ -34,7 +34,7 @@
|
||||||
<th field="itemFk">
|
<th field="itemFk">
|
||||||
<span translate>Item ID</span>
|
<span translate>Item ID</span>
|
||||||
</th>
|
</th>
|
||||||
<th field="itemName">
|
<th field="name">
|
||||||
<span translate>Description</span>
|
<span translate>Description</span>
|
||||||
</th>
|
</th>
|
||||||
<th field="warehouseFk">
|
<th field="warehouseFk">
|
||||||
|
|
|
@ -12,14 +12,6 @@ export default class Controller extends Section {
|
||||||
},
|
},
|
||||||
defaultSearch: true,
|
defaultSearch: true,
|
||||||
columns: [
|
columns: [
|
||||||
{
|
|
||||||
field: 'itemName',
|
|
||||||
autocomplete: {
|
|
||||||
url: 'Items',
|
|
||||||
showField: 'name',
|
|
||||||
valueField: 'id'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
field: 'warehouseFk',
|
field: 'warehouseFk',
|
||||||
autocomplete: {
|
autocomplete: {
|
||||||
|
@ -105,8 +97,8 @@ export default class Controller extends Section {
|
||||||
|
|
||||||
exprBuilder(param, value) {
|
exprBuilder(param, value) {
|
||||||
switch (param) {
|
switch (param) {
|
||||||
case 'itemName':
|
case 'name':
|
||||||
return {'i.id': value};
|
return {'i.name': {like: `%${value}%`}};
|
||||||
case 'itemFk':
|
case 'itemFk':
|
||||||
case 'warehouseFk':
|
case 'warehouseFk':
|
||||||
case 'rate2':
|
case 'rate2':
|
||||||
|
|
|
@ -23,6 +23,8 @@
|
||||||
model="model"
|
model="model"
|
||||||
options="$ctrl.smartTableOptions"
|
options="$ctrl.smartTableOptions"
|
||||||
expr-builder="$ctrl.exprBuilder(param, value)"
|
expr-builder="$ctrl.exprBuilder(param, value)"
|
||||||
|
disabled-table-filter="true"
|
||||||
|
disabled-table-order="true"
|
||||||
class="scrollable sm">
|
class="scrollable sm">
|
||||||
<slot-actions>
|
<slot-actions>
|
||||||
<vn-horizontal>
|
<vn-horizontal>
|
||||||
|
|
|
@ -39,7 +39,7 @@
|
||||||
"abstract": true,
|
"abstract": true,
|
||||||
"component": "vn-route-card"
|
"component": "vn-route-card"
|
||||||
}, {
|
}, {
|
||||||
"url": "/agency-term",
|
"url": "/agency-term?q",
|
||||||
"abstract": true,
|
"abstract": true,
|
||||||
"state": "route.agencyTerm",
|
"state": "route.agencyTerm",
|
||||||
"component": "ui-view"
|
"component": "ui-view"
|
||||||
|
|
Loading…
Reference in New Issue