mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(core) Fixing cursor position for filtered linked section.
Summary: In a selector table, when a selected row is filtered out of view, linked widgets should update based on the newly selected row. There were a few bugs that contributed to this wrong behavior: - Gridview wasn't subscribing to the current row id, and the row with id 'new' was being converted to the first row - Cursor was keeping track of the currently selected row id, it was hiding a problem behind the proper rowIndex - Undo/redo somehow leveraged the wrong rowId from the cursor during the position restore. The `No data` text was also changed to be more meaningful. Test Plan: Added and updated. Reviewers: georgegevoian Reviewed By: georgegevoian Differential Revision: https://phab.getgrist.com/D3937
This commit is contained in:
parent
9a80f4bdf5
commit
75d979abdb
@ -78,12 +78,13 @@ export class ActionLog extends dispose.Disposable implements IDomComponent {
|
|||||||
this._displayStack = koArray<ActionGroupWithState>();
|
this._displayStack = koArray<ActionGroupWithState>();
|
||||||
|
|
||||||
// Computed for the tableId of the table currently being viewed.
|
// Computed for the tableId of the table currently being viewed.
|
||||||
if (!this._gristDoc) {
|
this._selectedTableId = this.autoDispose(ko.computed(() => {
|
||||||
this._selectedTableId = this.autoDispose(ko.computed(() => ""));
|
if (!this._gristDoc || this._gristDoc.viewModel.isDisposed()) { return ""; }
|
||||||
} else {
|
const section = this._gristDoc.viewModel.activeSection();
|
||||||
this._selectedTableId = this.autoDispose(ko.computed(
|
if (!section || section.isDisposed()) { return ""; }
|
||||||
() => this._gristDoc!.viewModel.activeSection().table().tableId()));
|
const table = section.table();
|
||||||
}
|
return table && !table.isDisposed() ? table.tableId() : "";
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
public buildDom() {
|
public buildDom() {
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import type {ViewFieldRec} from 'app/client/models/entities/ViewFieldRec';
|
import type {ViewFieldRec} from 'app/client/models/entities/ViewFieldRec';
|
||||||
import type {CellValue} from 'app/common/DocActions';
|
import type {CellValue} from 'app/common/DocActions';
|
||||||
import type {TableData} from 'app/common/TableData';
|
import type {TableData, UIRowId} from 'app/common/TableData';
|
||||||
import type {UIRowId} from 'app/common/UIRowId';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The CopySelection class is an abstraction for a subset of currently selected cells.
|
* The CopySelection class is an abstraction for a subset of currently selected cells.
|
||||||
|
@ -8,12 +8,12 @@ import BaseView from 'app/client/components/BaseView';
|
|||||||
import * as commands from 'app/client/components/commands';
|
import * as commands from 'app/client/components/commands';
|
||||||
import BaseRowModel from 'app/client/models/BaseRowModel';
|
import BaseRowModel from 'app/client/models/BaseRowModel';
|
||||||
import {LazyArrayModel} from 'app/client/models/DataTableModel';
|
import {LazyArrayModel} from 'app/client/models/DataTableModel';
|
||||||
import type {RowId} from 'app/client/models/rowset';
|
import type {UIRowId} from 'app/common/TableData';
|
||||||
import {Disposable} from 'grainjs';
|
import {Disposable} from 'grainjs';
|
||||||
import * as ko from 'knockout';
|
import * as ko from 'knockout';
|
||||||
|
|
||||||
export interface CursorPos {
|
export interface CursorPos {
|
||||||
rowId?: RowId;
|
rowId?: UIRowId;
|
||||||
rowIndex?: number;
|
rowIndex?: number;
|
||||||
fieldIndex?: number;
|
fieldIndex?: number;
|
||||||
sectionId?: number;
|
sectionId?: number;
|
||||||
@ -60,7 +60,7 @@ export class Cursor extends Disposable {
|
|||||||
public rowIndex: ko.Computed<number|null>; // May be null when there are no rows.
|
public rowIndex: ko.Computed<number|null>; // May be null when there are no rows.
|
||||||
public fieldIndex: ko.Observable<number>;
|
public fieldIndex: ko.Observable<number>;
|
||||||
|
|
||||||
private _rowId: ko.Observable<RowId|null>; // May be null when there are no rows.
|
private _rowId: ko.Observable<UIRowId|null>; // May be null when there are no rows.
|
||||||
|
|
||||||
// The cursor's _rowId property is always fixed across data changes. When isLive is true,
|
// The cursor's _rowId property is always fixed across data changes. When isLive is true,
|
||||||
// the rowIndex of the cursor is recalculated to match _rowId. When false, they will
|
// the rowIndex of the cursor is recalculated to match _rowId. When false, they will
|
||||||
@ -68,13 +68,15 @@ export class Cursor extends Disposable {
|
|||||||
private _isLive: ko.Observable<boolean> = ko.observable(true);
|
private _isLive: ko.Observable<boolean> = ko.observable(true);
|
||||||
private _sectionId: ko.Computed<number>;
|
private _sectionId: ko.Computed<number>;
|
||||||
|
|
||||||
|
private _properRowId: ko.Computed<UIRowId|null>;
|
||||||
|
|
||||||
constructor(baseView: BaseView, optCursorPos?: CursorPos) {
|
constructor(baseView: BaseView, optCursorPos?: CursorPos) {
|
||||||
super();
|
super();
|
||||||
optCursorPos = optCursorPos || {};
|
optCursorPos = optCursorPos || {};
|
||||||
this.viewData = baseView.viewData;
|
this.viewData = baseView.viewData;
|
||||||
|
|
||||||
this._sectionId = this.autoDispose(ko.computed(() => baseView.viewSection.id()));
|
this._sectionId = this.autoDispose(ko.computed(() => baseView.viewSection.id()));
|
||||||
this._rowId = ko.observable<RowId|null>(optCursorPos.rowId || 0);
|
this._rowId = ko.observable<UIRowId|null>(optCursorPos.rowId || 0);
|
||||||
this.rowIndex = this.autoDispose(ko.computed({
|
this.rowIndex = this.autoDispose(ko.computed({
|
||||||
read: () => {
|
read: () => {
|
||||||
if (!this._isLive()) { return this.rowIndex.peek(); }
|
if (!this._isLive()) { return this.rowIndex.peek(); }
|
||||||
@ -82,7 +84,7 @@ export class Cursor extends Disposable {
|
|||||||
return rowId == null ? null : this.viewData.clampIndex(this.viewData.getRowIndexWithSub(rowId));
|
return rowId == null ? null : this.viewData.clampIndex(this.viewData.getRowIndexWithSub(rowId));
|
||||||
},
|
},
|
||||||
write: (index) => {
|
write: (index) => {
|
||||||
const rowIndex = this.viewData.clampIndex(index!);
|
const rowIndex = index === null ? null : this.viewData.clampIndex(index);
|
||||||
this._rowId(rowIndex == null ? null : this.viewData.getRowId(rowIndex));
|
this._rowId(rowIndex == null ? null : this.viewData.getRowId(rowIndex));
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
@ -90,8 +92,16 @@ export class Cursor extends Disposable {
|
|||||||
this.fieldIndex = baseView.viewSection.viewFields().makeLiveIndex(optCursorPos.fieldIndex || 0);
|
this.fieldIndex = baseView.viewSection.viewFields().makeLiveIndex(optCursorPos.fieldIndex || 0);
|
||||||
this.autoDispose(commands.createGroup(Cursor.editorCommands, this, baseView.viewSection.hasFocus));
|
this.autoDispose(commands.createGroup(Cursor.editorCommands, this, baseView.viewSection.hasFocus));
|
||||||
|
|
||||||
// Update the section's activeRowId when the cursor's rowId changes.
|
// RowId might diverge from the one stored in _rowId when the data changes (it is filtered out). So here
|
||||||
this.autoDispose(this._rowId.subscribe((rowId) => baseView.viewSection.activeRowId(rowId)));
|
// we will calculate rowId based on rowIndex (so in reverse order), to have a proper value.
|
||||||
|
this._properRowId = this.autoDispose(ko.computed(() => {
|
||||||
|
const rowIndex = this.rowIndex();
|
||||||
|
const rowId = rowIndex === null ? null : this.viewData.getRowId(rowIndex);
|
||||||
|
return rowId;
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Update the section's activeRowId when the cursor's rowIndex is changed.
|
||||||
|
this.autoDispose(this._properRowId.subscribe((rowId) => baseView.viewSection.activeRowId(rowId)));
|
||||||
|
|
||||||
// On dispose, save the current cursor position to the section model.
|
// On dispose, save the current cursor position to the section model.
|
||||||
this.onDispose(() => { baseView.viewSection.lastCursorPos = this.getCursorPos(); });
|
this.onDispose(() => { baseView.viewSection.lastCursorPos = this.getCursorPos(); });
|
||||||
@ -103,7 +113,7 @@ export class Cursor extends Disposable {
|
|||||||
// Returns the cursor position with rowId, rowIndex, and fieldIndex.
|
// Returns the cursor position with rowId, rowIndex, and fieldIndex.
|
||||||
public getCursorPos(): CursorPos {
|
public getCursorPos(): CursorPos {
|
||||||
return {
|
return {
|
||||||
rowId: nullAsUndefined(this._rowId()),
|
rowId: nullAsUndefined(this._properRowId()),
|
||||||
rowIndex: nullAsUndefined(this.rowIndex()),
|
rowIndex: nullAsUndefined(this.rowIndex()),
|
||||||
fieldIndex: this.fieldIndex(),
|
fieldIndex: this.fieldIndex(),
|
||||||
sectionId: this._sectionId()
|
sectionId: this._sectionId()
|
||||||
@ -117,7 +127,7 @@ export class Cursor extends Disposable {
|
|||||||
*/
|
*/
|
||||||
public setCursorPos(cursorPos: CursorPos): void {
|
public setCursorPos(cursorPos: CursorPos): void {
|
||||||
if (cursorPos.rowId !== undefined && this.viewData.getRowIndex(cursorPos.rowId) >= 0) {
|
if (cursorPos.rowId !== undefined && this.viewData.getRowIndex(cursorPos.rowId) >= 0) {
|
||||||
this._rowId(cursorPos.rowId);
|
this.rowIndex(this.viewData.getRowIndex(cursorPos.rowId) );
|
||||||
} else if (cursorPos.rowIndex !== undefined && cursorPos.rowIndex >= 0) {
|
} else if (cursorPos.rowIndex !== undefined && cursorPos.rowIndex >= 0) {
|
||||||
this.rowIndex(cursorPos.rowIndex);
|
this.rowIndex(cursorPos.rowIndex);
|
||||||
} else {
|
} else {
|
||||||
|
@ -1233,9 +1233,11 @@ GridView.prototype.buildDom = function() {
|
|||||||
kd.style("width", ROW_NUMBER_WIDTH + 'px'),
|
kd.style("width", ROW_NUMBER_WIDTH + 'px'),
|
||||||
dom('div.gridview_data_row_info',
|
dom('div.gridview_data_row_info',
|
||||||
kd.toggleClass('linked_dst', () => {
|
kd.toggleClass('linked_dst', () => {
|
||||||
|
const myRowId = row.id();
|
||||||
|
const linkedRowId = self.linkedRowId();
|
||||||
// Must ensure that linkedRowId is not null to avoid drawing on rows whose
|
// Must ensure that linkedRowId is not null to avoid drawing on rows whose
|
||||||
// row ids are null.
|
// row ids are null.
|
||||||
return self.linkedRowId() && self.linkedRowId() === row.getRowId();
|
return linkedRowId && linkedRowId === myRowId;
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
kd.text(function() { return row._index() + 1; }),
|
kd.text(function() { return row._index() + 1; }),
|
||||||
|
@ -476,6 +476,7 @@ export class GristDoc extends DisposableWithEvents {
|
|||||||
const viewId = toKo(ko, this.activeViewId)();
|
const viewId = toKo(ko, this.activeViewId)();
|
||||||
if (!isViewDocPage(viewId)) { return null; }
|
if (!isViewDocPage(viewId)) { return null; }
|
||||||
const section = this.viewModel.activeSection();
|
const section = this.viewModel.activeSection();
|
||||||
|
if (section?.isDisposed()) { return null; }
|
||||||
const view = section.viewInstance();
|
const view = section.viewInstance();
|
||||||
return view;
|
return view;
|
||||||
})));
|
})));
|
||||||
@ -620,6 +621,11 @@ export class GristDoc extends DisposableWithEvents {
|
|||||||
public async setCursorPos(cursorPos: CursorPos) {
|
public async setCursorPos(cursorPos: CursorPos) {
|
||||||
if (cursorPos.sectionId && cursorPos.sectionId !== this.externalSectionId.get()) {
|
if (cursorPos.sectionId && cursorPos.sectionId !== this.externalSectionId.get()) {
|
||||||
const desiredSection: ViewSectionRec = this.docModel.viewSections.getRowModel(cursorPos.sectionId);
|
const desiredSection: ViewSectionRec = this.docModel.viewSections.getRowModel(cursorPos.sectionId);
|
||||||
|
// If the section id is 0, the section doesn't exist (can happen during undo/redo), and should
|
||||||
|
// be fixed there. For now ignore it, to not create empty sections or views (peeking a view will create it).
|
||||||
|
if (!desiredSection.id.peek()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
// If this is completely unknown section (without a parent), it is probably an import preview.
|
// If this is completely unknown section (without a parent), it is probably an import preview.
|
||||||
if (!desiredSection.parentId.peek() && !desiredSection.isRaw.peek()) {
|
if (!desiredSection.parentId.peek() && !desiredSection.isRaw.peek()) {
|
||||||
const view = desiredSection.viewInstance.peek();
|
const view = desiredSection.viewInstance.peek();
|
||||||
|
@ -4,7 +4,7 @@ import {DocModel} from 'app/client/models/DocModel';
|
|||||||
import {ColumnRec} from "app/client/models/entities/ColumnRec";
|
import {ColumnRec} from "app/client/models/entities/ColumnRec";
|
||||||
import {TableRec} from "app/client/models/entities/TableRec";
|
import {TableRec} from "app/client/models/entities/TableRec";
|
||||||
import {ViewSectionRec} from "app/client/models/entities/ViewSectionRec";
|
import {ViewSectionRec} from "app/client/models/entities/ViewSectionRec";
|
||||||
import {RowId} from "app/client/models/rowset";
|
import {UIRowId} from "app/common/TableData";
|
||||||
import {LinkConfig} from "app/client/ui/selectBy";
|
import {LinkConfig} from "app/client/ui/selectBy";
|
||||||
import {ClientQuery, QueryOperation} from "app/common/ActiveDocAPI";
|
import {ClientQuery, QueryOperation} from "app/common/ActiveDocAPI";
|
||||||
import {isList, isListType, isRefListType} from "app/common/gristTypes";
|
import {isList, isListType, isRefListType} from "app/common/gristTypes";
|
||||||
@ -62,7 +62,7 @@ export type FilterColValues = Pick<ClientQuery, "filters" | "operations">;
|
|||||||
*/
|
*/
|
||||||
export class LinkingState extends Disposable {
|
export class LinkingState extends Disposable {
|
||||||
// If linking affects target section's cursor, this will be a computed for the cursor rowId.
|
// If linking affects target section's cursor, this will be a computed for the cursor rowId.
|
||||||
public readonly cursorPos?: ko.Computed<RowId>;
|
public readonly cursorPos?: ko.Computed<UIRowId>;
|
||||||
|
|
||||||
// If linking affects filtering, this is a computed for the current filtering state, as a
|
// If linking affects filtering, this is a computed for the current filtering state, as a
|
||||||
// {[colId]: colValues} mapping, with a dependency on srcSection.activeRowId()
|
// {[colId]: colValues} mapping, with a dependency on srcSection.activeRowId()
|
||||||
@ -136,7 +136,7 @@ export class LinkingState extends Disposable {
|
|||||||
const srcValueFunc = srcColId ? this._makeSrcCellGetter() : identity;
|
const srcValueFunc = srcColId ? this._makeSrcCellGetter() : identity;
|
||||||
if (srcValueFunc) {
|
if (srcValueFunc) {
|
||||||
this.cursorPos = this.autoDispose(ko.computed(() =>
|
this.cursorPos = this.autoDispose(ko.computed(() =>
|
||||||
srcValueFunc(srcSection.activeRowId()) as RowId
|
srcValueFunc(srcSection.activeRowId()) as UIRowId
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -172,7 +172,7 @@ export class LinkingState extends Disposable {
|
|||||||
|
|
||||||
// Value for this.filterColValues filtering based on a single column
|
// Value for this.filterColValues filtering based on a single column
|
||||||
private _simpleFilter(
|
private _simpleFilter(
|
||||||
colId: string, operation: QueryOperation, valuesFunc: (rowId: RowId|null) => any[]
|
colId: string, operation: QueryOperation, valuesFunc: (rowId: UIRowId|null) => any[]
|
||||||
): ko.Computed<FilterColValues> {
|
): ko.Computed<FilterColValues> {
|
||||||
return this.autoDispose(ko.computed(() => {
|
return this.autoDispose(ko.computed(() => {
|
||||||
const srcRowId = this._srcSection.activeRowId();
|
const srcRowId = this._srcSection.activeRowId();
|
||||||
@ -226,7 +226,7 @@ export class LinkingState extends Disposable {
|
|||||||
if (!srcCellObs) {
|
if (!srcCellObs) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (rowId: RowId | null) => {
|
return (rowId: UIRowId | null) => {
|
||||||
srcRowModel.assign(rowId);
|
srcRowModel.assign(rowId);
|
||||||
if (rowId === 'new') {
|
if (rowId === 'new') {
|
||||||
return 'new';
|
return 'new';
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import BaseView from 'app/client/components/BaseView';
|
import BaseView from 'app/client/components/BaseView';
|
||||||
import {GristDoc} from 'app/client/components/GristDoc';
|
import {GristDoc} from 'app/client/components/GristDoc';
|
||||||
import {ViewRec, ViewSectionRec} from 'app/client/models/DocModel';
|
import {ViewRec, ViewSectionRec} from 'app/client/models/DocModel';
|
||||||
|
import {makeT} from 'app/client/lib/localization';
|
||||||
import {filterBar} from 'app/client/ui/FilterBar';
|
import {filterBar} from 'app/client/ui/FilterBar';
|
||||||
import {cssIcon} from 'app/client/ui/RightPanelStyles';
|
import {cssIcon} from 'app/client/ui/RightPanelStyles';
|
||||||
import {makeCollapsedLayoutMenu} from 'app/client/ui/ViewLayoutMenu';
|
import {makeCollapsedLayoutMenu} from 'app/client/ui/ViewLayoutMenu';
|
||||||
@ -13,6 +14,7 @@ import {menu} from 'app/client/ui2018/menus';
|
|||||||
import {Computed, dom, DomElementArg, Observable, styled} from 'grainjs';
|
import {Computed, dom, DomElementArg, Observable, styled} from 'grainjs';
|
||||||
import {defaultMenuOptions} from 'popweasel';
|
import {defaultMenuOptions} from 'popweasel';
|
||||||
|
|
||||||
|
const t = makeT('ViewSection');
|
||||||
|
|
||||||
export function buildCollapsedSectionDom(options: {
|
export function buildCollapsedSectionDom(options: {
|
||||||
gristDoc: GristDoc,
|
gristDoc: GristDoc,
|
||||||
@ -69,8 +71,13 @@ export function buildViewSectionDom(options: {
|
|||||||
|
|
||||||
// Creating normal section dom
|
// Creating normal section dom
|
||||||
const vs: ViewSectionRec = gristDoc.docModel.viewSections.getRowModel(sectionRowId);
|
const vs: ViewSectionRec = gristDoc.docModel.viewSections.getRowModel(sectionRowId);
|
||||||
|
const selectedBySectionTitle = Computed.create(null, (use) => {
|
||||||
|
if (!use(vs.linkSrcSectionRef)) { return null; }
|
||||||
|
return use(use(vs.linkSrcSection).titleDef);
|
||||||
|
});
|
||||||
return dom('div.view_leaf.viewsection_content.flexvbox.flexauto',
|
return dom('div.view_leaf.viewsection_content.flexvbox.flexauto',
|
||||||
testId(`viewlayout-section-${sectionRowId}`),
|
testId(`viewlayout-section-${sectionRowId}`),
|
||||||
|
dom.autoDispose(selectedBySectionTitle),
|
||||||
!options.isResizing ? dom.autoDispose(isResizing) : null,
|
!options.isResizing ? dom.autoDispose(isResizing) : null,
|
||||||
cssViewLeaf.cls(''),
|
cssViewLeaf.cls(''),
|
||||||
cssViewLeafInactive.cls('', (use) => !vs.isDisposed() && !use(vs.hasFocus)),
|
cssViewLeafInactive.cls('', (use) => !vs.isDisposed() && !use(vs.hasFocus)),
|
||||||
@ -96,10 +103,14 @@ export function buildViewSectionDom(options: {
|
|||||||
dom('div.view_data_pane_container.flexvbox',
|
dom('div.view_data_pane_container.flexvbox',
|
||||||
cssResizing.cls('', isResizing),
|
cssResizing.cls('', isResizing),
|
||||||
dom.maybe(viewInstance.disableEditing, () =>
|
dom.maybe(viewInstance.disableEditing, () =>
|
||||||
dom('div.disable_viewpane.flexvbox', 'No data')
|
dom('div.disable_viewpane.flexvbox',
|
||||||
|
dom.domComputed(selectedBySectionTitle, (title) => title
|
||||||
|
? t(`No row selected in {{title}}`, {title})
|
||||||
|
: t('No data')),
|
||||||
|
)
|
||||||
),
|
),
|
||||||
dom.maybe(viewInstance.isTruncated, () =>
|
dom.maybe(viewInstance.isTruncated, () =>
|
||||||
dom('div.viewsection_truncated', 'Not all data is shown')
|
dom('div.viewsection_truncated', t('Not all data is shown'))
|
||||||
),
|
),
|
||||||
dom.cls((use) => 'viewsection_type_' + use(vs.parentKey)),
|
dom.cls((use) => 'viewsection_type_' + use(vs.parentKey)),
|
||||||
viewInstance.viewPane
|
viewInstance.viewPane
|
||||||
|
@ -5,6 +5,7 @@ import * as rowset from 'app/client/models/rowset';
|
|||||||
import { MANUALSORT } from 'app/common/gristTypes';
|
import { MANUALSORT } from 'app/common/gristTypes';
|
||||||
import { SortFunc } from 'app/common/SortFunc';
|
import { SortFunc } from 'app/common/SortFunc';
|
||||||
import { Sort } from 'app/common/SortSpec';
|
import { Sort } from 'app/common/SortSpec';
|
||||||
|
import { UIRowId } from 'app/common/TableData';
|
||||||
import * as ko from 'knockout';
|
import * as ko from 'knockout';
|
||||||
import range = require('lodash/range');
|
import range = require('lodash/range');
|
||||||
|
|
||||||
@ -44,7 +45,7 @@ export async function updatePositions(gristDoc: GristDoc, section: ViewSectionRe
|
|||||||
sortFunc.updateSpec(section.activeDisplaySortSpec.peek());
|
sortFunc.updateSpec(section.activeDisplaySortSpec.peek());
|
||||||
const sortedRows = rowset.SortedRowSet.create(
|
const sortedRows = rowset.SortedRowSet.create(
|
||||||
null,
|
null,
|
||||||
(a: rowset.RowId, b: rowset.RowId) => sortFunc.compare(a as number, b as number),
|
(a: UIRowId, b: UIRowId) => sortFunc.compare(a as number, b as number),
|
||||||
tableModel.tableData
|
tableModel.tableData
|
||||||
);
|
);
|
||||||
sortedRows.subscribeTo(tableModel);
|
sortedRows.subscribeTo(tableModel);
|
||||||
|
@ -29,8 +29,7 @@ import {isHiddenTable, isSummaryTable} from 'app/common/isHiddenTable';
|
|||||||
import {canEdit} from 'app/common/roles';
|
import {canEdit} from 'app/common/roles';
|
||||||
import {RowFilterFunc} from 'app/common/RowFilterFunc';
|
import {RowFilterFunc} from 'app/common/RowFilterFunc';
|
||||||
import {schema, SchemaTypes} from 'app/common/schema';
|
import {schema, SchemaTypes} from 'app/common/schema';
|
||||||
import {UIRowId} from 'app/common/UIRowId';
|
import {UIRowId} from 'app/common/TableData';
|
||||||
|
|
||||||
import {ACLRuleRec, createACLRuleRec} from 'app/client/models/entities/ACLRuleRec';
|
import {ACLRuleRec, createACLRuleRec} from 'app/client/models/entities/ACLRuleRec';
|
||||||
import {ColumnRec, createColumnRec} from 'app/client/models/entities/ColumnRec';
|
import {ColumnRec, createColumnRec} from 'app/client/models/entities/ColumnRec';
|
||||||
import {createDocInfoRec, DocInfoRec} from 'app/client/models/entities/DocInfoRec';
|
import {createDocInfoRec, DocInfoRec} from 'app/client/models/entities/DocInfoRec';
|
||||||
@ -318,7 +317,7 @@ export class DocModel {
|
|||||||
*/
|
*/
|
||||||
function createTablesArray(
|
function createTablesArray(
|
||||||
tablesModel: MetaTableModel<TableRec>,
|
tablesModel: MetaTableModel<TableRec>,
|
||||||
filterFunc: RowFilterFunc<rowset.RowId> = (_row) => true
|
filterFunc: RowFilterFunc<UIRowId> = (_row) => true
|
||||||
) {
|
) {
|
||||||
const rowSource = new rowset.FilteredRowSource(filterFunc);
|
const rowSource = new rowset.FilteredRowSource(filterFunc);
|
||||||
rowSource.subscribeTo(tablesModel);
|
rowSource.subscribeTo(tablesModel);
|
||||||
|
@ -28,7 +28,7 @@
|
|||||||
*/
|
*/
|
||||||
import DataTableModel from 'app/client/models/DataTableModel';
|
import DataTableModel from 'app/client/models/DataTableModel';
|
||||||
import {DocModel} from 'app/client/models/DocModel';
|
import {DocModel} from 'app/client/models/DocModel';
|
||||||
import {BaseFilteredRowSource, RowId, RowList, RowSource} from 'app/client/models/rowset';
|
import {BaseFilteredRowSource, RowList, RowSource} from 'app/client/models/rowset';
|
||||||
import {TableData} from 'app/client/models/TableData';
|
import {TableData} from 'app/client/models/TableData';
|
||||||
import {ActiveDocAPI, ClientQuery, QueryOperation} from 'app/common/ActiveDocAPI';
|
import {ActiveDocAPI, ClientQuery, QueryOperation} from 'app/common/ActiveDocAPI';
|
||||||
import {CellValue, TableDataAction} from 'app/common/DocActions';
|
import {CellValue, TableDataAction} from 'app/common/DocActions';
|
||||||
@ -37,7 +37,7 @@ import {isList} from "app/common/gristTypes";
|
|||||||
import {nativeCompare} from 'app/common/gutil';
|
import {nativeCompare} from 'app/common/gutil';
|
||||||
import {IRefCountSub, RefCountMap} from 'app/common/RefCountMap';
|
import {IRefCountSub, RefCountMap} from 'app/common/RefCountMap';
|
||||||
import {RowFilterFunc} from 'app/common/RowFilterFunc';
|
import {RowFilterFunc} from 'app/common/RowFilterFunc';
|
||||||
import {TableData as BaseTableData} from 'app/common/TableData';
|
import {TableData as BaseTableData, UIRowId} from 'app/common/TableData';
|
||||||
import {tbind} from 'app/common/tbind';
|
import {tbind} from 'app/common/tbind';
|
||||||
import {decodeObject} from "app/plugin/objtypes";
|
import {decodeObject} from "app/plugin/objtypes";
|
||||||
import {Disposable, Holder, IDisposableOwnerT} from 'grainjs';
|
import {Disposable, Holder, IDisposableOwnerT} from 'grainjs';
|
||||||
@ -303,7 +303,7 @@ export class TableQuerySets {
|
|||||||
/**
|
/**
|
||||||
* Returns a filtering function which tells whether a row matches the given query.
|
* Returns a filtering function which tells whether a row matches the given query.
|
||||||
*/
|
*/
|
||||||
export function getFilterFunc(docData: DocData, query: ClientQuery): RowFilterFunc<RowId> {
|
export function getFilterFunc(docData: DocData, query: ClientQuery): RowFilterFunc<UIRowId> {
|
||||||
// NOTE we rely without checking on tableId and colIds being valid.
|
// NOTE we rely without checking on tableId and colIds being valid.
|
||||||
const tableData: BaseTableData = docData.getTable(query.tableId)!;
|
const tableData: BaseTableData = docData.getTable(query.tableId)!;
|
||||||
const colFuncs = Object.keys(query.filters).sort().map(
|
const colFuncs = Object.keys(query.filters).sort().map(
|
||||||
@ -312,22 +312,22 @@ export function getFilterFunc(docData: DocData, query: ClientQuery): RowFilterFu
|
|||||||
const values = new Set(query.filters[colId]);
|
const values = new Set(query.filters[colId]);
|
||||||
switch (query.operations[colId]) {
|
switch (query.operations[colId]) {
|
||||||
case "intersects":
|
case "intersects":
|
||||||
return (rowId: RowId) => {
|
return (rowId: UIRowId) => {
|
||||||
const value = getter(rowId) as CellValue;
|
const value = getter(rowId) as CellValue;
|
||||||
return isList(value) &&
|
return isList(value) &&
|
||||||
(decodeObject(value) as unknown[]).some(v => values.has(v));
|
(decodeObject(value) as unknown[]).some(v => values.has(v));
|
||||||
};
|
};
|
||||||
case "empty":
|
case "empty":
|
||||||
return (rowId: RowId) => {
|
return (rowId: UIRowId) => {
|
||||||
const value = getter(rowId);
|
const value = getter(rowId);
|
||||||
// `isList(value) && value.length === 1` means `value == ['L']` i.e. an empty list
|
// `isList(value) && value.length === 1` means `value == ['L']` i.e. an empty list
|
||||||
return !value || isList(value) && value.length === 1;
|
return !value || isList(value) && value.length === 1;
|
||||||
};
|
};
|
||||||
case "in":
|
case "in":
|
||||||
return (rowId: RowId) => values.has(getter(rowId));
|
return (rowId: UIRowId) => values.has(getter(rowId));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return (rowId: RowId) => colFuncs.every(f => f(rowId));
|
return (rowId: UIRowId) => colFuncs.every(f => f(rowId));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import {ColumnFilter} from 'app/client/models/ColumnFilter';
|
import {ColumnFilter} from 'app/client/models/ColumnFilter';
|
||||||
import {ColumnRec, ViewFieldRec, ViewSectionRec} 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 {TableData} from 'app/client/models/TableData';
|
||||||
import {buildColFilter, ColumnFilterFunc} from 'app/common/ColumnFilterFunc';
|
import {buildColFilter, ColumnFilterFunc} from 'app/common/ColumnFilterFunc';
|
||||||
import {buildRowFilter, RowFilterFunc, RowValueFunc } from 'app/common/RowFilterFunc';
|
import {buildRowFilter, RowFilterFunc, RowValueFunc } from 'app/common/RowFilterFunc';
|
||||||
|
import {UIRowId} from 'app/common/TableData';
|
||||||
import {Computed, Disposable, MutableObsArray, obsArray, Observable, UseCB} from 'grainjs';
|
import {Computed, Disposable, MutableObsArray, obsArray, Observable, UseCB} from 'grainjs';
|
||||||
|
|
||||||
export type {ColumnFilterFunc};
|
export type {ColumnFilterFunc};
|
||||||
@ -26,10 +26,10 @@ type ColFilterCB = (fieldOrColumn: ViewFieldRec|ColumnRec, colFilter: ColumnFilt
|
|||||||
* results in their being displayed (obviating the need to maintain their rowId explicitly).
|
* results in their being displayed (obviating the need to maintain their rowId explicitly).
|
||||||
*/
|
*/
|
||||||
export class SectionFilter extends Disposable {
|
export class SectionFilter extends Disposable {
|
||||||
public readonly sectionFilterFunc: Observable<RowFilterFunc<RowId>>;
|
public readonly sectionFilterFunc: Observable<RowFilterFunc<UIRowId>>;
|
||||||
|
|
||||||
private _openFilterOverride: Observable<OpenColumnFilter|null> = Observable.create(this, null);
|
private _openFilterOverride: Observable<OpenColumnFilter|null> = Observable.create(this, null);
|
||||||
private _tempRows: MutableObsArray<RowId> = obsArray();
|
private _tempRows: MutableObsArray<UIRowId> = obsArray();
|
||||||
|
|
||||||
constructor(public viewSection: ViewSectionRec, private _tableData: TableData) {
|
constructor(public viewSection: ViewSectionRec, private _tableData: TableData) {
|
||||||
super();
|
super();
|
||||||
@ -89,8 +89,8 @@ export class SectionFilter extends Disposable {
|
|||||||
return this._addRowsToFilter(this._buildPlainFilterFunc(getFilterFunc, use), this._tempRows.get());
|
return this._addRowsToFilter(this._buildPlainFilterFunc(getFilterFunc, use), this._tempRows.get());
|
||||||
}
|
}
|
||||||
|
|
||||||
private _addRowsToFilter(filterFunc: RowFilterFunc<RowId>, rows: RowId[]) {
|
private _addRowsToFilter(filterFunc: RowFilterFunc<UIRowId>, rows: UIRowId[]) {
|
||||||
return (rowId: RowId) => rows.includes(rowId) || (typeof rowId !== 'number') || filterFunc(rowId);
|
return (rowId: UIRowId) => rows.includes(rowId) || (typeof rowId !== 'number') || filterFunc(rowId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -98,9 +98,9 @@ export class SectionFilter extends Disposable {
|
|||||||
* columns. You can use `getFilterFunc(column, colFilter)` to customize the filter func for each
|
* columns. You can use `getFilterFunc(column, colFilter)` to customize the filter func for each
|
||||||
* column. It calls `getFilterFunc` right away.
|
* column. It calls `getFilterFunc` right away.
|
||||||
*/
|
*/
|
||||||
private _buildPlainFilterFunc(getFilterFunc: ColFilterCB, use: UseCB): RowFilterFunc<RowId> {
|
private _buildPlainFilterFunc(getFilterFunc: ColFilterCB, use: UseCB): RowFilterFunc<UIRowId> {
|
||||||
const filters = use(this.viewSection.filters);
|
const filters = use(this.viewSection.filters);
|
||||||
const funcs: Array<RowFilterFunc<RowId> | null> = filters.map(({filter, fieldOrColumn}) => {
|
const funcs: Array<RowFilterFunc<UIRowId> | null> = filters.map(({filter, fieldOrColumn}) => {
|
||||||
const colFilter = buildColFilter(use(filter), use(use(fieldOrColumn.origCol).type));
|
const colFilter = buildColFilter(use(filter), use(use(fieldOrColumn.origCol).type));
|
||||||
const filterFunc = getFilterFunc(fieldOrColumn, colFilter);
|
const filterFunc = getFilterFunc(fieldOrColumn, colFilter);
|
||||||
|
|
||||||
@ -108,9 +108,9 @@ export class SectionFilter extends Disposable {
|
|||||||
|
|
||||||
if (!filterFunc || !getter) { return null; }
|
if (!filterFunc || !getter) { return null; }
|
||||||
|
|
||||||
return buildRowFilter(getter as RowValueFunc<RowId>, filterFunc);
|
return buildRowFilter(getter as RowValueFunc<UIRowId>, filterFunc);
|
||||||
}).filter(f => f !== null); // Filter out columns that don't have a filter
|
}).filter(f => f !== null); // Filter out columns that don't have a filter
|
||||||
|
|
||||||
return (rowId: RowId) => funcs.every(f => Boolean(f && f(rowId)));
|
return (rowId: UIRowId) => funcs.every(f => Boolean(f && f(rowId)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -62,8 +62,9 @@ export function createViewRec(this: ViewRec, docModel: DocModel): void {
|
|||||||
// Default to the first leaf from layoutSpec (which corresponds to the top-left section), or
|
// Default to the first leaf from layoutSpec (which corresponds to the top-left section), or
|
||||||
// fall back to the first item in the list if anything goes wrong (previous behavior).
|
// fall back to the first item in the list if anything goes wrong (previous behavior).
|
||||||
const firstLeaf = getFirstLeaf(this.layoutSpecObj.peek());
|
const firstLeaf = getFirstLeaf(this.layoutSpecObj.peek());
|
||||||
return visible.find(s => s.getRowId() === firstLeaf) ? firstLeaf as number :
|
const result = visible.find(s => s.id() === firstLeaf) ? firstLeaf as number :
|
||||||
(visible[0]?.getRowId() || 0);
|
(visible[0]?.id() || 0);
|
||||||
|
return result;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.activeSection = refRecord(docModel.viewSections, this.activeSectionId);
|
this.activeSection = refRecord(docModel.viewSections, this.activeSectionId);
|
||||||
|
@ -15,13 +15,13 @@ import {
|
|||||||
ViewRec
|
ViewRec
|
||||||
} from 'app/client/models/DocModel';
|
} from 'app/client/models/DocModel';
|
||||||
import * as modelUtil from 'app/client/models/modelUtil';
|
import * as modelUtil from 'app/client/models/modelUtil';
|
||||||
import {RowId} from 'app/client/models/rowset';
|
|
||||||
import {LinkConfig} from 'app/client/ui/selectBy';
|
import {LinkConfig} from 'app/client/ui/selectBy';
|
||||||
import {getWidgetTypes} from 'app/client/ui/widgetTypes';
|
import {getWidgetTypes} from 'app/client/ui/widgetTypes';
|
||||||
import {AccessLevel, ICustomWidget} from 'app/common/CustomWidget';
|
import {AccessLevel, ICustomWidget} from 'app/common/CustomWidget';
|
||||||
import {UserAction} from 'app/common/DocActions';
|
import {UserAction} from 'app/common/DocActions';
|
||||||
import {arrayRepeat} from 'app/common/gutil';
|
import {arrayRepeat} from 'app/common/gutil';
|
||||||
import {Sort} from 'app/common/SortSpec';
|
import {Sort} from 'app/common/SortSpec';
|
||||||
|
import {UIRowId} from 'app/common/TableData';
|
||||||
import {ColumnsToMap, WidgetColumnMap} from 'app/plugin/CustomSectionAPI';
|
import {ColumnsToMap, WidgetColumnMap} from 'app/plugin/CustomSectionAPI';
|
||||||
import {ColumnToMapImpl} from 'app/client/models/ColumnToMap';
|
import {ColumnToMapImpl} from 'app/client/models/ColumnToMap';
|
||||||
import {BEHAVIOR} from 'app/client/models/entities/ColumnRec';
|
import {BEHAVIOR} from 'app/client/models/entities/ColumnRec';
|
||||||
@ -120,19 +120,30 @@ export interface ViewSectionRec extends IRowModel<"_grist_Views_section">, RuleO
|
|||||||
|
|
||||||
hasFocus: ko.Computed<boolean>;
|
hasFocus: ko.Computed<boolean>;
|
||||||
|
|
||||||
activeLinkSrcSectionRef: modelUtil.CustomComputed<number>;
|
|
||||||
activeLinkSrcColRef: modelUtil.CustomComputed<number>;
|
|
||||||
activeLinkTargetColRef: modelUtil.CustomComputed<number>;
|
|
||||||
|
|
||||||
// Whether current linking state is as saved. It may be different during editing.
|
|
||||||
isActiveLinkSaved: ko.Computed<boolean>;
|
|
||||||
|
|
||||||
// Section-linking affects table if linkSrcSection is set. The controller value of the
|
// Section-linking affects table if linkSrcSection is set. The controller value of the
|
||||||
// link is the value of srcCol at activeRowId of linkSrcSection, or activeRowId itself when
|
// link is the value of srcCol at activeRowId of linkSrcSection, or activeRowId itself when
|
||||||
// srcCol is unset. If targetCol is set, we filter for all rows whose targetCol is equal to
|
// srcCol is unset. If targetCol is set, we filter for all rows whose targetCol is equal to
|
||||||
// the controller value. Otherwise, the controller value determines the rowId of the cursor.
|
// the controller value. Otherwise, the controller value determines the rowId of the cursor.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Section selected in the `Select By` dropdown. Used for filtering this section.
|
||||||
|
*/
|
||||||
linkSrcSection: ko.Computed<ViewSectionRec>;
|
linkSrcSection: ko.Computed<ViewSectionRec>;
|
||||||
|
/**
|
||||||
|
* Column selected in the `Select By` dropdown in the remote section. It points to a column in remote section
|
||||||
|
* that contains a reference to this table (or common table - because we can be linked by having the same reference
|
||||||
|
* to some other section).
|
||||||
|
* Used for filtering this section. Can be empty as user can just link by section.
|
||||||
|
* Watch out, it is not cleared, so it is only valid when we have linkSrcSection.
|
||||||
|
* In UI it is shown as Target Section (dot) Target Column.
|
||||||
|
*/
|
||||||
linkSrcCol: ko.Computed<ColumnRec>;
|
linkSrcCol: ko.Computed<ColumnRec>;
|
||||||
|
/**
|
||||||
|
* In case we have multiple reference columns, that are shown as
|
||||||
|
* Target Section -> My Column or
|
||||||
|
* Target Section . Target Column -> My Column
|
||||||
|
* store the reference to the column (my column) to use.
|
||||||
|
*/
|
||||||
linkTargetCol: ko.Computed<ColumnRec>;
|
linkTargetCol: ko.Computed<ColumnRec>;
|
||||||
|
|
||||||
// Linking state maintains .filterFunc and .cursorPos observables which we use for
|
// Linking state maintains .filterFunc and .cursorPos observables which we use for
|
||||||
@ -142,7 +153,7 @@ export interface ViewSectionRec extends IRowModel<"_grist_Views_section">, RuleO
|
|||||||
|
|
||||||
linkingFilter: ko.Computed<FilterColValues>;
|
linkingFilter: ko.Computed<FilterColValues>;
|
||||||
|
|
||||||
activeRowId: ko.Observable<RowId | null>; // May be null when there are no rows.
|
activeRowId: ko.Observable<UIRowId | null>; // May be null when there are no rows.
|
||||||
|
|
||||||
// If the view instance for section is instantiated, it will be accessible here.
|
// If the view instance for section is instantiated, it will be accessible here.
|
||||||
viewInstance: ko.Observable<BaseView | null>;
|
viewInstance: ko.Observable<BaseView | null>;
|
||||||
@ -594,30 +605,20 @@ export function createViewSectionRec(this: ViewSectionRec, docModel: DocModel):
|
|||||||
write: (val) => { this.view().activeSectionId(val ? this.id() : 0); }
|
write: (val) => { this.view().activeSectionId(val ? this.id() : 0); }
|
||||||
});
|
});
|
||||||
|
|
||||||
this.activeLinkSrcSectionRef = modelUtil.customValue(this.linkSrcSectionRef);
|
|
||||||
this.activeLinkSrcColRef = modelUtil.customValue(this.linkSrcColRef);
|
|
||||||
this.activeLinkTargetColRef = modelUtil.customValue(this.linkTargetColRef);
|
|
||||||
|
|
||||||
// Whether current linking state is as saved. It may be different during editing.
|
|
||||||
this.isActiveLinkSaved = this.autoDispose(ko.pureComputed(() =>
|
|
||||||
this.activeLinkSrcSectionRef.isSaved() &&
|
|
||||||
this.activeLinkSrcColRef.isSaved() &&
|
|
||||||
this.activeLinkTargetColRef.isSaved()));
|
|
||||||
|
|
||||||
// Section-linking affects this table if linkSrcSection is set. The controller value of the
|
// Section-linking affects this table if linkSrcSection is set. The controller value of the
|
||||||
// link is the value of srcCol at activeRowId of linkSrcSection, or activeRowId itself when
|
// link is the value of srcCol at activeRowId of linkSrcSection, or activeRowId itself when
|
||||||
// srcCol is unset. If targetCol is set, we filter for all rows whose targetCol is equal to
|
// srcCol is unset. If targetCol is set, we filter for all rows whose targetCol is equal to
|
||||||
// the controller value. Otherwise, the controller value determines the rowId of the cursor.
|
// the controller value. Otherwise, the controller value determines the rowId of the cursor.
|
||||||
this.linkSrcSection = refRecord(docModel.viewSections, this.activeLinkSrcSectionRef);
|
this.linkSrcSection = refRecord(docModel.viewSections, this.linkSrcSectionRef);
|
||||||
this.linkSrcCol = refRecord(docModel.columns, this.activeLinkSrcColRef);
|
this.linkSrcCol = refRecord(docModel.columns, this.linkSrcColRef);
|
||||||
this.linkTargetCol = refRecord(docModel.columns, this.activeLinkTargetColRef);
|
this.linkTargetCol = refRecord(docModel.columns, this.linkTargetColRef);
|
||||||
|
|
||||||
this.activeRowId = ko.observable<RowId|null>(null);
|
this.activeRowId = ko.observable<UIRowId|null>(null);
|
||||||
|
|
||||||
this._linkingState = Holder.create(this);
|
this._linkingState = Holder.create(this);
|
||||||
this.linkingState = this.autoDispose(ko.pureComputed(() => {
|
this.linkingState = this.autoDispose(ko.pureComputed(() => {
|
||||||
if (!this.activeLinkSrcSectionRef()) {
|
if (!this.linkSrcSectionRef()) {
|
||||||
// This view section isn't selecting by anything.
|
// This view section isn't selected by anything.
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
@ -24,7 +24,7 @@
|
|||||||
import koArray, {KoArray} from 'app/client/lib/koArray';
|
import koArray, {KoArray} from 'app/client/lib/koArray';
|
||||||
import {DisposableWithEvents} from 'app/common/DisposableWithEvents';
|
import {DisposableWithEvents} from 'app/common/DisposableWithEvents';
|
||||||
import {CompareFunc, sortedIndex} from 'app/common/gutil';
|
import {CompareFunc, sortedIndex} from 'app/common/gutil';
|
||||||
import {SkippableRows} from 'app/common/TableData';
|
import {SkippableRows, UIRowId} from 'app/common/TableData';
|
||||||
import {RowFilterFunc} from "app/common/RowFilterFunc";
|
import {RowFilterFunc} from "app/common/RowFilterFunc";
|
||||||
import {Observable} from 'grainjs';
|
import {Observable} from 'grainjs';
|
||||||
|
|
||||||
@ -36,8 +36,7 @@ export const ALL: unique symbol = Symbol("ALL");
|
|||||||
|
|
||||||
export type ChangeType = 'add' | 'remove' | 'update';
|
export type ChangeType = 'add' | 'remove' | 'update';
|
||||||
export type ChangeMethod = 'onAddRows' | 'onRemoveRows' | 'onUpdateRows';
|
export type ChangeMethod = 'onAddRows' | 'onRemoveRows' | 'onUpdateRows';
|
||||||
export type RowId = number | 'new';
|
export type RowList = Iterable<UIRowId>;
|
||||||
export type RowList = Iterable<RowId>;
|
|
||||||
export type RowsChanged = RowList | typeof ALL;
|
export type RowsChanged = RowList | typeof ALL;
|
||||||
|
|
||||||
// ----------------------------------------------------------------------
|
// ----------------------------------------------------------------------
|
||||||
@ -132,7 +131,7 @@ export class RowListener extends DisposableWithEvents {
|
|||||||
* A trivial RowSource returning a fixed list of rows.
|
* A trivial RowSource returning a fixed list of rows.
|
||||||
*/
|
*/
|
||||||
export abstract class ArrayRowSource extends RowSource {
|
export abstract class ArrayRowSource extends RowSource {
|
||||||
constructor(private _rows: RowId[]) { super(); }
|
constructor(private _rows: UIRowId[]) { super(); }
|
||||||
public getAllRows(): RowList { return this._rows; }
|
public getAllRows(): RowList { return this._rows; }
|
||||||
public getNumRows(): number { return this._rows.length; }
|
public getNumRows(): number { return this._rows.length; }
|
||||||
}
|
}
|
||||||
@ -146,11 +145,11 @@ export abstract class ArrayRowSource extends RowSource {
|
|||||||
* TODO: This class is not used anywhere at the moment, and is a candidate for removal.
|
* TODO: This class is not used anywhere at the moment, and is a candidate for removal.
|
||||||
*/
|
*/
|
||||||
export class MappedRowSource extends RowSource {
|
export class MappedRowSource extends RowSource {
|
||||||
private _mapperFunc: (row: RowId) => RowId;
|
private _mapperFunc: (row: UIRowId) => UIRowId;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public parentRowSource: RowSource,
|
public parentRowSource: RowSource,
|
||||||
mapperFunc: (row: RowId) => RowId,
|
mapperFunc: (row: UIRowId) => UIRowId,
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
@ -182,7 +181,7 @@ export class ExtendedRowSource extends RowSource {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public parentRowSource: RowSource,
|
public parentRowSource: RowSource,
|
||||||
public extras: RowId[]
|
public extras: UIRowId[]
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
@ -209,9 +208,9 @@ export class ExtendedRowSource extends RowSource {
|
|||||||
// ----------------------------------------------------------------------
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
interface FilterRowChanges {
|
interface FilterRowChanges {
|
||||||
adds?: RowId[];
|
adds?: UIRowId[];
|
||||||
updates?: RowId[];
|
updates?: UIRowId[];
|
||||||
removes?: RowId[];
|
removes?: UIRowId[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -219,9 +218,9 @@ interface FilterRowChanges {
|
|||||||
* does not maintain excluded rows, and does not allow changes to filterFunc.
|
* does not maintain excluded rows, and does not allow changes to filterFunc.
|
||||||
*/
|
*/
|
||||||
export class BaseFilteredRowSource extends RowListener implements RowSource {
|
export class BaseFilteredRowSource extends RowListener implements RowSource {
|
||||||
protected _matchingRows: Set<RowId> = new Set(); // Set of rows matching the filter.
|
protected _matchingRows: Set<UIRowId> = new Set(); // Set of rows matching the filter.
|
||||||
|
|
||||||
constructor(protected _filterFunc: RowFilterFunc<RowId>) {
|
constructor(protected _filterFunc: RowFilterFunc<UIRowId>) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -309,8 +308,8 @@ export class BaseFilteredRowSource extends RowListener implements RowSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// These are implemented by FilteredRowSource, but the base class doesn't need to do anything.
|
// These are implemented by FilteredRowSource, but the base class doesn't need to do anything.
|
||||||
protected _addExcludedRow(row: RowId): void { /* no-op */ }
|
protected _addExcludedRow(row: UIRowId): void { /* no-op */ }
|
||||||
protected _deleteExcludedRow(row: RowId): boolean { return true; }
|
protected _deleteExcludedRow(row: UIRowId): boolean { return true; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -321,13 +320,13 @@ export class BaseFilteredRowSource extends RowListener implements RowSource {
|
|||||||
* FilteredRowSource is also a RowListener, so to subscribe to a rowSource, use `subscribeTo()`.
|
* FilteredRowSource is also a RowListener, so to subscribe to a rowSource, use `subscribeTo()`.
|
||||||
*/
|
*/
|
||||||
export class FilteredRowSource extends BaseFilteredRowSource {
|
export class FilteredRowSource extends BaseFilteredRowSource {
|
||||||
private _excludedRows: Set<RowId> = new Set(); // Set of rows NOT matching the filter.
|
private _excludedRows: Set<UIRowId> = new Set(); // Set of rows NOT matching the filter.
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Change the filter function. This may trigger 'remove' and 'add' events as necessary to indicate
|
* Change the filter function. This may trigger 'remove' and 'add' events as necessary to indicate
|
||||||
* that rows stopped or started matching the new filter.
|
* that rows stopped or started matching the new filter.
|
||||||
*/
|
*/
|
||||||
public updateFilter(filterFunc: RowFilterFunc<RowId>) {
|
public updateFilter(filterFunc: RowFilterFunc<UIRowId>) {
|
||||||
this._filterFunc = filterFunc;
|
this._filterFunc = filterFunc;
|
||||||
const changes: FilterRowChanges = {};
|
const changes: FilterRowChanges = {};
|
||||||
// After the first call, _excludedRows may have additional rows, but there is no harm in it,
|
// After the first call, _excludedRows may have additional rows, but there is no harm in it,
|
||||||
@ -356,8 +355,8 @@ export class FilteredRowSource extends BaseFilteredRowSource {
|
|||||||
return this._excludedRows.values();
|
return this._excludedRows.values();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _addExcludedRow(row: RowId): void { this._excludedRows.add(row); }
|
protected _addExcludedRow(row: UIRowId): void { this._excludedRows.add(row); }
|
||||||
protected _deleteExcludedRow(row: RowId): boolean { return this._excludedRows.delete(row); }
|
protected _deleteExcludedRow(row: UIRowId): boolean { return this._excludedRows.delete(row); }
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------
|
// ----------------------------------------------------------------------
|
||||||
@ -368,7 +367,7 @@ export class FilteredRowSource extends BaseFilteredRowSource {
|
|||||||
* Private helper object that maintains a set of rows for a particular group.
|
* Private helper object that maintains a set of rows for a particular group.
|
||||||
*/
|
*/
|
||||||
class RowGroupHelper<Value> extends RowSource {
|
class RowGroupHelper<Value> extends RowSource {
|
||||||
private _rows: Set<RowId> = new Set();
|
private _rows: Set<UIRowId> = new Set();
|
||||||
constructor(public readonly groupValue: Value) {
|
constructor(public readonly groupValue: Value) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
@ -411,12 +410,12 @@ function _addToMapOfArrays<K, V>(map: Map<K, V[]>, key: K, r: V): void {
|
|||||||
*/
|
*/
|
||||||
export class RowGrouping<Value> extends RowListener {
|
export class RowGrouping<Value> extends RowListener {
|
||||||
// Maps row identifiers to groupValues.
|
// Maps row identifiers to groupValues.
|
||||||
private _rowsToValues: Map<RowId, Value> = new Map();
|
private _rowsToValues: Map<UIRowId, Value> = new Map();
|
||||||
|
|
||||||
// Maps group values to RowGroupHelpers
|
// Maps group values to RowGroupHelpers
|
||||||
private _valuesToGroups: Map<Value, RowGroupHelper<Value>> = new Map();
|
private _valuesToGroups: Map<Value, RowGroupHelper<Value>> = new Map();
|
||||||
|
|
||||||
constructor(private _groupFunc: (row: RowId) => Value) {
|
constructor(private _groupFunc: (row: UIRowId) => Value) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
// On disposal, dispose all RowGroupHelpers that we maintain.
|
// On disposal, dispose all RowGroupHelpers that we maintain.
|
||||||
@ -538,15 +537,15 @@ export class RowGrouping<Value> extends RowListener {
|
|||||||
* SortedRowSet re-emits 'rowNotify(rows, value)' events from RowSources that it subscribes to.
|
* SortedRowSet re-emits 'rowNotify(rows, value)' events from RowSources that it subscribes to.
|
||||||
*/
|
*/
|
||||||
export class SortedRowSet extends RowListener {
|
export class SortedRowSet extends RowListener {
|
||||||
private _allRows: Set<RowId> = new Set();
|
private _allRows: Set<UIRowId> = new Set();
|
||||||
private _isPaused: boolean = false;
|
private _isPaused: boolean = false;
|
||||||
private _koArray: KoArray<RowId>;
|
private _koArray: KoArray<UIRowId>;
|
||||||
private _keepFunc?: (rowId: number|'new') => boolean;
|
private _keepFunc?: (rowId: number|'new') => boolean;
|
||||||
|
|
||||||
constructor(private _compareFunc: CompareFunc<RowId>,
|
constructor(private _compareFunc: CompareFunc<UIRowId>,
|
||||||
private _skippableRows?: SkippableRows) {
|
private _skippableRows?: SkippableRows) {
|
||||||
super();
|
super();
|
||||||
this._koArray = this.autoDispose(koArray<RowId>());
|
this._koArray = this.autoDispose(koArray<UIRowId>());
|
||||||
this._keepFunc = _skippableRows?.getKeepFunc();
|
this._keepFunc = _skippableRows?.getKeepFunc();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -572,7 +571,7 @@ export class SortedRowSet extends RowListener {
|
|||||||
/**
|
/**
|
||||||
* Re-sorts the array according to the new compareFunc.
|
* Re-sorts the array according to the new compareFunc.
|
||||||
*/
|
*/
|
||||||
public updateSort(compareFunc: CompareFunc<RowId>): void {
|
public updateSort(compareFunc: CompareFunc<UIRowId>): void {
|
||||||
this._compareFunc = compareFunc;
|
this._compareFunc = compareFunc;
|
||||||
if (!this._isPaused) {
|
if (!this._isPaused) {
|
||||||
this._koArray.assign(Array.from(this._koArray.peek()).sort(this._compareFunc));
|
this._koArray.assign(Array.from(this._koArray.peek()).sort(this._compareFunc));
|
||||||
@ -650,7 +649,7 @@ export class SortedRowSet extends RowListener {
|
|||||||
|
|
||||||
// Filter out any rows that should be skipped. This is a no-op if no _keepFunc was found.
|
// Filter out any rows that should be skipped. This is a no-op if no _keepFunc was found.
|
||||||
// All rows that sort within nContext rows of something meant to be kept are also kept.
|
// All rows that sort within nContext rows of something meant to be kept are also kept.
|
||||||
private _keep(rows: RowId[], nContext: number = 2) {
|
private _keep(rows: UIRowId[], nContext: number = 2) {
|
||||||
// Nothing to be done if there's no _keepFunc.
|
// Nothing to be done if there's no _keepFunc.
|
||||||
if (!this._keepFunc) { return rows; }
|
if (!this._keepFunc) { return rows; }
|
||||||
|
|
||||||
@ -706,7 +705,7 @@ export class SortedRowSet extends RowListener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type RowTester = (rowId: RowId) => boolean;
|
type RowTester = (rowId: UIRowId) => boolean;
|
||||||
/**
|
/**
|
||||||
* RowWatcher is a RowListener that maintains an observable function that checks whether a row
|
* RowWatcher is a RowListener that maintains an observable function that checks whether a row
|
||||||
* is in the connected RowSource.
|
* is in the connected RowSource.
|
||||||
@ -718,7 +717,7 @@ export class RowWatcher extends RowListener {
|
|||||||
public rowFilter: Observable<RowTester> = Observable.create(this, () => false);
|
public rowFilter: Observable<RowTester> = Observable.create(this, () => false);
|
||||||
// We count the number of times the row is added or removed from the source.
|
// We count the number of times the row is added or removed from the source.
|
||||||
// In most cases row is added and removed only once.
|
// In most cases row is added and removed only once.
|
||||||
private _rowCounter: Map<RowId, number> = new Map();
|
private _rowCounter: Map<UIRowId, number> = new Map();
|
||||||
|
|
||||||
public clear() {
|
public clear() {
|
||||||
this._rowCounter.clear();
|
this._rowCounter.clear();
|
||||||
|
@ -10,7 +10,7 @@ import {ColumnFilter, NEW_FILTER_JSON} from 'app/client/models/ColumnFilter';
|
|||||||
import {ColumnFilterMenuModel, IFilterCount} from 'app/client/models/ColumnFilterMenuModel';
|
import {ColumnFilterMenuModel, IFilterCount} from 'app/client/models/ColumnFilterMenuModel';
|
||||||
import {ColumnRec, ViewFieldRec} from 'app/client/models/DocModel';
|
import {ColumnRec, ViewFieldRec} from 'app/client/models/DocModel';
|
||||||
import {FilterInfo} from 'app/client/models/entities/ViewSectionRec';
|
import {FilterInfo} from 'app/client/models/entities/ViewSectionRec';
|
||||||
import {RowId, RowSource} from 'app/client/models/rowset';
|
import {RowSource} from 'app/client/models/rowset';
|
||||||
import {ColumnFilterFunc, SectionFilter} from 'app/client/models/SectionFilter';
|
import {ColumnFilterFunc, SectionFilter} from 'app/client/models/SectionFilter';
|
||||||
import {TableData} from 'app/client/models/TableData';
|
import {TableData} from 'app/client/models/TableData';
|
||||||
import {cssInput} from 'app/client/ui/cssInput';
|
import {cssInput} from 'app/client/ui/cssInput';
|
||||||
@ -46,6 +46,7 @@ import {relativeDatesControl} from 'app/client/ui/ColumnFilterMenuUtils';
|
|||||||
import {FocusLayer} from 'app/client/lib/FocusLayer';
|
import {FocusLayer} from 'app/client/lib/FocusLayer';
|
||||||
import {DateRangeOptions, IDateRangeOption} from 'app/client/ui/DateRangeOptions';
|
import {DateRangeOptions, IDateRangeOption} from 'app/client/ui/DateRangeOptions';
|
||||||
import {createFormatter} from 'app/common/ValueFormatter';
|
import {createFormatter} from 'app/common/ValueFormatter';
|
||||||
|
import {UIRowId} from 'app/common/TableData';
|
||||||
|
|
||||||
const t = makeT('ColumnFilterMenu');
|
const t = makeT('ColumnFilterMenu');
|
||||||
|
|
||||||
@ -833,7 +834,7 @@ interface ICountOptions {
|
|||||||
* the possible choices as keys).
|
* the possible choices as keys).
|
||||||
* Note that this logic is replicated in BaseView.prototype.filterByThisCellValue.
|
* Note that this logic is replicated in BaseView.prototype.filterByThisCellValue.
|
||||||
*/
|
*/
|
||||||
function addCountsToMap(valueMap: Map<CellValue, IFilterCount>, rowIds: RowId[],
|
function addCountsToMap(valueMap: Map<CellValue, IFilterCount>, rowIds: UIRowId[],
|
||||||
{ keyMapFunc = identity, labelMapFunc = identity, columnType,
|
{ keyMapFunc = identity, labelMapFunc = identity, columnType,
|
||||||
areHiddenRows = false, valueMapFunc }: ICountOptions) {
|
areHiddenRows = false, valueMapFunc }: ICountOptions) {
|
||||||
|
|
||||||
|
@ -217,10 +217,12 @@ export class VisibleFieldsConfig extends Disposable {
|
|||||||
primaryButton(
|
primaryButton(
|
||||||
dom.text((use) => t("Hide {{label}}", {label: use(this._fieldLabel)})),
|
dom.text((use) => t("Hide {{label}}", {label: use(this._fieldLabel)})),
|
||||||
dom.on('click', () => this._removeSelectedFields()),
|
dom.on('click', () => this._removeSelectedFields()),
|
||||||
|
testId('visible-hide')
|
||||||
),
|
),
|
||||||
basicButton(
|
basicButton(
|
||||||
t("Clear"),
|
t("Clear"),
|
||||||
dom.on('click', () => this._setVisibleCheckboxes(fieldsDraggable, false)),
|
dom.on('click', () => this._setVisibleCheckboxes(fieldsDraggable, false)),
|
||||||
|
testId('visible-clear')
|
||||||
),
|
),
|
||||||
testId('visible-batch-buttons')
|
testId('visible-batch-buttons')
|
||||||
),
|
),
|
||||||
@ -259,10 +261,12 @@ export class VisibleFieldsConfig extends Disposable {
|
|||||||
primaryButton(
|
primaryButton(
|
||||||
dom.text((use) => t("Show {{label}}", {label: use(this._fieldLabel)})),
|
dom.text((use) => t("Show {{label}}", {label: use(this._fieldLabel)})),
|
||||||
dom.on('click', () => this._addSelectedFields()),
|
dom.on('click', () => this._addSelectedFields()),
|
||||||
|
testId('hidden-show')
|
||||||
),
|
),
|
||||||
basicButton(
|
basicButton(
|
||||||
t("Clear"),
|
t("Clear"),
|
||||||
dom.on('click', () => this._setHiddenCheckboxes(hiddenFieldsDraggable, false)),
|
dom.on('click', () => this._setHiddenCheckboxes(hiddenFieldsDraggable, false)),
|
||||||
|
testId('hidden-clear')
|
||||||
),
|
),
|
||||||
testId('hidden-batch-buttons')
|
testId('hidden-batch-buttons')
|
||||||
)
|
)
|
||||||
|
@ -2,7 +2,7 @@ import {ActionGroup} from 'app/common/ActionGroup';
|
|||||||
import {AssistanceRequest, AssistanceResponse} from 'app/common/AssistancePrompts';
|
import {AssistanceRequest, AssistanceResponse} from 'app/common/AssistancePrompts';
|
||||||
import {BulkAddRecord, CellValue, TableDataAction, UserAction} from 'app/common/DocActions';
|
import {BulkAddRecord, CellValue, TableDataAction, UserAction} from 'app/common/DocActions';
|
||||||
import {FormulaProperties} from 'app/common/GranularAccessClause';
|
import {FormulaProperties} from 'app/common/GranularAccessClause';
|
||||||
import {UIRowId} from 'app/common/UIRowId';
|
import {UIRowId} from 'app/common/TableData';
|
||||||
import {FetchUrlOptions, UploadResult} from 'app/common/uploads';
|
import {FetchUrlOptions, UploadResult} from 'app/common/uploads';
|
||||||
import {DocStateComparison, PermissionData, UserAccessData} from 'app/common/UserAPI';
|
import {DocStateComparison, PermissionData, UserAccessData} from 'app/common/UserAPI';
|
||||||
import {ParseOptions} from 'app/plugin/FileParserAPI';
|
import {ParseOptions} from 'app/plugin/FileParserAPI';
|
||||||
|
@ -8,12 +8,16 @@ import {
|
|||||||
import {getDefaultForType} from 'app/common/gristTypes';
|
import {getDefaultForType} from 'app/common/gristTypes';
|
||||||
import {arrayRemove, arraySplice, getDistinctValues} from 'app/common/gutil';
|
import {arrayRemove, arraySplice, getDistinctValues} from 'app/common/gutil';
|
||||||
import {SchemaTypes} from "app/common/schema";
|
import {SchemaTypes} from "app/common/schema";
|
||||||
import {UIRowId} from 'app/common/UIRowId';
|
|
||||||
import isEqual = require('lodash/isEqual');
|
import isEqual = require('lodash/isEqual');
|
||||||
import fromPairs = require('lodash/fromPairs');
|
import fromPairs = require('lodash/fromPairs');
|
||||||
|
|
||||||
export interface ColTypeMap { [colId: string]: string; }
|
export interface ColTypeMap { [colId: string]: string; }
|
||||||
|
|
||||||
|
// This is the row ID used in the client, but it's helpful to have available in some common code
|
||||||
|
// as well, which is why it's declared in app/common. Note that for data actions and stored data,
|
||||||
|
// 'new' is not used.
|
||||||
|
export type UIRowId = number | 'new';
|
||||||
|
|
||||||
type UIRowFunc<T> = (rowId: UIRowId) => T;
|
type UIRowFunc<T> = (rowId: UIRowId) => T;
|
||||||
|
|
||||||
interface ColData {
|
interface ColData {
|
||||||
|
@ -1,4 +0,0 @@
|
|||||||
// This is the row ID used in the client, but it's helpful to have available in some common code
|
|
||||||
// as well, which is why it's declared in app/common. Note that for data actions and stored data,
|
|
||||||
// 'new' is not used.
|
|
||||||
export type UIRowId = number | 'new';
|
|
@ -5,7 +5,7 @@ import {encodeQueryParams, isAffirmative} from 'app/common/gutil';
|
|||||||
import {LocalPlugin} from 'app/common/plugin';
|
import {LocalPlugin} from 'app/common/plugin';
|
||||||
import {StringUnion} from 'app/common/StringUnion';
|
import {StringUnion} from 'app/common/StringUnion';
|
||||||
import {TelemetryLevel} from 'app/common/Telemetry';
|
import {TelemetryLevel} from 'app/common/Telemetry';
|
||||||
import {UIRowId} from 'app/common/UIRowId';
|
import {UIRowId} from 'app/common/TableData';
|
||||||
import {getGristConfig} from 'app/common/urlUtils';
|
import {getGristConfig} from 'app/common/urlUtils';
|
||||||
import {Document} from 'app/common/UserAPI';
|
import {Document} from 'app/common/UserAPI';
|
||||||
import clone = require('lodash/clone');
|
import clone = require('lodash/clone');
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import {TableData} from 'app/common/TableData';
|
import {TableData, UIRowId} from 'app/common/TableData';
|
||||||
import {UIRowId} from 'app/common/UIRowId';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return whether a table (identified by the rowId of its metadata record) should
|
* Return whether a table (identified by the rowId of its metadata record) should
|
||||||
|
@ -72,9 +72,8 @@ import {InactivityTimer} from 'app/common/InactivityTimer';
|
|||||||
import {Interval} from 'app/common/Interval';
|
import {Interval} from 'app/common/Interval';
|
||||||
import * as roles from 'app/common/roles';
|
import * as roles from 'app/common/roles';
|
||||||
import {schema, SCHEMA_VERSION} from 'app/common/schema';
|
import {schema, SCHEMA_VERSION} from 'app/common/schema';
|
||||||
import {MetaRowRecord, SingleCell} from 'app/common/TableData';
|
import {MetaRowRecord, SingleCell, UIRowId} from 'app/common/TableData';
|
||||||
import {TelemetryEvent, TelemetryMetadataByLevel} from 'app/common/Telemetry';
|
import {TelemetryEvent, TelemetryMetadataByLevel} from 'app/common/Telemetry';
|
||||||
import {UIRowId} from 'app/common/UIRowId';
|
|
||||||
import {FetchUrlOptions, UploadResult} from 'app/common/uploads';
|
import {FetchUrlOptions, UploadResult} from 'app/common/uploads';
|
||||||
import {Document as APIDocument, DocReplacementOptions, DocState, DocStateComparison} from 'app/common/UserAPI';
|
import {Document as APIDocument, DocReplacementOptions, DocState, DocStateComparison} from 'app/common/UserAPI';
|
||||||
import {convertFromColumn} from 'app/common/ValueConverter';
|
import {convertFromColumn} from 'app/common/ValueConverter';
|
||||||
|
@ -7,30 +7,134 @@ describe('LinkingSelector', function() {
|
|||||||
|
|
||||||
const cleanup = setupTestSuite({team: true});
|
const cleanup = setupTestSuite({team: true});
|
||||||
let session: gu.Session;
|
let session: gu.Session;
|
||||||
|
let docId: string;
|
||||||
|
|
||||||
afterEach(() => gu.checkForErrors());
|
afterEach(() => gu.checkForErrors());
|
||||||
|
|
||||||
before(async function() {
|
before(async function() {
|
||||||
session = await gu.session().login();
|
session = await gu.session().login();
|
||||||
const doc = await session.tempDoc(cleanup, 'Class Enrollment.grist', {load: false});
|
docId = (await session.tempDoc(cleanup, 'Class Enrollment.grist')).id;
|
||||||
await session.loadDoc(`/doc/${doc.id}/p/7`);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
interface CursorSelectorInfo {
|
it('should update linked grid view on filter change', async function() {
|
||||||
linkSelector: false | number;
|
// Add new page with summarized classes by Start_Date.
|
||||||
cursor: false | {rowNum: number, col: number};
|
await gu.addNewPage('Table', 'Classes', {
|
||||||
}
|
summarize: ['Start_Date'],
|
||||||
|
});
|
||||||
|
|
||||||
async function getCursorSelectorInfo(section: WebElement): Promise<CursorSelectorInfo> {
|
// Add a new linked widget that shows classes by this date.
|
||||||
const hasCursor = await section.find('.active_cursor').isPresent();
|
await gu.addNewSection('Table', 'Classes', {
|
||||||
const hasSelector = await section.find('.link_selector_row').isPresent();
|
selectBy: 'CLASSES [by Start_Date]'
|
||||||
return {
|
});
|
||||||
linkSelector: hasSelector && Number(await section.find('.link_selector_row .gridview_data_row_num').getText()),
|
|
||||||
cursor: hasCursor && await gu.getCursorPosition(section),
|
// Click on the first date.
|
||||||
};
|
await gu.getCell('Start_Date', 1, 'CLASSES [by Start_Date]').click();
|
||||||
}
|
// Make sure we know which row is selected.
|
||||||
|
assert.equal(await gu.getCell('Start_Date', 1).getText(), '2018-09-13');
|
||||||
|
|
||||||
|
// Show only Start_Date in the linked section.
|
||||||
|
await gu.selectSectionByTitle('CLASSES');
|
||||||
|
await gu.openWidgetPanel();
|
||||||
|
await gu.selectAllVisibleColumns();
|
||||||
|
await gu.toggleVisibleColumn('Start_Date');
|
||||||
|
await gu.hideVisibleColumns();
|
||||||
|
|
||||||
|
// Make sure we see the same date.
|
||||||
|
await gu.getCell('Start_Date', 1, 'CLASSES').click();
|
||||||
|
assert.equal(await gu.getCell('Start_Date', 1).getText(), '2018-09-13');
|
||||||
|
|
||||||
|
// Now filter the summary table to not show the selected date.
|
||||||
|
await gu.selectSectionByTitle('CLASSES [by Start_Date]');
|
||||||
|
await gu.filterBy('Start_Date', false, ['2018-09-14']);
|
||||||
|
// Make sure this is filtered out.
|
||||||
|
assert.equal(await gu.getCell('Start_Date', 1, 'CLASSES [by Start_Date]').getText(), '2018-09-14');
|
||||||
|
// Make sure the linked section is updated.
|
||||||
|
assert.equal(await gu.getCell('Start_Date', 1, 'CLASSES').getText(), '2018-09-14');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update linked card on filter change with same record linking', async function() {
|
||||||
|
// Test the same for the card view and same record linking (it uses different code).
|
||||||
|
// Add a list and a card view of the same table and link them together.
|
||||||
|
await gu.addNewPage('Table', 'Classes');
|
||||||
|
|
||||||
|
// Hide all columns in the list view.
|
||||||
|
await gu.openWidgetPanel();
|
||||||
|
await gu.selectAllVisibleColumns();
|
||||||
|
await gu.toggleVisibleColumn('Start_Date');
|
||||||
|
await gu.hideVisibleColumns();
|
||||||
|
|
||||||
|
// Rename it to list.
|
||||||
|
await gu.renameActiveSection('List');
|
||||||
|
|
||||||
|
// Now add a card view.
|
||||||
|
await gu.addNewSection('Card', 'Classes', {
|
||||||
|
'selectBy': 'List'
|
||||||
|
});
|
||||||
|
await gu.renameActiveSection('Card');
|
||||||
|
await gu.selectAllVisibleColumns();
|
||||||
|
await gu.toggleVisibleColumn('Start_Date');
|
||||||
|
await gu.hideVisibleColumns();
|
||||||
|
|
||||||
|
// Select the second row.
|
||||||
|
await gu.selectSectionByTitle('List');
|
||||||
|
await gu.getCell('Start_Date', 2).click();
|
||||||
|
// Make sure we know the second row is selected.
|
||||||
|
assert.equal(await gu.getCell('Start_Date', 2).getText(), '2018-09-14');
|
||||||
|
assert.equal((await gu.getCursorPosition()).rowNum, 2);
|
||||||
|
|
||||||
|
// Make sure it was also updated in the card view.
|
||||||
|
await gu.selectSectionByTitle('Card');
|
||||||
|
assert.equal(await gu.getDetailCell('Start_Date', 1).getText(), '2018-09-14');
|
||||||
|
|
||||||
|
// Now filter it out, using pinned filters (to not alter the cursor position).
|
||||||
|
await gu.selectSectionByTitle('List');
|
||||||
|
// Pin the filter to the panel by just adding it (it is pinned by default).
|
||||||
|
await gu.sortAndFilter()
|
||||||
|
.then(x => x.addColumn())
|
||||||
|
.then(x => x.clickColumn('Start_Date'))
|
||||||
|
.then(x => x.close())
|
||||||
|
.then(x => x.click());
|
||||||
|
|
||||||
|
// Open the pinned filter, and filter out the date.
|
||||||
|
await gu.openPinnedFilter('Start_Date')
|
||||||
|
.then(x => x.toggleValue('2018-09-14'))
|
||||||
|
.then(x => x.close());
|
||||||
|
|
||||||
|
// Make sure we see it as the first row
|
||||||
|
assert.equal(await gu.getCell('Start_Date', 1).getText(), '2018-09-13');
|
||||||
|
assert.equal(await gu.getCell('Start_Date', 2).getText(), '2019-01-27');
|
||||||
|
// And cursor was moved to the first row.
|
||||||
|
assert.equal((await gu.getCursorPosition()).rowNum, 1);
|
||||||
|
// Make sure the card view is updated.
|
||||||
|
await gu.selectSectionByTitle('Card');
|
||||||
|
assert.equal(await gu.getDetailCell('Start_Date', 1).getText(), '2018-09-13');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update linked section for the first row', async function() {
|
||||||
|
// There was a bug here. First row wasn't somehow triggering the linked section to update itself,
|
||||||
|
// when it was filtered out, for self-linking.
|
||||||
|
await gu.selectSectionByTitle('List');
|
||||||
|
await gu.removeFilters();
|
||||||
|
|
||||||
|
// Select first row.
|
||||||
|
await gu.getCell('Start_Date', 1).click();
|
||||||
|
// Make sure we know what we selected.
|
||||||
|
assert.equal(await gu.getCell('Start_Date', 1, 'List').getText(), '2018-09-13');
|
||||||
|
// Make sure that card reflects it.
|
||||||
|
assert.equal(await gu.getDetailCell('Start_Date', 1, 'Card').getText(), '2018-09-13');
|
||||||
|
// Now unfilter it.
|
||||||
|
await gu.openColumnFilter('Start_Date')
|
||||||
|
.then(x => x.toggleValue('2018-09-13'))
|
||||||
|
.then(x => x.close());
|
||||||
|
// Make sure that List is updated in the first row.
|
||||||
|
assert.equal(await gu.getCell('Start_Date', 1, 'List').getText(), '2018-09-14');
|
||||||
|
// Make sure that Card is updated accordingly.
|
||||||
|
assert.equal(await gu.getDetailCell('Start_Date', 1, 'Card').getText(), '2018-09-14');
|
||||||
|
});
|
||||||
|
|
||||||
it('should mark selected row used for linking', async function() {
|
it('should mark selected row used for linking', async function() {
|
||||||
|
await session.loadDoc(`/doc/${docId}/p/7`);
|
||||||
|
|
||||||
const families = gu.getSection('FAMILIES');
|
const families = gu.getSection('FAMILIES');
|
||||||
const students = gu.getSection('STUDENTS');
|
const students = gu.getSection('STUDENTS');
|
||||||
const enrollments = gu.getSection('ENROLLMENTS');
|
const enrollments = gu.getSection('ENROLLMENTS');
|
||||||
@ -94,3 +198,18 @@ describe('LinkingSelector', function() {
|
|||||||
assert.deepEqual(await getCursorSelectorInfo(families), {linkSelector: 3, cursor: false});
|
assert.deepEqual(await getCursorSelectorInfo(families), {linkSelector: 3, cursor: false});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
interface CursorSelectorInfo {
|
||||||
|
linkSelector: false | number;
|
||||||
|
cursor: false | {rowNum: number, col: number};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getCursorSelectorInfo(section: WebElement): Promise<CursorSelectorInfo> {
|
||||||
|
const hasCursor = await section.find('.active_cursor').isPresent();
|
||||||
|
const hasSelector = await section.find('.link_selector_row').isPresent();
|
||||||
|
return {
|
||||||
|
linkSelector: hasSelector && Number(await section.find('.link_selector_row .gridview_data_row_num').getText()),
|
||||||
|
cursor: hasCursor && await gu.getCursorPosition(section),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@ -132,7 +132,7 @@ describe('Views.ntest', function() {
|
|||||||
await gu.openColumnMenu('A');
|
await gu.openColumnMenu('A');
|
||||||
await $(`.grist-floating-menu .test-sort-asc`).click();
|
await $(`.grist-floating-menu .test-sort-asc`).click();
|
||||||
// Delete the table.
|
// Delete the table.
|
||||||
await gu.removeTable("Table4");
|
await gu.sendActions([['RemoveTable', 'Table4']]);
|
||||||
await gu.actions.selectTabView('Table1');
|
await gu.actions.selectTabView('Table1');
|
||||||
// Assert that the default section (Table1 record) is now active.
|
// Assert that the default section (Table1 record) is now active.
|
||||||
assert.equal(await $('.active_section > .viewsection_title').text(), 'TABLE1');
|
assert.equal(await $('.active_section > .viewsection_title').text(), 'TABLE1');
|
||||||
|
@ -1467,6 +1467,29 @@ export async function moveToHidden(col: string) {
|
|||||||
await waitForServer();
|
await waitForServer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clicks `Select All` in visible columns section.
|
||||||
|
*/
|
||||||
|
export async function selectAllVisibleColumns() {
|
||||||
|
await driver.find('.test-vfc-visible-fields-select-all').click();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle checkbox for a column in visible columns section.
|
||||||
|
*/
|
||||||
|
export async function toggleVisibleColumn(col: string) {
|
||||||
|
const row = await driver.findContent(".test-vfc-visible-fields .kf_draggable_content", exactMatch(col));
|
||||||
|
await row.find('input').click();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clicks `Hide Columns` button in visible columns section.
|
||||||
|
*/
|
||||||
|
export async function hideVisibleColumns() {
|
||||||
|
await driver.find('.test-vfc-visible-hide').click();
|
||||||
|
await waitForServer();
|
||||||
|
}
|
||||||
|
|
||||||
export async function search(what: string) {
|
export async function search(what: string) {
|
||||||
await driver.find('.test-tb-search-icon').click();
|
await driver.find('.test-tb-search-icon').click();
|
||||||
await driver.sleep(500);
|
await driver.sleep(500);
|
||||||
@ -2815,18 +2838,123 @@ export async function scrollActiveViewTop() {
|
|||||||
* Filters a column in a Grid using the filter menu.
|
* Filters a column in a Grid using the filter menu.
|
||||||
*/
|
*/
|
||||||
export async function filterBy(col: IColHeader|string, save: boolean, values: (string|RegExp)[]) {
|
export async function filterBy(col: IColHeader|string, save: boolean, values: (string|RegExp)[]) {
|
||||||
await openColumnMenu(col, 'Filter');
|
const filter = await openColumnFilter(col);
|
||||||
// Select none at start
|
await filter.none();
|
||||||
await driver.findContent('.test-filter-menu-bulk-action', /None/).click();
|
for (const value of values) {
|
||||||
for(const value of values) {
|
await filter.toggleValue(value);
|
||||||
await driver.findContent('.test-filter-menu-list label', value).click();
|
|
||||||
}
|
}
|
||||||
// Save filters
|
await filter.close();
|
||||||
await driver.find('.test-filter-menu-apply-btn').click();
|
|
||||||
if (save) {
|
if (save) {
|
||||||
await driver.find('.test-section-menu-small-btn-save').click();
|
await filter.save();
|
||||||
}
|
}
|
||||||
await waitForServer();
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens a filter menu for a column and returns a controller for it.
|
||||||
|
*/
|
||||||
|
export async function openColumnFilter(col: IColHeader|string) {
|
||||||
|
await openColumnMenu(col, 'Filter');
|
||||||
|
return filterController;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens a filter menu for a column and returns a controller for it.
|
||||||
|
*/
|
||||||
|
export async function openPinnedFilter(col: string) {
|
||||||
|
const filterBar = driver.find('.active_section .test-filter-bar');
|
||||||
|
const pinnedFilter = filterBar.findContent('.test-filter-field', col);
|
||||||
|
await pinnedFilter.click();
|
||||||
|
return filterController;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filterController = {
|
||||||
|
async toggleValue(value: string|RegExp) {
|
||||||
|
await driver.findContent('.test-filter-menu-list label', value).click();
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
async none() {
|
||||||
|
await driver.findContent('.test-filter-menu-bulk-action', /None/).click();
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
async all() {
|
||||||
|
await driver.findContent('.test-filter-menu-bulk-action', /All/).click();
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
async close() {
|
||||||
|
await driver.find('.test-filter-menu-apply-btn').click();
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
async cancel() {
|
||||||
|
await driver.find('.test-filter-menu-cancel-btn').click();
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
async save() {
|
||||||
|
await driver.find('.test-section-menu-small-btn-save').click();
|
||||||
|
await waitForServer();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the filter menu in the current section, and removes all filters. Optionally saves it.
|
||||||
|
*/
|
||||||
|
export async function removeFilters(save = false) {
|
||||||
|
const sectionFilter = await sortAndFilter();
|
||||||
|
for(const filter of await sectionFilter.filters()) {
|
||||||
|
await filter.remove();
|
||||||
|
}
|
||||||
|
if (save) {
|
||||||
|
await sectionFilter.save();
|
||||||
|
} else {
|
||||||
|
await sectionFilter.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clicks on the filter icon in the current section, and returns a controller for it for interactions.
|
||||||
|
*/
|
||||||
|
export async function sortAndFilter() {
|
||||||
|
const ctrl = {
|
||||||
|
async addColumn() {
|
||||||
|
await driver.find('.test-filter-config-add-filter-btn').click();
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
async clickColumn(col: string) {
|
||||||
|
await driver.findContent(".test-sd-searchable-list-item", col).click();
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
async close() {
|
||||||
|
await driver.find('.test-filter-menu-apply-btn').click();
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
async save() {
|
||||||
|
await driver.find('.test-section-menu-btn-save').click();
|
||||||
|
await waitForServer();
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Clicks the filter icon in the current section (can be used to close the filter menu or open it)
|
||||||
|
*/
|
||||||
|
async click() {
|
||||||
|
await driver.find('.active_section .test-section-menu-filter-icon').click();
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
async filters() {
|
||||||
|
const items = await driver.findAll('.test-filter-config-filter');
|
||||||
|
return items.map(item => ({
|
||||||
|
async remove() {
|
||||||
|
await item.find('.test-filter-config-remove-filter').click();
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
async togglePin() {
|
||||||
|
await item.find('.test-filter-config-pin-filter').click();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
await ctrl.click();
|
||||||
|
return ctrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PinnedFilter {
|
export interface PinnedFilter {
|
||||||
|
Loading…
Reference in New Issue
Block a user