mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) Allow filtering hidden columns
Summary: Existing filters are now moved out of fields and into a new metadata table for filters, and the client is updated to retrieve/update/save filters from the new table. This enables storing of filters for columns that don't have fields (notably, hidden columns). Test Plan: Browser and server tests. Reviewers: jarek Reviewed By: jarek Differential Revision: https://phab.getgrist.com/D3138
This commit is contained in:
@@ -78,7 +78,7 @@ function BaseView(gristDoc, viewSectionModel, options) {
|
||||
// Create a section filter and a filtered row source that subscribes to its changes.
|
||||
// `sectionFilter` also provides an `addTemporaryRow()` to allow views to display newly inserted rows,
|
||||
// and `setFilterOverride()` to allow controlling a filter from a column menu.
|
||||
this._sectionFilter = SectionFilter.create(this, this.viewSection.viewFields, this.tableModel.tableData);
|
||||
this._sectionFilter = SectionFilter.create(this, this.viewSection, this.tableModel.tableData);
|
||||
this._filteredRowSource = rowset.FilteredRowSource.create(this, this._sectionFilter.sectionFilterFunc.get());
|
||||
this._filteredRowSource.subscribeTo(this._mainRowSource);
|
||||
this.autoDispose(this._sectionFilter.sectionFilterFunc.addListener(filterFunc => {
|
||||
@@ -669,10 +669,11 @@ BaseView.prototype.getLastDataRowIndex = function() {
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates and opens ColumnFilterMenu for a given field, and returns its PopupControl.
|
||||
* Creates and opens ColumnFilterMenu for a given field/column, and returns its PopupControl.
|
||||
*/
|
||||
BaseView.prototype.createFilterMenu = function(openCtl, field, onClose) {
|
||||
return createFilterMenu(openCtl, this._sectionFilter, field, this._mainRowSource, this.tableModel.tableData, onClose);
|
||||
BaseView.prototype.createFilterMenu = function(openCtl, filterInfo, onClose) {
|
||||
return createFilterMenu(openCtl, this._sectionFilter, filterInfo, this._mainRowSource,
|
||||
this.tableModel.tableData, onClose);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -1386,7 +1386,9 @@ GridView.prototype._getColumnMenuOptions = function(copySelection) {
|
||||
|
||||
GridView.prototype._columnFilterMenu = function(ctl, field) {
|
||||
this.ctxMenuHolder.autoDispose(ctl);
|
||||
return this.createFilterMenu(ctl, field);
|
||||
const filterInfo = this.viewSection.filters()
|
||||
.find(({fieldOrColumn}) => fieldOrColumn.origCol().origColRef() === field.column().origColRef());
|
||||
return this.createFilterMenu(ctl, filterInfo);
|
||||
};
|
||||
|
||||
GridView.prototype.maybeSelectColumn = function (elem, field) {
|
||||
|
||||
@@ -650,9 +650,9 @@ export class GristDoc extends DisposableWithEvents {
|
||||
}
|
||||
|
||||
public getCsvLink() {
|
||||
const filters = this.viewModel.activeSection.peek().filteredFields.get().map(field=> ({
|
||||
colRef : field.colRef.peek(),
|
||||
filter : field.activeFilter.peek()
|
||||
const filters = this.viewModel.activeSection.peek().activeFilters.get().map(filterInfo => ({
|
||||
colRef : filterInfo.fieldOrColumn.origCol().origColRef(),
|
||||
filter : filterInfo.filter()
|
||||
}));
|
||||
|
||||
const params = {
|
||||
|
||||
@@ -576,29 +576,34 @@ ViewConfigTab.prototype._buildFilterDom = function() {
|
||||
}
|
||||
|
||||
return [
|
||||
grainjsDom.forEach(section.filteredFields, (field) => {
|
||||
grainjsDom.forEach(section.activeFilters, (filterInfo) => {
|
||||
return cssRow(
|
||||
cssIconWrapper(
|
||||
cssFilterIcon('FilterSimple', cssNoMarginLeft.cls('')),
|
||||
attachColumnFilterMenu(section, field, {
|
||||
attachColumnFilterMenu(section, filterInfo, {
|
||||
placement: 'bottom-end', attach: 'body',
|
||||
trigger: ['click', (_el, popupControl) => popupControls.set(field, popupControl)],
|
||||
trigger: [
|
||||
'click',
|
||||
(_el, popupControl) => popupControls.set(filterInfo.fieldOrColumn.origCol(), popupControl)
|
||||
],
|
||||
}),
|
||||
),
|
||||
cssLabel(grainjsDom.text(field.label)),
|
||||
cssLabel(grainjsDom.text(filterInfo.fieldOrColumn.label)),
|
||||
cssIconWrapper(
|
||||
cssFilterIcon('Remove', dom.on('click', () => field.activeFilter('')),
|
||||
testId('remove-filter')),
|
||||
cssFilterIcon('Remove',
|
||||
dom.on('click', () => section.setFilter(filterInfo.fieldOrColumn.origCol().origColRef(), '')),
|
||||
testId('remove-filter')
|
||||
),
|
||||
),
|
||||
testId('filter'),
|
||||
);
|
||||
}),
|
||||
cssRow(
|
||||
grainjsDom.domComputed((use) => {
|
||||
const fields = use(use(section.viewFields).getObservable());
|
||||
const filters = use(section.filters);
|
||||
return cssTextBtn(
|
||||
cssPlusIcon('Plus'), 'Add Filter',
|
||||
addFilterMenu(fields, popupControls, {placement: 'bottom-end'}),
|
||||
addFilterMenu(filters, section, popupControls, {placement: 'bottom-end'}),
|
||||
testId('add-filter-btn'),
|
||||
);
|
||||
}),
|
||||
|
||||
5
app/client/declarations.d.ts
vendored
5
app/client/declarations.d.ts
vendored
@@ -38,7 +38,8 @@ declare module "app/client/components/BaseView" {
|
||||
import {DataRowModel} from 'app/client/models/DataRowModel';
|
||||
import {LazyArrayModel} from "app/client/models/DataTableModel";
|
||||
import * as DataTableModel from "app/client/models/DataTableModel";
|
||||
import {ViewFieldRec, ViewSectionRec} from "app/client/models/DocModel";
|
||||
import {ViewSectionRec} from "app/client/models/DocModel";
|
||||
import {FilterInfo} from 'app/client/models/entities/ViewSectionRec';
|
||||
import {SortedRowSet} from 'app/client/models/rowset';
|
||||
import {FieldBuilder} from "app/client/widgets/FieldBuilder";
|
||||
import {DomArg} from 'grainjs';
|
||||
@@ -64,7 +65,7 @@ declare module "app/client/components/BaseView" {
|
||||
|
||||
constructor(gristDoc: GristDoc, viewSectionModel: any);
|
||||
public setCursorPos(cursorPos: CursorPos): void;
|
||||
public createFilterMenu(ctl: IOpenController, field: ViewFieldRec, onClose?: () => void): HTMLElement;
|
||||
public createFilterMenu(ctl: IOpenController, filterInfo: FilterInfo, onClose?: () => void): HTMLElement;
|
||||
public buildTitleControls(): DomArg;
|
||||
public getLoadingDonePromise(): Promise<void>;
|
||||
public activateEditorAtCursor(options?: Options): void;
|
||||
|
||||
@@ -28,6 +28,7 @@ import {schema, SchemaTypes} from 'app/common/schema';
|
||||
import {ACLRuleRec, createACLRuleRec} from 'app/client/models/entities/ACLRuleRec';
|
||||
import {ColumnRec, createColumnRec} from 'app/client/models/entities/ColumnRec';
|
||||
import {createDocInfoRec, DocInfoRec} from 'app/client/models/entities/DocInfoRec';
|
||||
import {createFilterRec, FilterRec} from 'app/client/models/entities/FilterRec';
|
||||
import {createPageRec, PageRec} from 'app/client/models/entities/PageRec';
|
||||
import {createTabBarRec, TabBarRec} from 'app/client/models/entities/TabBarRec';
|
||||
import {createTableRec, TableRec} from 'app/client/models/entities/TableRec';
|
||||
@@ -41,6 +42,7 @@ import {createViewSectionRec, ViewSectionRec} from 'app/client/models/entities/V
|
||||
// import {ColumnRec, ViewFieldRec} from 'app/client/models/DocModel';
|
||||
export {ColumnRec} from 'app/client/models/entities/ColumnRec';
|
||||
export {DocInfoRec} from 'app/client/models/entities/DocInfoRec';
|
||||
export {FilterRec} from 'app/client/models/entities/FilterRec';
|
||||
export {PageRec} from 'app/client/models/entities/PageRec';
|
||||
export {TabBarRec} from 'app/client/models/entities/TabBarRec';
|
||||
export {TableRec} from 'app/client/models/entities/TableRec';
|
||||
@@ -111,6 +113,7 @@ export class DocModel {
|
||||
public validations: MTM<ValidationRec> = this._metaTableModel("_grist_Validations", createValidationRec);
|
||||
public pages: MTM<PageRec> = this._metaTableModel("_grist_Pages", createPageRec);
|
||||
public rules: MTM<ACLRuleRec> = this._metaTableModel("_grist_ACLRules", createACLRuleRec);
|
||||
public filters: MTM<FilterRec> = this._metaTableModel("_grist_Filters", createFilterRec);
|
||||
|
||||
public docInfoRow: DocInfoRec;
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {KoArray} from 'app/client/lib/koArray';
|
||||
import {ColumnFilter} from 'app/client/models/ColumnFilter';
|
||||
import {ViewFieldRec} from 'app/client/models/DocModel';
|
||||
import {ColumnRec, ViewFieldRec, ViewSectionRec} from 'app/client/models/DocModel';
|
||||
import {RowId} from 'app/client/models/rowset';
|
||||
import {TableData} from 'app/client/models/TableData';
|
||||
import {buildColFilter, ColumnFilterFunc} from 'app/common/ColumnFilterFunc';
|
||||
@@ -10,15 +9,15 @@ import {Computed, Disposable, MutableObsArray, obsArray, Observable, UseCB} from
|
||||
export {ColumnFilterFunc} from 'app/common/ColumnFilterFunc';
|
||||
|
||||
interface OpenColumnFilter {
|
||||
fieldRef: number;
|
||||
colRef: number;
|
||||
colFilter: ColumnFilter;
|
||||
}
|
||||
|
||||
type ColFilterCB = (field: ViewFieldRec, colFilter: ColumnFilterFunc|null) => ColumnFilterFunc|null;
|
||||
type ColFilterCB = (fieldOrColumn: ViewFieldRec|ColumnRec, colFilter: ColumnFilterFunc|null) => ColumnFilterFunc|null;
|
||||
|
||||
/**
|
||||
* 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
|
||||
* out of `filters` (in `viewSection`) and `tableData`, and provides a Computed `sectionFilterFunc` that users can
|
||||
* subscribe to in order to update their FilteredRowSource.
|
||||
*
|
||||
* Additionally, `setFilterOverride()` provides a way to override the current filter for a given colRef,
|
||||
@@ -32,13 +31,13 @@ export class SectionFilter extends Disposable {
|
||||
private _openFilterOverride: Observable<OpenColumnFilter|null> = Observable.create(this, null);
|
||||
private _tempRows: MutableObsArray<RowId> = obsArray();
|
||||
|
||||
constructor(public viewFields: ko.Computed<KoArray<ViewFieldRec>>, private _tableData: TableData) {
|
||||
constructor(public viewSection: ViewSectionRec, private _tableData: TableData) {
|
||||
super();
|
||||
|
||||
const columnFilterFunc = Computed.create(this, this._openFilterOverride, (use, openFilter) => {
|
||||
const openFilterFilterFunc = openFilter && use(openFilter.colFilter.filterFunc);
|
||||
function getFilterFunc(field: ViewFieldRec, colFilter: ColumnFilterFunc|null) {
|
||||
if (openFilter?.fieldRef === field.getRowId()) {
|
||||
function getFilterFunc(fieldOrColumn: ViewFieldRec|ColumnRec, colFilter: ColumnFilterFunc|null) {
|
||||
if (openFilter?.colRef === fieldOrColumn.getRowId()) {
|
||||
return openFilterFilterFunc;
|
||||
}
|
||||
return colFilter;
|
||||
@@ -56,11 +55,11 @@ export class SectionFilter extends Disposable {
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows to override a single filter for a given fieldRef. Multiple calls to `setFilterOverride` will overwrite
|
||||
* Allows to override a single filter for a given colRef. Multiple calls to `setFilterOverride` will overwrite
|
||||
* previously set values.
|
||||
*/
|
||||
public setFilterOverride(fieldRef: number, colFilter: ColumnFilter) {
|
||||
this._openFilterOverride.set(({fieldRef, colFilter}));
|
||||
public setFilterOverride(colRef: number, colFilter: ColumnFilter) {
|
||||
this._openFilterOverride.set(({colRef, colFilter}));
|
||||
colFilter.onDispose(() => {
|
||||
const override = this._openFilterOverride.get();
|
||||
if (override && override.colFilter === colFilter) {
|
||||
@@ -81,8 +80,8 @@ export class SectionFilter extends Disposable {
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a filter function that combines the filter function of all the fields. You can use
|
||||
* `getFilterFunc(field, colFilter)` to customize the filter func for each field. It calls
|
||||
* Builds a filter function that combines the filter function of all the columns. You can use
|
||||
* `getFilterFunc(column, colFilter)` to customize the filter func for each columns. It calls
|
||||
* `getFilterFunc` right away. Also, all the rows that were added with `addTemporaryRow()` bypass
|
||||
* the filter.
|
||||
*/
|
||||
@@ -96,16 +95,16 @@ export class SectionFilter extends Disposable {
|
||||
|
||||
/**
|
||||
* Internal that helps build a filter function that combines the filter function of all
|
||||
* fields. You can use `getFilterFunc(field, colFilter)` to customize the filter func for each
|
||||
* field. It calls `getFilterFunc` right away
|
||||
* columns. You can use `getFilterFunc(column, colFilter)` to customize the filter func for each
|
||||
* column. It calls `getFilterFunc` right away.
|
||||
*/
|
||||
private _buildPlainFilterFunc(getFilterFunc: ColFilterCB, use: UseCB): RowFilterFunc<RowId> {
|
||||
const fields = use(use(this.viewFields).getObservable());
|
||||
const funcs: Array<RowFilterFunc<RowId> | null> = fields.map(f => {
|
||||
const colFilter = buildColFilter(use(f.activeFilter), use(use(f.column).type));
|
||||
const filterFunc = getFilterFunc(f, colFilter);
|
||||
const filters = use(this.viewSection.filters);
|
||||
const funcs: Array<RowFilterFunc<RowId> | null> = filters.map(({filter, fieldOrColumn}) => {
|
||||
const colFilter = buildColFilter(use(filter), use(use(fieldOrColumn.origCol).type));
|
||||
const filterFunc = getFilterFunc(fieldOrColumn, colFilter);
|
||||
|
||||
const getter = this._tableData.getRowPropFunc(f.colId.peek());
|
||||
const getter = this._tableData.getRowPropFunc(fieldOrColumn.colId.peek());
|
||||
|
||||
if (!filterFunc || !getter) { return null; }
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import {DocModel, IRowModel, recordSet, refRecord, TableRec, ViewFieldRec} from
|
||||
import {jsonObservable, ObjObservable} from 'app/client/models/modelUtil';
|
||||
import * as gristTypes from 'app/common/gristTypes';
|
||||
import {getReferencedTableId} from 'app/common/gristTypes';
|
||||
import {BaseFormatter, createFormatter} from 'app/common/ValueFormatter';
|
||||
import * as ko from 'knockout';
|
||||
|
||||
// Represents a column in a user-defined table.
|
||||
@@ -35,6 +36,13 @@ export interface ColumnRec extends IRowModel<"_grist_Tables_column"> {
|
||||
// The column's display column
|
||||
_displayColModel: ko.Computed<ColumnRec>;
|
||||
|
||||
// Display col ref to use for the column, defaulting to the plain column itself.
|
||||
displayColRef: ko.Computed<number>;
|
||||
|
||||
// The display column to use for the column, or the column itself when no displayCol is set.
|
||||
displayColModel: ko.Computed<ColumnRec>;
|
||||
visibleColModel: ko.Computed<ColumnRec>;
|
||||
|
||||
disableModifyBase: ko.Computed<boolean>; // True if column config can't be modified (name, type, etc.)
|
||||
disableModify: ko.Computed<boolean>; // True if column can't be modified or is being transformed.
|
||||
disableEditData: ko.Computed<boolean>; // True to disable editing of the data in this column.
|
||||
@@ -46,6 +54,10 @@ export interface ColumnRec extends IRowModel<"_grist_Tables_column"> {
|
||||
|
||||
// Helper which adds/removes/updates column's displayCol to match the formula.
|
||||
saveDisplayFormula(formula: string): Promise<void>|undefined;
|
||||
|
||||
// Helper for Reference/ReferenceList columns, which returns a formatter according
|
||||
// to the visibleCol associated with column. Subscribes to observables if used within a computed.
|
||||
createVisibleColFormatter(): BaseFormatter;
|
||||
}
|
||||
|
||||
export function createColumnRec(this: ColumnRec, docModel: DocModel): void {
|
||||
@@ -84,6 +96,13 @@ export function createColumnRec(this: ColumnRec, docModel: DocModel): void {
|
||||
}
|
||||
};
|
||||
|
||||
// Display col ref to use for the column, defaulting to the plain column itself.
|
||||
this.displayColRef = ko.pureComputed(() => this.displayCol() || this.origColRef());
|
||||
|
||||
// The display column to use for the column, or the column itself when no displayCol is set.
|
||||
this.displayColModel = refRecord(docModel.columns, this.displayColRef);
|
||||
this.visibleColModel = refRecord(docModel.columns, this.visibleCol);
|
||||
|
||||
this.disableModifyBase = ko.pureComputed(() => Boolean(this.summarySourceCol()));
|
||||
this.disableModify = ko.pureComputed(() => this.disableModifyBase() || this.isTransforming());
|
||||
this.disableEditData = ko.pureComputed(() => Boolean(this.summarySourceCol()));
|
||||
@@ -95,4 +114,16 @@ export function createColumnRec(this: ColumnRec, docModel: DocModel): void {
|
||||
const refTableId = getReferencedTableId(this.type() || "");
|
||||
return refTableId ? docModel.allTables.all().find(t => t.tableId() === refTableId) || null : null;
|
||||
});
|
||||
|
||||
// Helper for Reference/ReferenceList columns, which returns a formatter according to the visibleCol
|
||||
// associated with this column. If no visible column available, return formatting for the column itself.
|
||||
// Subscribes to observables if used within a computed.
|
||||
// TODO: It would be better to replace this with a pureComputed whose value is a formatter.
|
||||
this.createVisibleColFormatter = function() {
|
||||
const vcol = this.visibleColModel();
|
||||
const documentSettings = docModel.docInfoRow.documentSettingsJson();
|
||||
return (vcol.getRowId() !== 0) ?
|
||||
createFormatter(vcol.type(), vcol.widgetOptionsJson(), documentSettings) :
|
||||
createFormatter(this.type(), this.widgetOptionsJson(), documentSettings);
|
||||
};
|
||||
}
|
||||
|
||||
22
app/client/models/entities/FilterRec.ts
Normal file
22
app/client/models/entities/FilterRec.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import {ColumnRec, DocModel, IRowModel, refRecord, ViewSectionRec} from 'app/client/models/DocModel';
|
||||
import * as modelUtil from 'app/client/models/modelUtil';
|
||||
import * as ko from 'knockout';
|
||||
|
||||
// Represents a column filter for a view section.
|
||||
export interface FilterRec extends IRowModel<"_grist_Filters"> {
|
||||
viewSection: ko.Computed<ViewSectionRec>;
|
||||
column: ko.Computed<ColumnRec>;
|
||||
|
||||
// Observable for the parsed filter object.
|
||||
activeFilter: modelUtil.CustomComputed<string>;
|
||||
}
|
||||
|
||||
export function createFilterRec(this: FilterRec, docModel: DocModel): void {
|
||||
this.viewSection = refRecord(docModel.viewSections, this.viewSectionRef);
|
||||
this.column = refRecord(docModel.columns, this.colRef);
|
||||
|
||||
// Observable for the active filter that's initialized from the value saved to the server.
|
||||
this.activeFilter = modelUtil.customComputed({
|
||||
read: () => { const f = this.filter(); return f === 'null' ? '' : f; }, // To handle old empty filters.
|
||||
});
|
||||
}
|
||||
@@ -7,7 +7,6 @@ import {DocumentSettings} from 'app/common/DocumentSettings';
|
||||
import {isFullReferencingType} from 'app/common/gristTypes';
|
||||
import {BaseFormatter, createFormatter} from 'app/common/ValueFormatter';
|
||||
import {createParser} from 'app/common/ValueParser';
|
||||
import {Computed, fromKo} from 'grainjs';
|
||||
import * as ko from 'knockout';
|
||||
|
||||
// Represents a page entry in the tree of pages.
|
||||
@@ -65,12 +64,6 @@ export interface ViewFieldRec extends IRowModel<"_grist_Views_section_field"> {
|
||||
// Whether lines should wrap in a cell.
|
||||
wrapping: ko.Computed<boolean>;
|
||||
|
||||
// Observable for the parsed filter object saved to the field.
|
||||
activeFilter: modelUtil.CustomComputed<string>;
|
||||
|
||||
// Computed boolean that's true when there's a saved filter
|
||||
isFiltered: Computed<boolean>;
|
||||
|
||||
disableModify: ko.Computed<boolean>;
|
||||
disableEditData: ko.Computed<boolean>;
|
||||
|
||||
@@ -232,14 +225,6 @@ export function createViewFieldRec(this: ViewFieldRec, docModel: DocModel): void
|
||||
return this.widgetOptionsJson().wrap ?? (this.viewSection().parentKey() !== 'record');
|
||||
});
|
||||
|
||||
// Observable for the active filter that's initialized from the value saved to the server.
|
||||
this.activeFilter = modelUtil.customComputed({
|
||||
read: () => { const f = this.filter(); return f === 'null' ? '' : f; }, // To handle old empty filters
|
||||
save: (val) => this.filter.saveOnly(val),
|
||||
});
|
||||
|
||||
this.isFiltered = Computed.create(this, fromKo(this.activeFilter), (_use, f) => f !== '');
|
||||
|
||||
this.disableModify = ko.pureComputed(() => this.column().disableModify());
|
||||
this.disableEditData = ko.pureComputed(() => this.column().disableEditData());
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as BaseView from 'app/client/components/BaseView';
|
||||
import { ColumnRec, TableRec, ViewFieldRec, ViewRec } from 'app/client/models/DocModel';
|
||||
import { ColumnRec, FilterRec, TableRec, ViewFieldRec, ViewRec } from 'app/client/models/DocModel';
|
||||
import * as modelUtil from 'app/client/models/modelUtil';
|
||||
import * as ko from 'knockout';
|
||||
import { CursorPos, } from 'app/client/components/Cursor';
|
||||
@@ -7,6 +7,7 @@ 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 { arrayRepeat, } from 'app/common/gutil';
|
||||
import { Sort, } from 'app/common/SortSpec';
|
||||
import { Computed, } from 'grainjs';
|
||||
import defaults = require('lodash/defaults');
|
||||
@@ -16,6 +17,9 @@ import defaults = require('lodash/defaults');
|
||||
export interface ViewSectionRec extends IRowModel<"_grist_Views_section"> {
|
||||
viewFields: ko.Computed<KoArray<ViewFieldRec>>;
|
||||
|
||||
// All table columns associated with this view section, excluding hidden helper columns.
|
||||
columns: ko.Computed<ColumnRec[]>;
|
||||
|
||||
optionsObj: modelUtil.SaveableObjObservable<any>;
|
||||
|
||||
customDef: CustomViewSectionDef;
|
||||
@@ -33,13 +37,36 @@ export interface ViewSectionRec extends IRowModel<"_grist_Views_section"> {
|
||||
|
||||
layoutSpecObj: modelUtil.ObjObservable<any>;
|
||||
|
||||
// Helper metadata item which indicates whether any of the section's fields have unsaved
|
||||
_savedFilters: ko.Computed<KoArray<FilterRec>>;
|
||||
|
||||
/**
|
||||
* Unsaved client-side filters, keyed by original col ref. Currently only wiped when unsaved filters
|
||||
* are applied or reverted.
|
||||
*
|
||||
* If saved filters exist for a col ref, unsaved filters take priority and are applied instead. This
|
||||
* prevents disruption when changes are made to saved filters for the same field/column, but there
|
||||
* may be some cases where we'd want to reset _unsavedFilters on some indirect change to the document.
|
||||
*
|
||||
* NOTE: See `filters`, where `_unsavedFilters` is merged with `savedFilters`.
|
||||
*/
|
||||
_unsavedFilters: Map<number, string>;
|
||||
|
||||
/**
|
||||
* Filter information for all fields/section in the section.
|
||||
*
|
||||
* Re-computed on changes to `savedFilters`, as well as any changes to `viewFields` or `columns`. Any
|
||||
* unsaved filters saved in `_unsavedFilters` are applied on computation, taking priority over saved
|
||||
* filters for the same field/column, if any exist.
|
||||
*/
|
||||
filters: ko.Computed<FilterInfo[]>;
|
||||
|
||||
// Subset of `filters` containing non-blank active filters.
|
||||
activeFilters: Computed<FilterInfo[]>;
|
||||
|
||||
// Helper metadata item which indicates whether any of the section's fields/columns have unsaved
|
||||
// changes to their filters. (True indicates unsaved changes)
|
||||
filterSpecChanged: Computed<boolean>;
|
||||
|
||||
// Array of fields with an active filter
|
||||
filteredFields: Computed<ViewFieldRec[]>;
|
||||
|
||||
// Customizable version of the JSON-stringified sort spec. It may diverge from the saved one.
|
||||
activeSortJson: modelUtil.CustomComputed<string>;
|
||||
|
||||
@@ -96,14 +123,14 @@ export interface ViewSectionRec extends IRowModel<"_grist_Views_section"> {
|
||||
// We won't freeze all the columns on a grid, it will leave at least 1 column unfrozen.
|
||||
numFrozen: ko.Computed<number>;
|
||||
|
||||
// Save all filters of fields in the section.
|
||||
// Save all filters of fields/columns in the section.
|
||||
saveFilters(): Promise<void>;
|
||||
|
||||
// Revert all filters of fields in the section.
|
||||
// Revert all filters of fields/columns in the section.
|
||||
revertFilters(): void;
|
||||
|
||||
// Clear and save all filters of fields in the section.
|
||||
clearFilters(): void;
|
||||
// Apply `filter` to the field or column identified by `colRef`.
|
||||
setFilter(colRef: number, filter: string): void;
|
||||
}
|
||||
|
||||
export interface CustomViewSectionDef {
|
||||
@@ -129,10 +156,22 @@ export interface CustomViewSectionDef {
|
||||
sectionId: ko.Observable<string>;
|
||||
}
|
||||
|
||||
// Information about filters for a field or hidden column.
|
||||
export interface FilterInfo {
|
||||
// The field or column associated with this filter info.
|
||||
fieldOrColumn: ViewFieldRec|ColumnRec;
|
||||
// Filter that applies to this field/column, if any.
|
||||
filter: modelUtil.CustomComputed<string>;
|
||||
// True if `filter` has a non-blank value.
|
||||
isFiltered: ko.PureComputed<boolean>;
|
||||
}
|
||||
|
||||
export function createViewSectionRec(this: ViewSectionRec, docModel: DocModel): void {
|
||||
this.viewFields = recordSet(this, docModel.viewFields, 'parentId', {sortBy: 'parentPos'});
|
||||
|
||||
// All table columns associated with this view section, excluding any hidden helper columns.
|
||||
this.columns = this.autoDispose(ko.pureComputed(() => this.table().columns().all().filter(c => !c.isHiddenCol())));
|
||||
|
||||
const defaultOptions = {
|
||||
verticalGridlines: true,
|
||||
horizontalGridlines: true,
|
||||
@@ -180,28 +219,128 @@ export function createViewSectionRec(this: ViewSectionRec, docModel: DocModel):
|
||||
|
||||
this.layoutSpecObj = modelUtil.jsonObservable(this.layoutSpec);
|
||||
|
||||
// Helper metadata item which indicates whether any of the section's fields have unsaved
|
||||
this._savedFilters = recordSet(this, docModel.filters, 'viewSectionRef');
|
||||
|
||||
/**
|
||||
* Unsaved client-side filters, keyed by original col ref. Currently only wiped when unsaved filters
|
||||
* are applied or reverted.
|
||||
*
|
||||
* If saved filters exist for a col ref, unsaved filters take priority and are applied instead. This
|
||||
* prevents disruption when changes are made to saved filters for the same field/column, but there
|
||||
* may be some cases where we'd want to reset _unsavedFilters on some indirect change to the document.
|
||||
*
|
||||
* NOTE: See `filters`, where `_unsavedFilters` is merged with `savedFilters`.
|
||||
*/
|
||||
this._unsavedFilters = new Map();
|
||||
|
||||
/**
|
||||
* Filter information for all fields/section in the section.
|
||||
*
|
||||
* Re-computed on changes to `savedFilters`, as well as any changes to `viewFields` or `columns`. Any
|
||||
* unsaved filters saved in `_unsavedFilters` are applied on computation, taking priority over saved
|
||||
* filters for the same field/column, if any exist.
|
||||
*/
|
||||
this.filters = this.autoDispose(ko.computed(() => {
|
||||
const savedFiltersByColRef = new Map(this._savedFilters().all().map(f => [f.colRef(), f]));
|
||||
const viewFieldsByColRef = new Map(this.viewFields().all().map(f => [f.colRef(), f]));
|
||||
|
||||
return this.columns().map(column => {
|
||||
const savedFilter = savedFiltersByColRef.get(column.origColRef());
|
||||
const filter = modelUtil.customComputed({
|
||||
// Initialize with a saved filter, if one exists. Otherwise, use a blank filter.
|
||||
read: () => { return savedFilter ? savedFilter.activeFilter() : ''; },
|
||||
});
|
||||
|
||||
// If an unsaved filter exists, overwrite `filter` with it.
|
||||
const unsavedFilter = this._unsavedFilters.get(column.origColRef());
|
||||
if (unsavedFilter !== undefined) { filter(unsavedFilter); }
|
||||
|
||||
return {
|
||||
filter,
|
||||
fieldOrColumn: viewFieldsByColRef.get(column.origColRef()) ?? column,
|
||||
isFiltered: ko.pureComputed(() => filter() !== '')
|
||||
};
|
||||
});
|
||||
}));
|
||||
|
||||
// List of `filters` that have non-blank active filters.
|
||||
this.activeFilters = Computed.create(this, use => use(this.filters).filter(col => use(col.isFiltered)));
|
||||
|
||||
// Helper metadata item which indicates whether any of the section's fields/columns have unsaved
|
||||
// changes to their filters. (True indicates unsaved changes)
|
||||
this.filterSpecChanged = Computed.create(this, use =>
|
||||
use(use(this.viewFields).getObservable()).some(field => !use(field.activeFilter.isSaved)));
|
||||
this.filterSpecChanged = Computed.create(this, use => {
|
||||
return use(this.filters).some(col => !use(col.filter.isSaved));
|
||||
});
|
||||
|
||||
this.filteredFields = Computed.create(this, use =>
|
||||
use(use(this.viewFields).getObservable()).filter(field => use(field.isFiltered)));
|
||||
|
||||
// Save all filters of fields in the section.
|
||||
// Save all filters of fields/columns in the section.
|
||||
this.saveFilters = () => {
|
||||
return docModel.docData.bundleActions(`Save all filters in ${this.titleDef()}`,
|
||||
async () => { await Promise.all(this.viewFields().all().map(field => field.activeFilter.save())); }
|
||||
async () => {
|
||||
const savedFiltersByColRef = new Map(this._savedFilters().all().map(f => [f.colRef(), f]));
|
||||
const updatedFilters: [number, string][] = []; // Pairs of row ids and filters to update.
|
||||
const removedFilterIds: number[] = []; // Row ids of filters to remove.
|
||||
const newFilters: [number, string][] = []; // Pairs of column refs and filters to add.
|
||||
|
||||
for (const c of this.filters()) {
|
||||
// Skip saved filters (i.e. filters whose local values are unchanged from server).
|
||||
if (c.filter.isSaved()) { continue; }
|
||||
|
||||
const savedFilter = savedFiltersByColRef.get(c.fieldOrColumn.origCol().origColRef());
|
||||
if (!savedFilter) {
|
||||
// Since no saved filter exists, we must add a new record to the filters table.
|
||||
newFilters.push([c.fieldOrColumn.origCol().origColRef(), c.filter()]);
|
||||
} else if (c.filter() === '') {
|
||||
// Mark the saved filter for removal from the filters table.
|
||||
removedFilterIds.push(savedFilter.id());
|
||||
} else {
|
||||
// Mark the saved filter for update in the filters table.
|
||||
updatedFilters.push([savedFilter.id(), c.filter()]);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove records of any deleted filters.
|
||||
if (removedFilterIds.length > 0) {
|
||||
await docModel.filters.sendTableAction(['BulkRemoveRecord', removedFilterIds]);
|
||||
}
|
||||
|
||||
// Update existing filter records with new filter values.
|
||||
if (updatedFilters.length > 0) {
|
||||
await docModel.filters.sendTableAction(['BulkUpdateRecord',
|
||||
updatedFilters.map(([id]) => id),
|
||||
{filter: updatedFilters.map(([, filter]) => filter)}
|
||||
]);
|
||||
}
|
||||
|
||||
// Add new filter records.
|
||||
if (newFilters.length > 0) {
|
||||
await docModel.filters.sendTableAction(['BulkAddRecord',
|
||||
arrayRepeat(newFilters.length, null),
|
||||
{
|
||||
viewSectionRef: arrayRepeat(newFilters.length, this.id()),
|
||||
colRef: newFilters.map(([colRef]) => colRef),
|
||||
filter: newFilters.map(([, filter]) => filter),
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
// Reset client filter state.
|
||||
this.revertFilters();
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// Revert all filters of fields in the section.
|
||||
// Revert all filters of fields/columns in the section.
|
||||
this.revertFilters = () => {
|
||||
this.viewFields().all().forEach(field => { field.activeFilter.revert(); });
|
||||
this._unsavedFilters = new Map();
|
||||
this.filters().forEach(c => { c.filter.revert(); });
|
||||
};
|
||||
|
||||
// Reset all filters of fields in the section to their default (i.e. unset) values.
|
||||
this.clearFilters = () => this.viewFields().all().forEach(field => field.activeFilter(''));
|
||||
// Apply `filter` to the field or column identified by `colRef`.
|
||||
this.setFilter = (colRef: number, filter: string) => {
|
||||
this._unsavedFilters.set(colRef, filter);
|
||||
const filterInfo = this.filters().find(c => c.fieldOrColumn.origCol().origColRef() === colRef);
|
||||
filterInfo?.filter(filter);
|
||||
};
|
||||
|
||||
// Customizable version of the JSON-stringified sort spec. It may diverge from the saved one.
|
||||
this.activeSortJson = modelUtil.customValue(this.sortColRefs);
|
||||
@@ -230,9 +369,7 @@ export function createViewSectionRec(this: ViewSectionRec, docModel: DocModel):
|
||||
// Evaluates to an array of column models, which are not referenced by anything in viewFields.
|
||||
this.hiddenColumns = this.autoDispose(ko.pureComputed(() => {
|
||||
const included = new Set(this.viewFields().all().map((f) => f.column().origColRef()));
|
||||
return this.table().columns().all().filter(function(col) {
|
||||
return !included.has(col.getRowId()) && !col.isHiddenCol();
|
||||
});
|
||||
return this.columns().filter(c => !included.has(c.getRowId()));
|
||||
}));
|
||||
|
||||
this.hasFocus = ko.pureComputed({
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
|
||||
import {allInclusive, ColumnFilter} from 'app/client/models/ColumnFilter';
|
||||
import {ColumnFilterMenuModel, IFilterCount} from 'app/client/models/ColumnFilterMenuModel';
|
||||
import {ViewFieldRec, ViewSectionRec} from 'app/client/models/DocModel';
|
||||
import {ColumnRec, ViewFieldRec, ViewSectionRec} from 'app/client/models/DocModel';
|
||||
import {FilterInfo} from 'app/client/models/entities/ViewSectionRec';
|
||||
import {RowId, RowSource} from 'app/client/models/rowset';
|
||||
import {ColumnFilterFunc, SectionFilter} from 'app/client/models/SectionFilter';
|
||||
import {TableData} from 'app/client/models/TableData';
|
||||
@@ -278,21 +279,22 @@ function formatUniqueCount(values: Array<[CellValue, IFilterCount]>) {
|
||||
/**
|
||||
* Returns content for the newly created columnFilterMenu; for use with setPopupToCreateDom().
|
||||
*/
|
||||
export function createFilterMenu(openCtl: IOpenController, sectionFilter: SectionFilter, field: ViewFieldRec,
|
||||
export function createFilterMenu(openCtl: IOpenController, sectionFilter: SectionFilter, filterInfo: FilterInfo,
|
||||
rowSource: RowSource, tableData: TableData, onClose: () => void = noop) {
|
||||
// Go through all of our shown and hidden rows, and count them up by the values in this column.
|
||||
const columnType = field.column().type.peek();
|
||||
const {keyMapFunc, labelMapFunc} = getMapFuncs(columnType, tableData, field);
|
||||
const activeFilterBar = field.viewSection.peek().activeFilterBar;
|
||||
const fieldOrColumn = filterInfo.fieldOrColumn;
|
||||
const columnType = fieldOrColumn.origCol.peek().type.peek();
|
||||
const {keyMapFunc, labelMapFunc} = getMapFuncs(columnType, tableData, filterInfo.fieldOrColumn);
|
||||
const activeFilterBar = sectionFilter.viewSection.activeFilterBar;
|
||||
|
||||
function getFilterFunc(f: ViewFieldRec, colFilter: ColumnFilterFunc|null) {
|
||||
return f.getRowId() === field.getRowId() ? null : colFilter;
|
||||
function getFilterFunc(col: ViewFieldRec|ColumnRec, colFilter: ColumnFilterFunc|null) {
|
||||
return col.getRowId() === fieldOrColumn.getRowId() ? null : colFilter;
|
||||
}
|
||||
const filterFunc = Computed.create(null, use => sectionFilter.buildFilterFunc(getFilterFunc, use));
|
||||
openCtl.autoDispose(filterFunc);
|
||||
|
||||
const columnFilter = ColumnFilter.create(openCtl, field.activeFilter.peek(), columnType);
|
||||
sectionFilter.setFilterOverride(field.getRowId(), columnFilter); // Will be removed on menu disposal
|
||||
const columnFilter = ColumnFilter.create(openCtl, filterInfo.filter.peek(), columnType);
|
||||
sectionFilter.setFilterOverride(fieldOrColumn.getRowId(), columnFilter); // Will be removed on menu disposal
|
||||
|
||||
const [allRows, hiddenRows] = partition(Array.from(rowSource.getAllRows()), filterFunc.get());
|
||||
const valueCounts: Map<CellValue, {label: string, count: number}> = new Map();
|
||||
@@ -310,12 +312,15 @@ export function createFilterMenu(openCtl: IOpenController, sectionFilter: Sectio
|
||||
doSave: (reset: boolean = false) => {
|
||||
const spec = columnFilter.makeFilterJson();
|
||||
// If filter is moot and filter bar is hidden, let's remove the filter.
|
||||
field.activeFilter((spec === allInclusive && !activeFilterBar.peek()) ? '' : spec);
|
||||
sectionFilter.viewSection.setFilter(
|
||||
fieldOrColumn.origCol().origColRef(),
|
||||
spec === allInclusive && !activeFilterBar.peek() ? '' : spec
|
||||
);
|
||||
if (reset) {
|
||||
sectionFilter.resetTemporaryRows();
|
||||
}
|
||||
},
|
||||
renderValue: getRenderFunc(columnType, field),
|
||||
renderValue: getRenderFunc(columnType, fieldOrColumn),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -330,10 +335,10 @@ export function createFilterMenu(openCtl: IOpenController, sectionFilter: Sectio
|
||||
* Used by ColumnFilterMenu to compute counts of unique cell
|
||||
* values and display them with an appropriate label.
|
||||
*/
|
||||
function getMapFuncs(columnType: string, tableData: TableData, field: ViewFieldRec) {
|
||||
const keyMapFunc = tableData.getRowPropFunc(field.column().colId())!;
|
||||
const labelGetter = tableData.getRowPropFunc(field.displayColModel().colId())!;
|
||||
const formatter = field.createVisibleColFormatter();
|
||||
function getMapFuncs(columnType: string, tableData: TableData, fieldOrColumn: ViewFieldRec|ColumnRec) {
|
||||
const keyMapFunc = tableData.getRowPropFunc(fieldOrColumn.colId())!;
|
||||
const labelGetter = tableData.getRowPropFunc(fieldOrColumn.displayColModel().colId())!;
|
||||
const formatter = fieldOrColumn.createVisibleColFormatter();
|
||||
|
||||
let labelMapFunc: (rowId: number) => string | string[];
|
||||
if (isRefListType(columnType)) {
|
||||
@@ -357,9 +362,9 @@ function getMapFuncs(columnType: string, tableData: TableData, field: ViewFieldR
|
||||
* column types by rendering their values as colored tokens instead of
|
||||
* text.
|
||||
*/
|
||||
function getRenderFunc(columnType: string, field: ViewFieldRec) {
|
||||
function getRenderFunc(columnType: string, fieldOrColumn: ViewFieldRec|ColumnRec) {
|
||||
if (['Choice', 'ChoiceList'].includes(columnType)) {
|
||||
const options = field.column().widgetOptionsJson.peek();
|
||||
const options = fieldOrColumn.widgetOptionsJson.peek();
|
||||
const choiceSet: Set<string> = new Set(options.choices || []);
|
||||
const choiceOptions: ChoiceOptions = options.choiceOptions || {};
|
||||
|
||||
@@ -460,13 +465,14 @@ interface IColumnFilterMenuOptions extends IPopupOptions {
|
||||
}
|
||||
|
||||
// Helper to attach the column filter menu.
|
||||
export function attachColumnFilterMenu(viewSection: ViewSectionRec, field: ViewFieldRec,
|
||||
export function attachColumnFilterMenu(viewSection: ViewSectionRec, filterInfo: FilterInfo,
|
||||
popupOptions: IColumnFilterMenuOptions): DomElementMethod {
|
||||
const options = {...defaultPopupOptions, ...popupOptions};
|
||||
return (elem) => {
|
||||
const instance = viewSection.viewInstance();
|
||||
if (instance && instance.createFilterMenu) { // Should be set if using BaseView
|
||||
setPopupToCreateDom(elem, ctl => instance.createFilterMenu(ctl, field, popupOptions.onCloseContent), options);
|
||||
setPopupToCreateDom(elem, ctl =>
|
||||
instance.createFilterMenu(ctl, filterInfo, popupOptions.onCloseContent), options);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { allInclusive } from "app/client/models/ColumnFilter";
|
||||
import { ViewFieldRec, ViewSectionRec } from "app/client/models/DocModel";
|
||||
import { ColumnRec, ViewFieldRec, ViewSectionRec } from "app/client/models/DocModel";
|
||||
import { FilterInfo } from "app/client/models/entities/ViewSectionRec";
|
||||
import { attachColumnFilterMenu } from "app/client/ui/ColumnFilterMenu";
|
||||
import { cssButton, cssButtonGroup } from "app/client/ui2018/buttons";
|
||||
import { colors, testId } from "app/client/ui2018/cssVars";
|
||||
@@ -9,46 +10,46 @@ import { dom, IDisposableOwner, IDomArgs, styled } from "grainjs";
|
||||
import { IMenuOptions, PopupControl } from "popweasel";
|
||||
|
||||
export function filterBar(_owner: IDisposableOwner, viewSection: ViewSectionRec) {
|
||||
const popupControls = new WeakMap<ViewFieldRec, PopupControl>();
|
||||
const popupControls = new WeakMap<ColumnRec, PopupControl>();
|
||||
return cssFilterBar(
|
||||
testId('filter-bar'),
|
||||
dom.forEach(viewSection.filteredFields, (field) => makeFilterField(viewSection, field, popupControls)),
|
||||
dom.forEach(viewSection.activeFilters, (filterInfo) => makeFilterField(viewSection, filterInfo, popupControls)),
|
||||
makePlusButton(viewSection, popupControls),
|
||||
);
|
||||
}
|
||||
|
||||
function makeFilterField(viewSection: ViewSectionRec, field: ViewFieldRec,
|
||||
popupControls: WeakMap<ViewFieldRec, PopupControl>) {
|
||||
function makeFilterField(viewSection: ViewSectionRec, filterInfo: FilterInfo,
|
||||
popupControls: WeakMap<ColumnRec, PopupControl>) {
|
||||
return cssFilterBarItem(
|
||||
testId('filter-field'),
|
||||
primaryButton(
|
||||
testId('btn'),
|
||||
cssIcon('FilterSimple'),
|
||||
cssMenuTextLabel(dom.text(field.label)),
|
||||
cssBtn.cls('-grayed', field.activeFilter.isSaved),
|
||||
attachColumnFilterMenu(viewSection, field, {
|
||||
cssMenuTextLabel(dom.text(filterInfo.fieldOrColumn.label)),
|
||||
cssBtn.cls('-grayed', filterInfo.filter.isSaved),
|
||||
attachColumnFilterMenu(viewSection, filterInfo, {
|
||||
placement: 'bottom-start', attach: 'body',
|
||||
trigger: ['click', (_el, popupControl) => popupControls.set(field, popupControl)]
|
||||
trigger: ['click', (_el, popupControl) => popupControls.set(filterInfo.fieldOrColumn.origCol(), popupControl)]
|
||||
}),
|
||||
),
|
||||
deleteButton(
|
||||
testId('delete'),
|
||||
cssIcon('CrossSmall'),
|
||||
cssBtn.cls('-grayed', field.activeFilter.isSaved),
|
||||
dom.on('click', () => field.activeFilter('')),
|
||||
cssBtn.cls('-grayed', filterInfo.filter.isSaved),
|
||||
dom.on('click', () => viewSection.setFilter(filterInfo.fieldOrColumn.origCol().origColRef(), '')),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function addFilterMenu(fields: ViewFieldRec[], popupControls: WeakMap<ViewFieldRec, PopupControl>,
|
||||
options?: IMenuOptions) {
|
||||
export function addFilterMenu(filters: FilterInfo[], viewSection: ViewSectionRec,
|
||||
popupControls: WeakMap<ColumnRec, PopupControl>, options?: IMenuOptions) {
|
||||
return (
|
||||
menu((ctl) => [
|
||||
...fields.map((f) => (
|
||||
...filters.map((filterInfo) => (
|
||||
menuItemAsync(
|
||||
() => turnOnAndOpenFilter(f, popupControls),
|
||||
f.label.peek(),
|
||||
dom.cls('disabled', f.isFiltered),
|
||||
() => turnOnAndOpenFilter(filterInfo.fieldOrColumn, viewSection, popupControls),
|
||||
filterInfo.fieldOrColumn.label.peek(),
|
||||
dom.cls('disabled', filterInfo.isFiltered),
|
||||
testId('add-filter-item'),
|
||||
)
|
||||
)),
|
||||
@@ -62,19 +63,20 @@ export function addFilterMenu(fields: ViewFieldRec[], popupControls: WeakMap<Vie
|
||||
);
|
||||
}
|
||||
|
||||
function turnOnAndOpenFilter(f: ViewFieldRec, popupControls: WeakMap<ViewFieldRec, PopupControl>) {
|
||||
f.activeFilter(allInclusive);
|
||||
popupControls.get(f)?.open();
|
||||
function turnOnAndOpenFilter(fieldOrColumn: ViewFieldRec|ColumnRec, viewSection: ViewSectionRec,
|
||||
popupControls: WeakMap<ColumnRec, PopupControl>) {
|
||||
viewSection.setFilter(fieldOrColumn.origCol().origColRef(), allInclusive);
|
||||
popupControls.get(fieldOrColumn.origCol())?.open();
|
||||
}
|
||||
|
||||
function makePlusButton(viewSectionRec: ViewSectionRec, popupControls: WeakMap<ViewFieldRec, PopupControl>) {
|
||||
function makePlusButton(viewSectionRec: ViewSectionRec, popupControls: WeakMap<ColumnRec, PopupControl>) {
|
||||
return dom.domComputed((use) => {
|
||||
const fields = use(use(viewSectionRec.viewFields).getObservable());
|
||||
const anyFilter = fields.find((f) => use(f.isFiltered));
|
||||
const filters = use(viewSectionRec.filters);
|
||||
const anyFilter = use(viewSectionRec.activeFilters).length > 0;
|
||||
return cssPlusButton(
|
||||
cssBtn.cls('-grayed'),
|
||||
cssIcon('Plus'),
|
||||
addFilterMenu(fields, popupControls),
|
||||
addFilterMenu(filters, viewSectionRec, popupControls),
|
||||
anyFilter ? null : cssPlusLabel('Add Filter'),
|
||||
testId('add-filter-btn')
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { reportError } from 'app/client/models/AppModel';
|
||||
import { ColumnRec, DocModel, ViewFieldRec, ViewRec, ViewSectionRec } from 'app/client/models/DocModel';
|
||||
import { ColumnRec, DocModel, ViewRec, ViewSectionRec } from 'app/client/models/DocModel';
|
||||
import { FilterInfo } from 'app/client/models/entities/ViewSectionRec';
|
||||
import { CustomComputed } from 'app/client/models/modelUtil';
|
||||
import { attachColumnFilterMenu } from 'app/client/ui/ColumnFilterMenu';
|
||||
import { addFilterMenu } from 'app/client/ui/FilterBar';
|
||||
@@ -35,8 +36,8 @@ function doRevert(viewSection: ViewSectionRec) {
|
||||
export function viewSectionMenu(owner: IDisposableOwner, docModel: DocModel, viewSection: ViewSectionRec,
|
||||
viewModel: ViewRec, isReadonly: Observable<boolean>) {
|
||||
|
||||
const popupControls = new WeakMap<ViewFieldRec, PopupControl>();
|
||||
const anyFilter = Computed.create(owner, (use) => Boolean(use(viewSection.filteredFields).length));
|
||||
const popupControls = new WeakMap<ColumnRec, PopupControl>();
|
||||
const anyFilter = Computed.create(owner, (use) => Boolean(use(viewSection.activeFilters).length));
|
||||
|
||||
const displaySaveObs: Computed<boolean> = Computed.create(owner, (use) => (
|
||||
use(viewSection.filterSpecChanged)
|
||||
@@ -65,8 +66,8 @@ export function viewSectionMenu(owner: IDisposableOwner, docModel: DocModel, vie
|
||||
return makeSortPanel(viewSection, use(viewSection.activeSortSpec),
|
||||
(row: number) => docModel.columns.getRowModel(row));
|
||||
}),
|
||||
dom.domComputed(viewSection.filteredFields, fields =>
|
||||
makeFilterPanel(viewSection, fields, popupControls, () => ctl.close())),
|
||||
dom.domComputed(viewSection.activeFilters, filters =>
|
||||
makeFilterPanel(viewSection, filters, popupControls, () => ctl.close())),
|
||||
makeAddFilterButton(viewSection, popupControls),
|
||||
makeFilterBarToggle(viewSection.activeFilterBar),
|
||||
dom.domComputed(displaySaveObs, displaySave => [
|
||||
@@ -139,14 +140,13 @@ function makeSortPanel(section: ViewSectionRec, sortSpec: Sort.SortSpec, getColu
|
||||
];
|
||||
}
|
||||
|
||||
export function makeAddFilterButton(viewSectionRec: ViewSectionRec,
|
||||
popupControls: WeakMap<ViewFieldRec, PopupControl>) {
|
||||
export function makeAddFilterButton(viewSectionRec: ViewSectionRec, popupControls: WeakMap<ColumnRec, PopupControl>) {
|
||||
return dom.domComputed((use) => {
|
||||
const fields = use(use(viewSectionRec.viewFields).getObservable());
|
||||
const filters = use(viewSectionRec.filters);
|
||||
return cssMenuText(
|
||||
cssMenuIconWrapper(
|
||||
cssIcon('Plus'),
|
||||
addFilterMenu(fields, popupControls, {
|
||||
addFilterMenu(filters, viewSectionRec, popupControls, {
|
||||
placement: 'bottom-end',
|
||||
// Attach content to triggerElem's parent, which is needed to prevent view section menu to
|
||||
// close when clicking an item of the add filter menu.
|
||||
@@ -179,32 +179,37 @@ export function makeFilterBarToggle(activeFilterBar: CustomComputed<boolean>) {
|
||||
}
|
||||
|
||||
|
||||
function makeFilterPanel(section: ViewSectionRec, filteredFields: ViewFieldRec[],
|
||||
popupControls: WeakMap<ViewFieldRec, PopupControl>,
|
||||
function makeFilterPanel(section: ViewSectionRec, activeFilters: FilterInfo[],
|
||||
popupControls: WeakMap<ColumnRec, PopupControl>,
|
||||
onCloseContent: () => void) {
|
||||
const fields = filteredFields.map(field => {
|
||||
const fieldChanged = Computed.create(null, fromKo(field.activeFilter.isSaved), (_use, isSaved) => !isSaved);
|
||||
const filters = activeFilters.map(filterInfo => {
|
||||
const filterChanged = Computed.create(null, fromKo(filterInfo.filter.isSaved), (_use, isSaved) => !isSaved);
|
||||
return cssMenuText(
|
||||
dom.autoDispose(fieldChanged),
|
||||
cssMenuIconWrapper(
|
||||
cssMenuIconWrapper.cls('-changed', fieldChanged),
|
||||
cssMenuIconWrapper.cls('-changed', filterChanged),
|
||||
cssIcon('FilterSimple'),
|
||||
attachColumnFilterMenu(section, field, {
|
||||
attachColumnFilterMenu(section, filterInfo, {
|
||||
placement: 'bottom-end',
|
||||
trigger: ['click', (_el, popupControl) => popupControls.set(field, popupControl)],
|
||||
trigger: [
|
||||
'click',
|
||||
(_el, popupControl) => popupControls.set(filterInfo.fieldOrColumn.origCol(), popupControl)
|
||||
],
|
||||
onCloseContent,
|
||||
}),
|
||||
testId('filter-icon'),
|
||||
),
|
||||
cssMenuTextLabel(field.label()),
|
||||
cssMenuIconWrapper(cssIcon('Remove', testId('btn-remove-filter')), dom.on('click', () => field.activeFilter(''))),
|
||||
cssMenuTextLabel(filterInfo.fieldOrColumn.label()),
|
||||
cssMenuIconWrapper(cssIcon('Remove',
|
||||
dom.on('click', () => section.setFilter(filterInfo.fieldOrColumn.origCol().origColRef(), ''))),
|
||||
testId('btn-remove-filter')
|
||||
),
|
||||
testId('filter-col')
|
||||
);
|
||||
});
|
||||
|
||||
return [
|
||||
cssMenuInfoHeader('Filtered by', {style: 'margin-top: 4px'}, testId('heading-filtered')),
|
||||
filteredFields.length > 0 ? fields : cssGrayedMenuText('(Not filtered)')
|
||||
activeFilters.length > 0 ? filters : cssGrayedMenuText('(Not filtered)')
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user