mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(core) Allow adding rows to widgets filtered by a link using a formula column
Summary: When a widget `A` is selected by a widget `B` so that `A` is filtered, adding a new row to `A` uses the values in the selected row of `B` and the columns relevant to the linking as default values for the new row. This ensures that the new row matches the current linking filter and remains visible. However this would previously cause a sandbox error when one of the linking columns was a formula column, which doesn't allow setting values. This diff ignores formula columns when picking default values. Since the value of the formula column in the new row typically won't match the linking filter, extra measures are needed to avoid the new row immediately disappearing. Regular filters already have a mechanism for this, but I didn't manage to extend it to also work for linking. Thanks @dsagal for creating `UnionRowSource` (originally in D4017) which is now used as the solution for temporarily exempting rows from both kinds of filtering. While testing, I also came across another bug in linking summary tables that caused incorrect filtering, which I fixed with some changes to `DynamicQuerySet`. Test Plan: Extended an nbrowser test, which both tests for the main change as well as the secondary bugfix. Reviewers: georgegevoian Reviewed By: georgegevoian Subscribers: dsagal Differential Revision: https://phab.getgrist.com/D4135
This commit is contained in:
parent
2f0dbb7d25
commit
bd52665f96
@ -13,6 +13,7 @@ const {DynamicQuerySet} = require('../models/QuerySet');
|
||||
const {SortFunc} = require('app/common/SortFunc');
|
||||
const rowset = require('../models/rowset');
|
||||
const Base = require('./Base');
|
||||
const {getDefaultColValues} = require("./BaseView2");
|
||||
const {Cursor} = require('./Cursor');
|
||||
const FieldBuilder = require('../widgets/FieldBuilder');
|
||||
const commands = require('./commands');
|
||||
@ -21,6 +22,7 @@ const {ClientColumnGetters} = require('app/client/models/ClientColumnGetters');
|
||||
const {reportError, reportSuccess} = require('app/client/models/errors');
|
||||
const {urlState} = require('app/client/models/gristUrlState');
|
||||
const {SectionFilter} = require('app/client/models/SectionFilter');
|
||||
const {UnionRowSource} = require('app/client/models/UnionRowSource');
|
||||
const {copyToClipboard} = require('app/client/lib/clipboardUtils');
|
||||
const {setTestState} = require('app/client/lib/testState');
|
||||
const {ExtraRows} = require('app/client/models/DataTableModelWithDiff');
|
||||
@ -70,17 +72,24 @@ function BaseView(gristDoc, viewSectionModel, options) {
|
||||
this._mainRowSource = rowset.ExtendedRowSource.create(this, this._mainRowSource, extraRowIds);
|
||||
}
|
||||
|
||||
// Rows that should temporarily be visible even if they don't match filters.
|
||||
// This is so that a newly added row doesn't immediately disappear, which would be confusing.
|
||||
this._exemptFromFilterRows = rowset.ExemptFromFilterRowSource.create(this);
|
||||
this._exemptFromFilterRows.subscribeTo(this.tableModel);
|
||||
|
||||
// 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, this.tableModel.tableData);
|
||||
// `sectionFilter` also provides `setFilterOverride()` to allow controlling a filter from a column menu.
|
||||
// Whenever changes are made to filters, exempt rows are reset.
|
||||
this._sectionFilter = SectionFilter.create(
|
||||
this, this.viewSection, this.tableModel.tableData, () => this._exemptFromFilterRows.reset()
|
||||
);
|
||||
this._filteredRowSource = rowset.FilteredRowSource.create(this, this._sectionFilter.sectionFilterFunc.get());
|
||||
this._filteredRowSource.subscribeTo(this._mainRowSource);
|
||||
this.autoDispose(this._sectionFilter.sectionFilterFunc.addListener(filterFunc => {
|
||||
this._filteredRowSource.updateFilter(filterFunc);
|
||||
}));
|
||||
|
||||
this.rowSource = this._filteredRowSource;
|
||||
this.rowSource = UnionRowSource.create(this, [this._filteredRowSource, this._exemptFromFilterRows]);
|
||||
|
||||
// Sorted collection of all rows to show in this view.
|
||||
this.sortedRows = rowset.SortedRowSet.create(this, null, this.tableModel.tableData);
|
||||
@ -96,7 +105,7 @@ function BaseView(gristDoc, viewSectionModel, options) {
|
||||
}, this));
|
||||
|
||||
// Here we are subscribed to the bulk of the data (main table, possibly filtered).
|
||||
this.sortedRows.subscribeTo(this._filteredRowSource);
|
||||
this.sortedRows.subscribeTo(this.rowSource);
|
||||
|
||||
// We create a special one-row RowSource for the "Add new" row, in case we need it.
|
||||
this.newRowSource = rowset.RowSource.create(this);
|
||||
@ -201,6 +210,7 @@ function BaseView(gristDoc, viewSectionModel, options) {
|
||||
this._queryRowSource.makeQuery(linkingFilter.filters, linkingFilter.operations, (err) => {
|
||||
if (this.isDisposed()) { return; }
|
||||
if (err) { reportError(err); }
|
||||
this._exemptFromFilterRows.reset();
|
||||
this.onTableLoaded();
|
||||
});
|
||||
}));
|
||||
@ -442,7 +452,7 @@ BaseView.prototype.insertRow = function(index) {
|
||||
return this.sendTableAction(['AddRecord', null, { 'manualSort': insertPos }])
|
||||
.then(rowId => {
|
||||
if (!this.isDisposed()) {
|
||||
this._sectionFilter.addTemporaryRow(rowId);
|
||||
this._exemptFromFilterRows.addExemptRow(rowId);
|
||||
this.setCursorPos({rowId});
|
||||
}
|
||||
return rowId;
|
||||
@ -450,11 +460,7 @@ BaseView.prototype.insertRow = function(index) {
|
||||
};
|
||||
|
||||
BaseView.prototype._getDefaultColValues = function() {
|
||||
const linkingState = this.viewSection.linkingState.peek();
|
||||
if (!linkingState) {
|
||||
return {};
|
||||
}
|
||||
return linkingState.getDefaultColValues();
|
||||
return getDefaultColValues(this.viewSection);
|
||||
};
|
||||
|
||||
/**
|
||||
@ -562,7 +568,7 @@ BaseView.prototype._saveEditRowField = function(editRowModel, colName, value) {
|
||||
// Once we know the new row's rowId, add it to column filters to make sure it's displayed.
|
||||
.then(rowId => {
|
||||
if (!this.isDisposed()) {
|
||||
this._sectionFilter.addTemporaryRow(rowId);
|
||||
this._exemptFromFilterRows.addExemptRow(rowId);
|
||||
this.setCursorPos({rowId});
|
||||
}
|
||||
return rowId;
|
||||
@ -581,7 +587,7 @@ BaseView.prototype._saveEditRowField = function(editRowModel, colName, value) {
|
||||
// unless the filter is on a Bool column
|
||||
.then((result) => {
|
||||
if (!this.isDisposed() && this.currentColumn().pureType() !== 'Bool') {
|
||||
this._sectionFilter.addTemporaryRow(rowId);
|
||||
this._exemptFromFilterRows.addExemptRow(rowId);
|
||||
}
|
||||
return result;
|
||||
})
|
||||
|
@ -9,7 +9,9 @@ import {UserAction} from 'app/common/DocActions';
|
||||
import {isFullReferencingType} from 'app/common/gristTypes';
|
||||
import {SchemaTypes} from 'app/common/schema';
|
||||
import {BulkColValues} from 'app/plugin/GristData';
|
||||
import {ViewSectionRec} from "app/client/models/entities/ViewSectionRec";
|
||||
import omit = require('lodash/omit');
|
||||
import pick = require('lodash/pick');
|
||||
|
||||
/**
|
||||
* Given a 2-d paste column-oriented paste data and target cols, transform the data to omit
|
||||
@ -87,3 +89,18 @@ export async function parsePasteForView(
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default values for a new record so that it continues to satisfy the current linking filters.
|
||||
* Exclude formula columns since we can't set their values.
|
||||
*/
|
||||
export function getDefaultColValues(viewSection: ViewSectionRec): Record<string, any> {
|
||||
const linkingState = viewSection.linkingState.peek();
|
||||
if (!linkingState) {
|
||||
return {};
|
||||
}
|
||||
const dataColIds = viewSection.columns.peek()
|
||||
.filter(col => !col.isRealFormula.peek())
|
||||
.map(col => col.colId.peek());
|
||||
return pick(linkingState.getDefaultColValues(), dataColIds);
|
||||
}
|
||||
|
@ -117,6 +117,22 @@ export class QuerySetManager extends Disposable {
|
||||
* RowSource.
|
||||
*/
|
||||
export class DynamicQuerySet extends RowSource {
|
||||
/**
|
||||
* Replace the query represented by this DynamicQuerySet. If multiple makeQuery() calls are made
|
||||
* quickly (while waiting for the server), cb() may only be called for the latest one.
|
||||
*
|
||||
* If there is an error fetching data, cb(err) will be called with that error. The second
|
||||
* argument to cb() is true if any data was changed, and false if not. Note that for a series of
|
||||
* makeQuery() calls, cb() is always called at least once, and always asynchronously.
|
||||
*
|
||||
* It's possible for this to be called very quickly in succession,
|
||||
* e.g. when selecting another row of a linked summary table grouped by multiple columns,
|
||||
* as an observable gets triggered for each one.
|
||||
* We only want to keep the last call, and _updateQuerySetDebounced may not be called
|
||||
* in the correct order, so we first debounce here.
|
||||
*/
|
||||
public readonly makeQuery = debounce(tbind(this._makeQuery, this), 0);
|
||||
|
||||
// Holds a reference to the currently active QuerySet.
|
||||
private _holder = Holder.create<IRefCountSub<QuerySet>>(this);
|
||||
|
||||
@ -147,17 +163,13 @@ export class DynamicQuerySet extends RowSource {
|
||||
return this._querySet ? this._querySet.isTruncated : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace the query represented by this DynamicQuerySet. If multiple makeQuery() calls are made
|
||||
* quickly (while waiting for the server), cb() may only be called for the latest one.
|
||||
*
|
||||
* If there is an error fetching data, cb(err) will be called with that error. The second
|
||||
* argument to cb() is true if any data was changed, and false if not. Note that for a series of
|
||||
* makeQuery() calls, cb() is always called at least once, and always asynchronously.
|
||||
*/
|
||||
public makeQuery(filters: {[colId: string]: any[]},
|
||||
operations: {[colId: string]: QueryOperation},
|
||||
cb: (err: Error|null, changed: boolean) => void): void {
|
||||
private _makeQuery(filters: { [colId: string]: any[] },
|
||||
operations: {[colId: string]: QueryOperation},
|
||||
cb: (err: Error|null, changed: boolean) => void): void {
|
||||
if (this.isDisposed()) {
|
||||
cb(new Error('Disposed'), false);
|
||||
return;
|
||||
}
|
||||
const query: ClientQuery = {tableId: this._tableModel.tableData.tableId, filters, operations};
|
||||
const newQuerySet = this._querySetManager.useQuerySet(this._holder, query);
|
||||
|
||||
|
@ -4,7 +4,7 @@ import {TableData} from 'app/client/models/TableData';
|
||||
import {buildColFilter, ColumnFilterFunc} from 'app/common/ColumnFilterFunc';
|
||||
import {buildRowFilter, RowFilterFunc, RowValueFunc } from 'app/common/RowFilterFunc';
|
||||
import {UIRowId} from 'app/plugin/GristAPI';
|
||||
import {Computed, Disposable, MutableObsArray, obsArray, Observable, UseCB} from 'grainjs';
|
||||
import {Computed, Disposable, Observable, UseCB} from 'grainjs';
|
||||
|
||||
export type {ColumnFilterFunc};
|
||||
|
||||
@ -21,20 +21,21 @@ type ColFilterCB = (fieldOrColumn: ViewFieldRec|ColumnRec, colFilter: ColumnFilt
|
||||
* subscribe to in order to update their FilteredRowSource.
|
||||
*
|
||||
* Additionally, `setFilterOverride()` provides a way to override the current filter for a given colRef,
|
||||
* to reflect the changes in an open filter dialog. Also, `addTemporaryRow()` allows to add a rowId
|
||||
* that should be present regardless of filters. These rows are removed automatically when an update to the filter
|
||||
* results in their being displayed (obviating the need to maintain their rowId explicitly).
|
||||
* to reflect the changes in an open filter dialog.
|
||||
*/
|
||||
export class SectionFilter extends Disposable {
|
||||
public readonly sectionFilterFunc: Observable<RowFilterFunc<UIRowId>>;
|
||||
|
||||
private _openFilterOverride: Observable<OpenColumnFilter|null> = Observable.create(this, null);
|
||||
private _tempRows: MutableObsArray<UIRowId> = obsArray();
|
||||
|
||||
constructor(public viewSection: ViewSectionRec, private _tableData: TableData) {
|
||||
constructor(
|
||||
public viewSection: ViewSectionRec,
|
||||
private _tableData: TableData,
|
||||
private _resetExemptRows: () => void,
|
||||
) {
|
||||
super();
|
||||
|
||||
const columnFilterFunc = Computed.create(this, this._openFilterOverride, (use, openFilter) => {
|
||||
this.sectionFilterFunc = Computed.create(this, this._openFilterOverride, (use, openFilter) => {
|
||||
const openFilterFilterFunc = openFilter && use(openFilter.colFilter.filterFunc);
|
||||
function getFilterFunc(fieldOrColumn: ViewFieldRec|ColumnRec, colFilter: ColumnFilterFunc|null) {
|
||||
if (openFilter?.colRef === fieldOrColumn.origCol().getRowId()) {
|
||||
@ -42,16 +43,8 @@ export class SectionFilter extends Disposable {
|
||||
}
|
||||
return colFilter;
|
||||
}
|
||||
return this._buildPlainFilterFunc(getFilterFunc, use);
|
||||
return this.buildFilterFunc(getFilterFunc, use);
|
||||
});
|
||||
|
||||
this.sectionFilterFunc = Computed.create(this, columnFilterFunc, this._tempRows,
|
||||
(_use, filterFunc, tempRows) => this._addRowsToFilter(filterFunc, tempRows));
|
||||
|
||||
// Prune temporary rowIds that are no longer being filtered out.
|
||||
this.autoDispose(columnFilterFunc.addListener(f => {
|
||||
this._tempRows.set(this._tempRows.get().filter(rowId => !f(rowId)));
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -68,37 +61,14 @@ export class SectionFilter extends Disposable {
|
||||
});
|
||||
}
|
||||
|
||||
public addTemporaryRow(rowId: number) {
|
||||
// Only add the rowId if it would otherwise be filtered out
|
||||
if (!this.sectionFilterFunc.get()(rowId)) {
|
||||
this._tempRows.push(rowId);
|
||||
}
|
||||
}
|
||||
|
||||
public resetTemporaryRows() {
|
||||
this._tempRows.set([]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* `getFilterFunc(column, colFilter)` to customize the filter func for each column. It calls
|
||||
* `getFilterFunc` right away.
|
||||
* This also immediately resets rows that were temporarily exempted from filtering.
|
||||
*/
|
||||
public buildFilterFunc(getFilterFunc: ColFilterCB, use: UseCB) {
|
||||
return this._addRowsToFilter(this._buildPlainFilterFunc(getFilterFunc, use), this._tempRows.get());
|
||||
}
|
||||
|
||||
private _addRowsToFilter(filterFunc: RowFilterFunc<UIRowId>, rows: UIRowId[]) {
|
||||
return (rowId: UIRowId) => rows.includes(rowId) || (typeof rowId !== 'number') || filterFunc(rowId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal that helps build a filter function that combines the filter function of all
|
||||
* 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<UIRowId> {
|
||||
this._resetExemptRows();
|
||||
const filters = use(this.viewSection.filters);
|
||||
const funcs: Array<RowFilterFunc<UIRowId> | null> = filters.map(({filter, fieldOrColumn}) => {
|
||||
const colFilter = buildColFilter(use(filter), use(use(fieldOrColumn.origCol).type));
|
||||
@ -111,6 +81,6 @@ export class SectionFilter extends Disposable {
|
||||
return buildRowFilter(getter as RowValueFunc<UIRowId>, filterFunc);
|
||||
}).filter(f => f !== null); // Filter out columns that don't have a filter
|
||||
|
||||
return (rowId: UIRowId) => funcs.every(f => Boolean(f && f(rowId)));
|
||||
return (rowId: UIRowId) => rowId === 'new' || funcs.every(f => Boolean(f && f(rowId)));
|
||||
}
|
||||
}
|
||||
|
59
app/client/models/UnionRowSource.ts
Normal file
59
app/client/models/UnionRowSource.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import {RowList, RowListener, RowSource} from 'app/client/models/rowset';
|
||||
import {UIRowId} from "app/plugin/GristAPI";
|
||||
|
||||
export class UnionRowSource extends RowListener implements RowSource {
|
||||
protected _allRows = new Map<UIRowId, Set<RowSource>>();
|
||||
|
||||
constructor(parentRowSources: RowSource[]) {
|
||||
super();
|
||||
for (const p of parentRowSources) {
|
||||
this.subscribeTo(p);
|
||||
}
|
||||
}
|
||||
|
||||
public getAllRows(): RowList {
|
||||
return this._allRows.keys();
|
||||
}
|
||||
|
||||
public getNumRows(): number {
|
||||
return this._allRows.size;
|
||||
}
|
||||
|
||||
public onAddRows(rows: RowList, rowSource: RowSource) {
|
||||
const outputRows = [];
|
||||
for (const r of rows) {
|
||||
let sources = this._allRows.get(r);
|
||||
if (!sources) {
|
||||
sources = new Set();
|
||||
this._allRows.set(r, sources);
|
||||
outputRows.push(r);
|
||||
}
|
||||
sources.add(rowSource);
|
||||
}
|
||||
if (outputRows.length > 0) {
|
||||
this.trigger('rowChange', 'add', outputRows);
|
||||
}
|
||||
}
|
||||
|
||||
public onRemoveRows(rows: RowList, rowSource: RowSource) {
|
||||
const outputRows = [];
|
||||
for (const r of rows) {
|
||||
const sources = this._allRows.get(r);
|
||||
if (!sources) {
|
||||
continue;
|
||||
}
|
||||
sources.delete(rowSource);
|
||||
if (sources.size === 0) {
|
||||
outputRows.push(r);
|
||||
this._allRows.delete(r);
|
||||
}
|
||||
}
|
||||
if (outputRows.length > 0) {
|
||||
this.trigger('rowChange', 'remove', outputRows);
|
||||
}
|
||||
}
|
||||
|
||||
public onUpdateRows(rows: RowList) {
|
||||
this.trigger('rowChange', 'update', rows);
|
||||
}
|
||||
}
|
@ -82,10 +82,10 @@ export class RowListener extends DisposableWithEvents {
|
||||
* Subscribes to the given rowSource and adds the rows currently in it.
|
||||
*/
|
||||
public subscribeTo(rowSource: RowSource): void {
|
||||
this.onAddRows(rowSource.getAllRows());
|
||||
this.onAddRows(rowSource.getAllRows(), rowSource);
|
||||
this.listenTo(rowSource, 'rowChange', (changeType: ChangeType, rows: RowList) => {
|
||||
const method: ChangeMethod = _changeTypes[changeType];
|
||||
this[method](rows);
|
||||
this[method](rows, rowSource);
|
||||
});
|
||||
this.listenTo(rowSource, 'rowNotify', this.onRowNotify);
|
||||
}
|
||||
@ -103,17 +103,17 @@ export class RowListener extends DisposableWithEvents {
|
||||
/**
|
||||
* Process row additions. To be implemented by derived classes.
|
||||
*/
|
||||
protected onAddRows(rows: RowList) { /* no-op */ }
|
||||
protected onAddRows(rows: RowList, rowSource?: RowSource) { /* no-op */ }
|
||||
|
||||
/**
|
||||
* Process row removals. To be implemented by derived classes.
|
||||
*/
|
||||
protected onRemoveRows(rows: RowList) { /* no-op */ }
|
||||
protected onRemoveRows(rows: RowList, rowSource?: RowSource) { /* no-op */ }
|
||||
|
||||
/**
|
||||
* Process row updates. To be implemented by derived classes.
|
||||
*/
|
||||
protected onUpdateRows(rows: RowList) { /* no-op */ }
|
||||
protected onUpdateRows(rows: RowList, rowSource?: RowSource) { /* no-op */ }
|
||||
|
||||
/**
|
||||
* Derived classes may override this event to handle row notifications. By default, it re-triggers
|
||||
@ -128,15 +128,6 @@ export class RowListener extends DisposableWithEvents {
|
||||
// MappedRowSource
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* A trivial RowSource returning a fixed list of rows.
|
||||
*/
|
||||
export abstract class ArrayRowSource extends RowSource {
|
||||
constructor(private _rows: UIRowId[]) { super(); }
|
||||
public getAllRows(): RowList { return this._rows; }
|
||||
public getNumRows(): number { return this._rows.length; }
|
||||
}
|
||||
|
||||
/**
|
||||
* MappedRowSource wraps any other RowSource, and passes through all rows, replacing each row
|
||||
* identifier with the result of mapperFunc(row) call.
|
||||
@ -773,3 +764,42 @@ function _allRowsSorted<T>(array: T[], allRows: Set<T>, sortedRows: Iterable<T>,
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Track rows that should temporarily be visible even if they don't match filters.
|
||||
* This is so that a newly added row doesn't immediately disappear, which would be confusing.
|
||||
* This doesn't have much to do with BaseFilteredRowSource, it's just reusing some implementation.
|
||||
*/
|
||||
export class ExemptFromFilterRowSource extends BaseFilteredRowSource {
|
||||
public constructor() {
|
||||
super(() => false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Call this when one or more new rows are added to keep them temporarily visible.
|
||||
*/
|
||||
public addExemptRows(rows: RowList) {
|
||||
const newRows = [];
|
||||
for (const r of rows) {
|
||||
if (!this._matchingRows.has(r)) {
|
||||
this._matchingRows.add(r);
|
||||
newRows.push(r);
|
||||
}
|
||||
}
|
||||
if (newRows.length > 0) {
|
||||
this.trigger('rowChange', 'add', newRows);
|
||||
}
|
||||
}
|
||||
|
||||
public addExemptRow(rowId: number) {
|
||||
this.addExemptRows([rowId]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Call this when linking or filters change to clear out the temporary rows.
|
||||
*/
|
||||
public reset() {
|
||||
this.onRemoveRows(this.getAllRows());
|
||||
}
|
||||
}
|
||||
|
@ -54,7 +54,7 @@ export interface IFilterMenuOptions {
|
||||
rangeInputOptions?: IRangeInputOptions;
|
||||
showAllFiltersButton?: boolean;
|
||||
doCancel(): void;
|
||||
doSave(reset: boolean): void;
|
||||
doSave(): void;
|
||||
renderValue(key: CellValue, value: IFilterCount): DomElementArg;
|
||||
onClose(): void;
|
||||
valueParser?(val: string): any;
|
||||
@ -104,7 +104,6 @@ export function columnFilterMenu(owner: IDisposableOwner, opts: IFilterMenuOptio
|
||||
|
||||
let searchInput: HTMLInputElement;
|
||||
let cancel = false;
|
||||
let reset = false;
|
||||
|
||||
const filterMenu: HTMLElement = cssMenu(
|
||||
{ tabindex: '-1' }, // Allow menu to be focused
|
||||
@ -128,7 +127,7 @@ export function columnFilterMenu(owner: IDisposableOwner, opts: IFilterMenuOptio
|
||||
dom.cls(menuCssClass),
|
||||
dom.autoDispose(filterListener),
|
||||
// Save or cancel on disposal, which should always happen as part of closing.
|
||||
dom.onDispose(() => cancel ? doCancel() : doSave(reset)),
|
||||
dom.onDispose(() => cancel ? doCancel() : doSave()),
|
||||
dom.onKeyDown({
|
||||
Enter: () => onClose(),
|
||||
Escape: () => onClose(),
|
||||
@ -327,7 +326,6 @@ export function columnFilterMenu(owner: IDisposableOwner, opts: IFilterMenuOptio
|
||||
dom('div',
|
||||
cssPrimaryButton('Close', testId('apply-btn'),
|
||||
dom.on('click', () => {
|
||||
reset = true;
|
||||
onClose();
|
||||
}),
|
||||
),
|
||||
@ -696,16 +694,13 @@ export function createFilterMenu(params: ICreateFilterMenuParams) {
|
||||
model,
|
||||
valueCounts,
|
||||
onClose: () => { openCtl.close(); onClose(); },
|
||||
doSave: (reset: boolean = false) => {
|
||||
doSave: () => {
|
||||
const spec = columnFilter.makeFilterJson();
|
||||
const {viewSection} = sectionFilter;
|
||||
viewSection.setFilter(
|
||||
fieldOrColumn.origCol().origColRef(),
|
||||
{filter: spec}
|
||||
);
|
||||
if (reset) {
|
||||
sectionFilter.resetTemporaryRows();
|
||||
}
|
||||
|
||||
// Check if the save was for a new filter, and if that new filter was pinned. If it was, and
|
||||
// it is the second pinned filter in the section, trigger a tip that explains how multiple
|
||||
|
@ -7,6 +7,7 @@ import {dom} from 'grainjs';
|
||||
|
||||
export function buildErrorDom(row: DataRowModel, field: ViewFieldRec) {
|
||||
const value = row.cells[field.colId.peek()];
|
||||
if (value === undefined) { return null; } // Work around JS errors during field removal.
|
||||
const options = field.widgetOptionsJson;
|
||||
// The "invalid" class sets the pink background, as long as the error text is non-empty.
|
||||
return dom('div.field_clip.invalid',
|
||||
|
@ -685,6 +685,7 @@ export class FieldBuilder extends Disposable {
|
||||
kd.scope(widgetObs, (widget: NewAbstractWidget) => {
|
||||
if (this.isDisposed()) { return null; } // Work around JS errors during field removal.
|
||||
const cellDom = widget ? widget.buildDom(row) : buildErrorDom(row, this.field);
|
||||
if (cellDom === null) { return null; }
|
||||
return dom(cellDom, kd.toggleClass('has_cursor', isActive),
|
||||
kd.toggleClass('field-error-from-style', errorInStyle),
|
||||
kd.toggleClass('font-bold', fontBold),
|
||||
|
@ -15,15 +15,15 @@ describe('rowset', function() {
|
||||
sinon.spy(lis, "onUpdateRows");
|
||||
|
||||
lis.subscribeTo(src);
|
||||
assert.deepEqual(lis.onAddRows.args, [[[1, 2, 3]]]);
|
||||
assert.deepEqual(lis.onAddRows.args, [[[1, 2, 3], src]]);
|
||||
lis.onAddRows.resetHistory();
|
||||
|
||||
src.trigger('rowChange', 'add', [5, 6]);
|
||||
src.trigger('rowChange', 'remove', [6, 1]);
|
||||
src.trigger('rowChange', 'update', [3, 5]);
|
||||
assert.deepEqual(lis.onAddRows.args, [[[5, 6]]]);
|
||||
assert.deepEqual(lis.onRemoveRows.args, [[[6, 1]]]);
|
||||
assert.deepEqual(lis.onUpdateRows.args, [[[3, 5]]]);
|
||||
assert.deepEqual(lis.onAddRows.args, [[[5, 6], src]]);
|
||||
assert.deepEqual(lis.onRemoveRows.args, [[[6, 1], src]]);
|
||||
assert.deepEqual(lis.onUpdateRows.args, [[[3, 5], src]]);
|
||||
});
|
||||
|
||||
it('should support subscribing to multiple sources', function() {
|
||||
@ -40,18 +40,18 @@ describe('rowset', function() {
|
||||
|
||||
lis.subscribeTo(src1);
|
||||
lis.subscribeTo(src2);
|
||||
assert.deepEqual(lis.onAddRows.args, [[[1, 2, 3]], [["a", "b", "c"]]]);
|
||||
assert.deepEqual(lis.onAddRows.args, [[[1, 2, 3], src1], [["a", "b", "c"], src2]]);
|
||||
|
||||
src1.trigger('rowChange', 'update', [2, 3]);
|
||||
src2.trigger('rowChange', 'remove', ["b"]);
|
||||
assert.deepEqual(lis.onUpdateRows.args, [[[2, 3]]]);
|
||||
assert.deepEqual(lis.onRemoveRows.args, [[["b"]]]);
|
||||
assert.deepEqual(lis.onUpdateRows.args, [[[2, 3], src1]]);
|
||||
assert.deepEqual(lis.onRemoveRows.args, [[["b"], src2]]);
|
||||
|
||||
lis.onAddRows.resetHistory();
|
||||
lis.unsubscribeFrom(src1);
|
||||
src1.trigger('rowChange', 'add', [4]);
|
||||
src2.trigger('rowChange', 'add', ["d"]);
|
||||
assert.deepEqual(lis.onAddRows.args, [[["d"]]]);
|
||||
assert.deepEqual(lis.onAddRows.args, [[["d"], src2]]);
|
||||
});
|
||||
});
|
||||
|
||||
|
BIN
test/fixtures/docs/SelectBySummary.grist
vendored
BIN
test/fixtures/docs/SelectBySummary.grist
vendored
Binary file not shown.
@ -106,10 +106,9 @@ describe('SelectByRefList', function() {
|
||||
],
|
||||
];
|
||||
// LINKTARGET is being filtered by the `id` column
|
||||
// There's no column to set a default value for that would help
|
||||
// The newly added row disappears immediately
|
||||
// There's no column to set a default value for.
|
||||
// TODO should we be appending the new row ID to the reflist in the source table?
|
||||
newRow = ['', '', ''];
|
||||
newRow = ['99', '', ''];
|
||||
await checkSelectingRecords('REFLISTS • LinkTarget_reflist', sourceData, newRow);
|
||||
|
||||
// Similar to the above but indirect. We connect LINKTARGET.ref and REFLISTS.reflist,
|
||||
|
@ -118,17 +118,60 @@ describe('SelectBySummary', function() {
|
||||
// so those values will be used as defaults in the source table
|
||||
await gu.getCell({section: 'TABLE1 [by onetwo, choices]', col: 'rownum', rowNum: 2}).click();
|
||||
|
||||
// Create a new record with rownum=99
|
||||
// Create new records with rownum = 99 and 100
|
||||
await gu.getCell({section: 'TABLE1', col: 'rownum', rowNum: 3}).click();
|
||||
await gu.enterCell('99');
|
||||
await gu.enterCell('100');
|
||||
|
||||
assert.deepEqual(
|
||||
await gu.getVisibleGridCells({
|
||||
section: 'TABLE1',
|
||||
cols: ['onetwo', 'choices', 'rownum'],
|
||||
rowNums: [3],
|
||||
rowNums: [1, 2, 3, 4, 5],
|
||||
}),
|
||||
['2', 'a', '99'],
|
||||
[
|
||||
'2', 'a', '2',
|
||||
'2', 'a\nb', '6',
|
||||
// The two rows we just added.
|
||||
// The filter link sets the default value 'a'.
|
||||
// It can't set a default value for 'onetwo' because that's a formula column.
|
||||
// This first row doesn't match the filter link, but it still shows temporarily.
|
||||
'1', 'a', '99',
|
||||
'2', 'a', '100',
|
||||
'', '', '', // new row
|
||||
],
|
||||
);
|
||||
|
||||
// Select a different record in the summary table, sanity check the linked table.
|
||||
await gu.getCell({section: 'TABLE1 [by onetwo, choices]', col: 'rownum', rowNum: 3}).click();
|
||||
assert.deepEqual(
|
||||
await gu.getVisibleGridCells({
|
||||
section: 'TABLE1',
|
||||
cols: ['onetwo', 'choices', 'rownum'],
|
||||
rowNums: [1, 2, 3],
|
||||
}),
|
||||
[
|
||||
'1', 'b', '3',
|
||||
'1', 'a\nb', '5',
|
||||
'', '', '', // new row
|
||||
],
|
||||
);
|
||||
|
||||
// Now go back to the previously selected summary table row.
|
||||
await gu.getCell({section: 'TABLE1 [by onetwo, choices]', col: 'rownum', rowNum: 2}).click();
|
||||
assert.deepEqual(
|
||||
await gu.getVisibleGridCells({
|
||||
section: 'TABLE1',
|
||||
cols: ['onetwo', 'choices', 'rownum'],
|
||||
rowNums: [1, 2, 3, 4],
|
||||
}),
|
||||
[
|
||||
'2', 'a', '2',
|
||||
'2', 'a\nb', '6',
|
||||
// The row ['1', 'a', '99'] is now filtered out as normal.
|
||||
'2', 'a', '100',
|
||||
'', '', '', // new row
|
||||
],
|
||||
);
|
||||
})
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user