mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) Adds new range filter for numeric columns
Summary: Shows the range filter next to the filter by values on filter menu. When users set min and/or max, it takes precendence over the filter by values. If users set: - `[] < [max]` behaves as `less than max`. - `[min] < []` behaves as `more than min`. - `[min] < [max]` behaves as `between min and max` - bounds are always inclusives. - when users change min or max the values of the by values filter gets checked/unchecked depending on whether they are included by the range filter. - when users clicks any btn/checkbox of the by values filter both min and max input gets cleared, and the filter convert to a filter by values. Test Plan: Adds both projets and nbrowser tests. Reviewers: jarek Reviewed By: jarek Differential Revision: https://phab.getgrist.com/D3435
This commit is contained in:
@@ -1,14 +1,34 @@
|
||||
import {CellValue} from "app/common/DocActions";
|
||||
import {FilterState, makeFilterState} from "app/common/FilterState";
|
||||
import {FilterState, isRangeFilter, makeFilterState} from "app/common/FilterState";
|
||||
import {decodeObject} from "app/plugin/objtypes";
|
||||
import {isList, isListType} from "./gristTypes";
|
||||
import {isList, isListType, isNumberType} from "./gristTypes";
|
||||
|
||||
export type ColumnFilterFunc = (value: CellValue) => boolean;
|
||||
|
||||
// Returns a filter function for a particular column: the function takes a cell value and returns
|
||||
// whether it's accepted according to the given FilterState.
|
||||
export function makeFilterFunc({ include, values }: FilterState,
|
||||
export function makeFilterFunc(state: FilterState,
|
||||
columnType?: string): ColumnFilterFunc {
|
||||
|
||||
if (isRangeFilter(state)) {
|
||||
const {min, max} = state;
|
||||
if (isNumberType(columnType)) {
|
||||
return (val) => {
|
||||
if (typeof val !== 'number') { return false; }
|
||||
return (
|
||||
(max === undefined ? true : val <= max) &&
|
||||
(min === undefined ? true : min <= val)
|
||||
);
|
||||
};
|
||||
} else {
|
||||
// Although it is not possible to set a range filter for non numeric columns, this still can
|
||||
// happen as a result of a column type conversion. In this case, let's include all values.
|
||||
return () => true;
|
||||
}
|
||||
}
|
||||
|
||||
const {include, values} = state;
|
||||
|
||||
// NOTE: This logic results in complex values and their stringified JSON representations as equivalent.
|
||||
// For example, a TypeError in the formula column and the string '["E","TypeError"]' would be seen as the same.
|
||||
// TODO: This narrow corner case seems acceptable for now, but may be worth revisiting.
|
||||
@@ -17,7 +37,6 @@ export function makeFilterFunc({ include, values }: FilterState,
|
||||
const list = decodeObject(val) as unknown[];
|
||||
return list.some(item => values.has(item as any) === include);
|
||||
}
|
||||
|
||||
return (values.has(Array.isArray(val) ? JSON.stringify(val) : val) === include);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,19 +4,32 @@ import { CellValue } from "app/common/DocActions";
|
||||
export interface FilterSpec {
|
||||
included?: CellValue[];
|
||||
excluded?: CellValue[];
|
||||
min?: number;
|
||||
max?: number;
|
||||
}
|
||||
|
||||
|
||||
export type FilterState = ByValueFilterState | RangeFilterState
|
||||
|
||||
// A more efficient representation of filter state for a column than FilterSpec.
|
||||
export interface FilterState {
|
||||
interface ByValueFilterState {
|
||||
include: boolean;
|
||||
values: Set<CellValue>;
|
||||
}
|
||||
|
||||
interface RangeFilterState {
|
||||
min?: number;
|
||||
max?: number;
|
||||
}
|
||||
|
||||
// Creates a FilterState. Accepts spec as a json string or a FilterSpec.
|
||||
export function makeFilterState(spec: string | FilterSpec): FilterState {
|
||||
if (typeof (spec) === 'string') {
|
||||
return makeFilterState((spec && JSON.parse(spec)) || {});
|
||||
}
|
||||
if (spec.min !== undefined || spec.max !== undefined) {
|
||||
return {min: spec.min, max: spec.max};
|
||||
}
|
||||
return {
|
||||
include: Boolean(spec.included),
|
||||
values: new Set(spec.included || spec.excluded || []),
|
||||
@@ -26,8 +39,23 @@ export function makeFilterState(spec: string | FilterSpec): FilterState {
|
||||
// Returns true if state and spec are equivalent, false otherwise.
|
||||
export function isEquivalentFilter(state: FilterState, spec: FilterSpec): boolean {
|
||||
const other = makeFilterState(spec);
|
||||
if (state.include !== other.include) { return false; }
|
||||
if (state.values.size !== other.values.size) { return false; }
|
||||
for (const val of other.values) { if (!state.values.has(val)) { return false; } }
|
||||
if (!isRangeFilter(state) && !isRangeFilter(other)) {
|
||||
if (state.include !== other.include) { return false; }
|
||||
if (state.values.size !== other.values.size) { return false; }
|
||||
if (other.values) {
|
||||
for (const val of other.values) { if (!state.values.has(val)) { return false; } }
|
||||
}
|
||||
} else {
|
||||
if (isRangeFilter(state) && isRangeFilter(other)) {
|
||||
if (state.min !== other.min || state.max !== other.max) { return false; }
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function isRangeFilter(state: FilterState): state is RangeFilterState {
|
||||
const {min, max} = state as any;
|
||||
return min !== undefined || max !== undefined;
|
||||
}
|
||||
|
||||
@@ -337,6 +337,10 @@ export function isListType(type: string) {
|
||||
return type === "ChoiceList" || isRefListType(type);
|
||||
}
|
||||
|
||||
export function isNumberType(type: string|undefined) {
|
||||
return ['Numeric', 'Int'].includes(type || '');
|
||||
}
|
||||
|
||||
export function isFullReferencingType(type: string) {
|
||||
return type.startsWith('Ref:') || isRefListType(type);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user