From 2f6eafff352ccee04c687f9fa330578ab4734249 Mon Sep 17 00:00:00 2001 From: Cyprien P Date: Tue, 1 Feb 2022 20:51:40 +0100 Subject: [PATCH] (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 --- app/client/components/LinkingState.ts | 16 ++++++++++++++-- app/client/components/WidgetFrame.ts | 8 ++++++++ app/client/models/entities/ViewSectionRec.ts | 11 ++++++++++- app/client/ui/selectBy.ts | 17 +++++++++++++++-- app/plugin/GristAPI-ti.ts | 5 +++-- app/plugin/GristAPI.ts | 6 ++++++ app/plugin/grist-plugin-api.ts | 2 ++ 7 files changed, 58 insertions(+), 7 deletions(-) diff --git a/app/client/components/LinkingState.ts b/app/client/components/LinkingState.ts index c71d3d9b..11aa8cc6 100644 --- a/app/client/components/LinkingState.ts +++ b/app/client/components/LinkingState.ts @@ -10,7 +10,7 @@ import {ClientQuery, QueryOperation} from "app/common/ActiveDocAPI"; import {isList, isRefListType} from "app/common/gristTypes"; import * as gutil from "app/common/gutil"; import {encodeObject} from 'app/plugin/objtypes'; -import {Disposable} from "grainjs"; +import {Disposable, toKo} from "grainjs"; import * as ko from "knockout"; import mapValues = require('lodash/mapValues'); import pickBy = require('lodash/pickBy'); @@ -87,7 +87,9 @@ export class LinkingState extends Disposable { if (tgtColId) { 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); } else { this.filterColValues = this._simpleFilter(tgtColId, operation, (rowId => [rowId])); @@ -122,6 +124,8 @@ export class LinkingState extends Disposable { } 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 // column or map representing the inverse of summary table's "group" column. + } else if (srcSection.parentKey() === 'custom') { + this.filterColValues = this._srcCustomFilter('id', 'in'); } else { const srcValueFunc = srcColId ? this._makeSrcCellGetter() : identity; 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 | 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 // in srcCol in the selected record of srcSection. // Uses a row model to create a dependency on the cell's value, diff --git a/app/client/components/WidgetFrame.ts b/app/client/components/WidgetFrame.ts index 76ff44f0..9deb74f5 100644 --- a/app/client/components/WidgetFrame.ts +++ b/app/client/components/WidgetFrame.ts @@ -360,6 +360,14 @@ export class GristViewImpl implements GristView { return data; } + public async allowSelectBy(): Promise { + this._baseView.viewSection.allowSelectBy.set(true); + } + + public async setSelectedRows(rowIds: number[]): Promise { + this._baseView.viewSection.selectedRows.set(rowIds); + } + private _visibleColumns() { const columns: ColumnRec[] = this._baseView.viewSection.columns.peek(); const hiddenCols = this._baseView.viewSection.hiddenColumns.peek().map(c => c.id.peek()); diff --git a/app/client/models/entities/ViewSectionRec.ts b/app/client/models/entities/ViewSectionRec.ts index cfb73289..1e60ff16 100644 --- a/app/client/models/entities/ViewSectionRec.ts +++ b/app/client/models/entities/ViewSectionRec.ts @@ -22,7 +22,7 @@ import {arrayRepeat} from 'app/common/gutil'; import {Sort} from 'app/common/SortSpec'; import {ColumnsToMap, WidgetColumnMap} from 'app/plugin/CustomSectionAPI'; import {ColumnToMapImpl} from 'app/client/models/ColumnToMap'; -import {Computed} from 'grainjs'; +import {Computed, Observable} from 'grainjs'; import * as ko from 'knockout'; 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). desiredAccessLevel: ko.Observable; + // Show widget as linking source. Used by custom widget. + allowSelectBy: Observable; + + // List of selected rows + selectedRows: Observable; + // Save all filters of fields/columns in the section. saveFilters(): Promise; @@ -562,4 +568,7 @@ export function createViewSectionRec(this: ViewSectionRec, docModel: DocModel): } return result; }); + + this.allowSelectBy = Observable.create(this, false); + this.selectedRows = Observable.create(this, []); } diff --git a/app/client/ui/selectBy.ts b/app/client/ui/selectBy.ts index 4960cb49..44a5f05e 100644 --- a/app/client/ui/selectBy.ts +++ b/app/client/ui/selectBy.ts @@ -74,11 +74,24 @@ function isValidLink(source: LinkNode, target: LinkNode) { return false; } - // cannot select from chart or custom - if (['chart', 'custom'].includes(source.widgetType)) { + // cannot select from chart + if (source.widgetType === 'chart') { 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 if (source.ancestors.has(target.section.getRowId())) { return false; diff --git a/app/plugin/GristAPI-ti.ts b/app/plugin/GristAPI-ti.ts index c1a599bf..f195b8b7 100644 --- a/app/plugin/GristAPI-ti.ts +++ b/app/plugin/GristAPI-ti.ts @@ -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 GristAPI = t.iface([], { - "render": t.func("number", t.param("path", "string"), t.param("target", "RenderTarget"), - t.param("options", "RenderOptions", true)), + "render": t.func("number", t.param("path", "string"), t.param("target", "RenderTarget"), t.param("options", "RenderOptions", true)), "dispose": t.func("void", t.param("procId", "number")), "subscribe": 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([], { "fetchSelectedTable": t.func("any"), "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 = { diff --git a/app/plugin/GristAPI.ts b/app/plugin/GristAPI.ts index 405cef60..843f3d93 100644 --- a/app/plugin/GristAPI.ts +++ b/app/plugin/GristAPI.ts @@ -99,4 +99,10 @@ export interface GristView { // Similar TODO to fetchSelectedTable for return type. fetchSelectedRecord(rowId: number): Promise; + + // Allow custom widget to be listed as a possible source for linking with SELECT BY. + allowSelectBy(): Promise; + + // Set the list of selected rows to be used against any linked widget. Requires `allowSelectBy()`. + setSelectedRows(rowIds: number[]): Promise; } diff --git a/app/plugin/grist-plugin-api.ts b/app/plugin/grist-plugin-api.ts index 6bd9166c..f9fb2859 100644 --- a/app/plugin/grist-plugin-api.ts +++ b/app/plugin/grist-plugin-api.ts @@ -47,6 +47,8 @@ export const coreDocApi = rpc.getStub('GristDocAPI@grist', checkers export const viewApi = rpc.getStub('GristView', checkers.GristView); export const widgetApi = rpc.getStub('WidgetAPI', checkers.WidgetAPI); export const sectionApi = rpc.getStub('CustomSectionAPI', checkers.CustomSectionAPI); +export const allowSelectBy = viewApi.allowSelectBy; +export const setSelectedRows = viewApi.setSelectedRows; export const docApi: GristDocAPI & GristView = { ...coreDocApi,