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; 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(` Enter a new search `)(this.$); tbody.appendChild(noSearch[0]); const noRows = this.$compile(` No data `)(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(` `)(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(` `)(this.$inputsScope); } if (options && options.checkbox) { input = this.$compile(` `)(this.$inputsScope); } if (options && options.datepicker) { input = this.$compile(` `)(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'}) .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: '