From b6a431dd582369b5a49c7979f49ccec81310d939 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaros=C5=82aw=20Sadzi=C5=84ski?= Date: Mon, 28 Aug 2023 11:16:17 +0200 Subject: [PATCH] (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 --- app/client/components/CellPosition.ts | 2 +- app/client/components/CopySelection.ts | 3 +- app/client/components/Cursor.ts | 9 +---- app/client/components/CursorMonitor.ts | 2 +- app/client/components/GristDoc.ts | 6 +-- app/client/components/LinkingState.ts | 10 ++--- app/client/components/UndoStack.ts | 2 +- app/client/components/WidgetFrame.ts | 24 +++++++++-- app/client/declarations.d.ts | 3 +- app/client/lib/sortUtil.ts | 2 +- app/client/models/DocModel.ts | 2 +- app/client/models/QuerySet.ts | 5 ++- app/client/models/SearchModel.ts | 4 +- app/client/models/SectionFilter.ts | 2 +- app/client/models/entities/ViewSectionRec.ts | 18 +++++---- app/client/models/rowset.ts | 3 +- app/client/ui/ColumnFilterMenu.ts | 38 +++++++++--------- app/client/ui/DescriptionConfig.ts | 4 +- app/client/ui/FieldConfig.ts | 2 +- app/client/ui/selectBy.ts | 2 +- app/client/widgets/FieldEditor.ts | 3 +- app/common/ActiveDocAPI.ts | 3 +- app/common/TableData.ts | 11 ++--- app/common/gristUrls.ts | 2 +- app/common/isHiddenTable.ts | 3 +- app/plugin/CustomSectionAPI-ti.ts | 1 + app/plugin/CustomSectionAPI.ts | 4 ++ app/plugin/GristAPI-ti.ts | 14 ++++++- app/plugin/GristAPI.ts | 42 ++++++++++++++++++-- app/plugin/grist-plugin-api.ts | 4 ++ app/server/lib/ActiveDoc.ts | 4 +- test/fixtures/sites/hello/index.html | 1 + test/nbrowser/gristUtils.ts | 4 +- 33 files changed, 155 insertions(+), 84 deletions(-) diff --git a/app/client/components/CellPosition.ts b/app/client/components/CellPosition.ts index e846147e..8ffacdef 100644 --- a/app/client/components/CellPosition.ts +++ b/app/client/components/CellPosition.ts @@ -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"); /** diff --git a/app/client/components/CopySelection.ts b/app/client/components/CopySelection.ts index d2bce441..5524a570 100644 --- a/app/client/components/CopySelection.ts +++ b/app/client/components/CopySelection.ts @@ -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. diff --git a/app/client/components/Cursor.ts b/app/client/components/Cursor.ts index 1f9f1f03..ad35bcc7 100644 --- a/app/client/components/Cursor.ts +++ b/app/client/components/Cursor.ts @@ -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(value: T|null|undefined): T|undefined { return value == null ? undefined : value; } diff --git a/app/client/components/CursorMonitor.ts b/app/client/components/CursorMonitor.ts index 2590ee40..1c875770 100644 --- a/app/client/components/CursorMonitor.ts +++ b/app/client/components/CursorMonitor.ts @@ -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 diff --git a/app/client/components/GristDoc.ts b/app/client/components/GristDoc.ts index 40b8c72b..610ef14f 100644 --- a/app/client/components/GristDoc.ts +++ b/app/client/components/GristDoc.ts @@ -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}); } } } diff --git a/app/client/components/LinkingState.ts b/app/client/components/LinkingState.ts index ea066f0a..310cb2e3 100644 --- a/app/client/components/LinkingState.ts +++ b/app/client/components/LinkingState.ts @@ -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 | 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; })); } diff --git a/app/client/components/UndoStack.ts b/app/client/components/UndoStack.ts index 17306cc8..f14ff8f2 100644 --- a/app/client/components/UndoStack.ts +++ b/app/client/components/UndoStack.ts @@ -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'); diff --git a/app/client/components/WidgetFrame.ts b/app/client/components/WidgetFrame.ts index b5eeb7b4..645590ee 100644 --- a/app/client/components/WidgetFrame.ts +++ b/app/client/components/WidgetFrame.ts @@ -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 { - 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 { - this._baseView.viewSection.selectedRows.set(rowIds); + public async setSelectedRows(rowIds: number[]|null): Promise { + this._baseView.viewSection.selectedRows(rowIds); + } + + public setCursorPos(cursorPos: CursorPos): Promise { + 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); + } } } diff --git a/app/client/declarations.d.ts b/app/client/declarations.d.ts index 56bfca96..7883bda5 100644 --- a/app/client/declarations.d.ts +++ b/app/client/declarations.d.ts @@ -25,7 +25,7 @@ declare module "app/client/components/Base" { declare module "app/client/components/BaseView" { - import {Cursor, CursorPos} from 'app/client/components/Cursor'; + import {Cursor} from 'app/client/components/Cursor'; import {GristDoc} from 'app/client/components/GristDoc'; import {IGristUrlState} from 'app/common/gristUrls'; import {SelectionSummary} from 'app/client/components/SelectionSummary'; @@ -39,6 +39,7 @@ declare module "app/client/components/BaseView" { import {SortedRowSet} from 'app/client/models/rowset'; import {IColumnFilterMenuOptions} from 'app/client/ui/ColumnFilterMenu'; import {FieldBuilder} from "app/client/widgets/FieldBuilder"; + import {CursorPos} from 'app/plugin/GristAPI'; import {DomArg} from 'grainjs'; import {IOpenController} from 'popweasel'; diff --git a/app/client/lib/sortUtil.ts b/app/client/lib/sortUtil.ts index 5618332c..0c587478 100644 --- a/app/client/lib/sortUtil.ts +++ b/app/client/lib/sortUtil.ts @@ -5,7 +5,7 @@ import * as rowset from 'app/client/models/rowset'; import { MANUALSORT } from 'app/common/gristTypes'; import { SortFunc } from 'app/common/SortFunc'; import { Sort } from 'app/common/SortSpec'; -import { UIRowId } from 'app/common/TableData'; +import { UIRowId } from 'app/plugin/GristAPI'; import * as ko from 'knockout'; import range = require('lodash/range'); diff --git a/app/client/models/DocModel.ts b/app/client/models/DocModel.ts index ee3bf8c5..2f1f4074 100644 --- a/app/client/models/DocModel.ts +++ b/app/client/models/DocModel.ts @@ -29,7 +29,6 @@ import {isHiddenTable, isSummaryTable} from 'app/common/isHiddenTable'; import {canEdit} from 'app/common/roles'; import {RowFilterFunc} from 'app/common/RowFilterFunc'; import {schema, SchemaTypes} from 'app/common/schema'; -import {UIRowId} from 'app/common/TableData'; import {ACLRuleRec, createACLRuleRec} from 'app/client/models/entities/ACLRuleRec'; import {ColumnRec, createColumnRec} from 'app/client/models/entities/ColumnRec'; import {createDocInfoRec, DocInfoRec} from 'app/client/models/entities/DocInfoRec'; @@ -45,6 +44,7 @@ import {CellRec, createCellRec} from 'app/client/models/entities/CellRec'; import {RefListValue} from 'app/common/gristTypes'; import {decodeObject} from 'app/plugin/objtypes'; import {toKo} from 'grainjs'; +import {UIRowId} from 'app/plugin/GristAPI'; // Re-export all the entity types available. The recommended usage is like this: // import {ColumnRec, ViewFieldRec} from 'app/client/models/DocModel'; diff --git a/app/client/models/QuerySet.ts b/app/client/models/QuerySet.ts index e34afda8..269f1583 100644 --- a/app/client/models/QuerySet.ts +++ b/app/client/models/QuerySet.ts @@ -26,6 +26,7 @@ * TODO: client-side should show "..." or "50000 more rows not shown" in that case. * TODO: Reference columns don't work properly because always use a displayCol which relies on formulas */ +import {ClientColumnGettersByColId} from 'app/client/models/ClientColumnGetters'; import DataTableModel from 'app/client/models/DataTableModel'; import {DocModel} from 'app/client/models/DocModel'; import {BaseFilteredRowSource, RowList, RowSource} from 'app/client/models/rowset'; @@ -36,11 +37,11 @@ import {DocData} from 'app/common/DocData'; import {nativeCompare} from 'app/common/gutil'; import {IRefCountSub, RefCountMap} from 'app/common/RefCountMap'; import {getLinkingFilterFunc, RowFilterFunc} from 'app/common/RowFilterFunc'; -import {TableData as BaseTableData, UIRowId} from 'app/common/TableData'; +import {TableData as BaseTableData} from 'app/common/TableData'; import {tbind} from 'app/common/tbind'; +import {UIRowId} from 'app/plugin/GristAPI'; import {Disposable, Holder, IDisposableOwnerT} from 'grainjs'; import * as ko from 'knockout'; -import {ClientColumnGettersByColId} from 'app/client/models/ClientColumnGetters'; import debounce = require('lodash/debounce'); // Limit on the how many rows to request for OnDemand tables. diff --git a/app/client/models/SearchModel.ts b/app/client/models/SearchModel.ts index 2a791e04..6280e391 100644 --- a/app/client/models/SearchModel.ts +++ b/app/client/models/SearchModel.ts @@ -1,7 +1,6 @@ // tslint:disable:no-console // TODO: Add documentation and clean up log statements. -import {CursorPos} from 'app/client/components/Cursor'; import {GristDoc} from 'app/client/components/GristDoc'; import {PageRec, ViewFieldRec, ViewSectionRec} from 'app/client/models/DocModel'; import {reportError} from 'app/client/models/errors'; @@ -10,9 +9,10 @@ import {IDocPage} from 'app/common/gristUrls'; import {nativeCompare, waitObs} from 'app/common/gutil'; import {TableData} from 'app/common/TableData'; import {BaseFormatter} from 'app/common/ValueFormatter'; +import { makeT } from 'app/client/lib/localization'; +import {CursorPos} from 'app/plugin/GristAPI'; import {Computed, Disposable, Observable} from 'grainjs'; import debounce = require('lodash/debounce'); -import { makeT } from 'app/client/lib/localization'; const t = makeT('SearchModel'); diff --git a/app/client/models/SectionFilter.ts b/app/client/models/SectionFilter.ts index a858c319..bbf3f74b 100644 --- a/app/client/models/SectionFilter.ts +++ b/app/client/models/SectionFilter.ts @@ -3,7 +3,7 @@ import {ColumnRec, ViewFieldRec, ViewSectionRec} from 'app/client/models/DocMode import {TableData} from 'app/client/models/TableData'; import {buildColFilter, ColumnFilterFunc} from 'app/common/ColumnFilterFunc'; import {buildRowFilter, RowFilterFunc, RowValueFunc } from 'app/common/RowFilterFunc'; -import {UIRowId} from 'app/common/TableData'; +import {UIRowId} from 'app/plugin/GristAPI'; import {Computed, Disposable, MutableObsArray, obsArray, Observable, UseCB} from 'grainjs'; export type {ColumnFilterFunc}; diff --git a/app/client/models/entities/ViewSectionRec.ts b/app/client/models/entities/ViewSectionRec.ts index 40167f35..d43123bd 100644 --- a/app/client/models/entities/ViewSectionRec.ts +++ b/app/client/models/entities/ViewSectionRec.ts @@ -1,5 +1,4 @@ import BaseView from 'app/client/components/BaseView'; -import {CursorPos} from 'app/client/components/Cursor'; import {LinkingState} from 'app/client/components/LinkingState'; import {KoArray} from 'app/client/lib/koArray'; import { @@ -22,11 +21,11 @@ import {AccessLevel, ICustomWidget} from 'app/common/CustomWidget'; import {UserAction} from 'app/common/DocActions'; import {arrayRepeat} from 'app/common/gutil'; import {Sort} from 'app/common/SortSpec'; -import {UIRowId} from 'app/common/TableData'; import {ColumnsToMap, WidgetColumnMap} from 'app/plugin/CustomSectionAPI'; import {ColumnToMapImpl} from 'app/client/models/ColumnToMap'; import {BEHAVIOR} from 'app/client/models/entities/ColumnRec'; import {removeRule, RuleOwner} from 'app/client/models/RuleOwner'; +import {CursorPos, UIRowId} from 'app/plugin/GristAPI'; import {Computed, Holder, Observable} from 'grainjs'; import * as ko from 'knockout'; import defaults = require('lodash/defaults'); @@ -192,10 +191,14 @@ export interface ViewSectionRec extends IRowModel<"_grist_Views_section">, RuleO desiredAccessLevel: ko.Observable; // Show widget as linking source. Used by custom widget. - allowSelectBy: Observable; + allowSelectBy: ko.Observable; - // List of selected rows - selectedRows: Observable; + // List of selected rows from a custom widget, or null if a filter shouldn't be applied. + selectedRows: ko.Observable; + + // If the row filter is active (i.e. if selectedRows is non-null). Separate computed to avoid + // re-computing the filter when selectedRows changes. + selectedRowsActive: ko.Computed; editingFormula: ko.Computed; @@ -714,8 +717,9 @@ export function createViewSectionRec(this: ViewSectionRec, docModel: DocModel): return result; }); - this.allowSelectBy = Observable.create(this, false); - this.selectedRows = Observable.create(this, []); + this.allowSelectBy = ko.observable(false); + this.selectedRows = ko.observable(null as number[]|null); + this.selectedRowsActive = this.autoDispose(ko.pureComputed(() => this.selectedRows() !== null)); this.tableId = this.autoDispose(ko.pureComputed(() => this.table().tableId())); const rawSection = this.autoDispose(ko.pureComputed(() => this.table().rawViewSection())); diff --git a/app/client/models/rowset.ts b/app/client/models/rowset.ts index bbae95ab..f11fcb36 100644 --- a/app/client/models/rowset.ts +++ b/app/client/models/rowset.ts @@ -24,8 +24,9 @@ import koArray, {KoArray} from 'app/client/lib/koArray'; import {DisposableWithEvents} from 'app/common/DisposableWithEvents'; import {CompareFunc, sortedIndex} from 'app/common/gutil'; -import {SkippableRows, UIRowId} from 'app/common/TableData'; +import {SkippableRows} from 'app/common/TableData'; import {RowFilterFunc} from "app/common/RowFilterFunc"; +import {UIRowId} from 'app/plugin/GristAPI'; import {Observable} from 'grainjs'; /** diff --git a/app/client/ui/ColumnFilterMenu.ts b/app/client/ui/ColumnFilterMenu.ts index d1f00b1f..64fbd16b 100644 --- a/app/client/ui/ColumnFilterMenu.ts +++ b/app/client/ui/ColumnFilterMenu.ts @@ -5,6 +5,7 @@ */ import * as commands from 'app/client/components/commands'; import {GristDoc} from 'app/client/components/GristDoc'; +import {FocusLayer} from 'app/client/lib/FocusLayer'; import {makeT} from 'app/client/lib/localization'; import {ColumnFilter, NEW_FILTER_JSON} from 'app/client/models/ColumnFilter'; import {ColumnFilterMenuModel, IFilterCount} from 'app/client/models/ColumnFilterMenuModel'; @@ -13,21 +14,30 @@ import {FilterInfo} from 'app/client/models/entities/ViewSectionRec'; import {RowSource} from 'app/client/models/rowset'; import {ColumnFilterFunc, SectionFilter} from 'app/client/models/SectionFilter'; import {TableData} from 'app/client/models/TableData'; +import {ColumnFilterCalendarView} from 'app/client/ui/ColumnFilterCalendarView'; +import {relativeDatesControl} from 'app/client/ui/ColumnFilterMenuUtils'; import {cssInput} from 'app/client/ui/cssInput'; +import {DateRangeOptions, IDateRangeOption} from 'app/client/ui/DateRangeOptions'; import {cssPinButton} from 'app/client/ui/RightPanelStyles'; import {basicButton, primaryButton, textButton} from 'app/client/ui2018/buttons'; -import {cssLabel as cssCheckboxLabel, cssCheckboxSquare, cssLabelText, Indeterminate, labeledTriStateSquareCheckbox - } from 'app/client/ui2018/checkbox'; +import {cssLabel as cssCheckboxLabel, cssCheckboxSquare, + cssLabelText, Indeterminate, labeledTriStateSquareCheckbox} from 'app/client/ui2018/checkbox'; import {theme, vars} from 'app/client/ui2018/cssVars'; import {icon} from 'app/client/ui2018/icons'; import {cssOptionRowIcon, menu, menuCssClass, menuDivider, menuItem} from 'app/client/ui2018/menus'; +import {cssDeleteButton, cssDeleteIcon, cssToken as cssTokenTokenBase} from 'app/client/widgets/ChoiceListEditor'; +import {ChoiceOptions} from 'app/client/widgets/ChoiceTextBox'; +import {choiceToken} from 'app/client/widgets/ChoiceToken'; import {CellValue} from 'app/common/DocActions'; -import {IRelativeDateSpec, isEquivalentFilter, isRelativeBound} from "app/common/FilterState"; -import {formatRelBounds} from "app/common/RelativeDates"; -import { - Computed, dom, DomArg, DomElementArg, DomElementMethod, IDisposableOwner, input, makeTestId, - Observable, styled -} from 'grainjs'; +import {IRelativeDateSpec, isEquivalentFilter, isRelativeBound} from 'app/common/FilterState'; +import {extractTypeFromColType, isDateLikeType, isList, isNumberType, isRefListType} from 'app/common/gristTypes'; +import {formatRelBounds} from 'app/common/RelativeDates'; +import {createFormatter} from 'app/common/ValueFormatter'; +import {UIRowId} from 'app/plugin/GristAPI'; +import {decodeObject} from 'app/plugin/objtypes'; +import {Computed, dom, DomArg, DomElementArg, DomElementMethod, IDisposableOwner, + input, makeTestId, Observable, styled} from 'grainjs'; +import {IOpenController, IPopupOptions, setPopupToCreateDom} from 'popweasel'; import concat = require('lodash/concat'); import identity = require('lodash/identity'); import noop = require('lodash/noop'); @@ -35,18 +45,6 @@ import partition = require('lodash/partition'); import some = require('lodash/some'); import tail = require('lodash/tail'); import debounce = require('lodash/debounce'); -import {IOpenController, IPopupOptions, setPopupToCreateDom} from 'popweasel'; -import {decodeObject} from 'app/plugin/objtypes'; -import {extractTypeFromColType, isDateLikeType, isList, isNumberType, isRefListType} from 'app/common/gristTypes'; -import {choiceToken} from 'app/client/widgets/ChoiceToken'; -import {ChoiceOptions} from 'app/client/widgets/ChoiceTextBox'; -import {ColumnFilterCalendarView} from 'app/client/ui/ColumnFilterCalendarView'; -import {cssDeleteButton, cssDeleteIcon, cssToken as cssTokenTokenBase} from 'app/client/widgets/ChoiceListEditor'; -import {relativeDatesControl} from 'app/client/ui/ColumnFilterMenuUtils'; -import {FocusLayer} from 'app/client/lib/FocusLayer'; -import {DateRangeOptions, IDateRangeOption} from 'app/client/ui/DateRangeOptions'; -import {createFormatter} from 'app/common/ValueFormatter'; -import {UIRowId} from 'app/common/TableData'; const t = makeT('ColumnFilterMenu'); diff --git a/app/client/ui/DescriptionConfig.ts b/app/client/ui/DescriptionConfig.ts index 65f03042..b838a40b 100644 --- a/app/client/ui/DescriptionConfig.ts +++ b/app/client/ui/DescriptionConfig.ts @@ -1,10 +1,10 @@ -import {CursorPos} from 'app/client/components/Cursor'; import {makeT} from 'app/client/lib/localization'; -import { KoSaveableObservable } from 'app/client/models/modelUtil'; +import {KoSaveableObservable} from 'app/client/models/modelUtil'; import {autoGrow} from 'app/client/ui/forms'; import {textarea} from 'app/client/ui/inputs'; import {cssLabel, cssRow} from 'app/client/ui/RightPanelStyles'; import {testId, theme} from 'app/client/ui2018/cssVars'; +import {CursorPos} from 'app/plugin/GristAPI'; import {dom, fromKo, MultiHolder, styled} from 'grainjs'; const t = makeT('DescriptionConfig'); diff --git a/app/client/ui/FieldConfig.ts b/app/client/ui/FieldConfig.ts index b100807a..808fa8a9 100644 --- a/app/client/ui/FieldConfig.ts +++ b/app/client/ui/FieldConfig.ts @@ -1,5 +1,4 @@ import {makeT} from 'app/client/lib/localization'; -import {CursorPos} from 'app/client/components/Cursor'; import {GristDoc} from 'app/client/components/GristDoc'; import {BEHAVIOR, ColumnRec} from 'app/client/models/entities/ColumnRec'; import {buildHighlightedCode, cssCodeBlock} from 'app/client/ui/CodeHighlight'; @@ -16,6 +15,7 @@ import {selectMenu, selectOption, selectTitle} from 'app/client/ui2018/menus'; import {createFormulaErrorObs, cssError} from 'app/client/widgets/FormulaEditor'; import {sanitizeIdent} from 'app/common/gutil'; import {Theme} from 'app/common/ThemePrefs'; +import {CursorPos} from 'app/plugin/GristAPI'; import {bundleChanges, Computed, dom, DomContents, DomElementArg, fromKo, MultiHolder, Observable, styled} from 'grainjs'; import * as ko from 'knockout'; diff --git a/app/client/ui/selectBy.ts b/app/client/ui/selectBy.ts index e7596c40..ced09836 100644 --- a/app/client/ui/selectBy.ts +++ b/app/client/ui/selectBy.ts @@ -127,7 +127,7 @@ function isValidLink(source: LinkNode, target: LinkNode) { } // custom widget must allow select by - if (!source.section.allowSelectBy.get()) { + if (!source.section.allowSelectBy()) { return false; } } diff --git a/app/client/widgets/FieldEditor.ts b/app/client/widgets/FieldEditor.ts index 973fb16f..3156db9c 100644 --- a/app/client/widgets/FieldEditor.ts +++ b/app/client/widgets/FieldEditor.ts @@ -1,5 +1,5 @@ import * as commands from 'app/client/components/commands'; -import {Cursor, CursorPos} from 'app/client/components/Cursor'; +import {Cursor} from 'app/client/components/Cursor'; import {GristDoc} from 'app/client/components/GristDoc'; import {UnsavedChange} from 'app/client/components/UnsavedChanges'; import {makeT} from 'app/client/lib/localization'; @@ -14,6 +14,7 @@ import {CellValue} from "app/common/DocActions"; import * as gutil from 'app/common/gutil'; import {CellPosition} from "app/client/components/CellPosition"; import {FloatingEditor} from 'app/client/widgets/FloatingEditor'; +import {CursorPos} from 'app/plugin/GristAPI'; import isEqual = require('lodash/isEqual'); import {Disposable, dom, Emitter, Holder, MultiHolder, Observable} from 'grainjs'; diff --git a/app/common/ActiveDocAPI.ts b/app/common/ActiveDocAPI.ts index 1b67f976..d428e45e 100644 --- a/app/common/ActiveDocAPI.ts +++ b/app/common/ActiveDocAPI.ts @@ -1,11 +1,10 @@ import {ActionGroup} from 'app/common/ActionGroup'; import {BulkAddRecord, CellValue, TableDataAction, UserAction} from 'app/common/DocActions'; import {FormulaProperties} from 'app/common/GranularAccessClause'; -import {UIRowId} from 'app/common/TableData'; import {FetchUrlOptions, UploadResult} from 'app/common/uploads'; import {DocStateComparison, PermissionData, UserAccessData} from 'app/common/UserAPI'; import {ParseOptions} from 'app/plugin/FileParserAPI'; -import {AccessTokenOptions, AccessTokenResult} from 'app/plugin/GristAPI'; +import {AccessTokenOptions, AccessTokenResult, UIRowId} from 'app/plugin/GristAPI'; import {IMessage} from 'grain-rpc'; export interface ApplyUAOptions { diff --git a/app/common/TableData.ts b/app/common/TableData.ts index bfb7bfce..2d2c8fb2 100644 --- a/app/common/TableData.ts +++ b/app/common/TableData.ts @@ -2,21 +2,18 @@ * TableData maintains a single table's data. */ import {ActionDispatcher} from 'app/common/ActionDispatcher'; -import { - BulkAddRecord, BulkColValues, CellValue, ColInfo, ColInfoWithId, ColValues, DocAction, +import {BulkAddRecord, BulkColValues, CellValue, ColInfo, ColInfoWithId, ColValues, DocAction, isSchemaAction, ReplaceTableData, RowRecord, TableDataAction} from 'app/common/DocActions'; import {getDefaultForType} from 'app/common/gristTypes'; import {arrayRemove, arraySplice, getDistinctValues} from 'app/common/gutil'; -import {SchemaTypes} from "app/common/schema"; +import {SchemaTypes} from 'app/common/schema'; +import {UIRowId} from 'app/plugin/GristAPI'; import isEqual = require('lodash/isEqual'); import fromPairs = require('lodash/fromPairs'); 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 = (rowId: UIRowId) => T; diff --git a/app/common/gristUrls.ts b/app/common/gristUrls.ts index c0202e89..d4f06b6d 100644 --- a/app/common/gristUrls.ts +++ b/app/common/gristUrls.ts @@ -5,9 +5,9 @@ import {encodeQueryParams, isAffirmative} from 'app/common/gutil'; import {LocalPlugin} from 'app/common/plugin'; import {StringUnion} from 'app/common/StringUnion'; import {TelemetryLevel} from 'app/common/Telemetry'; -import {UIRowId} from 'app/common/TableData'; import {getGristConfig} from 'app/common/urlUtils'; import {Document} from 'app/common/UserAPI'; +import {UIRowId} from 'app/plugin/GristAPI'; import clone = require('lodash/clone'); import pickBy = require('lodash/pickBy'); import {ThemeAppearance, ThemeAppearanceChecker, ThemeName, ThemeNameChecker} from './ThemePrefs'; diff --git a/app/common/isHiddenTable.ts b/app/common/isHiddenTable.ts index cb746bb8..2fcf145a 100644 --- a/app/common/isHiddenTable.ts +++ b/app/common/isHiddenTable.ts @@ -1,4 +1,5 @@ -import {TableData, UIRowId} from 'app/common/TableData'; +import {TableData} from 'app/common/TableData'; +import {UIRowId} from 'app/plugin/GristAPI'; /** * Return whether a table (identified by the rowId of its metadata record) should diff --git a/app/plugin/CustomSectionAPI-ti.ts b/app/plugin/CustomSectionAPI-ti.ts index 6d7b8853..bc972d80 100644 --- a/app/plugin/CustomSectionAPI-ti.ts +++ b/app/plugin/CustomSectionAPI-ti.ts @@ -19,6 +19,7 @@ export const InteractionOptionsRequest = t.iface([], { "requiredAccess": t.opt("string"), "hasCustomOptions": t.opt("boolean"), "columns": t.opt("ColumnsToMap"), + "allowSelectBy": t.opt("boolean"), }); export const InteractionOptions = t.iface([], { diff --git a/app/plugin/CustomSectionAPI.ts b/app/plugin/CustomSectionAPI.ts index 2d1b68b9..7608d968 100644 --- a/app/plugin/CustomSectionAPI.ts +++ b/app/plugin/CustomSectionAPI.ts @@ -50,6 +50,10 @@ export interface InteractionOptionsRequest { * and those requested by Custom Widget. */ columns?: ColumnsToMap, + /** + * Show widget as linking source. + */ + allowSelectBy?: boolean, } /** diff --git a/app/plugin/GristAPI-ti.ts b/app/plugin/GristAPI-ti.ts index 1f592689..e05717eb 100644 --- a/app/plugin/GristAPI-ti.ts +++ b/app/plugin/GristAPI-ti.ts @@ -4,6 +4,15 @@ import * as t from "ts-interface-checker"; // tslint:disable:object-literal-key-quotes +export const UIRowId = t.union("number", t.lit('new')); + +export const CursorPos = t.iface([], { + "rowId": t.opt("UIRowId"), + "rowIndex": t.opt("number"), + "fieldIndex": t.opt("number"), + "sectionId": t.opt("number"), +}); + export const ComponentKind = t.union(t.lit("safeBrowser"), t.lit("safePython"), t.lit("unsafeNode")); export const GristAPI = t.iface([], { @@ -25,7 +34,8 @@ 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"))), + "setSelectedRows": t.func("void", t.param("rowIds", t.union(t.array("number"), "null"))), + "setCursorPos": t.func("void", t.param("pos", "CursorPos")), }); export const AccessTokenOptions = t.iface([], { @@ -39,6 +49,8 @@ export const AccessTokenResult = t.iface([], { }); const exportedTypeSuite: t.ITypeSuite = { + UIRowId, + CursorPos, ComponentKind, GristAPI, GristDocAPI, diff --git a/app/plugin/GristAPI.ts b/app/plugin/GristAPI.ts index c4647d6c..9f7a12da 100644 --- a/app/plugin/GristAPI.ts +++ b/app/plugin/GristAPI.ts @@ -37,6 +37,36 @@ import {RenderOptions, RenderTarget} from './RenderOptions'; +// 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 here. Note that for data actions and stored data, +// 'new' is not used. +/** + * Represents the id of a row in a table. The value of the `id` column. Might be a number or 'new' value for a new row. + */ +export type UIRowId = number | 'new'; + +/** + * Represents the position of an active cursor on a page. + */ +export interface CursorPos { + /** + * The rowId (value of the `id` column) of the current cursor position, or 'new' if the cursor is on a new row. + */ + rowId?: UIRowId; + /** + * The index of the current row in the current view. + */ + rowIndex?: number; + /** + * The index of the selected field in the current view. + */ + fieldIndex?: number; + /** + * The id of a section that this cursor is in. Ignored when setting a cursor position for a particular view. + */ + sectionId?: number; +} + export type ComponentKind = "safeBrowser" | "safePython" | "unsafeNode"; export const RPC_GRISTAPI_INTERFACE = '_grist_api'; @@ -123,14 +153,20 @@ export interface GristView { // because ts-interface-builder does not properly support index-signature. /** - * Allow custom widget to be listed as a possible source for linking with SELECT BY. + * Deprecated now. It was used for filtering selected table by `setSelectedRows` method. + * Now the preferred way it to use ready message. */ allowSelectBy(): Promise; /** - * Set the list of selected rows to be used against any linked widget. Requires `allowSelectBy()`. + * Set the list of selected rows to be used against any linked widget. + */ + setSelectedRows(rowIds: number[]|null): Promise; + + /** + * Sets the cursor position to a specific row and field. `sectionId` is ignored. Used for widget linking. */ - setSelectedRows(rowIds: number[]): Promise; + setCursorPos(pos: CursorPos): Promise } /** diff --git a/app/plugin/grist-plugin-api.ts b/app/plugin/grist-plugin-api.ts index 375c232f..9480ac2f 100644 --- a/app/plugin/grist-plugin-api.ts +++ b/app/plugin/grist-plugin-api.ts @@ -75,6 +75,10 @@ export const allowSelectBy = viewApi.allowSelectBy; export const setSelectedRows = viewApi.setSelectedRows; +export const setCursorPos = viewApi.setCursorPos; + + + /** * Fetches data backing the widget as for [[GristView.fetchSelectedTable]], * but decoding data by default, replacing e.g. ['D', timestamp] with diff --git a/app/server/lib/ActiveDoc.ts b/app/server/lib/ActiveDoc.ts index 34e7f53c..6740a843 100644 --- a/app/server/lib/ActiveDoc.ts +++ b/app/server/lib/ActiveDoc.ts @@ -72,7 +72,7 @@ import {InactivityTimer} from 'app/common/InactivityTimer'; import {Interval} from 'app/common/Interval'; import * as roles from 'app/common/roles'; import {schema, SCHEMA_VERSION} from 'app/common/schema'; -import {MetaRowRecord, SingleCell, UIRowId} from 'app/common/TableData'; +import {MetaRowRecord, SingleCell} from 'app/common/TableData'; import {TelemetryEvent, TelemetryMetadataByLevel} from 'app/common/Telemetry'; import {FetchUrlOptions, UploadResult} from 'app/common/uploads'; import {Document as APIDocument, DocReplacementOptions, DocState, DocStateComparison} from 'app/common/UserAPI'; @@ -81,7 +81,7 @@ import {guessColInfo} from 'app/common/ValueGuesser'; import {parseUserAction} from 'app/common/ValueParser'; import {Document} from 'app/gen-server/entity/Document'; import {ParseFileResult, ParseOptions} from 'app/plugin/FileParserAPI'; -import {AccessTokenOptions, AccessTokenResult, GristDocAPI} from 'app/plugin/GristAPI'; +import {AccessTokenOptions, AccessTokenResult, GristDocAPI, UIRowId} from 'app/plugin/GristAPI'; import {compileAclFormula} from 'app/server/lib/ACLFormula'; import {AssistanceSchemaPromptV1Context} from 'app/server/lib/Assistance'; import {AssistanceContext} from 'app/common/AssistancePrompts'; diff --git a/test/fixtures/sites/hello/index.html b/test/fixtures/sites/hello/index.html index 21435435..d8783129 100644 --- a/test/fixtures/sites/hello/index.html +++ b/test/fixtures/sites/hello/index.html @@ -1,5 +1,6 @@ +

Hello World

diff --git a/test/nbrowser/gristUtils.ts b/test/nbrowser/gristUtils.ts index a074abce..e584be61 100644 --- a/test/nbrowser/gristUtils.ts +++ b/test/nbrowser/gristUtils.ts @@ -1281,10 +1281,10 @@ export async function checkForErrors() { /** * Opens a Creator Panel on Widget/Table settings tab. */ -export async function openWidgetPanel() { +export async function openWidgetPanel(tab: 'widget'|'sortAndFilter'|'data' = 'widget') { await toggleSidePanel('right', 'open'); await driver.find('.test-right-tab-pagewidget').click(); - await driver.find(".test-config-widget").click(); + await driver.find(`.test-config-${tab}`).click(); } /**