mirror of
https://github.com/FH-Complete/FHC-Core.git
synced 2026-06-01 20:29:29 +00:00
Merge branch 'feature-28886/Filter_Component_vs_Table_Component' into feature-29144/Prestudent_status_nach_Bismeldung_sperren
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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: `
|
||||
<!-- Load filter data -->
|
||||
<core-fetch-cmpt
|
||||
@@ -580,178 +511,53 @@ export const CoreFilterCmpt = {
|
||||
|
||||
<div :id="'filterCollapsables' + idExtra">
|
||||
|
||||
<div class="filter-header-title">
|
||||
<span v-if="!tableOnly" class="filter-header-title-span-filter">[ {{ filterName }} ]</span>
|
||||
<span v-if="!tableOnly" data-bs-toggle="collapse" :data-bs-target="'#collapseFilters' + idExtra" class="filter-header-title-span-icon fa-solid fa-filter fa-xl"></span>
|
||||
<span data-bs-toggle="collapse" :data-bs-target="'#collapseColumns' + idExtra" class="filter-header-title-span-icon fa-solid fa-table-columns fa-xl"></span>
|
||||
</div>
|
||||
|
||||
<div :id="'collapseColumns' + idExtra" class="card-body collapse" :data-bs-parent="'#filterCollapsables' + idExtra">
|
||||
<div class="card">
|
||||
<!-- Filter fields options -->
|
||||
<div class="row card-body filter-options-div">
|
||||
<div class="filter-fields-area">
|
||||
<template v-for="fieldToDisplay in fields">
|
||||
<div
|
||||
class="filter-fields-field"
|
||||
v-bind:class="selectedFields.indexOf(fieldToDisplay) != -1 ? 'text-light bg-dark' : '' "
|
||||
@click="handlerToggleSelectedField(fieldToDisplay)"
|
||||
>
|
||||
{{ fieldNames[fieldToDisplay] || fieldToDisplay }}
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex flex-row justify-content-between flex-wrap">
|
||||
<div v-if="newBtnShow || reload || $slots.actions" class="d-flex gap-2 align-items-baseline flex-wrap">
|
||||
<button v-if="newBtnShow" class="btn btn-outline-secondary" :class="newBtnClass" :title="newBtnLabel ? undefined : 'New'" :aria-label="newBtnLabel ? undefined : 'New'" @click="$emit('click:new', $event)" :disabled="newBtnDisabled">
|
||||
<span class="fa-solid fa-plus" aria-hidden="true"></span>
|
||||
{{ newBtnLabel }}
|
||||
</button>
|
||||
<button v-if="reload" class="btn btn-outline-secondary" aria-label="Reload" @click="reloadTable">
|
||||
<span class="fa-solid fa-rotate-right" aria-hidden="true"></span>
|
||||
</button>
|
||||
<span v-if="$slots.actions && tabulatorHasSelector">Mit {{selectedData.length}} ausgewählten: </span>
|
||||
<slot name="actions" v-bind="tabulatorHasSelector ? selectedData : []"></slot>
|
||||
</div>
|
||||
<div class="d-flex gap-1 align-items-baseline flex-grow-1 justify-content-end">
|
||||
<span v-if="!tableOnly">[ {{ filterName }} ]</span>
|
||||
<a v-if="!tableOnly" href="#" class="btn btn-link px-0 text-dark" data-bs-toggle="collapse" :data-bs-target="'#collapseFilters' + idExtra">
|
||||
<span class="fa-solid fa-xl fa-filter"></span>
|
||||
</a>
|
||||
<a href="#" class="btn btn-link px-0 text-dark" data-bs-toggle="collapse" :data-bs-target="'#collapseColumns' + idExtra">
|
||||
<span class="fa-solid fa-xl fa-table-columns"></span>
|
||||
</a>
|
||||
<table-download class="btn btn-link px-0 text-dark" :tabulator="tabulator" :config="download"></table-download>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!tableOnly" :id="'collapseFilters' + idExtra" class="card-body collapse" :data-bs-parent="'#filterCollapsables' + idExtra">
|
||||
<div class="card">
|
||||
<!-- Filter options -->
|
||||
<div class="card-body" v-if="!sideMenu">
|
||||
<select
|
||||
class="form-select"
|
||||
@change="handlerGetFilterById"
|
||||
>
|
||||
<option value="">Bitte auswählen...</option>
|
||||
<template v-for="availableFilter in availableFilters">
|
||||
<option v-bind:value="availableFilter.option">{{ availableFilter.description }}</option>
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
<div class="card-body filter-options-div">
|
||||
<div>
|
||||
<span>
|
||||
Neuer Filter
|
||||
</span>
|
||||
<span>
|
||||
<button class="btn btn-outline-dark" type="button" @click=handlerAddNewFilter>+</button>
|
||||
</span>
|
||||
</div>
|
||||
<div :id="'filterFields' + idExtra" class="filter-filter-fields">
|
||||
<template v-for="(filterField, index) in filterFields">
|
||||
<div class="row">
|
||||
<filter-columns
|
||||
:id="'collapseColumns' + idExtra"
|
||||
class="card-body collapse"
|
||||
:data-bs-parent="'#filterCollapsables' + idExtra"
|
||||
:fields="fields"
|
||||
:selected="selectedFields"
|
||||
:names="fieldNames"
|
||||
@hide="tabulator.hideColumn($event)"
|
||||
@show="tabulator.showColumn($event)"
|
||||
></filter-columns>
|
||||
|
||||
<div class="col-5">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">Filter {{ index + 1 }}</span>
|
||||
<select
|
||||
class="form-select"
|
||||
name="fieldName"
|
||||
v-bind:value="filterField.name"
|
||||
@change="handlerChangeFilterField(filterField.name, $event.target.value)"
|
||||
>
|
||||
<option value="">Feld zum Filter hinzufügen...</option>
|
||||
<template v-for="columnAlias in filteredColumns">
|
||||
<option v-bind:value="columnAlias.field">{{ columnAlias.title }}</option>
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Numeric -->
|
||||
<template
|
||||
v-if="filterField.type.toLowerCase().indexOf('int') >= 0">
|
||||
<div class="col-2">
|
||||
<select class="form-select" name="operation" v-model="filterField.operation">
|
||||
<option value="equal">Gleich</option>
|
||||
<option value="nequal">Nicht gleich</option>
|
||||
<option value="gt">Größer als</option>
|
||||
<option value="lt">Weniger als</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<input type="number" class="form-control" v-bind:value="filterField.condition" name="condition">
|
||||
</div>
|
||||
<div class="col">
|
||||
<button
|
||||
class="btn btn-outline-dark"
|
||||
type="button"
|
||||
v-bind:field-to-remove="filterField.name"
|
||||
@click=handlerRemoveFilterField>
|
||||
 X 
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Text -->
|
||||
<template
|
||||
v-if="filterField.type.toLowerCase().indexOf('varchar') >= 0
|
||||
|| filterField.type.toLowerCase().indexOf('text') >= 0
|
||||
|| filterField.type.toLowerCase().indexOf('bpchar') >= 0">
|
||||
<div class="col-2">
|
||||
<select class="form-select" name="operation" v-model="filterField.operation">
|
||||
<option value="equal">Gleich</option>
|
||||
<option value="nequal">Nicht gleich</option>
|
||||
<option value="contains">Enthält</option>
|
||||
<option value="ncontains">Enthält nicht</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<input type="text" class="form-control" v-bind:value="filterField.condition" name="condition">
|
||||
</div>
|
||||
<div class="col">
|
||||
<button
|
||||
class="btn btn-outline-dark"
|
||||
type="button"
|
||||
v-bind:field-to-remove="filterField.name"
|
||||
@click=handlerRemoveFilterField>
|
||||
 X 
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Timestamp and date -->
|
||||
<template
|
||||
v-if="filterField.type.toLowerCase().indexOf('timestamp') >= 0
|
||||
|| filterField.type.toLowerCase().indexOf('date') >= 0">
|
||||
<div class="col-2">
|
||||
<select class="form-select" name="operation" v-model="filterField.operation">
|
||||
<option value="gt">Größer als</option>
|
||||
<option value="lt">Weniger als</option>
|
||||
<option value="set">Eingestellt ist</option>
|
||||
<option value="nset">Eingestellt nicht ist</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-1">
|
||||
<input type="number" class="form-control" v-bind:value="filterField.condition" name="condition">
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<select class="form-select" name="option" v-model="filterField.option">
|
||||
<option value="minutes">Minuten</option>
|
||||
<option value="hours">Stunden</option>
|
||||
<option value="days">Tage</option>
|
||||
<option value="months">Monate</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col">
|
||||
<button
|
||||
class="btn btn-outline-dark"
|
||||
type="button"
|
||||
v-bind:field-to-remove="filterField.name"
|
||||
@click=handlerRemoveFilterField
|
||||
> - </button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Filter save options -->
|
||||
<div class="row">
|
||||
<div class="col-7">
|
||||
<div class="input-group">
|
||||
<input ref="customFilterName" type="text" class="form-control" placeholder="Filternamen eingeben..." :id="'customFilterName' + idExtra">
|
||||
<button type="button" class="btn btn-outline-secondary" @click=handlerSaveCustomFilter>Filter speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<button type="button" class="btn btn-outline-dark" @click=handlerApplyFilterFields>Filter anwenden</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<filter-config
|
||||
v-if="!tableOnly"
|
||||
:id="'collapseFilters' + idExtra"
|
||||
class="card-body collapse"
|
||||
:data-bs-parent="'#filterCollapsables' + idExtra"
|
||||
:filters="!sideMenu ? (availableFilters || []) : []"
|
||||
:columns="columnsForFilter"
|
||||
:fields="filterFields || []"
|
||||
@switch-filter="switchFilter"
|
||||
@apply-filter-config="applyFilterConfig"
|
||||
@save-custom-filter="handlerSaveCustomFilter"
|
||||
></filter-config>
|
||||
</div>
|
||||
|
||||
<!-- Tabulator -->
|
||||
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
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: `
|
||||
<div class="filter-columns">
|
||||
<div class="card">
|
||||
<div class="d-flex flex-wrap gap-2 p-3 justify-content-center">
|
||||
<div
|
||||
v-for="fieldToDisplay in fields"
|
||||
class="btn"
|
||||
:class="selected.indexOf(fieldToDisplay) != -1 ? 'btn-dark' : 'btn-outline-dark' "
|
||||
@click="toggle(fieldToDisplay)"
|
||||
>
|
||||
{{ names[fieldToDisplay] || fieldToDisplay }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
};
|
||||
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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: `
|
||||
<div class="filter-config">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div v-if="filters.length" class="mb-4">
|
||||
<select class="form-select" @change="switchFilter">
|
||||
<option value="">Bitte auswählen...</option>
|
||||
<option v-for="filter in filters" :value="filter.id">
|
||||
{{ filter.description }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<button class="btn btn-outline-dark" type="button" @click=addField>
|
||||
<span class="fa-solid fa-plus" aria-hidden="true"></span> Neuer Filter
|
||||
</button>
|
||||
</div>
|
||||
<div class="filter-config-fields my-3">
|
||||
<div v-for="(filterField, index) in currentFields" class="filter-config-field row">
|
||||
|
||||
<div class="col-5">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">Filter {{ index + 1 }}</span>
|
||||
<select
|
||||
class="form-select"
|
||||
v-model="filterField.name"
|
||||
>
|
||||
<option value="">Feld zum Filter hinzufügen...</option>
|
||||
<option v-for="col in columns" :value="col.name">
|
||||
{{ col.title }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Numeric -->
|
||||
<template v-if="types[filterField.name] == 'Numeric'">
|
||||
<div class="col-2">
|
||||
<select class="form-select" v-model="filterField.operation">
|
||||
<option value="equal">Gleich</option>
|
||||
<option value="nequal">Nicht gleich</option>
|
||||
<option value="gt">Größer als</option>
|
||||
<option value="lt">Weniger als</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<input type="number" class="form-control" v-model="filterField.condition">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Text -->
|
||||
<template v-if="types[filterField.name] == 'Text'">
|
||||
<div class="col-2">
|
||||
<select class="form-select" v-model="filterField.operation">
|
||||
<option value="equal">Gleich</option>
|
||||
<option value="nequal">Nicht gleich</option>
|
||||
<option value="contains">Enthält</option>
|
||||
<option value="ncontains">Enthält nicht</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<input type="text" class="form-control" v-model="filterField.condition">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Timestamp and date -->
|
||||
<template v-if="types[filterField.name] == 'Date'">
|
||||
<div class="col-2">
|
||||
<select class="form-select" v-model="filterField.operation">
|
||||
<option value="gt">Größer als</option>
|
||||
<option value="lt">Weniger als</option>
|
||||
<option value="set">Eingestellt ist</option>
|
||||
<option value="nset">Eingestellt nicht ist</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-1">
|
||||
<input type="number" class="form-control" v-model="filterField.condition">
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<select class="form-select" v-model="filterField.option">
|
||||
<option value="minutes">Minuten</option>
|
||||
<option value="hours">Stunden</option>
|
||||
<option value="days">Tage</option>
|
||||
<option value="months">Monate</option>
|
||||
</select>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="col text-end">
|
||||
<button
|
||||
class="btn btn-outline-dark"
|
||||
type="button"
|
||||
@click="removeField(index)"
|
||||
title="Filter entfernen"
|
||||
aria-title="Filter entfernen"
|
||||
>
|
||||
<span class="fa-solid fa-minus" aria-hidden="true"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filter save options -->
|
||||
<div class="row">
|
||||
<div class="col-7">
|
||||
<div class="input-group">
|
||||
<input ref="filterName" type="text" class="form-control" placeholder="Filternamen eingeben...">
|
||||
<button type="button" class="btn btn-outline-secondary" @click="$emit('saveCustomFilter', $refs.filterName.value)">Filter speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<button type="button" class="btn btn-outline-dark" @click="applyFilterConfig">Filter anwenden</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
};
|
||||
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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: `
|
||||
<template v-if="currentConfig">
|
||||
<template v-if="currentConfig.length == 1">
|
||||
<a
|
||||
href="#"
|
||||
class="table-download"
|
||||
v-bind="$attrs"
|
||||
title="Download"
|
||||
aria-title="Download"
|
||||
@click.prevent="download(currentConfig[0])"
|
||||
>
|
||||
<span :class="iconClass || 'fa-solid fa-xl fa-download'" aria-hidden="true"></span>
|
||||
</a>
|
||||
</template>
|
||||
<div v-else class="dropdown d-inline">
|
||||
<a
|
||||
href="#"
|
||||
class="table-download"
|
||||
v-bind="$attrs"
|
||||
title="Download"
|
||||
aria-title="Download"
|
||||
role="button"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<span :class="iconClass || 'fa-solid fa-xl fa-download'" aria-hidden="true"></span>
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<li v-for="(conf, i) in currentConfig" :key="i">
|
||||
<a class="dropdown-item" href="#" @click.prevent="download(conf)">
|
||||
<span v-if="conf.icon" :class="conf.icon" aria-hidden="true"></span>
|
||||
{{conf.label}}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
`
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user