Initiated Phase 2 of Advanced Table Filtering - Filter Menu feature

pull/973/head
Cagatay Civici 2021-02-06 14:23:00 +03:00
parent 716ff58bde
commit a981d5504b
8 changed files with 275 additions and 75 deletions

View File

@ -117,10 +117,6 @@
}
}
.p-column-filter {
width: 100%;
}
.country-item {
display: flex;
align-items: center;

View File

@ -1,3 +1,4 @@
import FilterMatchMode from './FilterMatchMode';
import FilterOperator from './FilterOperator';
export {FilterMatchMode};
export {FilterMatchMode,FilterOperator};

View File

@ -62,10 +62,6 @@ export default {
type: Boolean,
default: true
},
filterOperator: {
type: String,
default: 'and'
},
showFilterOperator: {
type: Boolean,
default: true

View File

@ -17,8 +17,25 @@
<li class="p-column-filter-row-item" @click="onRowClearItemClick()" @keydown="onRowMatchModeKeyDown($event)" @keydown.enter="onRowClearItemClick()">{{noFilterLabel}}</li>
</ul>
</template>
<template>
<div>TODO - Phase 2 Menu</div>
<template v-else>
<div class="p-column-filter-operator" v-if="isShowOperator">
<CFDropdown :options="operatorOptions" :modelValue="operator" @update:modelValue="onOperatorChange($event)" class="p-column-filter-operator-dropdown" optionLabel="label" optionValue="value"></CFDropdown>
</div>
<div class="p-column-filter-constraints">
<div v-for="(fieldConstraint,i) of fieldConstraints" :key="fieldConstraint.matchMode + i" class="p-column-filter-constraint">
<CFDropdown v-if="showMatchModes && matchModes" :options="matchModes" :modelValue="fieldConstraint.matchMode" optionLabel="label" optionValue="value"
@update:modelValue="onMenuMatchModeChange($event, i)" class="p-column-filter-matchmode-dropdown"></CFDropdown>
<component v-if="display === 'menu'" :is="filterElement" :field="field" />
<CFButton v-if="showRemoveIcon" type="button" icon="pi pi-trash" class="p-column-filter-remove-button p-button-text p-button-danger p-button-sm" @click="removeConstraint(fieldConstraint)" :label="removeRuleButtonLabel"></CFButton>
</div>
</div>
<div class="p-column-filter-add-rule" v-if="isShowAddConstraint">
<CFButton type="button" :label="addRuleButtonLabel" icon="pi pi-plus" class="p-column-filter-add-button p-button-text p-button-sm" @click="addConstraint()"></CFButton>
</div>
<div class="p-column-filter-buttonbar">
<CFButton type="button" class="p-button-outlined p-button-sm" @click="clearFilter()" :label="clearButtonLabel"></CFButton>
<CFButton type="button" class="p-button-sm" @click="applyFilter()" :label="applyButtonLabel"></CFButton>
</div>
</template>
<component :is="filterFooter" :field="field" />
</div>
@ -28,6 +45,9 @@
<script>
import {DomHandler,ConnectedOverlayScrollHandler} from 'primevue/utils';
import {FilterOperator} from 'primevue/api';
import Dropdown from 'primevue/dropdown';
import Button from 'primevue/button';
export default {
emits: ['filtermeta-change'],
@ -52,10 +72,6 @@ export default {
type: String,
default: null
},
operator: {
type: String,
default: 'and'
},
showOperator: {
type: Boolean,
default: true
@ -81,8 +97,8 @@ export default {
default: null
},
maxConstraints: {
type: Boolean,
default: true
type: Number,
default: 2
},
filterElement: null,
filterHeader: null,
@ -95,7 +111,8 @@ export default {
data() {
return {
overlayVisible: false,
defaultMatchMode: null
defaultMatchMode: null,
defaultOperator: null
}
},
overlay: null,
@ -107,7 +124,13 @@ export default {
},
mounted() {
if (this.filters && this.filters[this.field]) {
this.defaultMatchMode = this.filters[this.field].matchMode;
if (this.display === 'row') {
this.defaultMatchMode = this.filters[this.field].matchMode;
}
else {
this.defaultMatchMode = this.filters[this.field][0].matchMode;
this.defaultOperator = this.filters[this.field][0].operator;
}
}
},
methods: {
@ -118,11 +141,16 @@ export default {
_filters[this.field].matchMode = this.defaultMatchMode;
}
else {
_filters[this.field] = {value: null, matchMode: this.defaultMatchMode, operator: this.operator};
_filters[this.field] = {value: null, matchMode: this.defaultMatchMode, operator: this.defaultOperator};
}
this.$emit('filtermeta-change', _filters);
},
applyFilter() {
let _filters = {...this.filters};
this.$emit('filtermeta-change', _filters);
this.hide();
},
hasFilter() {
let fieldFilter = this.filters[this.field];
if (fieldFilter) {
@ -309,6 +337,34 @@ export default {
window.removeEventListener('resize', this.resizeListener);
this.resizeListener = null;
}
},
onOperatorChange(value) {
let _filters = {...this.filters};
_filters[this.field].forEach(filterMeta => {
filterMeta.operator = value;
});
if (!this.showApplyButton) {
this.$emit('filtermeta-change', _filters);
}
},
onMenuMatchModeChange(value, index) {
let _filters = {...this.filters};
_filters[this.field][index].matchMode = value;
if (!this.showApplyButton) {
this.$emit('filtermeta-change', _filters);
}
},
addConstraint() {
let _filters = {...this.filters};
_filters[this.field].push({value: null, matchMode: this.defaultMatchMode()});
this.$emit('filtermeta-change', _filters);
},
removeConstraint(filterMeta) {
let _filters = {...this.filters};
_filters[this.field] = _filters[this.field].filter(meta => meta !== filterMeta);
this.$emit('filtermeta-change', _filters);
}
},
computed: {
@ -332,10 +388,46 @@ export default {
return {label: this.$primevue.config.locale[key], value: key}
});
},
operatorOptions() {
return [
{label: this.$primevue.config.locale.matchAll, value: FilterOperator.AND},
{label: this.$primevue.config.locale.matchAny, value: FilterOperator.OR}
];
},
noFilterLabel() {
return this.$primevue.config.locale.noFilter;
},
isShowOperator() {
return this.showOperator && this.type !== 'boolean';
},
operator() {
return this.filters[this.field][0].operator;
},
fieldConstraints() {
return this.filters[this.field];
},
showRemoveIcon() {
return this.fieldConstraints.length > 1;
},
removeRuleButtonLabel() {
return this.$primevue.config.locale.removeRule;
},
addRuleButtonLabel() {
return this.$primevue.config.locale.addRule;
},
isShowAddConstraint() {
return this.showAddButton && this.type !== 'boolean' && (this.fieldConstraints && this.fieldConstraints.length < this.maxConstraints);
},
clearButtonLabel() {
return this.$primevue.config.locale.clear;
},
applyButtonLabel() {
return this.$primevue.config.locale.apply;
}
},
components: {
'CFDropdown': Dropdown,
'CFButton': Button
}
}
</script>

View File

@ -14,6 +14,11 @@
<span v-if="columnProp(col, 'sortable')" :class="getSortableColumnIcon(col)"></span>
<span v-if="isMultiSorted(col)" class="p-sortable-column-badge">{{getMultiSortMetaIndex(col) + 1}}</span>
<DTHeaderCheckbox :checked="allRowsSelected" @change="onHeaderCheckboxChange($event)" :disabled="empty" v-if="columnProp(col, 'selectionMode') ==='multiple' && filterDisplay !== 'row'" />
<DTColumnFilter v-if="filterDisplay === 'menu' && filters && filters[columnProp(col, 'filterField')||columnProp(col, 'field')]" :field="columnProp(col, 'filterField')||columnProp(col, 'field')" :type="columnProp(col, 'dataType')" display="menu"
:showMenu="columnProp(col, 'showFilterMenu')" :filterElement="col.children && col.children.filter" :filterHeader="col.children && col.children.filterHeader" :filterFooter="col.children && col.children.filterFooter"
:filters="filters" @filtermeta-change="$emit('filtermeta-change', $event)"
:showOperator="columnProp(col, 'showFilterOperator')" :showClearButton="columnProp(col, 'showClearButton')" :showApplyButton="columnProp(col, 'showApplyButton')"
:showMatchModes="columnProp(col, 'showFilterMatchModes')" :showAddButton="columnProp(col, 'showAddButton')" :matchModeOptions="columnProp(col, 'filterMatchModeOptions')" :maxConstraints="columnProp(col, 'maxConstraints')" />
</th>
</template>
</tr>
@ -21,11 +26,11 @@
<template v-for="(col,i) of columns">
<th v-if="rowGroupMode !== 'subheader' || (groupRowsBy !== columnProp(col, 'field'))" :key="columnProp(col, 'columnKey')||columnProp(col, 'field')||i"
:class="getFilterColumnHeaderClass(col)" :style="columnProp(col, 'filterHeaderStyle')">
<DTColumnFilter v-if="filters[columnProp(col, 'filterField')||columnProp(col, 'field')]" :field="columnProp(col, 'filterField')||columnProp(col, 'field')" :type="columnProp(col, 'dataType')" display="row"
<DTColumnFilter v-if="filters && filters[columnProp(col, 'filterField')||columnProp(col, 'field')]" :field="columnProp(col, 'filterField')||columnProp(col, 'field')" :type="columnProp(col, 'dataType')" display="row"
:showMenu="columnProp(col, 'showFilterMenu')" :filterElement="col.children && col.children.filter" :filterHeader="col.children && col.children.filterHeader" :filterFooter="col.children && col.children.filterFooter"
:filters="filters" @filtermeta-change="$emit('filtermeta-change', $event)"
:operator="columnProp(col, 'filterOperator')" :showFilterOperator="columnProp(col, 'showFilterOperator')" :showClearButton="columnProp(col, 'showClearButton')" :showApplyButton="columnProp(col, 'showApplyButton')"
:showFilterMatchModes="columnProp(col, 'showFilterMatchModes')" :showAddButton="columnProp(col, 'showAddButton')" :matchModeOptions="columnProp(col, 'filterMatchModeOptions')" :maxConstraints="columnProp(col, 'maxConstraints')" />
:showOperator="columnProp(col, 'showFilterOperator')" :showClearButton="columnProp(col, 'showClearButton')" :showApplyButton="columnProp(col, 'showApplyButton')"
:showMatchModes="columnProp(col, 'showFilterMatchModes')" :showAddButton="columnProp(col, 'showAddButton')" :matchModeOptions="columnProp(col, 'filterMatchModeOptions')" :maxConstraints="columnProp(col, 'maxConstraints')" />
<DTHeaderCheckbox :checked="allRowsSelected" @change="onHeaderCheckboxChange($event)" :disabled="empty" v-if="columnProp(col, 'selectionMode')==='multiple'" />
</th>
</template>

View File

@ -182,7 +182,18 @@ export default class FilterUtils {
}
static is(value, filter, filterLocale) {
return this.filters.equals(value, filter, filterLocale);
if (filter === undefined || filter === null || (typeof filter === 'string' && filter.trim() === '')) {
return true;
}
if (value === undefined || value === null) {
return false;
}
if (value.getTime && filter.getTime)
return value.getTime() === filter.getTime();
else
return ObjectUtils.removeAccents(value.toString()).toLocaleLowerCase(filterLocale) == ObjectUtils.removeAccents(filter.toString()).toLocaleLowerCase(filterLocale);
}
static isNot(value, filter, filterLocale) {

View File

@ -2,22 +2,22 @@
<div>
<div class="content-section introduction">
<div class="feature-intro">
<h1>DataTable - Filter</h1>
<p>Filtering is enabled by defining a filter template per column to populate the filters property of the DataTable.</p>
<h1>DataTable <span>Filter</span></h1>
<p>Filtering feature provides advanced and flexible options to query the data.</p>
</div>
</div>
<div class="content-section implementation">
<div class="card">
<h5>Filter Row</h5>
<p>Filters are displayed inline within a separate row.</p>
<DataTable :value="customers" :paginator="true" class="p-datatable-customers" :rows="10"
dataKey="id" :filters="filters" filterDisplay="row" :loading="loading">
<h5>Filter Menu</h5>
<p>Filters are displayed in an overlay..</p>
<DataTable :value="customers1" :paginator="true" class="p-datatable-customers p-datatable-gridlines" :rows="10"
dataKey="id" :filters="filters1" filterDisplay="menu" :loading="loading1">
<template #header>
<div class="p-d-flex p-jc-end">
<span class="p-input-icon-left ">
<i class="pi pi-search" />
<InputText v-model="filters['global'].value" placeholder="Keyword Search" />
<InputText v-model="filters1['global'].value" placeholder="Keyword Search" />
</span>
</div>
</template>
@ -33,27 +33,28 @@
{{slotProps.data.name}}
</template>
<template #filter>
<InputText type="text" v-model="filters['name'].value" class="p-column-filter" placeholder="Search by name"/>
<InputText type="text" v-model="filters1['name'].value" class="p-column-filter" placeholder="Search by name"/>
</template>
</Column>
<Column header="Country" filterField="country.name" filterMatchMode="contains">
<Column header="Country" filterField="country.name">
<template #body="slotProps">
<span class="p-column-title">Country</span>
<img src="../../assets/images/flag_placeholder.png" :class="'flag flag-' + slotProps.data.country.code" width="30" />
<span class="image-text">{{slotProps.data.country.name}}</span>
</template>
<template #filter>
<InputText type="text" v-model="filters['country.name'].value" class="p-column-filter" placeholder="Search by country"/>
<InputText type="text" v-model="filters1['country.name'].value" class="p-column-filter" placeholder="Search by country"/>
</template>
</Column>
<Column header="Agent" filterField="representative" filterMatchMode="in" :showFilterMenu="false">
<Column header="Agent" filterField="representative" :showFilterMatchModes="false" :showFilterOperator="false" :showAddButton="false">
<template #body="slotProps">
<span class="p-column-title">Agent</span>
<img :alt="slotProps.data.representative.name" :src="'demo/images/avatar/' + slotProps.data.representative.image" width="32" style="vertical-align: middle" />
<span class="image-text">{{slotProps.data.representative.name}}</span>
</template>
<template #filter>
<MultiSelect v-model="filters['representative'].value" :options="representatives" optionLabel="name" placeholder="Any" class="p-column-filter">
<div class="p-mb-3 p-text-bold">Agent Picker</div>
<MultiSelect v-model="filters1['representative'].value" :options="representatives" optionLabel="name" placeholder="Any" class="p-column-filter">
<template #option="slotProps">
<div class="p-multiselect-representative-option">
<img :alt="slotProps.option.name" :src="'demo/images/avatar/' + slotProps.option.image" width="32" style="vertical-align: middle" />
@ -63,26 +64,137 @@
</MultiSelect>
</template>
</Column>
<Column field="status" header="Status" filterMatchMode="equals" :showFilterMenu="false">
<Column header="Date" filterField="date" dataType="date">
<template #body="slotProps">
<span class="p-column-title">Date</span>
{{slotProps.data.date}}
</template>
<template #filter>
<Calendar v-model="filters1['date'].value" dateFormat="mm/dd/yy" />
</template>
</Column>
<Column header="Balance" filterField="balance" dataType="numeric">
<template #body="slotProps">
<span class="p-column-title">Balance</span>
{{formatCurrency(slotProps.data.balance)}}
</template>
<template #filter>
<InputNumber v-model="filters1['balance'].value" mode="currency" currency="USD" locale="en-US" />
</template>
</Column>
<Column field="status" header="Status">
<template #body="slotProps">
<span class="p-column-title">Status</span>
<span :class="'customer-badge status-' + slotProps.data.status">{{slotProps.data.status}}</span>
</template>
<template #filter>
<Dropdown v-model="filters['status'].value" :options="statuses" placeholder="Any" class="p-column-filter" :showClear="true">
<Dropdown v-model="filters1['status'].value" :options="statuses" placeholder="Any" class="p-column-filter" :showClear="true">
<template #option="slotProps">
<span :class="'customer-badge status-' + slotProps.option">{{slotProps.option}}</span>
</template>
</Dropdown>
</template>
</Column>
<Column field="verified" header="Verified" filterMatchMode="equals" dataType="boolean" headerStyle="width: 6rem">
<Column field="activity" header="Activity" :showFilterMatchModes="false" :showFilterOperator="false" :showAddButton="false">
<template #body="slotProps">
<span class="p-column-title">Status</span>
<ProgressBar :value="slotProps.data.activity" :showValue="false"></ProgressBar>
</template>
<template #filter>
<Slider v-model="filters1['activity'][0].value" range class="p-m-3"></Slider>
<div class="p-d-flex p-ai-center p-jc-between p-px-2">
<span>{{filters1['activity'][0].value[0]}}</span>
<span>{{filters1['activity'][0].value[1]}}</span>
</div>
</template>
</Column>
<Column field="verified" header="Verified" dataType="boolean" headerStyle="width: 8rem" :showFilterMatchModes="false" :showFilterOperator="false" :showAddButton="false">
<template #body="slotProps">
<span class="p-column-title">Verified</span>
<i class="pi" :class="{'true-icon pi-check-circle': slotProps.data.verified, 'false-icon pi-times-circle': !slotProps.data.verified}"></i>
</template>
<template #filter>
<TriStateCheckbox v-model="filters['verified'].value" />
<TriStateCheckbox v-model="filters1['verified'].value" />
</template>
</Column>
</DataTable>
</div>
<div class="card">
<h5>Filter Row</h5>
<p>Filters are displayed inline within a separate row.</p>
<DataTable :value="customers2" :paginator="true" class="p-datatable-customers" :rows="10"
dataKey="id" :filters="filters2" filterDisplay="row" :loading="loading2">
<template #header>
<div class="p-d-flex p-jc-end">
<span class="p-input-icon-left ">
<i class="pi pi-search" />
<InputText v-model="filters2['global'].value" placeholder="Keyword Search" />
</span>
</div>
</template>
<template #empty>
No customers found.
</template>
<template #loading>
Loading customers data. Please wait.
</template>
<Column field="name" header="Name">
<template #body="slotProps">
<span class="p-column-title">Name</span>
{{slotProps.data.name}}
</template>
<template #filter>
<InputText type="text" v-model="filters2['name'].value" class="p-column-filter" placeholder="Search by name"/>
</template>
</Column>
<Column header="Country" filterField="country.name">
<template #body="slotProps">
<span class="p-column-title">Country</span>
<img src="../../assets/images/flag_placeholder.png" :class="'flag flag-' + slotProps.data.country.code" width="30" />
<span class="image-text">{{slotProps.data.country.name}}</span>
</template>
<template #filter>
<InputText type="text" v-model="filters2['country.name'].value" class="p-column-filter" placeholder="Search by country"/>
</template>
</Column>
<Column header="Agent" filterField="representative" :showFilterMenu="false">
<template #body="slotProps">
<span class="p-column-title">Agent</span>
<img :alt="slotProps.data.representative.name" :src="'demo/images/avatar/' + slotProps.data.representative.image" width="32" style="vertical-align: middle" />
<span class="image-text">{{slotProps.data.representative.name}}</span>
</template>
<template #filter>
<MultiSelect v-model="filters2['representative'].value" :options="representatives" optionLabel="name" placeholder="Any" class="p-column-filter">
<template #option="slotProps">
<div class="p-multiselect-representative-option">
<img :alt="slotProps.option.name" :src="'demo/images/avatar/' + slotProps.option.image" width="32" style="vertical-align: middle" />
<span class="image-text">{{slotProps.option.name}}</span>
</div>
</template>
</MultiSelect>
</template>
</Column>
<Column field="status" header="Status" :showFilterMenu="false">
<template #body="slotProps">
<span class="p-column-title">Status</span>
<span :class="'customer-badge status-' + slotProps.data.status">{{slotProps.data.status}}</span>
</template>
<template #filter>
<Dropdown v-model="filters2['status'].value" :options="statuses" placeholder="Any" class="p-column-filter" :showClear="true">
<template #option="slotProps">
<span :class="'customer-badge status-' + slotProps.option">{{slotProps.option}}</span>
</template>
</Dropdown>
</template>
</Column>
<Column field="verified" header="Verified" dataType="boolean" headerStyle="width: 6rem">
<template #body="slotProps">
<span class="p-column-title">Verified</span>
<i class="pi" :class="{'true-icon pi-check-circle': slotProps.data.verified, 'false-icon pi-times-circle': !slotProps.data.verified}"></i>
</template>
<template #filter>
<TriStateCheckbox v-model="filters2['verified'].value" />
</template>
</Column>
</DataTable>
@ -108,13 +220,25 @@
<script>
import CustomerService from '../../service/CustomerService';
import {FilterMatchMode} from 'primevue/api';
import {FilterMatchMode,FilterOperator} from 'primevue/api';
export default {
data() {
return {
customers: null,
filters: {
customers1: null,
customers2: null,
filters1: {
'global': {value: null, matchMode: FilterMatchMode.CONTAINS},
'name': [{value: null, matchMode: FilterMatchMode.STARTS_WITH, operator: FilterOperator.AND}],
'country.name': [{value: null, matchMode: FilterMatchMode.STARTS_WITH, operator: FilterOperator.AND}],
'representative': [{value: null, matchMode: FilterMatchMode.IN}],
'date': [{value: null, matchMode: FilterMatchMode.IS, operator: FilterOperator.AND}],
'balance': [{value: null, matchMode: FilterMatchMode.EQUALS, operator: FilterOperator.AND}],
'status': [{value: null, matchMode: FilterMatchMode.EQUALS, operator: FilterOperator.AND}],
'activity': [{value: [0,100], matchMode: FilterMatchMode.BETWEEN}],
'verified': [{value: null, matchMode: FilterMatchMode.EQUALS, operator: FilterOperator.AND}]
},
filters2: {
'global': {value: null, matchMode: FilterMatchMode.CONTAINS},
'name': {value: null, matchMode: FilterMatchMode.STARTS_WITH},
'country.name': {value: null, matchMode: FilterMatchMode.STARTS_WITH},
@ -122,7 +246,6 @@ export default {
'status': {value: null, matchMode: FilterMatchMode.EQUALS},
'verified': {value: null, matchMode: FilterMatchMode.EQUALS}
},
loading: true,
representatives: [
{name: "Amy Elsner", image: 'amyelsner.png'},
{name: "Anna Fali", image: 'annafali.png'},
@ -138,40 +261,20 @@ export default {
statuses: [
'unqualified', 'qualified', 'new', 'negotiation', 'renewal', 'proposal'
],
loading1: true,
loading2: true
}
},
created() {
this.customerService = new CustomerService();
},
mounted() {
this.customerService.getCustomersLarge().then(data => this.customers = data);
this.loading = false;
this.customerService.getCustomersLarge().then(data => {this.customers1 = data; this.loading1 = false;});
this.customerService.getCustomersLarge().then(data => {this.customers2 = data; this.loading2 = false;});
},
methods: {
filterDate(value, filter) {
if (filter === undefined || filter === null || (typeof filter === 'string' && filter.trim() === '')) {
return true;
}
if (value === undefined || value === null) {
return false;
}
return value === this.formatDate(filter);
},
formatDate(date) {
let month = date.getMonth() + 1;
let day = date.getDate();
if (month < 10) {
month = '0' + month;
}
if (day < 10) {
day = '0' + day;
}
return date.getFullYear() + '-' + month + '-' + day;
formatCurrency(value) {
return value.toLocaleString('en-US', {style: 'currency', currency: 'USD'});
}
}
}

View File

@ -527,10 +527,6 @@ input[type="number"]::-webkit-inner-spin-button {
margin-top: .125rem;
}
.p-column-filter {
width: 100%;
}
.country-item {
display: flex;
align-items: center;