gristlabs_grist-core/app/common/ActiveDocAPI.ts

363 lines
12 KiB
TypeScript
Raw Normal View History

import {ActionGroup} from 'app/common/ActionGroup';
import {CellValue, TableDataAction, UserAction} from 'app/common/DocActions';
import {FormulaProperties} from 'app/common/GranularAccessClause';
(core) Show example values in formula autocomplete Summary: This diff adds a preview of the value of certain autocomplete suggestions, especially of the form `$foo.bar` or `user.email`. The main initial motivation was to show the difference between `$Ref` and `$Ref.DisplayCol`, but the feature is more general. The client now sends the row ID of the row being edited (along with the table and column IDs which were already sent) to the server to fetch autocomplete suggestions. The returned suggestions are now tuples `(suggestion, example_value)` where `example_value` is a string or null. The example value is simply obtained by evaluating (in a controlled way) the suggestion in the context of the given record and the current user. The string representation is similar to the standard `repr` but dates and datetimes are formatted, and the whole thing is truncated for efficiency. The example values are shown in the autocomplete popup separated from the actual suggestion by a number of spaces calculated to: 1. Clearly separate the suggestion from the values 2. Left-align the example values in most cases 3. Avoid having so much space such that connecting suggestions and values becomes visually difficult. The tokenization of the row is then tweaked to show the example in light grey to deemphasise it. Main discussion where the above was decided: https://grist.slack.com/archives/CDHABLZJT/p1661795588100009 The diff also includes various other small improvements and fixes: - The autocomplete popup is much wider to make room for long suggestions, particularly lookups, as pointed out in https://phab.getgrist.com/D3580#inline-41007. The wide popup is the reason a fancy solution was needed to position the example values. I didn't see a way to dynamically resize the popup based on suggestions, and it didn't seem like a good idea to try. - The `grist` and `python` labels previously shown on the right are removed. They were not helpful (https://grist.slack.com/archives/CDHABLZJT/p1659697086155179) and would get in the way of the example values. - Fixed a bug in our custom tokenization that caused function arguments to be weirdly truncated in the middle: https://grist.slack.com/archives/CDHABLZJT/p1661956353699169?thread_ts=1661953258.342739&cid=CDHABLZJT and https://grist.slack.com/archives/C069RUP71/p1659696778991339 - Hide suggestions involving helper columns like `$gristHelper_Display` or `Table.lookupRecords(gristHelper_Display=` (https://grist.slack.com/archives/CDHABLZJT/p1661953258342739). The former has been around for a while and seems to be a mistake. The fix is simply to use `is_visible_column` instead of `is_user_column`. Since the latter is not used anywhere else, and using it in the first place seems like a mistake more than anything else, I've also removed the function to prevent similar mistakes in the future. - Don't suggest private columns as lookup arguments: https://grist.slack.com/archives/CDHABLZJT/p1662133416652499?thread_ts=1661795588.100009&cid=CDHABLZJT - Only fetch fresh suggestions specifically after typing `lookupRecords(` or `lookupOne(` rather than just `(`, as this would needlessly hide function suggestions which could still be useful to see the arguments. However this only makes a difference when there are still multiple matching suggestions, otherwise Ace hides them anyway. Test Plan: Extended and updated several Python and browser tests. Reviewers: paulfitz Reviewed By: paulfitz Differential Revision: https://phab.getgrist.com/D3611
2022-09-28 14:47:55 +00:00
import {UIRowId} from 'app/common/UIRowId';
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 {IMessage} from 'grain-rpc';
export interface ApplyUAOptions {
desc?: string; // Overrides the description of the action.
otherId?: number; // For undo/redo; the actionNum of the original action to which it applies.
linkId?: number; // For bundled actions, actionNum of the previous action in the bundle.
parseStrings?: boolean; // If true, parses string values in some actions based on the column
}
export interface ApplyUAExtendedOptions extends ApplyUAOptions {
bestEffort?: boolean; // If set, action may be applied in part if it cannot be applied completely.
fromOwnHistory?: boolean; // If set, action is confirmed to be a redo/undo taken from history, from
// an action marked as being by the current user.
oldestSource?: number; // If set, gives the timestamp of the oldest source the undo/redo
// action was built from, expressed as number of milliseconds
// elapsed since January 1, 1970 00:00:00 UTC
}
export interface ApplyUAResult {
actionNum: number; // number of the action that got recorded.
retValues: any[]; // array of return values, one for each of the passed-in user actions.
isModification: boolean; // true if document was modified.
}
export interface DataSourceTransformed {
// Identifies the upload, which may include multiple files.
uploadId: number;
// For each file in the upload, the transform rules for that file.
transforms: TransformRuleMap[];
}
export interface TransformRuleMap {
[origTableName: string]: TransformRule;
}
// Special values for import destinations; null means "new table", "" means skip table.
// Both special options exposed as consts.
export type DestId = string | null;
export const NEW_TABLE = null;
export const SKIP_TABLE = "";
export interface TransformRule {
destTableId: DestId;
destCols: TransformColumn[];
sourceCols: string[];
}
export interface TransformColumn {
label: string;
colId: string|null;
type: string;
formula: string;
widgetOptions: string;
}
export interface ImportResult {
options: ParseOptions;
tables: ImportTableResult[];
}
export interface ImportTableResult {
hiddenTableId: string;
2022-02-19 09:46:49 +00:00
uploadFileIndex: number; // Index into upload.files array, for the file responsible for this table.
origTableName: string;
transformSectionRef: number;
destTableId: string|null;
}
export interface ImportOptions {
parseOptions?: ParseOptions; // Options for parsing the source file.
mergeOptionMaps?: MergeOptionsMap[]; // Options for merging fields, indexed by uploadFileIndex.
}
export interface MergeOptionsMap {
// Map of original GristTable name of imported table to its merge options, if any.
[origTableName: string]: MergeOptions|undefined;
}
export interface MergeOptions {
mergeCols: string[]; // Columns to use as merge keys for incremental imports.
mergeStrategy: MergeStrategy; // Determines how matched records should be merged between 2 tables.
}
export interface MergeStrategy {
type: 'replace-with-nonblank-source' | 'replace-all-fields' | 'replace-blank-fields-only';
}
/**
* Represents a query for Grist data. The tableId is required. An empty set of filters indicates
* the full table. Examples:
* {tableId: "Projects", filters: {}}
* {tableId: "Employees", filters: {Status: ["Active"], Dept: ["Sales", "HR"]}}
*/
interface BaseQuery {
tableId: string;
filters: QueryFilters;
}
/**
* Query that can only be used on the client side.
* Allows filtering with more complex operations.
*/
export interface ClientQuery extends BaseQuery {
operations: {
[colId: string]: QueryOperation;
};
}
/**
* Query intended to be sent to a server.
*/
export interface ServerQuery extends BaseQuery {
// Queries to server for onDemand tables will set a limit to avoid bringing down the browser.
limit?: number;
}
/**
* Type of the filters option to queries.
*/
export interface QueryFilters {
// TODO: check if "any" can be replaced with "CellValue".
[colId: string]: any[];
}
// - in: value should be contained in filters array
// - intersects: value should be a list with some overlap with filters array
// - empty: value should be falsy (e.g. null) or an empty list, filters is ignored
export type QueryOperation = "in" | "intersects" | "empty";
/**
* Response from useQuerySet(). A query returns data AND creates a subscription to receive
* DocActions that affect this data. The querySubId field identifies this subscription, and must
* be used in a disposeQuerySet() call to unsubscribe.
*/
export interface QueryResult {
querySubId: number; // ID of the subscription, to use with disposeQuerySet.
tableData: TableDataAction;
}
/**
* Result of a fork operation, with newly minted ids.
* For a document with docId XXXXX and urlId UUUUU, the fork will have a
* docId of XXXXX~FORKID[~USERID] and a urlId of UUUUU~FORKID[~USERID].
*/
export interface ForkResult {
docId: string;
urlId: string;
}
/**
* An extension of PermissionData to cover not just users with whom a document is shared,
* but also users mentioned in the document (in user attribute tables), and suggested
* example users. This is for use in the "View As" feature of the access rules page.
*/
export interface PermissionDataWithExtraUsers extends PermissionData {
attributeTableUsers: UserAccessData[];
exampleUsers: UserAccessData[];
}
/**
* Basic metadata about a table returned by `getAclResources()`.
*/
export interface AclTableDescription {
title: string; // Raw data widget title
colIds: string[]; // IDs of all columns in table
groupByColLabels: string[] | null; // Labels of groupby columns for summary tables, or null.
}
export function getTableTitle(table: AclTableDescription): string {
let {title} = table;
if (table.groupByColLabels) {
title += ' ' + summaryGroupByDescription(table.groupByColLabels);
}
return title;
}
export function summaryGroupByDescription(groupByColumnLabels: string[]): string {
return `[${groupByColumnLabels.length ? 'by ' + groupByColumnLabels.join(", ") : "Totals"}]`;
}
(core) Show example values in formula autocomplete Summary: This diff adds a preview of the value of certain autocomplete suggestions, especially of the form `$foo.bar` or `user.email`. The main initial motivation was to show the difference between `$Ref` and `$Ref.DisplayCol`, but the feature is more general. The client now sends the row ID of the row being edited (along with the table and column IDs which were already sent) to the server to fetch autocomplete suggestions. The returned suggestions are now tuples `(suggestion, example_value)` where `example_value` is a string or null. The example value is simply obtained by evaluating (in a controlled way) the suggestion in the context of the given record and the current user. The string representation is similar to the standard `repr` but dates and datetimes are formatted, and the whole thing is truncated for efficiency. The example values are shown in the autocomplete popup separated from the actual suggestion by a number of spaces calculated to: 1. Clearly separate the suggestion from the values 2. Left-align the example values in most cases 3. Avoid having so much space such that connecting suggestions and values becomes visually difficult. The tokenization of the row is then tweaked to show the example in light grey to deemphasise it. Main discussion where the above was decided: https://grist.slack.com/archives/CDHABLZJT/p1661795588100009 The diff also includes various other small improvements and fixes: - The autocomplete popup is much wider to make room for long suggestions, particularly lookups, as pointed out in https://phab.getgrist.com/D3580#inline-41007. The wide popup is the reason a fancy solution was needed to position the example values. I didn't see a way to dynamically resize the popup based on suggestions, and it didn't seem like a good idea to try. - The `grist` and `python` labels previously shown on the right are removed. They were not helpful (https://grist.slack.com/archives/CDHABLZJT/p1659697086155179) and would get in the way of the example values. - Fixed a bug in our custom tokenization that caused function arguments to be weirdly truncated in the middle: https://grist.slack.com/archives/CDHABLZJT/p1661956353699169?thread_ts=1661953258.342739&cid=CDHABLZJT and https://grist.slack.com/archives/C069RUP71/p1659696778991339 - Hide suggestions involving helper columns like `$gristHelper_Display` or `Table.lookupRecords(gristHelper_Display=` (https://grist.slack.com/archives/CDHABLZJT/p1661953258342739). The former has been around for a while and seems to be a mistake. The fix is simply to use `is_visible_column` instead of `is_user_column`. Since the latter is not used anywhere else, and using it in the first place seems like a mistake more than anything else, I've also removed the function to prevent similar mistakes in the future. - Don't suggest private columns as lookup arguments: https://grist.slack.com/archives/CDHABLZJT/p1662133416652499?thread_ts=1661795588.100009&cid=CDHABLZJT - Only fetch fresh suggestions specifically after typing `lookupRecords(` or `lookupOne(` rather than just `(`, as this would needlessly hide function suggestions which could still be useful to see the arguments. However this only makes a difference when there are still multiple matching suggestions, otherwise Ace hides them anyway. Test Plan: Extended and updated several Python and browser tests. Reviewers: paulfitz Reviewed By: paulfitz Differential Revision: https://phab.getgrist.com/D3611
2022-09-28 14:47:55 +00:00
//// Types for autocomplete suggestions
// Suggestion may be a string, or a tuple [funcname, argSpec, isGrist], where:
// - funcname (e.g. "DATEADD") will be auto-completed with "(", AND linked to Grist
// documentation.
// - argSpec (e.g. "(start_date, days=0, ...)") is to be shown as autocomplete caption.
// - isGrist is no longer used
type ISuggestion = string | [string, string, boolean];
// Suggestion paired with an optional example value to show on the right
export type ISuggestionWithValue = [ISuggestion, string | null];
export interface ActiveDocAPI {
/**
* Closes a document, and unsubscribes from its userAction events.
*/
closeDoc(): Promise<void>;
/**
* Fetches a particular table from the data engine to return to the client.
*/
fetchTable(tableId: string): Promise<TableDataAction>;
/**
* Fetches the generated Python code for this document. (TODO rename this misnomer.)
*/
fetchTableSchema(): Promise<string>;
/**
* Makes a query (documented elsewhere) and subscribes to it, so that the client receives
* docActions that affect this query's results. The subscription remains functional even when
* tables or columns get renamed.
*/
useQuerySet(query: ServerQuery): Promise<QueryResult>;
/**
* Removes the subscription to a Query, identified by QueryResult.querySubId, so that the
* client stops receiving docActions relevant only to that query.
*/
disposeQuerySet(querySubId: number): Promise<void>;
/**
* Applies an array of user actions to the document.
*/
applyUserActions(actions: UserAction[], options?: ApplyUAOptions): Promise<ApplyUAResult>;
/**
* A variant of applyUserActions where actions are passed in by ids (actionNum, actionHash)
* rather than by value.
*/
applyUserActionsById(actionNums: number[], actionHashes: string[],
undo: boolean, options?: ApplyUAOptions): Promise<ApplyUAResult>;
/**
* Imports files, removes previously created temporary hidden tables and creates the new ones.
*/
importFiles(dataSource: DataSourceTransformed,
parseOptions: ParseOptions, prevTableIds: string[]): Promise<ImportResult>;
/**
* Finishes import files, creates the new tables, and cleans up temporary hidden tables and uploads.
*/
finishImportFiles(dataSource: DataSourceTransformed, prevTableIds: string[],
options: ImportOptions): Promise<ImportResult>;
/**
* Cancels import files, cleans up temporary hidden tables and uploads.
*/
cancelImportFiles(uploadId: number, prevTableIds: string[]): Promise<void>;
/**
* Returns a diff of changes that will be applied to the destination table from `transformRule`
* if the data from `hiddenTableId` is imported with the specified `mergeOptions`.
*/
generateImportDiff(hiddenTableId: string, transformRule: TransformRule,
mergeOptions: MergeOptions): Promise<DocStateComparison>;
/**
* Saves attachments from a given upload and creates an entry for them in the database. It
* returns the list of rowIds for the rows created in the _grist_Attachments table.
*/
addAttachments(uploadId: number): Promise<number[]>;
/**
* Returns up to n columns in the document, or a specific table, which contain the given values.
* Columns are returned ordered from best to worst based on an estimate for number of matches.
*/
findColFromValues(values: any[], n: number, optTableId?: string): Promise<number[]>;
/**
* Returns cell value with an error message (traceback) for one invalid formula cell.
*/
getFormulaError(tableId: string, colId: string, rowId: number): Promise<CellValue>;
/**
* Fetch content at a url.
*/
fetchURL(url: string, options?: FetchUrlOptions): Promise<UploadResult>;
/**
* Find and return a list of auto-complete suggestions that start with `txt`, when editing a
* formula in table `tableId` and column `columnId`.
*/
(core) Show example values in formula autocomplete Summary: This diff adds a preview of the value of certain autocomplete suggestions, especially of the form `$foo.bar` or `user.email`. The main initial motivation was to show the difference between `$Ref` and `$Ref.DisplayCol`, but the feature is more general. The client now sends the row ID of the row being edited (along with the table and column IDs which were already sent) to the server to fetch autocomplete suggestions. The returned suggestions are now tuples `(suggestion, example_value)` where `example_value` is a string or null. The example value is simply obtained by evaluating (in a controlled way) the suggestion in the context of the given record and the current user. The string representation is similar to the standard `repr` but dates and datetimes are formatted, and the whole thing is truncated for efficiency. The example values are shown in the autocomplete popup separated from the actual suggestion by a number of spaces calculated to: 1. Clearly separate the suggestion from the values 2. Left-align the example values in most cases 3. Avoid having so much space such that connecting suggestions and values becomes visually difficult. The tokenization of the row is then tweaked to show the example in light grey to deemphasise it. Main discussion where the above was decided: https://grist.slack.com/archives/CDHABLZJT/p1661795588100009 The diff also includes various other small improvements and fixes: - The autocomplete popup is much wider to make room for long suggestions, particularly lookups, as pointed out in https://phab.getgrist.com/D3580#inline-41007. The wide popup is the reason a fancy solution was needed to position the example values. I didn't see a way to dynamically resize the popup based on suggestions, and it didn't seem like a good idea to try. - The `grist` and `python` labels previously shown on the right are removed. They were not helpful (https://grist.slack.com/archives/CDHABLZJT/p1659697086155179) and would get in the way of the example values. - Fixed a bug in our custom tokenization that caused function arguments to be weirdly truncated in the middle: https://grist.slack.com/archives/CDHABLZJT/p1661956353699169?thread_ts=1661953258.342739&cid=CDHABLZJT and https://grist.slack.com/archives/C069RUP71/p1659696778991339 - Hide suggestions involving helper columns like `$gristHelper_Display` or `Table.lookupRecords(gristHelper_Display=` (https://grist.slack.com/archives/CDHABLZJT/p1661953258342739). The former has been around for a while and seems to be a mistake. The fix is simply to use `is_visible_column` instead of `is_user_column`. Since the latter is not used anywhere else, and using it in the first place seems like a mistake more than anything else, I've also removed the function to prevent similar mistakes in the future. - Don't suggest private columns as lookup arguments: https://grist.slack.com/archives/CDHABLZJT/p1662133416652499?thread_ts=1661795588.100009&cid=CDHABLZJT - Only fetch fresh suggestions specifically after typing `lookupRecords(` or `lookupOne(` rather than just `(`, as this would needlessly hide function suggestions which could still be useful to see the arguments. However this only makes a difference when there are still multiple matching suggestions, otherwise Ace hides them anyway. Test Plan: Extended and updated several Python and browser tests. Reviewers: paulfitz Reviewed By: paulfitz Differential Revision: https://phab.getgrist.com/D3611
2022-09-28 14:47:55 +00:00
autocomplete(txt: string, tableId: string, columnId: string, rowId: UIRowId): Promise<ISuggestionWithValue[]>;
/**
* Removes the current instance from the doc.
*/
removeInstanceFromDoc(): Promise<void>;
/**
* Get recent actions in ActionGroup format with summaries included.
*/
getActionSummaries(): Promise<ActionGroup[]>;
/**
* Initiates user actions bandling for undo.
*/
startBundleUserActions(): Promise<void>;
/**
* Stopes user actions bandling for undo.
*/
stopBundleUserActions(): Promise<void>;
/**
* Forward a grain-rpc message to a given plugin.
*/
forwardPluginRpc(pluginId: string, msg: IMessage): Promise<any>;
/**
* Reload documents plugins.
*/
reloadPlugins(): Promise<void>;
/**
* Immediately close the document and data engine, to be reloaded from scratch, and cause all
* browser clients to reopen it.
*/
reloadDoc(): Promise<void>;
/**
* Prepare a fork of the document, and return the id(s) of the fork.
*/
fork(): Promise<ForkResult>;
/**
* Check if an ACL formula is valid. If not, will throw an error with an explanation.
*/
checkAclFormula(text: string): Promise<FormulaProperties>;
/**
* Get a token for out-of-band access to the document.
*/
getAccessToken(options: AccessTokenOptions): Promise<AccessTokenResult>;
/**
* Returns the full set of tableIds, with the list of colIds for each table. This is intended
* for editing ACLs. It is only available to users who can edit ACLs, and lists all resources
* regardless of rules that may block access to them.
*/
getAclResources(): Promise<{[tableId: string]: AclTableDescription}>;
/**
* Wait for document to finish initializing.
*/
waitForInitialization(): Promise<void>;
/**
* Get users that are worth proposing to "View As" for access control purposes.
*/
getUsersForViewAs(): Promise<PermissionDataWithExtraUsers>;
}