(core) Adds setSelectedRows to the grist api for custom view

Summary:
This is needed to let custom widget driver filtering of other widget in the same page.

Descripion here:
 - https://grist.quip.com/ctytAQJoFMsM/Hopefully-Small-Projects#temp:C:NNCfe2030b27647439886ca83595

Test Plan: New api tested in a new nbrowser test

Reviewers: paulfitz

Reviewed By: paulfitz

Differential Revision: https://phab.getgrist.com/D3253
This commit is contained in:
Cyprien P 2022-02-01 20:51:40 +01:00
parent e264094412
commit 2f6eafff35
7 changed files with 58 additions and 7 deletions

View File

@ -10,7 +10,7 @@ import {ClientQuery, QueryOperation} from "app/common/ActiveDocAPI";
import {isList, isRefListType} from "app/common/gristTypes"; import {isList, isRefListType} from "app/common/gristTypes";
import * as gutil from "app/common/gutil"; import * as gutil from "app/common/gutil";
import {encodeObject} from 'app/plugin/objtypes'; import {encodeObject} from 'app/plugin/objtypes';
import {Disposable} from "grainjs"; import {Disposable, toKo} from "grainjs";
import * as ko from "knockout"; import * as ko from "knockout";
import mapValues = require('lodash/mapValues'); import mapValues = require('lodash/mapValues');
import pickBy = require('lodash/pickBy'); import pickBy = require('lodash/pickBy');
@ -87,7 +87,9 @@ export class LinkingState extends Disposable {
if (tgtColId) { if (tgtColId) {
const operation = isRefListType(tgtCol.type()) ? 'intersects' : 'in'; const operation = isRefListType(tgtCol.type()) ? 'intersects' : 'in';
if (srcColId) { if (srcSection.parentKey() === 'custom') {
this.filterColValues = this._srcCustomFilter(tgtColId, operation);
} else if (srcColId) {
this.filterColValues = this._srcCellFilter(tgtColId, operation); this.filterColValues = this._srcCellFilter(tgtColId, operation);
} else { } else {
this.filterColValues = this._simpleFilter(tgtColId, operation, (rowId => [rowId])); this.filterColValues = this._simpleFilter(tgtColId, operation, (rowId => [rowId]));
@ -122,6 +124,8 @@ export class LinkingState extends Disposable {
} else if (isSummaryOf(tgtSection.table(), srcSection.table())) { } else if (isSummaryOf(tgtSection.table(), srcSection.table())) {
// TODO: We should move the cursor, but don't currently it for summaries. For that, we need a // TODO: We should move the cursor, but don't currently it for summaries. For that, we need a
// column or map representing the inverse of summary table's "group" column. // column or map representing the inverse of summary table's "group" column.
} else if (srcSection.parentKey() === 'custom') {
this.filterColValues = this._srcCustomFilter('id', 'in');
} else { } else {
const srcValueFunc = srcColId ? this._makeSrcCellGetter() : identity; const srcValueFunc = srcColId ? this._makeSrcCellGetter() : identity;
if (srcValueFunc) { if (srcValueFunc) {
@ -196,6 +200,14 @@ 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)();
return {filters: {[colId]: values}, operations: {[colId]: operation}} as FilterColValues;
}));
}
// Returns a function which returns the value of the cell // Returns a function which returns the value of the cell
// in srcCol in the selected record of srcSection. // in srcCol in the selected record of srcSection.
// Uses a row model to create a dependency on the cell's value, // Uses a row model to create a dependency on the cell's value,

View File

@ -360,6 +360,14 @@ export class GristViewImpl implements GristView {
return data; return data;
} }
public async allowSelectBy(): Promise<void> {
this._baseView.viewSection.allowSelectBy.set(true);
}
public async setSelectedRows(rowIds: number[]): Promise<void> {
this._baseView.viewSection.selectedRows.set(rowIds);
}
private _visibleColumns() { private _visibleColumns() {
const columns: ColumnRec[] = this._baseView.viewSection.columns.peek(); const columns: ColumnRec[] = this._baseView.viewSection.columns.peek();
const hiddenCols = this._baseView.viewSection.hiddenColumns.peek().map(c => c.id.peek()); const hiddenCols = this._baseView.viewSection.hiddenColumns.peek().map(c => c.id.peek());

View File

@ -22,7 +22,7 @@ import {arrayRepeat} from 'app/common/gutil';
import {Sort} from 'app/common/SortSpec'; import {Sort} from 'app/common/SortSpec';
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 {Computed} from 'grainjs'; import {Computed, Observable} from 'grainjs';
import * as ko from 'knockout'; import * as ko from 'knockout';
import defaults = require('lodash/defaults'); import defaults = require('lodash/defaults');
@ -159,6 +159,12 @@ export interface ViewSectionRec extends IRowModel<"_grist_Views_section"> {
// Temporary variable holding widget desired access (changed either from manifest or via API). // Temporary variable holding widget desired access (changed either from manifest or via API).
desiredAccessLevel: ko.Observable<AccessLevel|null>; desiredAccessLevel: ko.Observable<AccessLevel|null>;
// Show widget as linking source. Used by custom widget.
allowSelectBy: Observable<boolean>;
// List of selected rows
selectedRows: Observable<number[]>;
// Save all filters of fields/columns in the section. // Save all filters of fields/columns in the section.
saveFilters(): Promise<void>; saveFilters(): Promise<void>;
@ -562,4 +568,7 @@ export function createViewSectionRec(this: ViewSectionRec, docModel: DocModel):
} }
return result; return result;
}); });
this.allowSelectBy = Observable.create(this, false);
this.selectedRows = Observable.create(this, []);
} }

View File

@ -74,11 +74,24 @@ function isValidLink(source: LinkNode, target: LinkNode) {
return false; return false;
} }
// cannot select from chart or custom // cannot select from chart
if (['chart', 'custom'].includes(source.widgetType)) { if (source.widgetType === 'chart') {
return false; return false;
} }
if (source.widgetType === 'custom') {
// custom widget do not support linking by columns
if (source.tableId !== source.section.table.peek().primaryTableId.peek()) {
return false;
}
// custom widget must allow select by
if (!source.section.allowSelectBy.get()) {
return false;
}
}
// The link must not create a cycle // The link must not create a cycle
if (source.ancestors.has(target.section.getRowId())) { if (source.ancestors.has(target.section.getRowId())) {
return false; return false;

View File

@ -7,8 +7,7 @@ import * as t from "ts-interface-checker";
export const ComponentKind = t.union(t.lit("safeBrowser"), t.lit("safePython"), t.lit("unsafeNode")); export const ComponentKind = t.union(t.lit("safeBrowser"), t.lit("safePython"), t.lit("unsafeNode"));
export const GristAPI = t.iface([], { export const GristAPI = t.iface([], {
"render": t.func("number", t.param("path", "string"), t.param("target", "RenderTarget"), "render": t.func("number", t.param("path", "string"), t.param("target", "RenderTarget"), t.param("options", "RenderOptions", true)),
t.param("options", "RenderOptions", true)),
"dispose": t.func("void", t.param("procId", "number")), "dispose": t.func("void", t.param("procId", "number")),
"subscribe": t.func("void", t.param("tableId", "string")), "subscribe": t.func("void", t.param("tableId", "string")),
"unsubscribe": t.func("void", t.param("tableId", "string")), "unsubscribe": t.func("void", t.param("tableId", "string")),
@ -24,6 +23,8 @@ export const GristDocAPI = t.iface([], {
export const GristView = t.iface([], { export const GristView = t.iface([], {
"fetchSelectedTable": t.func("any"), "fetchSelectedTable": t.func("any"),
"fetchSelectedRecord": t.func("any", t.param("rowId", "number")), "fetchSelectedRecord": t.func("any", t.param("rowId", "number")),
"allowSelectBy": t.func("void"),
"setSelectedRows": t.func("void", t.param("rowIds", t.array("number"))),
}); });
const exportedTypeSuite: t.ITypeSuite = { const exportedTypeSuite: t.ITypeSuite = {

View File

@ -99,4 +99,10 @@ export interface GristView {
// Similar TODO to fetchSelectedTable for return type. // Similar TODO to fetchSelectedTable for return type.
fetchSelectedRecord(rowId: number): Promise<any>; fetchSelectedRecord(rowId: number): Promise<any>;
// Allow custom widget to be listed as a possible source for linking with SELECT BY.
allowSelectBy(): Promise<void>;
// Set the list of selected rows to be used against any linked widget. Requires `allowSelectBy()`.
setSelectedRows(rowIds: number[]): Promise<void>;
} }

View File

@ -47,6 +47,8 @@ export const coreDocApi = rpc.getStub<GristDocAPI>('GristDocAPI@grist', checkers
export const viewApi = rpc.getStub<GristView>('GristView', checkers.GristView); export const viewApi = rpc.getStub<GristView>('GristView', checkers.GristView);
export const widgetApi = rpc.getStub<WidgetAPI>('WidgetAPI', checkers.WidgetAPI); export const widgetApi = rpc.getStub<WidgetAPI>('WidgetAPI', checkers.WidgetAPI);
export const sectionApi = rpc.getStub<CustomSectionAPI>('CustomSectionAPI', checkers.CustomSectionAPI); export const sectionApi = rpc.getStub<CustomSectionAPI>('CustomSectionAPI', checkers.CustomSectionAPI);
export const allowSelectBy = viewApi.allowSelectBy;
export const setSelectedRows = viewApi.setSelectedRows;
export const docApi: GristDocAPI & GristView = { export const docApi: GristDocAPI & GristView = {
...coreDocApi, ...coreDocApi,