(core) Adding sort options for columns.

Summary:
Adding sort options for columns.
- Sort menu has a new option "More sort options" that opens up Sort left menu
- Each sort entry has an additional menu with 3 options
-- Order by choice index (for the Choice column, orders by choice position)
-- Empty last (puts empty values last in ascending order, first in descending order)
-- Natural sort (for Text column, compares strings with numbers as numbers)
Updated also CSV/Excel export and api sorting.
Most of the changes in this diff is a sort expression refactoring. Pulling out all the methods
that works on sortExpression array into a single namespace.

Test Plan: Browser tests

Reviewers: alexmojaki

Reviewed By: alexmojaki

Subscribers: dsagal, alexmojaki

Differential Revision: https://phab.getgrist.com/D3077
This commit is contained in:
Jarosław Sadziński
2021-11-03 12:44:28 +01:00
parent 0f946616b6
commit 3c72639e25
20 changed files with 992 additions and 267 deletions

View File

@@ -1,6 +1,8 @@
import * as DataTableModel from 'app/client/models/DataTableModel';
import {ColumnGetters} from 'app/common/ColumnGetters';
import { ColumnGetter, ColumnGetters } from 'app/common/ColumnGetters';
import * as gristTypes from 'app/common/gristTypes';
import { choiceGetter } from 'app/common/SortFunc';
import { Sort } from 'app/common/SortSpec';
/**
*
@@ -18,13 +20,15 @@ export class ClientColumnGetters implements ColumnGetters {
unversioned?: boolean} = {}) {
}
public getColGetter(colRef: number): ((rowId: number) => any) | null {
const colId = this._tableModel.docModel.columns.getRowModel(Math.abs(colRef)).colId();
const getter = this._tableModel.tableData.getRowPropFunc(colId);
if (!getter) { return getter || null; }
public getColGetter(colSpec: Sort.ColSpec): ColumnGetter | null {
const rowModel = this._tableModel.docModel.columns.getRowModel(Sort.getColRef(colSpec));
const colId = rowModel.colId();
let getter: ColumnGetter|undefined = this._tableModel.tableData.getRowPropFunc(colId);
if (!getter) { return null; }
if (this._options.unversioned && this._tableModel.tableData.mayHaveVersions()) {
return (rowId) => {
const value = getter(rowId);
const valueGetter = getter;
getter = (rowId) => {
const value = valueGetter(rowId);
if (value && gristTypes.isVersions(value)) {
const versions = value[1];
return ('parent' in versions) ? versions.parent :
@@ -33,6 +37,13 @@ export class ClientColumnGetters implements ColumnGetters {
return value;
};
}
const details = Sort.specToDetails(colSpec);
if (details.orderByChoice) {
if (rowModel.pureType() === 'Choice') {
const choices: string[] = rowModel.widgetOptionsJson.peek()?.choices || [];
getter = choiceGetter(getter, choices);
}
}
return getter;
}

View File

@@ -1,13 +1,14 @@
import * as BaseView from 'app/client/components/BaseView';
import {CursorPos} from 'app/client/components/Cursor';
import {KoArray} from 'app/client/lib/koArray';
import {ColumnRec, TableRec, ViewFieldRec, ViewRec} from 'app/client/models/DocModel';
import {DocModel, IRowModel, recordSet, refRecord} from 'app/client/models/DocModel';
import { ColumnRec, TableRec, ViewFieldRec, ViewRec } from 'app/client/models/DocModel';
import * as modelUtil from 'app/client/models/modelUtil';
import {RowId} from 'app/client/models/rowset';
import {getWidgetTypes} from 'app/client/ui/widgetTypes';
import {Computed} from 'grainjs';
import * as ko from 'knockout';
import { CursorPos, } from 'app/client/components/Cursor';
import { KoArray, } from 'app/client/lib/koArray';
import { DocModel, IRowModel, recordSet, refRecord, } from 'app/client/models/DocModel';
import { RowId, } from 'app/client/models/rowset';
import { getWidgetTypes, } from 'app/client/ui/widgetTypes';
import { Sort, } from 'app/common/SortSpec';
import { Computed, } from 'grainjs';
import defaults = require('lodash/defaults');
// Represents a section of user views, now also known as a "page widget" (e.g. a view may contain
@@ -44,10 +45,10 @@ export interface ViewSectionRec extends IRowModel<"_grist_Views_section"> {
// is an array (parsed from JSON) of colRefs (i.e. rowIds into the columns table), with a
// twist: a rowId may be positive or negative, for ascending or descending respectively.
activeSortSpec: modelUtil.ObjObservable<number[]>;
activeSortSpec: modelUtil.ObjObservable<Sort.SortSpec>;
// Modified sort spec to take into account any active display columns.
activeDisplaySortSpec: ko.Computed<number[]>;
activeDisplaySortSpec: ko.Computed<Sort.SortSpec>;
// Evaluates to an array of column models, which are not referenced by anything in viewFields.
hiddenColumns: ko.Computed<ColumnRec[]>;
@@ -209,9 +210,9 @@ export function createViewSectionRec(this: ViewSectionRec, docModel: DocModel):
// twist: a rowId may be positive or negative, for ascending or descending respectively.
// TODO: This method of ignoring columns which are deleted is inefficient and may cause conflicts
// with sharing.
this.activeSortSpec = modelUtil.jsonObservable(this.activeSortJson, (obj: any) => {
return (obj || []).filter((sortRef: number) => {
const colModel = docModel.columns.getRowModel(Math.abs(sortRef));
this.activeSortSpec = modelUtil.jsonObservable(this.activeSortJson, (obj: Sort.SortSpec|null) => {
return (obj || []).filter((sortRef: Sort.ColSpec) => {
const colModel = docModel.columns.getRowModel(Sort.getColRef(sortRef));
return !colModel._isDeleted() && colModel.getRowId();
});
});
@@ -219,10 +220,10 @@ export function createViewSectionRec(this: ViewSectionRec, docModel: DocModel):
// Modified sort spec to take into account any active display columns.
this.activeDisplaySortSpec = this.autoDispose(ko.computed(() => {
return this.activeSortSpec().map(directionalColRef => {
const colRef = Math.abs(directionalColRef);
const colRef = Sort.getColRef(directionalColRef);
const field = this.viewFields().all().find(f => f.column().origColRef() === colRef);
const effectiveColRef = field ? field.displayColRef() : colRef;
return directionalColRef > 0 ? effectiveColRef : -effectiveColRef;
return Sort.swapColRef(directionalColRef, effectiveColRef);
});
}));