(core) Filter rows based on linked widgets when exporting view

Summary:
Fixes a problem reported here: https://community.getgrist.com/t/exporting-the-records-in-a-linked-view/2556/4

The download CSV/Excel link now contains an additional `linkingFilter` URL parameter containing JSON-encoded `filters` and `operations`. This object is originally created in the frontend in `LinkingState`, and previously it was only used internally in the frontend. It would make its way via `QuerySetManager` to `QuerySet.getFilterFunc` where the actual filtering logic happened. Now most of that logic has been moved to a similar function in `common`. The new function works with a new interface `ColumnGettersByColId` which abstract over the different ways data is accessed in the client and server in this context. There's no significant new logic in the diff, just refactoring and wiring.

Test Plan: Expanded two `nbrowser/SelectBy*.ts` test suites to also check the contents of a downloaded CSV in different linking scenarios.

Reviewers: paulfitz

Reviewed By: paulfitz

Differential Revision: https://phab.getgrist.com/D3961
This commit is contained in:
Alex Hall
2023-07-19 19:37:22 +02:00
parent ba16d50080
commit bc54a6646e
16 changed files with 161 additions and 66 deletions

View File

@@ -116,6 +116,8 @@ export interface ClientQuery extends BaseQuery {
};
}
export type FilterColValues = Pick<ClientQuery, "filters" | "operations">;
/**
* Query intended to be sent to a server.
*/

View File

@@ -26,4 +26,12 @@ export interface ColumnGetters {
getManualSortGetter(): ColumnGetter | null;
}
/**
* Like ColumnGetters, but takes the string `colId` rather than a `ColSpec`
* or numeric row ID.
*/
export interface ColumnGettersByColId {
getColGetterByColId(colId: string): ColumnGetter | null;
}
export type ColumnGetter = (rowId: number) => any;

View File

@@ -1,5 +1,9 @@
import { CellValue } from "app/common/DocActions";
import { ColumnFilterFunc } from "app/common/ColumnFilterFunc";
import {CellValue} from "app/common/DocActions";
import {ColumnFilterFunc} from "app/common/ColumnFilterFunc";
import {FilterColValues} from 'app/common/ActiveDocAPI';
import {isList} from 'app/common/gristTypes';
import {decodeObject} from 'app/plugin/objtypes';
import {ColumnGettersByColId} from 'app/common/ColumnGetters';
export type RowFilterFunc<T> = (row: T) => boolean;
@@ -14,3 +18,32 @@ export function buildRowFilter<T>(
}
export type RowValueFunc<T> = (rowId: T) => CellValue;
// Filter rows for the purpose of linked widgets
export function getLinkingFilterFunc(
columnGetters: ColumnGettersByColId, {filters, operations}: FilterColValues
): RowFilterFunc<number> {
const colFuncs = Object.keys(filters).sort().map(
(colId) => {
const getter = columnGetters.getColGetterByColId(colId);
if (!getter) { return () => true; }
const values = new Set(filters[colId]);
switch (operations[colId]) {
case "intersects":
return (rowId: number) => {
const value = getter(rowId) as CellValue;
return isList(value) &&
(decodeObject(value) as unknown[]).some(v => values.has(v));
};
case "empty":
return (rowId: number) => {
const value = getter(rowId);
// `isList(value) && value.length === 1` means `value == ['L']` i.e. an empty list
return !value || isList(value) && value.length === 1;
};
case "in":
return (rowId: number) => values.has(getter(rowId));
}
});
return (rowId: number) => colFuncs.every(f => f(rowId));
}