Skip to content

Commit

Permalink
Merge pull request #302 from islarky88/master
Browse files Browse the repository at this point in the history
Multi Filtering improvement to current filter plugin
  • Loading branch information
m2a2x authored Mar 25, 2022
2 parents fab376d + 65d6ae5 commit b611f74
Show file tree
Hide file tree
Showing 6 changed files with 421 additions and 119 deletions.
6 changes: 4 additions & 2 deletions src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import { ColumnCollection } from "./services/column.data.provider";
import { DataInput } from "./plugins/export/types";
import { VNode } from "@stencil/core";
import { ColumnSource, RowSource } from "./components/data/columnService";
import { MultiFilterItem, ShowData } from "./plugins/filter/filter.pop";
import { LogicFunction } from "./plugins/filter/filter.types";
import { FilterItem, ShowData } from "./plugins/filter/filter.pop";
import { DataSourceState, Groups } from "./store/dataSource/data.store";
import { ViewportData } from "./components/revo-grid/viewport.interfaces";
import { ElementScroll } from "./components/revo-grid/viewport.scrolling.service";
Expand Down Expand Up @@ -238,6 +238,7 @@ export namespace Components {
interface RevogrFilterPanel {
"filterCaptions": FilterCaptions | undefined;
"filterEntities": Record<string, LogicFunction>;
"filterItems": MultiFilterItem;
"filterNames": Record<string, string>;
"filterTypes": Record<string, string[]>;
"getChanges": () => Promise<ShowData>;
Expand Down Expand Up @@ -696,9 +697,10 @@ declare namespace LocalJSX {
interface RevogrFilterPanel {
"filterCaptions"?: FilterCaptions | undefined;
"filterEntities"?: Record<string, LogicFunction>;
"filterItems"?: MultiFilterItem;
"filterNames"?: Record<string, string>;
"filterTypes"?: Record<string, string[]>;
"onFilterChange"?: (event: CustomEvent<FilterItem>) => void;
"onFilterChange"?: (event: CustomEvent<MultiFilterItem>) => void;
"uuid"?: string;
}
interface RevogrFocus {
Expand Down
8 changes: 7 additions & 1 deletion src/plugins/filter/conditions/equal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@ const eq: LogicFunction = (value: LogicFunctionParam, extra?: LogicFunctionExtra
if (typeof value !== 'string') {
value = JSON.stringify(value);
}
return value.toLocaleLowerCase() === extra.toString().toLocaleLowerCase();

const filterVal = extra.toString().toLocaleLowerCase();
if (filterVal.length === 0) {
return true;
}

return value.toLocaleLowerCase() === filterVal;
};

export const notEq: LogicFunction = (value: LogicFunctionParam, extra?: LogicFunctionExtraParam) => !eq(value, extra);
Expand Down
15 changes: 15 additions & 0 deletions src/plugins/filter/filter.button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { RevoGrid } from '../../interfaces';
export const FILTER_BUTTON_CLASS = 'rv-filter';
export const FILTER_BUTTON_ACTIVE = 'active';
export const FILTER_PROP = 'hasFilter';
export const AND_OR_BUTTON = 'and-or-button';
export const TRASH_BUTTON = 'trash-button';

type Props = {
column: RevoGrid.ColumnRegular;
Expand All @@ -27,6 +29,19 @@ export const FilterButton = ({ column }: Props) => {
);
};

export const TrashButton = () => {
return (
<div class={{ [TRASH_BUTTON]: true }}>
<svg style={{ width: '24px', height: '24px' }} viewBox="0 0 24 24">
<path fill="currentColor" d="M9,3V4H4V6H5V19A2,2 0 0,0 7,21H17A2,2 0 0,0 19,19V6H20V4H15V3H9M7,6H17V19H7V6M9,8V17H11V8H9M13,8V17H15V8H13Z" />
</svg>
</div>
);
};
export const AndOrButton = ({ isAnd }: any) => {
return <button class={{ [AND_OR_BUTTON]: true }}>{isAnd ? 'and' : 'or'}</button>;
};

export function isFilterBtn(e: HTMLElement) {
if (e.classList.contains(FILTER_BUTTON_CLASS)) {
return true;
Expand Down
166 changes: 115 additions & 51 deletions src/plugins/filter/filter.plugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { h } from '@stencil/core';
import BasePlugin from '../basePlugin';
import { RevoGrid } from '../../interfaces';
import { FILTER_PROP, isFilterBtn } from './filter.button';
import { FilterItem } from './filter.pop';
import { MultiFilterItem } from './filter.pop';
import { filterEntities, filterNames, FilterType, filterTypes } from './filter.service';
import { LogicFunction } from './filter.types';

Expand All @@ -17,12 +17,12 @@ export type FilterCaptions = {
save: string;
reset: string;
cancel: string;
}
};

export type FilterLocalization = {
captions: FilterCaptions;
filterNames: Record<FilterType, string>;
}
};

/**
* @typedef ColumnFilterConfig
Expand All @@ -31,13 +31,15 @@ export type FilterLocalization = {
* @property {string[]|undefined} include - filters to be included, if defined everything else out of scope will be ignored
* @property {Record<string, CustomFilter>|undefined} customFilters - hash map of {FilterType:CustomFilter}.
* @property {FilterLocalization|undefined} localization - translation for filter popup captions.
* @property {MultiFilterItem|undefined} multiFilterItems - data for multi filtering.
* A way to define your own filter types per column
*/
export type ColumnFilterConfig = {
collection?: FilterCollection;
include?: string[];
customFilters?: Record<string, CustomFilter>;
localization?: FilterLocalization;
multiFilterItems?: MultiFilterItem;
};
type HeaderEvent = CustomEvent<RevoGrid.ColumnRegular>;
type FilterCollectionItem = {
Expand All @@ -53,6 +55,7 @@ export const FILTER_TRIMMED_TYPE = 'filter';
export default class FilterPlugin extends BasePlugin {
private pop: HTMLRevogrFilterPanelElement;
private filterCollection: FilterCollection = {};
private multiFilterItems: MultiFilterItem = {};
private possibleFilters: Record<string, string[]> = { ...filterTypes };
private possibleFilterNames: Record<string, string> = { ...filterNames };
private possibleFilterEntities: Record<string, LogicFunction> = { ...filterEntities };
Expand All @@ -62,21 +65,38 @@ export default class FilterPlugin extends BasePlugin {
if (config) {
this.initConfig(config);
}

const headerclick = (e: HeaderEvent) => this.headerclick(e);
const aftersourceset = () => {
if (Object.keys(this.filterCollection).length) {
this.filterByProps(this.filterCollection);

const aftersourceset = async () => {
const filterCollectionProps = Object.keys(this.filterCollection);
if (filterCollectionProps.length > 0) {
// handle old way of filtering by reworking FilterCollection to new MultiFilterItem
filterCollectionProps.forEach((prop, index) => {
if (!this.multiFilterItems[prop]) {
this.multiFilterItems[prop] = [
{
id: index,
type: this.filterCollection[prop].type,
value: this.filterCollection[prop].value,
relation: 'and',
},
];
}
});
}
await this.runFiltering();
};
this.addEventListener('headerclick', headerclick);
this.addEventListener('aftersourceset', aftersourceset);

this.revogrid.registerVNode([
<revogr-filter-panel
uuid={`filter-${uiid}`}
filterItems={this.multiFilterItems}
filterNames={this.possibleFilterNames}
filterEntities={this.possibleFilterEntities}
filterCaptions={config.localization?.captions}
filterCaptions={config?.localization?.captions}
onFilterChange={e => this.onFilterChange(e.detail)}
ref={e => (this.pop = e)}
/>,
Expand All @@ -87,6 +107,9 @@ export default class FilterPlugin extends BasePlugin {
if (config.collection) {
this.filterCollection = { ...config.collection };
}
if (config.multiFilterItems) {
this.multiFilterItems = { ...config.multiFilterItems };
}
if (config.customFilters) {
for (let cType in config.customFilters) {
const cFilter = config.customFilters[cType];
Expand Down Expand Up @@ -184,44 +207,20 @@ export default class FilterPlugin extends BasePlugin {
}

// called on internal component change
private async onFilterChange(filterItem: FilterItem) {
this.filterByProps({ [filterItem.prop]: filterItem });
}

/**
* Apply filters collection to extend existing one or override
* @method
* @param conditions - list of filters to apply
*/
async filterByProps(conditions: Record<RevoGrid.ColumnProp, FilterItem>, override = false) {
if (override) {
this.filterCollection = {};
}
for (const prop in conditions) {
const { type, value } = conditions[prop];
if (type === 'none') {
delete this.filterCollection[prop];
} else {
const filter = this.possibleFilterEntities[type];
this.filterCollection[prop] = {
filter,
value,
type,
};
}
}
await this.runFiltering();
private async onFilterChange(filterItems: MultiFilterItem) {
this.multiFilterItems = filterItems;
this.runFiltering();
}

/**
* Triggers grid filtering
*/
async doFiltering(collection: FilterCollection, items: RevoGrid.DataType[], columns: RevoGrid.ColumnRegular[]) {
async doFiltering(collection: FilterCollection, items: RevoGrid.DataType[], columns: RevoGrid.ColumnRegular[], filterItems: MultiFilterItem) {
const columnsToUpdate: RevoGrid.ColumnRegular[] = [];
// todo improvement: loop through collection of props

columns.forEach(rgCol => {
const column = { ...rgCol };
const hasFilter = collection[column.prop];
const hasFilter = filterItems[column.prop];
if (column[FILTER_PROP] && !hasFilter) {
delete column[FILTER_PROP];
columnsToUpdate.push(column);
Expand All @@ -231,54 +230,119 @@ export default class FilterPlugin extends BasePlugin {
column[FILTER_PROP] = true;
}
});
const itemsToFilter = this.getRowFilter(items, collection);
const itemsToFilter = this.getRowFilter(items, filterItems);
// check is filter event prevented
const { defaultPrevented, detail } = this.emit('beforefiltertrimmed', { collection, itemsToFilter, source: items });
const { defaultPrevented, detail } = this.emit('beforefiltertrimmed', { collection, itemsToFilter, source: items, filterItems });
if (defaultPrevented) {
return;
}

// check is trimmed event prevented
const isAddedEvent = await this.revogrid.addTrimmed(detail.itemsToFilter, FILTER_TRIMMED_TYPE);
if (isAddedEvent.defaultPrevented) {
return;
}

// applies the hasFilter to the columns to show filter icon
await this.revogrid.updateColumns(columnsToUpdate);
this.emit('afterFilterApply');
}

async clearFiltering() {
this.filterCollection = {};
this.multiFilterItems = {};
await this.runFiltering();
}

private async runFiltering() {
const collection: FilterCollection = {};

// handle old filterCollection to return the first filter only (if any) from multiFilterItems
const filterProps = Object.keys(this.multiFilterItems);

for (const prop of filterProps) {
// check if we have any filter for a column
if (this.multiFilterItems[prop].length > 0) {
const firstFilterItem = this.multiFilterItems[prop][0];
collection[prop] = {
filter: filterEntities[firstFilterItem.type],
type: firstFilterItem.type,
value: firstFilterItem.value,
};
}
}

this.filterCollection = collection;

const { source, columns } = await this.getData();
const { defaultPrevented, detail } = this.emit('beforefilterapply', { collection: this.filterCollection, source, columns });
const { defaultPrevented, detail } = this.emit('beforefilterapply', { collection: this.filterCollection, source, columns, filterItems: this.multiFilterItems });
if (defaultPrevented) {
return;
}
this.doFiltering(detail.collection, detail.source, detail.columns);
this.doFiltering(detail.collection, detail.source, detail.columns, detail.filterItems);
}

private async getData() {
const source = await this.revogrid.getSource();
const columns = await this.revogrid.getColumns();
return {
source,
columns
columns,
};
}

private getRowFilter(rows: RevoGrid.DataType[], collection: FilterCollection) {
private getRowFilter(rows: RevoGrid.DataType[], filterItems: MultiFilterItem) {
const propKeys = Object.keys(filterItems);

const trimmed: Record<number, boolean> = {};
let propFilterSatisfiedCount: number = 0;
let lastFilterResults: boolean[] = [];

// each rows
rows.forEach((model, rowIndex) => {
for (const prop in collection) {
const filterItem = collection[prop];
const filter = filterItem.filter;
if (!filter(model[prop], filterItem.value)) {
trimmed[rowIndex] = true;
}
}
// working on all props
for (const prop of propKeys) {
const propFilters = filterItems[prop];

propFilterSatisfiedCount = 0;
lastFilterResults = [];

// testing each filter for a prop
for (const [filterIndex, filterData] of propFilters.entries()) {
// the filter LogicFunction based on the type
const filter = filterEntities[filterData.type];

// THE MAGIC OF FILTERING IS HERE
if (filterData.relation === 'or') {
lastFilterResults = [];
if (filter(model[prop], filterData.value)) {
continue;
}
propFilterSatisfiedCount++;
} else {
// 'and' relation will need to know the next filter
// so we save this current filter to include it in the next filter
lastFilterResults.push(!filter(model[prop], filterData.value));

// check first if we have a filter on the next index to pair it with this current filter
const nextFilterData = propFilters[filterIndex + 1];
// stop the sequence if there is no next filter or if the next filter is not an 'and' relation
if (!nextFilterData || nextFilterData.relation !== 'and') {
// let's just continue since for sure propFilterSatisfiedCount cannot be satisfied
if (lastFilterResults.indexOf(true) === -1) {
lastFilterResults = [];
continue;
}

// we need to add all of the lastFilterResults since we need to satisfy all
propFilterSatisfiedCount += lastFilterResults.length;
lastFilterResults = [];
}
}
} // end of propFilters forEach

// add to the list of removed/trimmed rows of filter condition is satisfied
if (propFilterSatisfiedCount === propFilters.length) trimmed[rowIndex] = true;
} // end of for-of propKeys
});
return trimmed;
}
Expand Down
Loading

0 comments on commit b611f74

Please sign in to comment.