(core) Support user variable in dropdown conditions

Summary:
Dropdown conditions can now reference a `user` variable, similar to the
one available in Access Rules.

Test Plan: Browser test.

Reviewers: jarek, paulfitz

Reviewed By: jarek, paulfitz

Differential Revision: https://phab.getgrist.com/D4255
This commit is contained in:
George Gevoian
2024-05-29 14:55:21 -07:00
parent 50077540e2
commit 72066bf0e4
27 changed files with 426 additions and 268 deletions

View File

@@ -3,6 +3,7 @@ import {TableDataAction} from 'app/common/DocActions';
import {FilteredDocUsageSummary} from 'app/common/DocUsage';
import {Role} from 'app/common/roles';
import {StringUnion} from 'app/common/StringUnion';
import {UserInfo} from 'app/common/User';
import {FullUser} from 'app/common/UserAPI';
// Possible flavors of items in a list of documents.
@@ -75,6 +76,7 @@ export interface OpenLocalDocResult {
doc: {[tableId: string]: TableDataAction};
log: MinimalActionGroup[];
isTimingOn: boolean;
user: UserInfo;
recoveryMode?: boolean;
userOverride?: UserOverride;
docUsage?: FilteredDocUsageSummary;

View File

@@ -1,7 +1,6 @@
import {PartialPermissionSet} from 'app/common/ACLPermissions';
import {CellValue, RowRecord} from 'app/common/DocActions';
import {CompiledPredicateFormula} from 'app/common/PredicateFormula';
import {Role} from 'app/common/roles';
import {MetaRowRecord} from 'app/common/TableData';
export interface RuleSet {
@@ -25,12 +24,6 @@ export interface RulePart {
memo?: string;
}
// Light wrapper for reading records or user attributes.
export interface InfoView {
get(key: string): CellValue;
toJSON(): {[key: string]: any};
}
// As InfoView, but also supporting writing.
export interface InfoEditor {
get(key: string): CellValue;
@@ -38,22 +31,6 @@ export interface InfoEditor {
toJSON(): {[key: string]: any};
}
// Represents user info, which may include properties which are themselves RowRecords.
export interface UserInfo {
Name: string | null;
Email: string | null;
Access: Role | null;
Origin: string | null;
LinkKey: Record<string, string | undefined>;
UserID: number | null;
UserRef: string | null;
SessionID: string | null;
ShareRef: number | null; // This is a rowId in the _grist_Shares table, if the user
// is accessing a document via a share. Otherwise null.
[attributes: string]: unknown;
toJSON(): {[key: string]: any};
}
export interface UserAttributeRule {
origRecord?: RowRecord; // Original record used to create this UserAttributeRule.
name: string; // Should be unique among UserAttributeRules.

View File

@@ -10,7 +10,8 @@
*/
import {CellValue, RowRecord} from 'app/common/DocActions';
import {ErrorWithCode} from 'app/common/ErrorWithCode';
import {InfoView, UserInfo} from 'app/common/GranularAccessClause';
import {InfoView} from 'app/common/RecordView';
import {UserInfo} from 'app/common/User';
import {decodeObject} from 'app/plugin/objtypes';
import constant = require('lodash/constant');
@@ -31,11 +32,6 @@ export interface PredicateFormulaInput {
choice?: string|RowRecord|InfoView;
}
export class EmptyRecordView implements InfoView {
public get(_colId: string): CellValue { return null; }
public toJSON() { return {}; }
}
/**
* The result of compiling ParsedPredicateFormula.
*/
@@ -102,7 +98,7 @@ export function compilePredicateFormula(
break;
}
case 'dropdown-condition': {
validNames = ['rec', 'choice'];
validNames = ['rec', 'choice', 'user'];
break;
}
}

43
app/common/RecordView.ts Normal file
View File

@@ -0,0 +1,43 @@
import {CellValue, TableDataAction} from 'app/common/DocActions';
/** Light wrapper for reading records or user attributes. */
export interface InfoView {
get(key: string): CellValue;
toJSON(): {[key: string]: any};
}
/**
* A row-like view of TableDataAction, which is columnar in nature.
*
* If index value is undefined, acts as an EmptyRecordRow.
*/
export class RecordView implements InfoView {
public constructor(public data: TableDataAction, public index: number|undefined) {
}
public get(colId: string): CellValue {
if (this.index === undefined) { return null; }
if (colId === 'id') {
return this.data[2][this.index];
}
return this.data[3][colId]?.[this.index];
}
public has(colId: string) {
return colId === 'id' || colId in this.data[3];
}
public toJSON() {
if (this.index === undefined) { return {}; }
const results: {[key: string]: any} = {id: this.index};
for (const key of Object.keys(this.data[3])) {
results[key] = this.data[3][key]?.[this.index];
}
return results;
}
}
export class EmptyRecordView implements InfoView {
public get(_colId: string): CellValue { return null; }
public toJSON() { return {}; }
}

89
app/common/User.ts Normal file
View File

@@ -0,0 +1,89 @@
import {getTableId} from 'app/common/DocActions';
import {EmptyRecordView, RecordView} from 'app/common/RecordView';
import {Role} from 'app/common/roles';
/**
* Information about a user, including any user attributes.
*/
export interface UserInfo {
Name: string | null;
Email: string | null;
Access: Role | null;
Origin: string | null;
LinkKey: Record<string, string | undefined>;
UserID: number | null;
UserRef: string | null;
SessionID: string | null;
/**
* This is a rowId in the _grist_Shares table, if the user is accessing a document
* via a share. Otherwise null.
*/
ShareRef: number | null;
[attributes: string]: unknown;
}
/**
* Wrapper class for `UserInfo`.
*
* Contains methods for converting itself to different representations.
*/
export class User implements UserInfo {
public Name: string | null = null;
public UserID: number | null = null;
public Access: Role | null = null;
public Origin: string | null = null;
public LinkKey: Record<string, string | undefined> = {};
public Email: string | null = null;
public SessionID: string | null = null;
public UserRef: string | null = null;
public ShareRef: number | null = null;
[attribute: string]: any;
constructor(info: Record<string, unknown> = {}) {
Object.assign(this, info);
}
/**
* Returns a JSON representation of this class that excludes full row data,
* only keeping user info and table/row ids for any user attributes.
*
* Used by the sandbox to support `user` variables in formulas (see `user.py`).
*/
public toJSON() {
return this._toObject((value) => {
if (value instanceof RecordView) {
return [getTableId(value.data), value.get('id')];
} else if (value instanceof EmptyRecordView) {
return null;
} else {
return value;
}
});
}
/**
* Returns a record representation of this class, with all user attributes
* converted from `RecordView` instances to their JSON representations.
*
* Used by the client to support `user` variables in dropdown conditions.
*/
public toUserInfo(): UserInfo {
return this._toObject((value) => {
if (value instanceof RecordView) {
return value.toJSON();
} else if (value instanceof EmptyRecordView) {
return null;
} else {
return value;
}
}) as UserInfo;
}
private _toObject(mapValue: (value: unknown) => unknown) {
const results: {[key: string]: any} = {};
for (const [key, value] of Object.entries(this)) {
results[key] = mapValue(value);
}
return results;
}
}