mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-20 01:02:22 +00:00
6793377579
Summary: Column filter menu use to mess up the ordering of the items for numeric and dates values, and also for ref/reflist columns when the visible column is a numeric a date column. Solution was to: - use the actual value of the visible column for comparison. - use native comparison. - tweak the native comparison to make blanks appears before valid value. Indeed, it came up several time that it's convenient to have invalid values show up first in the filter panel, it makes for a convenient way to detect them. Test Plan: Adds new nbrowser test Reviewers: alexmojaki Reviewed By: alexmojaki Differential Revision: https://phab.getgrist.com/D3441
86 lines
3.1 KiB
TypeScript
86 lines
3.1 KiB
TypeScript
import { ColumnFilter } from "app/client/models/ColumnFilter";
|
|
import { CellValue } from "app/plugin/GristData";
|
|
import { Computed, Disposable, Observable } from "grainjs";
|
|
import escapeRegExp = require("lodash/escapeRegExp");
|
|
import isNull = require("lodash/isNull");
|
|
|
|
const MAXIMUM_SHOWN_FILTER_ITEMS = 500;
|
|
|
|
export interface IFilterCount {
|
|
|
|
// label is the formatted value
|
|
label: string;
|
|
|
|
// number of occurences in the table
|
|
count: number;
|
|
|
|
// displayValue is the underlying value (from the display column, if any), useful to perform
|
|
// comparison
|
|
displayValue: any;
|
|
}
|
|
|
|
type ICompare<T> = (a: T, b: T) => number
|
|
|
|
const localeCompare = new Intl.Collator('en-US', {numeric: true}).compare;
|
|
|
|
export class ColumnFilterMenuModel extends Disposable {
|
|
|
|
public readonly searchValue = Observable.create(this, '');
|
|
|
|
public readonly isSortedByCount = Observable.create(this, false);
|
|
|
|
// computes a set of all keys that matches the search text.
|
|
public readonly filterSet = Computed.create(this, this.searchValue, (_use, searchValue) => {
|
|
const searchRegex = new RegExp(escapeRegExp(searchValue), 'i');
|
|
const showAllOptions = ['Bool', 'Choice', 'ChoiceList'].includes(this.columnFilter.columnType);
|
|
return new Set(
|
|
this._valueCount
|
|
.filter(([_, {label, count}]) => (showAllOptions ? true : count) && searchRegex.test(label))
|
|
.map(([key]) => key)
|
|
);
|
|
});
|
|
|
|
// computes the sorted array of all values (ie: pair of key and IFilterCount) that matches the search text.
|
|
public readonly filteredValues = Computed.create(
|
|
this, this.filterSet, this.isSortedByCount,
|
|
(_use, filter, isSortedByCount) => {
|
|
const prop: keyof IFilterCount = isSortedByCount ? 'count' : 'displayValue';
|
|
let isShownFirst: (val: any) => boolean = isNull;
|
|
if (['Date', 'DateTime', 'Numeric', 'Int'].includes(this.columnFilter.visibleColumnType)) {
|
|
isShownFirst = (val) => isNull(val) || isNaN(val);
|
|
}
|
|
|
|
const comparator: ICompare<any> = (a, b) => {
|
|
if (isShownFirst(a)) { return -1; }
|
|
if (isShownFirst(b)) { return 1; }
|
|
return localeCompare(a, b);
|
|
};
|
|
|
|
return this._valueCount
|
|
.filter(([key]) => filter.has(key))
|
|
.sort((a, b) => comparator(a[1][prop], b[1][prop]));
|
|
}
|
|
);
|
|
|
|
// computes the array of all values that does NOT matches the search text
|
|
public readonly otherValues = Computed.create(this, this.filterSet, (_use, filter) => {
|
|
return this._valueCount.filter(([key]) => !filter.has(key));
|
|
});
|
|
|
|
// computes the array of keys that matches the search text
|
|
public readonly filteredKeys = Computed.create(this, this.filterSet, (_use, filter) => {
|
|
return this._valueCount
|
|
.filter(([key]) => filter.has(key))
|
|
.map(([key]) => key);
|
|
});
|
|
|
|
public readonly valuesBeyondLimit = Computed.create(this, this.filteredValues, (_use, filteredValues) => {
|
|
return filteredValues.slice(this.limitShown);
|
|
});
|
|
|
|
constructor(public columnFilter: ColumnFilter, private _valueCount: Array<[CellValue, IFilterCount]>,
|
|
public limitShown: number = MAXIMUM_SHOWN_FILTER_ITEMS) {
|
|
super();
|
|
}
|
|
}
|