salix/front/core/components/smart-table/index.js

621 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;
options.defaultSearch = true;
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;
this.refresh();
}
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');
if (!tbody) return;
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 !== '' && value !== null && value !== undefined) {
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'})
.then(() => this.refresh());
}
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'})
.then(() => this.refresh());
}
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()
.finally(() => 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: '<?',
}
});