(core) Cursor in custom widgets

Summary:
Adding a new method `setCursorPos` in the widget API, and a new configuration option for the ready message `allowSelectBy` that exposes custom widgets in the `Select by` dropdown.
With this, a custom widget can control the position of the linked widgets and is able to change the column in the creator panel.

Test Plan: Added new test. Existing tests should pass.

Reviewers: JakubSerafin

Reviewed By: JakubSerafin

Subscribers: JakubSerafin

Differential Revision: https://phab.getgrist.com/D3993
This commit is contained in:
Jarosław Sadziński
2023-08-28 11:16:17 +02:00
parent c02acff361
commit b6a431dd58
33 changed files with 155 additions and 84 deletions

View File

@@ -1,5 +1,5 @@
import { CursorPos } from "app/client/components/Cursor";
import { DocModel, ViewFieldRec } from "app/client/models/DocModel";
import { CursorPos } from 'app/plugin/GristAPI';
import BaseRowModel = require("app/client/models/BaseRowModel");
/**

View File

@@ -1,6 +1,7 @@
import type {ViewFieldRec} from 'app/client/models/entities/ViewFieldRec';
import type {CellValue} from 'app/common/DocActions';
import type {TableData, UIRowId} from 'app/common/TableData';
import type {TableData} from 'app/common/TableData';
import type {UIRowId} from 'app/plugin/GristAPI';
/**
* The CopySelection class is an abstraction for a subset of currently selected cells.

View File

@@ -8,17 +8,10 @@ import BaseView from 'app/client/components/BaseView';
import * as commands from 'app/client/components/commands';
import BaseRowModel from 'app/client/models/BaseRowModel';
import {LazyArrayModel} from 'app/client/models/DataTableModel';
import type {UIRowId} from 'app/common/TableData';
import {CursorPos, UIRowId} from 'app/plugin/GristAPI';
import {Disposable} from 'grainjs';
import * as ko from 'knockout';
export interface CursorPos {
rowId?: UIRowId;
rowIndex?: number;
fieldIndex?: number;
sectionId?: number;
}
function nullAsUndefined<T>(value: T|null|undefined): T|undefined {
return value == null ? undefined : value;
}

View File

@@ -1,9 +1,9 @@
import {CursorPos} from 'app/client/components/Cursor';
import {GristDoc} from 'app/client/components/GristDoc';
import {getStorage} from 'app/client/lib/storage';
import {IDocPage, isViewDocPage, ViewDocPage} from 'app/common/gristUrls';
import {Disposable, Listener, Observable} from 'grainjs';
import {reportError} from 'app/client/models/errors';
import {CursorPos} from 'app/plugin/GristAPI';
/**
* Enriched cursor position with a view id

View File

@@ -9,7 +9,6 @@ import BaseView from 'app/client/components/BaseView';
import {isNumericLike, isNumericOnly} from 'app/client/components/ChartView';
import {CodeEditorPanel} from 'app/client/components/CodeEditorPanel';
import * as commands from 'app/client/components/commands';
import {CursorPos} from 'app/client/components/Cursor';
import {CursorMonitor, ViewCursorPos} from "app/client/components/CursorMonitor";
import {DocComm} from 'app/client/components/DocComm';
import * as DocConfigTab from 'app/client/components/DocConfigTab';
@@ -70,6 +69,7 @@ import {LocalPlugin} from "app/common/plugin";
import {StringUnion} from 'app/common/StringUnion';
import {TableData} from 'app/common/TableData';
import {DocStateComparison} from 'app/common/UserAPI';
import {CursorPos} from 'app/plugin/GristAPI';
import {
bundleChanges,
Computed,
@@ -1256,7 +1256,7 @@ export class GristDoc extends DisposableWithEvents {
const fieldIndex = activeSection.viewFields.peek().all().findIndex(f => f.colRef.peek() === hash.colRef);
if (fieldIndex >= 0) {
const view = await this._waitForView(activeSection);
view?.setCursorPos({sectionId: hash.sectionId, rowId: hash.rowId, fieldIndex});
view?.setCursorPos({rowId: hash.rowId, fieldIndex});
}
}
this.viewLayout?.maximized.set(hash.sectionId);
@@ -1306,7 +1306,7 @@ export class GristDoc extends DisposableWithEvents {
const fieldIndex = popupSection.viewFields.peek().all().findIndex(f => f.colRef.peek() === hash.colRef);
if (fieldIndex >= 0) {
const view = await this._waitForView(popupSection);
view?.setCursorPos({sectionId: hash.sectionId, rowId: hash.rowId, fieldIndex});
view?.setCursorPos({rowId: hash.rowId, fieldIndex});
}
}
}

View File

@@ -4,13 +4,13 @@ import {DocModel} from 'app/client/models/DocModel';
import {ColumnRec} from "app/client/models/entities/ColumnRec";
import {TableRec} from "app/client/models/entities/TableRec";
import {ViewSectionRec} from "app/client/models/entities/ViewSectionRec";
import {UIRowId} from "app/common/TableData";
import {LinkConfig} from "app/client/ui/selectBy";
import {FilterColValues, QueryOperation} from "app/common/ActiveDocAPI";
import {isList, isListType, isRefListType} from "app/common/gristTypes";
import * as gutil from "app/common/gutil";
import {UIRowId} from 'app/plugin/GristAPI';
import {encodeObject} from 'app/plugin/objtypes';
import {Disposable, toKo} from "grainjs";
import {Disposable} from "grainjs";
import * as ko from "knockout";
import identity = require('lodash/identity');
import mapValues = require('lodash/mapValues');
@@ -85,7 +85,7 @@ export class LinkingState extends Disposable {
if (tgtColId) {
const operation = isRefListType(tgtCol.type()) ? 'intersects' : 'in';
if (srcSection.parentKey() === 'custom') {
if (srcSection.selectedRowsActive()) {
this.filterColValues = this._srcCustomFilter(tgtColId, operation);
} else if (srcColId) {
this.filterColValues = this._srcCellFilter(tgtColId, operation);
@@ -128,7 +128,7 @@ export class LinkingState extends Disposable {
}
_filterColValues(result);
}
} else if (srcSection.parentKey() === 'custom') {
} else if (srcSection.selectedRowsActive()) {
this.filterColValues = this._srcCustomFilter('id', 'in');
} else {
const srcValueFunc = srcColId ? this._makeSrcCellGetter() : identity;
@@ -207,7 +207,7 @@ export class LinkingState extends Disposable {
// Value for this.filterColValues based on the values in srcSection.selectedRows
private _srcCustomFilter(colId: string, operation: QueryOperation): ko.Computed<FilterColValues> | undefined {
return this.autoDispose(ko.computed(() => {
const values = toKo(ko, this._srcSection.selectedRows)();
const values = this._srcSection.selectedRows();
return {filters: {[colId]: values}, operations: {[colId]: operation}} as FilterColValues;
}));
}

View File

@@ -1,8 +1,8 @@
import {CursorPos} from 'app/client/components/Cursor';
import {GristDoc} from 'app/client/components/GristDoc';
import * as dispose from 'app/client/lib/dispose';
import {MinimalActionGroup} from 'app/common/ActionGroup';
import {PromiseChain, setDefault} from 'app/common/gutil';
import {CursorPos} from 'app/plugin/GristAPI';
import {fromKo, Observable} from 'grainjs';
import * as ko from 'knockout';
import sortBy = require('lodash/sortBy');

View File

@@ -7,7 +7,7 @@ import {AccessLevel, isSatisfied} from 'app/common/CustomWidget';
import {DisposableWithEvents} from 'app/common/DisposableWithEvents';
import {BulkColValues, fromTableDataAction, RowRecord} from 'app/common/DocActions';
import {extractInfoFromColType, reencodeAsAny} from 'app/common/gristTypes';
import {AccessTokenOptions, CustomSectionAPI, GristDocAPI, GristView,
import {AccessTokenOptions, CursorPos, CustomSectionAPI, GristDocAPI, GristView,
InteractionOptionsRequest, WidgetAPI, WidgetColumnMap} from 'app/plugin/grist-plugin-api';
import {MsgType, Rpc} from 'grain-rpc';
import {Computed, Disposable, dom, Observable} from 'grainjs';
@@ -380,12 +380,25 @@ export class GristViewImpl implements GristView {
return data;
}
/**
* This is deprecated method to turn on cursor linking. Previously it was used
* to create a custom row id filter. Now widgets can be treated as normal source of linking.
* Now allowSelectBy should be set using the ready event.
*/
public async allowSelectBy(): Promise<void> {
this._baseView.viewSection.allowSelectBy.set(true);
this._baseView.viewSection.allowSelectBy(true);
// This is to preserve a legacy behavior, where when allowSelectBy is called widget expected
// that the filter was already applied to clear all rows.
this._baseView.viewSection.selectedRows([]);
}
public async setSelectedRows(rowIds: number[]): Promise<void> {
this._baseView.viewSection.selectedRows.set(rowIds);
public async setSelectedRows(rowIds: number[]|null): Promise<void> {
this._baseView.viewSection.selectedRows(rowIds);
}
public setCursorPos(cursorPos: CursorPos): Promise<void> {
this._baseView.setCursorPos(cursorPos);
return Promise.resolve();
}
private _visibleColumns() {
@@ -615,5 +628,8 @@ export class CustomSectionAPIImpl extends Disposable implements CustomSectionAPI
} else {
this._section.columnsToMap(null);
}
if (settings.allowSelectBy !== undefined) {
this._section.allowSelectBy(settings.allowSelectBy);
}
}
}