diff --git a/application/controllers/components/Filter.php b/application/controllers/components/Filter.php index ab7e1493e..bde7d7ed7 100644 --- a/application/controllers/components/Filter.php +++ b/application/controllers/components/Filter.php @@ -26,6 +26,9 @@ class Filter extends FHC_Controller // Loads authentication library and starts authentication $this->load->library('AuthLib'); + // Loads the FiltersModel + $this->load->model('system/Filters_model', 'FiltersModel'); + // Loads the FilterCmptLib with HTTP GET/POST parameters $this->_startFilterCmptLib(); } diff --git a/public/js/components/filter/Filter.js b/public/js/components/filter/Filter.js index 2bf878e5a..2b6a4fe6d 100644 --- a/public/js/components/filter/Filter.js +++ b/public/js/components/filter/Filter.js @@ -18,6 +18,9 @@ import {CoreFilterAPIs} from './API.js'; import {CoreRESTClient} from '../../RESTClient.js'; import {CoreFetchCmpt} from '../../components/Fetch.js'; +import FilterConfig from './Filter/Config.js'; +import FilterColumns from './Filter/Columns.js'; +import TableDownload from './Table/Download.js'; // const FILTER_COMPONENT_NEW_FILTER = 'Filter Component New Filter'; @@ -29,11 +32,18 @@ var _uuid = 0; * */ export const CoreFilterCmpt = { - emits: ['nwNewEntry'], components: { - CoreFetchCmpt + CoreFetchCmpt, + FilterConfig, + FilterColumns, + TableDownload }, + emits: [ + 'nwNewEntry', + 'click:new' + ], props: { + onNwNewEntry: Function, // NOTE(chris): Hack to get the nwNewEntry listener into $props title: String, sideMenu: { type: Boolean, @@ -45,7 +55,16 @@ export const CoreFilterCmpt = { }, tabulatorOptions: Object, tabulatorEvents: Array, - tableOnly: Boolean + tableOnly: Boolean, + reload: Boolean, + download: { + type: [Boolean, String, Function, Array, Object], + default: false + }, + newBtnShow: Boolean, + newBtnClass: [String, Array, Object], + newBtnDisabled: Boolean, + newBtnLabel: String }, data: function() { return { @@ -60,6 +79,7 @@ export const CoreFilterCmpt = { filterFields: null, availableFilters: null, + selectedFilter: null, // FetchCmpt binded properties fetchCmptRefresh: false, @@ -68,7 +88,9 @@ export const CoreFilterCmpt = { fetchCmptDataFetched: null, tabulator: null, - tableBuilt: false + tableBuilt: false, + tabulatorHasSelector: false, + selectedData: [] }; }, computed: { @@ -115,6 +137,8 @@ export const CoreFilterCmpt = { { // If the column has to be displayed or not col.visible = selectedFields.indexOf(col.field) >= 0; + if (col.formatter == 'rowSelection') + col.visible = true; if (col.hasOwnProperty('resizable')) col.resizable = col.visible; @@ -135,21 +159,24 @@ export const CoreFilterCmpt = { if (!this.uuid) return ''; return '-' + this.uuid; + }, + columnsForFilter() { + if (!this.filteredColumns || !this.datasetMetadata) + return []; + const filterTitles = this.filteredColumns.reduce((a,c) => { + a[c.field] = c.title; + return a; + }, {}); + return this.datasetMetadata.map(el => ({...el, ...{title: filterTitles[el.name]}})); } }, - beforeCreate() { - if (!this.tableOnly == !this.filterType) - alert('You can not have a filter-type in table-only mode!'); - }, - created() { - this.uuid = _uuid++; - if (!this.tableOnly) - this.getFilter(); // get the filter data - }, - mounted() { - this.initTabulator(); - }, methods: { + reloadTable() { + if (this.tableOnly) + this.tabulator.reload(); + else + this.getFilter(); + }, initTabulator() { // Define a default tabulator options in case it was not provided let tabulatorOptions = {...{ @@ -164,6 +191,9 @@ export const CoreFilterCmpt = { tabulatorOptions.columns = this.filteredColumns; } + if (tabulatorOptions.columns && tabulatorOptions.columns.filter(el => el.formatter == 'rowSelection').length) + this.tabulatorHasSelector = true; + // Start the tabulator with the build options this.tabulator = new Tabulator( this.$refs.table, @@ -177,6 +207,9 @@ export const CoreFilterCmpt = { this.tabulator.on(evt.event, evt.handler); } this.tabulator.on('tableBuilt', () => this.tableBuilt = true); + this.tabulator.on("rowSelectionChanged", data => { + this.selectedData = data; + }); if (this.tableOnly) { this.tabulator.on('tableBuilt', () => { const cols = this.tabulator.getColumns(); @@ -194,15 +227,24 @@ export const CoreFilterCmpt = { } }, _updateTabulator() { - this.tabulator.setData(this.filteredData); + this.tabulatorHasSelector = this.filteredColumns.filter(el => el.formatter == 'rowSelection').length; this.tabulator.setColumns(this.filteredColumns); + this.tabulator.setData(this.filteredData); }, /** * */ getFilter: function() { - // - this.startFetchCmpt(CoreFilterAPIs.getFilter, null, this.render); + if (this.selectedFilter === null) + this.startFetchCmpt(CoreFilterAPIs.getFilter, null, this.render); + else + this.startFetchCmpt( + CoreFilterAPIs.getFilterById, + { + filterId: this.selectedFilter + }, + this.render + ); }, /** * @@ -230,7 +272,7 @@ export const CoreFilterCmpt = { filter.type = data.datasetMetadata[i].type; this.filterFields.push(filter); - break; + //break; } } } @@ -266,6 +308,7 @@ export const CoreFilterCmpt = { if (link == null) link = '#'; filtersArray[filtersArray.length] = { + id: filters[filtersCount].filter_id, link: link + filters[filtersCount].filter_id, description: filters[filtersCount].desc, sort: filtersCount, @@ -280,6 +323,7 @@ export const CoreFilterCmpt = { if (link == null) link = '#'; filtersArray[filtersArray.length] = { + id: personalFilters[filtersCount].filter_id, link: link + personalFilters[filtersCount].filter_id, description: personalFilters[filtersCount].desc, subscriptDescription: personalFilters[filtersCount].subscriptDescription, @@ -318,6 +362,7 @@ export const CoreFilterCmpt = { if (link == null) link = '#'; filtersArray[filtersArray.length] = { + id: filters[filtersCount].filter_id, option: filters[filtersCount].filter_id, description: filters[filtersCount].desc }; @@ -330,6 +375,7 @@ export const CoreFilterCmpt = { if (link == null) link = '#'; filtersArray[filtersArray.length] = { + id: personalFilters[filtersCount].filter_id, option: personalFilters[filtersCount].filter_id, description: personalFilters[filtersCount].desc }; @@ -366,12 +412,13 @@ export const CoreFilterCmpt = { /** * */ - handlerSaveCustomFilter: function(event) { + handlerSaveCustomFilter: function(customFilterName) { + this.selectedFilter = null; // this.startFetchCmpt( CoreFilterAPIs.saveCustomFilter, { - customFilterName: this.$refscustomFilterName.value + customFilterName }, this.getFilter ); @@ -380,159 +427,22 @@ export const CoreFilterCmpt = { * */ handlerRemoveCustomFilter: function(event) { + filterId = event.currentTarget.getAttribute("href").substring(1); + if (filterId === this.selectedFilter) + this.selectedFilter = null; // this.startFetchCmpt( CoreFilterAPIs.removeCustomFilter, { - filterId: event.currentTarget.getAttribute("href").substring(1) + filterId: filterId }, this.getFilter ); }, - /** - * - */ - handlerApplyFilterFields: function(event) { - let filterFields = []; - let filterFieldDivRows = document.getElementById('filterFields').getElementsByClassName('row'); - for (let i = 0; i< filterFieldDivRows.length; i++) - { - let filterField = {}; - - for (let j = 0; j< filterFieldDivRows[i].children.length; j++) - { - let filterColumn = filterFieldDivRows[i].children[j]; - let filterColumnElement = filterColumn.children[0]; - - // If the first column then search for the fields dropdown - if (j == 0) filterColumnElement = filterColumnElement.querySelector('select[name=fieldName]'); - - // If the filter name is _not_ null and it is _not_ a new filter - if (filterColumnElement.name != null && filterColumnElement.name != FILTER_COMPONENT_NEW_FILTER) - { - // Condition - if (filterColumnElement.name == 'condition' && filterColumnElement.value == "") - { - alert("Please fill all the filter options"); - return; - } - - // Name - if (filterColumnElement.name == 'fieldName') - { - filterField.name = filterColumnElement.value; - } - // Operation - if (filterColumnElement.name == 'operation') - { - filterField.operation = filterColumnElement.value; - } - // Condition - if (filterColumnElement.name == 'condition') - { - filterField.condition = filterColumnElement.value; - } - // Option - if (filterColumnElement.name == 'option') - { - filterField.option = filterColumnElement.value; - } - } - } - - if (Object.entries(filterField).length > 0) filterFields.push(filterField); - } - - // - this.startFetchCmpt( - CoreFilterAPIs.applyFilterFields, - { - filterFields: filterFields - }, - this.getFilter - ); - }, - /** - * - */ - handlerChangeFilterField: function(oldValue, newValue) { - - // If an old filter has been changed - if (oldValue != "") - { - for (let i = 0; i < this.filterFields.length; i++) - { - if (this.filterFields[i].name == oldValue) - { - this.filterFields.splice(i, 1); - break; - } - } - } - - // Then add the new filter - for (let i = 0; i < this.datasetMetadata.length; i++) - { - if (this.datasetMetadata[i].name == newValue) - { - let filter = { - name: this.datasetMetadata[i].name, - type: this.datasetMetadata[i].type - }; - - this.filterFields.push(filter); - break; - } - } - }, - /** - * - */ - handlerAddNewFilter: function(event) { - // Adds a new empty filter - this.filterFields.push({ - name: FILTER_COMPONENT_NEW_FILTER, - type: FILTER_COMPONENT_NEW_FILTER_TYPE - }); - }, /* * */ - handlerToggleSelectedField(field) { - - // If it is a selected field - if (this.selectedFields.indexOf(field) != -1) - { - // then hide it - this.tabulator.hideColumn(field); - // and remove it from the this.selectedFields property - this.selectedFields.splice(this.selectedFields.indexOf(field), 1); - } - else // otherwise - { - // show it - this.tabulator.showColumn(field); - // and add it to the this.selectedFields property - this.selectedFields.push(field); - } - }, - /** - * - */ - handlerRemoveFilterField: function(event) { - // - this.startFetchCmpt( - CoreFilterAPIs.removeFilterField, - { - filterField: event.currentTarget.getAttribute('field-to-remove') - }, - this.getFilter - ); - }, - /** - * - */ handlerGetFilterById: function(event) { let filterId = null; @@ -550,16 +460,37 @@ export const CoreFilterCmpt = { filterId = attr.substring(1); } - // Ajax call + this.switchFilter(filterId); + }, + switchFilter(filterId) { + this.selectedFilter = filterId; + this.getFilter(); + }, + applyFilterConfig(filterFields) { + this.selectedFilter = null; this.startFetchCmpt( - CoreFilterAPIs.getFilterById, + CoreFilterAPIs.applyFilterFields, { - filterId: filterId + filterFields }, - this.render + this.getFilter ); } }, + beforeCreate() { + if (!this.tableOnly == !this.filterType) + alert('You can not have a filter-type in table-only mode!'); + }, + created() { + if (this.sideMenu && (!this.$props.onNwNewEntry || !(this.$props.onNwNewEntry instanceof Function))) + alert('"nwNewEntry" listener is mandatory when sideMenu is true'); + this.uuid = _uuid++; + if (!this.tableOnly) + this.getFilter(); // get the filter data + }, + mounted() { + this.initTabulator(); + }, template: ` -
- [ {{ filterName }} ] - - -
- -
-
- -
-
- -
-
+
+
+ + + Mit {{selectedData.length}} ausgewählten: + +
+
+ [ {{ filterName }} ] + + + + + + +
-
-
- -
- -
-
-
- - Neuer Filter - - - - -
-
- -
- - -
-
-
- - -
-
-
- -
-
-
-
-
+
diff --git a/public/js/components/filter/Filter/Columns.js b/public/js/components/filter/Filter/Columns.js new file mode 100644 index 000000000..8c26df984 --- /dev/null +++ b/public/js/components/filter/Filter/Columns.js @@ -0,0 +1,81 @@ +/** + * Copyright (C) 2022 fhcomplete.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/** + * + */ +export default { + props: { + fields: Array, + selected: { + type: Array, + default: [] + }, + names: { + type: Array, + default: [] + } + }, + emits: { + hide: ['fieldName'], + show: ['fieldName'] + }, + data: function() { + return { + selectedFields: [] + }; + }, + computed: { + + }, + watch: { + selected(n) { + this.selectedFields = n; + } + }, + methods: { + toggle(field) { + if (this.selectedFields.indexOf(field) != -1) + { + this.selectedFields.splice(this.selectedFields.indexOf(field), 1); + this.$emit('hide', field); + } + else + { + this.selectedFields.push(field); + this.$emit('show', field); + } + } + }, + template: ` +
+
+
+
+ {{ names[fieldToDisplay] || fieldToDisplay }} +
+
+
+
+ ` +}; + diff --git a/public/js/components/filter/Filter/Config.js b/public/js/components/filter/Filter/Config.js new file mode 100644 index 000000000..3c75c5a3c --- /dev/null +++ b/public/js/components/filter/Filter/Config.js @@ -0,0 +1,215 @@ +/** + * Copyright (C) 2022 fhcomplete.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +const FILTER_COMPONENT_NEW_FILTER = 'Filter Component New Filter'; + +/** + * + */ +export default { + props: { + filters: { + type: Array, + default: [] + }, + columns: { + type: Array, + default: [] + }, + fields: { + type: Array, + default: [] + } + }, + emits: { + switchFilter: ['filterId'], + applyFilterConfig: ['filterFields'], + saveCustomFilter: ['customFilterName'] + }, + data: function() { + return { + currentFields: [] + }; + }, + computed: { + types() { + return this.columns.reduce((a,c) => { + let type = c.type.toLowerCase(); + if (type.indexOf('int') >= 0) + a[c.name] = 'Numeric'; + else if ( + type.indexOf('varchar') >= 0 || + type.indexOf('text') >= 0 || + type.indexOf('bpchar') >= 0 + ) + a[c.name] = 'Text'; + else if ( + type.indexOf('timestamp') >= 0 || + type.indexOf('date') >= 0 + ) + a[c.name] = 'Date'; + else + a[c.name] = ''; + return a; + }, {}); + } + }, + watch: { + fields(n) { + this.currentFields = n; + } + }, + methods: { + switchFilter(evt) { + this.$emit('switchFilter', evt.currentTarget.value); + }, + applyFilterConfig() { + const filteredFields = this.currentFields.filter(el => el.name != FILTER_COMPONENT_NEW_FILTER); + if (filteredFields.filter(el => el.condition == "").length) + alert("Please fill all the filter options"); + else + this.$emit('applyFilterConfig', filteredFields); + }, + addField(evt) { + this.currentFields.push({ + name: FILTER_COMPONENT_NEW_FILTER + }); + }, + removeField(index) { + this.currentFields.splice(index, 1); + } + }, + template: ` +
+
+
+
+ +
+
+ +
+
+
+ +
+
+ Filter {{ index + 1 }} + +
+
+ + + + + + + + + + +
+ +
+
+
+ + +
+
+
+ + +
+
+
+ +
+
+
+
+
+ ` +}; + diff --git a/public/js/components/filter/Table/Download.js b/public/js/components/filter/Table/Download.js new file mode 100644 index 000000000..057fdad5e --- /dev/null +++ b/public/js/components/filter/Table/Download.js @@ -0,0 +1,247 @@ +/** + * Copyright (C) 2022 fhcomplete.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +const DEFAULT_ICONS = { + jsonLines: 'fa-file-lines', + xlsx: 'fa-file-excel', + pdf: 'fa-file-pdf', + html: 'fa-file-code', + json: 'fa-file', + csv: 'fa-file-csv' +}; +const DEFAULT_LABELS = { + jsonLines: 'Download as JSONLINES', + xlsx: 'Download as XLSX', + pdf: 'Download as PDF', + html: 'Download as HTML', + json: 'Download as JSON', + csv: 'Download as CSV ' +}; + + +/** + * + */ +export default { + props: { + tabulator: Object, + config: { + type: [Boolean, String, Function, Array, Object], + default: false + }, + iconClass: [String, Array, Object] + }, + computed: { + currentConfig() { + if (!this.config) + return false; + + let config = this.config; + + if (config instanceof Function) + return [config]; + + if (config === null) + return []; + + if (this.config === true) + config = ['csv']; + + if (Object.prototype.toString.call(config) === "[object String]") + config = config.split(','); + + if (typeof config === 'object' && !Array.isArray(config)) { + let newConfig = []; + for (var k in config) { + var v = config[k], type; + + if (!v) + continue; + + if (Object.prototype.toString.call(v) === "[object String]") { + type = this.stringToFileFormatter(v); + if (type !== null) { + newConfig.push({ + icon: 'fa-solid ' + DEFAULT_ICONS[type], + label: v === k ? DEFAULT_LABELS[type] : k, + formatter: type + }); + } else { + type = this.stringToFileFormatter(k); + if(type !== null) { + newConfig.push({ + icon: 'fa-solid ' + DEFAULT_ICONS[type], + label: v, + formatter: type + }); + } else { + alert('neither ' + k + ' nor ' + v + ' are supported download file types'); + } + } + } else if (typeof v === 'object' && !Array.isArray(v)) { + type = this.stringToFileFormatter(k); + if (type !== null) { + if (v.formatter === undefined) + v.formatter = type; + if (v.label === undefined) + v.label = DEFAULT_LABELS[type]; + if (v.icon === undefined) + v.icon = DEFAULT_ICONS[type]; + newConfig.push(v); + } else { + if (v.label === undefined) + v.label = k; + newConfig.push(v); + } + } else { + type = this.stringToFileFormatter(k); + if (type !== null) { + newConfig.push({ + icon: 'fa-solid ' + DEFAULT_ICONS[type], + label: DEFAULT_LABELS[type], + formatter: type + }); + } else { + alert(k + ' is not a supported download file type'); + } + } + } + config = newConfig; + } + + if (Array.isArray(config)) + { + config = config.map(el => { + if (Object.prototype.toString.call(el) === "[object String]") { + let formatter = this.stringToFileFormatter(el); + if (formatter === null) + return null; + return { + icon: 'fa-solid ' + DEFAULT_ICONS[formatter], + label: DEFAULT_LABELS[formatter], + formatter + }; + } + + if (el instanceof Function) + return { + formatter: el + } + + if (typeof el === 'object' && !Array.isArray(el) && el !== null) { + if (el.formatter instanceof Function) + return el; + if (this.validateFileFormatter(el.formatter)) + return el; + } + + return null; + }).filter(el => el !== null); + + if (config.length < 2) + return config; + + if (config.filter(el => el.label || el.icon).length == config.length) + return config; + + alert('Config not valid'); + } + + return []; + } + }, + methods: { + stringToFileFormatter(input) { + let lcInput = input.toLowerCase(); + + if (lcInput == 'jsonlines') + return 'jsonLines'; + + if (['xlsx', 'pdf', 'html', 'json', 'csv'].includes(lcInput)) + return lcInput; + + return null; + }, + validateFileFormatter(input) { + let formatter = this.stringToFileFormatter(input); + if (!formatter) { + alert(input + ' is not a supported file formatter'); + return false; + } + if (formatter == 'xlsx') { + if (!window.XLSX) { + alert('XLSX Library not loaded'); + return false; + } + } + if (formatter == 'pdf') { + if (!window.jspdf) { + alert('jsPDF Library not loaded'); + return false; + } + var doc = new jspdf.jsPDF({}); + if (!doc.autoTable) { + alert('jsPDF-AutoTable Plugin not loaded'); + return false; + } + } + return true; + }, + download(config) { + this.tabulator.download(config.formatter, config.file, config.options) + } + }, + template: ` + + ` +}; +