mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) Download as CSV button on sections
Summary: Adding "Download as CSV" button that exports filtred section data to csv Test Plan: Browser tests Reviewers: paulfitz, dsagal Reviewed By: paulfitz Differential Revision: https://phab.getgrist.com/D2830
This commit is contained in:
@@ -1,53 +1,8 @@
|
||||
import {CellValue} from 'app/common/DocActions';
|
||||
import {nativeCompare} from 'app/common/gutil';
|
||||
import {Computed, Disposable, Observable} from 'grainjs';
|
||||
|
||||
export type ColumnFilterFunc = (value: CellValue) => boolean;
|
||||
|
||||
interface FilterSpec { // Filter object as stored in the db
|
||||
included?: CellValue[];
|
||||
excluded?: CellValue[];
|
||||
}
|
||||
|
||||
// A more efficient representation of filter state for a column than FilterSpec.
|
||||
interface FilterState {
|
||||
include: boolean;
|
||||
values: Set<CellValue>;
|
||||
}
|
||||
|
||||
// Creates a FilterState. Accepts spec as a json string or a FilterSpec.
|
||||
function makeFilterState(spec: string | FilterSpec): FilterState {
|
||||
if (typeof(spec) === 'string') {
|
||||
return makeFilterState((spec && JSON.parse(spec)) || {});
|
||||
}
|
||||
return {
|
||||
include: Boolean(spec.included),
|
||||
values: new Set(spec.included || spec.excluded || []),
|
||||
};
|
||||
}
|
||||
|
||||
// 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; } }
|
||||
return true;
|
||||
}
|
||||
|
||||
// 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.
|
||||
function makeFilterFunc({include, values}: FilterState): ColumnFilterFunc {
|
||||
// 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.
|
||||
return (val: CellValue) => (values.has(Array.isArray(val) ? JSON.stringify(val) : val) === include);
|
||||
}
|
||||
|
||||
// Given a JSON string, returns a ColumnFilterFunc
|
||||
export function getFilterFunc(filterJson: string): ColumnFilterFunc|null {
|
||||
return filterJson ? makeFilterFunc(makeFilterState(filterJson)) : null;
|
||||
}
|
||||
import {ColumnFilterFunc, makeFilterFunc} from "app/common/ColumnFilterFunc";
|
||||
import {FilterSpec, FilterState, makeFilterState} from "app/common/FilterState";
|
||||
|
||||
/**
|
||||
* ColumnFilter implements a custom filter on a column, i.e. a filter that's diverged from what's
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
*/
|
||||
import * as DataTableModel from 'app/client/models/DataTableModel';
|
||||
import {DocModel} from 'app/client/models/DocModel';
|
||||
import {BaseFilteredRowSource, FilterFunc, RowId, RowList, RowSource} from 'app/client/models/rowset';
|
||||
import {BaseFilteredRowSource, RowId, RowList, RowSource} from 'app/client/models/rowset';
|
||||
import {TableData} from 'app/client/models/TableData';
|
||||
import {ActiveDocAPI, Query} from 'app/common/ActiveDocAPI';
|
||||
import {TableDataAction} from 'app/common/DocActions';
|
||||
@@ -36,6 +36,7 @@ import {DocData} from 'app/common/DocData';
|
||||
import {nativeCompare} from 'app/common/gutil';
|
||||
import {IRefCountSub, RefCountMap} from 'app/common/RefCountMap';
|
||||
import {TableData as BaseTableData} from 'app/common/TableData';
|
||||
import {RowFilterFunc} from 'app/common/RowFilterFunc';
|
||||
import {tbind} from 'app/common/tbind';
|
||||
import {Disposable, Holder, IDisposableOwnerT} from 'grainjs';
|
||||
import * as ko from 'knockout';
|
||||
@@ -295,7 +296,7 @@ export class TableQuerySets {
|
||||
/**
|
||||
* Returns a filtering function which tells whether a row matches the given query.
|
||||
*/
|
||||
export function getFilterFunc(docData: DocData, query: Query): FilterFunc {
|
||||
export function getFilterFunc(docData: DocData, query: Query): RowFilterFunc<RowId> {
|
||||
// NOTE we rely without checking on tableId and colIds being valid.
|
||||
const tableData: BaseTableData = docData.getTable(query.tableId)!;
|
||||
const colIds = Object.keys(query.filters).sort();
|
||||
|
||||
@@ -1,22 +1,17 @@
|
||||
import {KoArray} from 'app/client/lib/koArray';
|
||||
import {ViewFieldRec} from 'app/client/models/DocModel';
|
||||
import {FilterFunc, RowId} from 'app/client/models/rowset';
|
||||
import {RowId} from 'app/client/models/rowset';
|
||||
import {TableData} from 'app/client/models/TableData';
|
||||
import {CellValue} from 'app/common/DocActions';
|
||||
import {Computed, Disposable, MutableObsArray, obsArray, Observable} from 'grainjs';
|
||||
import {ColumnFilter, ColumnFilterFunc, getFilterFunc} from './ColumnFilter';
|
||||
|
||||
type RowValueFunc = (rowId: RowId) => CellValue;
|
||||
import {ColumnFilter} from './ColumnFilter';
|
||||
import {buildRowFilter, RowFilterFunc, RowValueFunc } from "app/common/RowFilterFunc";
|
||||
import {buildColFilter} from "app/common/ColumnFilterFunc";
|
||||
|
||||
interface OpenColumnFilter {
|
||||
fieldRef: number;
|
||||
colFilter: ColumnFilter;
|
||||
}
|
||||
|
||||
function buildColFunc(getter: RowValueFunc, filterFunc: ColumnFilterFunc): FilterFunc {
|
||||
return (rowId: RowId) => filterFunc(getter(rowId));
|
||||
}
|
||||
|
||||
/**
|
||||
* SectionFilter represents a collection of column filters in place for a view section. It is created
|
||||
* out of `viewFields` and `tableData`, and provides a Computed `sectionFilterFunc` that users can
|
||||
@@ -28,7 +23,7 @@ function buildColFunc(getter: RowValueFunc, filterFunc: ColumnFilterFunc): Filte
|
||||
* results in their being displayed (obviating the need to maintain their rowId explicitly).
|
||||
*/
|
||||
export class SectionFilter extends Disposable {
|
||||
public readonly sectionFilterFunc: Observable<FilterFunc>;
|
||||
public readonly sectionFilterFunc: Observable<RowFilterFunc<RowId>>;
|
||||
|
||||
private _openFilterOverride: Observable<OpenColumnFilter|null> = Observable.create(this, null);
|
||||
private _tempRows: MutableObsArray<RowId> = obsArray();
|
||||
@@ -38,16 +33,16 @@ export class SectionFilter extends Disposable {
|
||||
|
||||
const columnFilterFunc = Computed.create(this, this._openFilterOverride, (use, openFilter) => {
|
||||
const fields = use(use(viewFields).getObservable());
|
||||
const funcs: Array<FilterFunc | null> = fields.map(f => {
|
||||
const funcs: Array<RowFilterFunc<RowId> | null> = fields.map(f => {
|
||||
const filterFunc = (openFilter && openFilter.fieldRef === f.getRowId()) ?
|
||||
use(openFilter.colFilter.filterFunc) :
|
||||
getFilterFunc(use(f.activeFilter));
|
||||
buildColFilter(use(f.activeFilter));
|
||||
|
||||
const getter = tableData.getRowPropFunc(use(f.colId));
|
||||
|
||||
if (!filterFunc || !getter) { return null; }
|
||||
|
||||
return buildColFunc(getter as RowValueFunc, filterFunc);
|
||||
return buildRowFilter(getter as RowValueFunc<RowId>, filterFunc);
|
||||
})
|
||||
.filter(f => f !== null); // Filter out columns that don't have a filter
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ import koArray, {KoArray} from 'app/client/lib/koArray';
|
||||
import {DisposableWithEvents} from 'app/common/DisposableWithEvents';
|
||||
import {CompareFunc, sortedIndex} from 'app/common/gutil';
|
||||
import {SkippableRows} from 'app/common/TableData';
|
||||
import {RowFilterFunc} from "app/common/RowFilterFunc";
|
||||
|
||||
/**
|
||||
* Special constant value that can be used for the `rows` array for the 'rowNotify'
|
||||
@@ -206,8 +207,6 @@ export class ExtendedRowSource extends RowSource {
|
||||
// FilteredRowSource
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export type FilterFunc = (row: RowId) => boolean;
|
||||
|
||||
interface FilterRowChanges {
|
||||
adds?: RowId[];
|
||||
updates?: RowId[];
|
||||
@@ -221,7 +220,7 @@ interface FilterRowChanges {
|
||||
export class BaseFilteredRowSource extends RowListener implements RowSource {
|
||||
protected _matchingRows: Set<RowId> = new Set(); // Set of rows matching the filter.
|
||||
|
||||
constructor(protected _filterFunc: FilterFunc) {
|
||||
constructor(protected _filterFunc: RowFilterFunc<RowId>) {
|
||||
super();
|
||||
}
|
||||
|
||||
@@ -327,7 +326,7 @@ export class FilteredRowSource extends BaseFilteredRowSource {
|
||||
* Change the filter function. This may trigger 'remove' and 'add' events as necessary to indicate
|
||||
* that rows stopped or started matching the new filter.
|
||||
*/
|
||||
public updateFilter(filterFunc: FilterFunc) {
|
||||
public updateFilter(filterFunc: RowFilterFunc<RowId>) {
|
||||
this._filterFunc = filterFunc;
|
||||
const changes: FilterRowChanges = {};
|
||||
// After the first call, _excludedRows may have additional rows, but there is no harm in it,
|
||||
|
||||
Reference in New Issue
Block a user