615 lines
19 KiB
JavaScript
615 lines
19 KiB
JavaScript
import ngModule from '../../module';
|
|
import Component from '../../lib/component';
|
|
import {buildFilter} from 'vn-loopback/util/filter';
|
|
import angular from 'angular';
|
|
import {camelToKebab} from '../../lib/string';
|
|
import './style.scss';
|
|
import './table.scss';
|
|
|
|
export default class SmartTable extends Component {
|
|
constructor($element, $, $transclude) {
|
|
super($element, $);
|
|
this.currentUserId = window.localStorage.currentUserWorkerId;
|
|
this.$transclude = $transclude;
|
|
this.sortCriteria = [];
|
|
this.$inputsScope;
|
|
this.columns = [];
|
|
this.autoSave = false;
|
|
this.autoState = true;
|
|
this.transclude();
|
|
}
|
|
|
|
$onChanges() {
|
|
if (this.model) {
|
|
this.defaultFilter();
|
|
this.defaultOrder();
|
|
}
|
|
}
|
|
|
|
$onDestroy() {
|
|
const styleElement = document.querySelector('style[id="smart-table"]');
|
|
if (this.$.css && styleElement)
|
|
styleElement.parentNode.removeChild(styleElement);
|
|
}
|
|
|
|
get options() {
|
|
return this._options;
|
|
}
|
|
|
|
set options(options) {
|
|
this._options = options;
|
|
if (!options) return;
|
|
|
|
if (options.defaultSearch)
|
|
this.displaySearch();
|
|
|
|
const activeButtons = options.activeButtons;
|
|
const missingId = activeButtons && activeButtons.shownColumns && !this.viewConfigId;
|
|
if (missingId)
|
|
throw new Error('vnSmartTable: View identifier not defined');
|
|
}
|
|
|
|
get model() {
|
|
return this._model;
|
|
}
|
|
|
|
set model(value) {
|
|
this._model = value;
|
|
if (value)
|
|
this.$.model = value;
|
|
}
|
|
|
|
getDefaultViewConfig() {
|
|
const url = 'DefaultViewConfigs';
|
|
const filter = {where: {tableCode: this.viewConfigId}};
|
|
return this.$http.get(url, {filter})
|
|
.then(res => {
|
|
if (res && res.data.length)
|
|
return res.data[0].columns;
|
|
});
|
|
}
|
|
|
|
get viewConfig() {
|
|
return this._viewConfig;
|
|
}
|
|
|
|
set viewConfig(value) {
|
|
this._viewConfig = value;
|
|
|
|
if (!value) return;
|
|
|
|
if (!value.length) {
|
|
this.getDefaultViewConfig().then(columns => {
|
|
const defaultViewConfig = columns ? columns : {};
|
|
|
|
const userViewModel = this.$.userViewModel;
|
|
for (const column of this.columns) {
|
|
if (defaultViewConfig[column.field] == undefined)
|
|
defaultViewConfig[column.field] = true;
|
|
}
|
|
|
|
userViewModel.insert({
|
|
userFk: this.currentUserId,
|
|
tableConfig: this.viewConfigId,
|
|
configuration: defaultViewConfig
|
|
});
|
|
}).finally(() => this.applyViewConfig());
|
|
} else
|
|
this.applyViewConfig();
|
|
}
|
|
|
|
get checkedRows() {
|
|
const model = this.model;
|
|
if (model && model.data)
|
|
return model.data.filter(row => row.$checked);
|
|
|
|
return null;
|
|
}
|
|
|
|
get checkAll() {
|
|
return this._checkAll;
|
|
}
|
|
|
|
set checkAll(value) {
|
|
this._checkAll = value;
|
|
if (value !== undefined) {
|
|
const shownColumns = this.viewConfig[0].configuration;
|
|
for (let param in shownColumns)
|
|
shownColumns[param] = value;
|
|
}
|
|
}
|
|
|
|
transclude() {
|
|
const slotTable = this.element.querySelector('#table');
|
|
this.$transclude($clone => {
|
|
const table = $clone[0];
|
|
slotTable.appendChild(table);
|
|
this.registerColumns();
|
|
this.emptyDataRows();
|
|
}, null, 'table');
|
|
}
|
|
|
|
saveViewConfig() {
|
|
const userViewModel = this.$.userViewModel;
|
|
const [viewConfig] = userViewModel.data;
|
|
viewConfig.configuration = Object.assign({}, viewConfig.configuration);
|
|
userViewModel.save()
|
|
.then(() => this.vnApp.showSuccess(this.$t('Data saved!')))
|
|
.then(() => this.applyViewConfig())
|
|
.then(() => this.$.smartTableColumns.hide());
|
|
}
|
|
|
|
applyViewConfig() {
|
|
const userViewModel = this.$.userViewModel;
|
|
const [viewConfig] = userViewModel.data;
|
|
|
|
const selectors = [];
|
|
for (const column of this.columns) {
|
|
if (viewConfig.configuration[column.field] == false) {
|
|
const baseSelector = `smart-table[view-config-id="${this.viewConfigId}"] table`;
|
|
selectors.push(`${baseSelector} thead > tr:not([second-header]) > th:nth-child(${column.index + 1})`);
|
|
selectors.push(`${baseSelector} tbody > tr > td:nth-child(${column.index + 1})`);
|
|
}
|
|
}
|
|
|
|
const styleElement = document.querySelector('style[id="smart-table"]');
|
|
|
|
if (styleElement)
|
|
styleElement.parentNode.removeChild(styleElement);
|
|
|
|
if (selectors.length) {
|
|
const rule = selectors.join(', ') + '{display: none}';
|
|
this.$.css = document.createElement('style');
|
|
this.$.css.setAttribute('id', 'smart-table');
|
|
document.head.appendChild(this.$.css);
|
|
this.$.css.appendChild(document.createTextNode(rule));
|
|
}
|
|
}
|
|
|
|
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() {
|
|
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;
|
|
|
|
const orderFields = order.split(', ');
|
|
|
|
for (const fieldString of orderFields) {
|
|
const field = fieldString.split(' ');
|
|
const fieldName = field[0];
|
|
|
|
let sortType = 'ASC';
|
|
if (field.length === 2)
|
|
sortType = field[1];
|
|
|
|
const priority = this.sortCriteria.length + 1;
|
|
const column = this.columns.find(column => column.field == fieldName);
|
|
if (column) {
|
|
this.sortCriteria.push({field: fieldName, sortType: sortType, priority: priority});
|
|
|
|
const isASC = sortType == 'ASC';
|
|
const isDESC = sortType == 'DESC';
|
|
|
|
if (isDESC) {
|
|
column.element.classList.remove('asc');
|
|
column.element.classList.add('desc');
|
|
}
|
|
|
|
if (isASC) {
|
|
column.element.classList.remove('desc');
|
|
column.element.classList.add('asc');
|
|
}
|
|
|
|
this.setPriority(column.element, priority);
|
|
}
|
|
}
|
|
|
|
this.model.order = order;
|
|
}
|
|
|
|
registerColumns() {
|
|
const header = this.element.querySelector('thead > tr:not([second-header])');
|
|
if (!header) return;
|
|
const columns = header.querySelectorAll('th');
|
|
|
|
// Click handler
|
|
for (const [index, column] of columns.entries()) {
|
|
const field = column.getAttribute('field');
|
|
if (field) {
|
|
const columnElement = angular.element(column);
|
|
const caption = columnElement.text().trim();
|
|
|
|
this.columns.push({field, caption, index, element: column});
|
|
|
|
column.addEventListener('click', () => this.orderHandler(column));
|
|
}
|
|
}
|
|
}
|
|
|
|
emptyDataRows() {
|
|
const header = this.element.querySelector('thead > tr:not([second-header])');
|
|
const columns = header.querySelectorAll('th');
|
|
const tbody = this.element.querySelector('tbody');
|
|
if (tbody) {
|
|
const noSearch = this.$compile(`
|
|
<tr class="empty-rows" ng-if="!model.data">
|
|
<td colspan="${columns.length}" translate>Enter a new search</td>
|
|
</tr>
|
|
`)(this.$);
|
|
tbody.appendChild(noSearch[0]);
|
|
|
|
const noRows = this.$compile(`
|
|
<tr class="empty-rows" ng-if="model.data.length == 0">
|
|
<td colspan="${columns.length}" translate>No data</td>
|
|
</tr>
|
|
`)(this.$);
|
|
tbody.appendChild(noRows[0]);
|
|
}
|
|
}
|
|
|
|
orderHandler(element) {
|
|
const field = element.getAttribute('field');
|
|
const existingCriteria = this.sortCriteria.find(criteria => {
|
|
return criteria.field == field;
|
|
});
|
|
|
|
const isASC = existingCriteria && existingCriteria.sortType == 'ASC';
|
|
const isDESC = existingCriteria && existingCriteria.sortType == 'DESC';
|
|
|
|
if (!existingCriteria) {
|
|
const priority = this.sortCriteria.length + 1;
|
|
|
|
this.sortCriteria.push({field: field, sortType: 'ASC', priority: priority});
|
|
element.classList.remove('desc');
|
|
element.classList.add('asc');
|
|
|
|
this.setPriority(element, priority);
|
|
}
|
|
|
|
if (isDESC) {
|
|
this.sortCriteria.splice(this.sortCriteria.findIndex(criteria => {
|
|
return criteria.field == field;
|
|
}), 1);
|
|
element.classList.remove('desc');
|
|
element.classList.remove('asc');
|
|
|
|
element.querySelector('sort-priority').remove();
|
|
}
|
|
|
|
if (isASC) {
|
|
existingCriteria.sortType = 'DESC';
|
|
element.classList.remove('asc');
|
|
element.classList.add('desc');
|
|
}
|
|
|
|
let priority = 0;
|
|
for (const criteria of this.sortCriteria) {
|
|
const column = this.columns.find(column => column.field == criteria.field);
|
|
if (column) {
|
|
criteria.priority = priority;
|
|
priority++;
|
|
|
|
column.element.querySelector('sort-priority').remove();
|
|
|
|
this.setPriority(column.element, priority);
|
|
}
|
|
}
|
|
|
|
this.applySort();
|
|
}
|
|
|
|
setPriority(column, priority) {
|
|
const sortPriority = document.createElement('sort-priority');
|
|
sortPriority.setAttribute('class', 'sort-priority');
|
|
sortPriority.innerHTML = priority;
|
|
column.appendChild(sortPriority);
|
|
}
|
|
|
|
displaySearch() {
|
|
const header = this.element.querySelector('thead > tr:not([second-header])');
|
|
if (!header) return;
|
|
|
|
const tbody = this.element.querySelector('tbody');
|
|
const columns = header.querySelectorAll('th');
|
|
|
|
const hasSearchRow = tbody.querySelector('tr#searchRow');
|
|
if (hasSearchRow) {
|
|
if (this.$inputsScope)
|
|
this.$inputsScope.$destroy();
|
|
|
|
return hasSearchRow.remove();
|
|
}
|
|
|
|
const searchRow = document.createElement('tr');
|
|
searchRow.setAttribute('id', 'searchRow');
|
|
|
|
this.$inputsScope = this.$.$new();
|
|
|
|
for (let column of columns) {
|
|
const field = column.getAttribute('field');
|
|
const cell = document.createElement('td');
|
|
cell.setAttribute('centered', '');
|
|
|
|
if (field) {
|
|
let input;
|
|
let options;
|
|
const columnOptions = this.options && this.options.columns;
|
|
|
|
if (columnOptions)
|
|
options = columnOptions.find(column => column.field == field);
|
|
|
|
if (options && options.searchable == false) {
|
|
searchRow.appendChild(cell);
|
|
continue;
|
|
}
|
|
|
|
input = this.$compile(`
|
|
<vn-textfield
|
|
class="dense"
|
|
name="${field}"
|
|
ng-model="searchProps['${field}']"
|
|
ng-keydown="$ctrl.searchWithEvent($event, '${field}')"
|
|
clear-disabled="true"
|
|
/>`)(this.$inputsScope);
|
|
|
|
if (options && options.autocomplete) {
|
|
let props = ``;
|
|
|
|
const autocomplete = options.autocomplete;
|
|
for (const prop in autocomplete)
|
|
props += `${camelToKebab(prop)}="${autocomplete[prop]}"\n`;
|
|
input = this.$compile(`
|
|
<vn-autocomplete
|
|
class="dense"
|
|
name="${field}"
|
|
ng-model="searchProps['${field}']"
|
|
${props}
|
|
on-change="$ctrl.searchByColumn('${field}')"
|
|
clear-disabled="true"
|
|
/>`)(this.$inputsScope);
|
|
}
|
|
|
|
if (options && options.checkbox) {
|
|
input = this.$compile(`
|
|
<vn-check
|
|
class="dense"
|
|
name="${field}"
|
|
ng-model="searchProps['${field}']"
|
|
on-change="$ctrl.searchByColumn('${field}')"
|
|
triple-state="true"
|
|
/>`)(this.$inputsScope);
|
|
}
|
|
|
|
if (options && options.datepicker) {
|
|
input = this.$compile(`
|
|
<vn-date-picker
|
|
class="dense"
|
|
name="${field}"
|
|
ng-model="searchProps['${field}']"
|
|
on-change="$ctrl.searchByColumn('${field}')"
|
|
/>`)(this.$inputsScope);
|
|
}
|
|
|
|
cell.appendChild(input[0]);
|
|
}
|
|
searchRow.appendChild(cell);
|
|
}
|
|
|
|
tbody.prepend(searchRow);
|
|
}
|
|
|
|
searchWithEvent($event, field) {
|
|
if ($event.key != 'Enter') return;
|
|
|
|
this.searchByColumn(field);
|
|
}
|
|
|
|
searchByColumn(field) {
|
|
const filters = this.filterSanitizer(field);
|
|
|
|
if (filters && filters.userFilter)
|
|
this.model.userFilter = filters.userFilter;
|
|
this.addFilter(field, this.$inputsScope.searchProps[field]);
|
|
}
|
|
|
|
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) {
|
|
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) {
|
|
where = buildFilter(whereParams, (param, value) =>
|
|
this.exprBuilder({param, value})
|
|
);
|
|
}
|
|
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'});
|
|
}
|
|
|
|
applySort() {
|
|
let order = this.sortCriteria.map(criteria => `${criteria.field} ${criteria.sortType}`);
|
|
order = order.join(', ');
|
|
|
|
if (order)
|
|
this.model.order = order;
|
|
|
|
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'});
|
|
}
|
|
|
|
filterSanitizer(field) {
|
|
const userFilter = this.model.userFilter;
|
|
const userParams = this.model.userParams;
|
|
const where = userFilter && userFilter.where;
|
|
|
|
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') {
|
|
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};
|
|
}
|
|
|
|
removeFilter() {
|
|
this.model.applyFilter(userFilter, userParams);
|
|
}
|
|
|
|
createRow() {
|
|
let data = {};
|
|
|
|
if (this.defaultNewData)
|
|
data = this.defaultNewData();
|
|
|
|
this.model.insert(data);
|
|
}
|
|
|
|
deleteAll() {
|
|
for (let row of this.checkedRows)
|
|
this.model.removeRow(row);
|
|
|
|
if (this.autoSave)
|
|
this.saveAll();
|
|
}
|
|
|
|
saveAll() {
|
|
const model = this.model;
|
|
|
|
if (!model.isChanged)
|
|
return this.vnApp.showError(this.$t('No changes to save'));
|
|
|
|
return this.model.save()
|
|
.then(() => this.vnApp.showSuccess(this.$t('Data saved!')));
|
|
}
|
|
|
|
refresh() {
|
|
this.isRefreshing = true;
|
|
this.model.refresh()
|
|
.then(() => this.isRefreshing = false)
|
|
.catch(() => this.isRefreshing = false);
|
|
}
|
|
}
|
|
|
|
SmartTable.$inject = ['$element', '$scope', '$transclude'];
|
|
|
|
ngModule.vnComponent('smartTable', {
|
|
template: require('./index.html'),
|
|
controller: SmartTable,
|
|
transclude: {
|
|
table: '?slotTable',
|
|
actions: '?slotActions',
|
|
pagination: '?slotPagination'
|
|
},
|
|
bindings: {
|
|
model: '<?',
|
|
viewConfigId: '@?',
|
|
autoSave: '<?',
|
|
exprBuilder: '&?',
|
|
defaultNewData: '&?',
|
|
options: '<?',
|
|
disabledTableFilter: '<?',
|
|
disabledTableOrder: '<?',
|
|
}
|
|
});
|