(core) Add new UI for writing memos

Summary:
Adds a new UI for writing access rule memos.

Migrates old memos (written as Python comments) to the new UI.

Test Plan: Browser and migration tests.

Reviewers: jarek, dsagal

Reviewed By: jarek

Subscribers: dsagal, paulfitz

Differential Revision: https://phab.getgrist.com/D3726
This commit is contained in:
George Gevoian 2022-12-12 01:29:20 -05:00
parent aaf32ece50
commit e146f95c1c
14 changed files with 233 additions and 53 deletions

View File

@ -0,0 +1,35 @@
import {theme} from 'app/client/ui2018/cssVars';
import {dom, DomElementArg, Observable, styled} from 'grainjs';
export function aclMemoEditor(obs: Observable<string>, ...args: DomElementArg[]): HTMLInputElement {
return cssMemoInput(
dom.prop('value', obs),
dom.on('input', (_e, elem) => obs.set(elem.value)),
...args,
);
}
const cssMemoInput = styled('input', `
width: 100%;
min-height: 28px;
padding: 4px 5px;
border-radius: 3px;
border: 1px solid transparent;
cursor: pointer;
color: ${theme.accentText};
background-color: ${theme.inputBg};
caret-color : ${theme.inputFg};
font: 12px 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', monospace;
overflow: hidden;
text-overflow: ellipsis;
&:hover {
border: 1px solid ${theme.inputBorder};
}
&:not(&-disabled):focus-within {
outline: none !important;
cursor: text;
box-shadow: inset 0 0 0 1px ${theme.accentBorder};
border-color: ${theme.accentBorder};
}
`);

View File

@ -3,6 +3,7 @@
*/
import {aclColumnList} from 'app/client/aclui/ACLColumnList';
import {aclFormulaEditor} from 'app/client/aclui/ACLFormulaEditor';
import {aclMemoEditor} from 'app/client/aclui/ACLMemoEditor';
import {aclSelect} from 'app/client/aclui/ACLSelect';
import {ACLUsersPopup} from 'app/client/aclui/ACLUsers';
import {PermissionKey, permissionsWidget} from 'app/client/aclui/PermissionsWidget';
@ -264,6 +265,7 @@ export class AccessRules extends Disposable {
aclFormula: rule.aclFormula!,
permissionsText: rule.permissionsText!,
rulePos: rule.rulePos || null,
memo: rule.memo ?? '',
});
}
@ -397,6 +399,7 @@ export class AccessRules extends Disposable {
cssCellIcon(),
cssCell2(cssColHeaderCell(t('Condition'))),
cssCell1(cssColHeaderCell(t('Permissions'))),
cssCellIconWithMargins(),
cssCellIcon(),
)
)
@ -641,6 +644,7 @@ class TableRules extends Disposable {
cssCellIcon(),
cssCell2(cssColHeaderCell(t('Condition'))),
cssCell1(cssColHeaderCell(t('Permissions'))),
cssCellIconWithMargins(),
cssCellIcon(),
)
),
@ -1050,6 +1054,7 @@ class SpecialObsRuleSet extends ColumnObsRuleSet {
cssCellIcon(),
cssCell4(cssColHeaderCell(getSpecialRuleName(this.getColIds()))),
cssCell1(cssColHeaderCell('Permissions')),
cssCellIconWithMargins(),
cssCellIcon(),
),
cssTableRow(
@ -1272,6 +1277,16 @@ class ObsRulePart extends Disposable {
private _permissions = Observable.create<PartialPermissionSet>(
this, this._rulePart?.permissions || emptyPermissionSet());
// The memo text. Updated whenever changes are made within `_memoEditor`.
private _memo: Observable<string>;
// Reference to the memo editor element, for triggering focus. Shown when
// `_showMemoEditor` is true.
private _memoEditor: HTMLInputElement | undefined;
// Is the memo editor visible? Initialized to true if a saved memo exists for this rule.
private _showMemoEditor: Observable<boolean>;
// Whether the rule is being checked after a change. Saving will wait for such checks to finish.
private _checkPending = Observable.create(this, false);
@ -1283,13 +1298,20 @@ class ObsRulePart extends Disposable {
// Error message if any validation failed.
private _error: Computed<string>;
// rulePart is omitted for a new ObsRulePart added by the user. If given, isNew may be set to
// treat the rule as new and only use the rulePart for its initialization.
constructor(private _ruleSet: ObsRuleSet, private _rulePart?: RulePart, isNew = false) {
super();
this._memo = Observable.create(this, _rulePart?.memo ?? '');
// If this rule has a blank memo, don't show the editor.
this._showMemoEditor = Observable.create(this, !this.isBuiltIn() && this._memo.get() !== '');
if (_rulePart && isNew) {
// rulePart is omitted for a new ObsRulePart added by the user. If given, isNew may be set to
// treat the rule as new and only use the rulePart for its initialization.
this._rulePart = undefined;
}
this._error = Computed.create(this, (use) => {
return use(this._formulaError) ||
this._warnInvalidColIds(use(this._formulaProperties).usedColIds) ||
@ -1305,6 +1327,7 @@ class ObsRulePart extends Disposable {
if (use(this._checkPending)) { return RuleStatus.CheckPending; }
return getChangedStatus(
use(this._aclFormula) !== this._rulePart?.aclFormula ||
use(this._memo) !== (this._rulePart?.memo ?? '') ||
!isEqual(use(this._permissions), this._rulePart?.permissions)
);
});
@ -1318,6 +1341,7 @@ class ObsRulePart extends Disposable {
aclFormula: this._aclFormula.get(),
permissionsText: permissionSetToText(this._permissions.get()),
rulePos: this._rulePart?.origRecord?.rulePos as number|undefined,
memo: this._memo.get(),
};
}
@ -1347,50 +1371,98 @@ class ObsRulePart extends Disposable {
}
public buildRulePartDom(wide: boolean = false) {
return cssColumnGroup(
cssCellIcon(
(this._isNonFirstBuiltIn() ?
null :
cssIconButton(icon('Plus'),
dom.on('click', () => this._ruleSet.addRulePart(this)),
testId('rule-add'),
)
return cssRulePartAndMemo(
cssColumnGroup(
cssCellIcon(
(this._isNonFirstBuiltIn() ?
null :
cssIconButton(icon('Plus'),
dom.on('click', () => this._ruleSet.addRulePart(this)),
testId('rule-add'),
)
),
),
),
cssCell2(
wide ? cssCell4.cls('') : null,
aclFormulaEditor({
initialValue: this._aclFormula.get(),
readOnly: this.isBuiltIn(),
setValue: (value) => this._setAclFormula(value),
placeholder: dom.text((use) => {
return (
this._ruleSet.isSoleCondition(use, this) ? t('Everyone') :
this._ruleSet.isLastCondition(use, this) ? t('EveryoneElse') :
t('EnterCondition')
);
cssCell2(
wide ? cssCell4.cls('') : null,
aclFormulaEditor({
initialValue: this._aclFormula.get(),
readOnly: this.isBuiltIn(),
setValue: (value) => this._setAclFormula(value),
placeholder: dom.text((use) => {
return (
this._ruleSet.isSoleCondition(use, this) ? t('Everyone') :
this._ruleSet.isLastCondition(use, this) ? t('EveryoneElse') :
t('EnterCondition')
);
}),
getSuggestions: (prefix) => this._completions.get(),
}),
getSuggestions: (prefix) => this._completions.get(),
}),
testId('rule-acl-formula'),
testId('rule-acl-formula'),
),
cssCell1(cssCell.cls('-stretch'),
permissionsWidget(this._ruleSet.getAvailableBits(), this._permissions,
{disabled: this.isBuiltIn(), sanityCheck: (pset) => this.sanityCheck(pset)},
testId('rule-permissions')
),
),
cssCellIconWithMargins(
dom.maybe(use => !this.isBuiltIn() && !use(this._showMemoEditor), () =>
cssIconButton(icon('Memo'),
dom.on('click', () => {
this._showMemoEditor.set(true);
// Note that focus is set when the memo icon is clicked, and not when
// the editor is attached to the DOM; because rules with non-blank
// memos have their editors visible by default when the page is first
// loaded, focusing on creation could cause unintended focusing.
setTimeout(() => this._memoEditor?.focus(), 0);
}),
testId('rule-memo-add'),
)
),
),
cssCellIcon(
(this.isBuiltIn() ?
null :
cssIconButton(icon('Remove'),
dom.on('click', () => this._ruleSet.removeRulePart(this)),
testId('rule-remove'),
)
),
),
dom.maybe(this._error, (msg) => cssConditionError(msg, testId('rule-error'))),
testId('rule-part'),
),
cssCell1(cssCell.cls('-stretch'),
permissionsWidget(this._ruleSet.getAvailableBits(), this._permissions,
{disabled: this.isBuiltIn(), sanityCheck: (pset) => this.sanityCheck(pset)},
testId('rule-permissions')
dom.maybe(this._showMemoEditor, () =>
cssMemoColumnGroup(
cssCellIcon(),
cssMemoIcon('Memo'),
cssCell2(
wide ? cssCell4.cls('') : null,
this._memoEditor = aclMemoEditor(this._memo,
{
placeholder: t('MemoEditorPlaceholder'),
},
dom.onKeyDown({
// Match the behavior of the formula editor.
Enter: (_ev, el) => el.blur(),
}),
),
testId('rule-memo-editor'),
),
cssCellIconWithMargins(),
cssCellIcon(
cssIconButton(icon('Remove'),
dom.on('click', () => {
this._showMemoEditor.set(false);
this._memo.set('');
}),
testId('rule-memo-remove'),
),
),
testId('rule-memo'),
),
),
cssCellIcon(
(this.isBuiltIn() ?
null :
cssIconButton(icon('Remove'),
dom.on('click', () => this._ruleSet.removeRulePart(this)),
testId('rule-remove'),
)
),
),
dom.maybe(this._error, (msg) => cssConditionError(msg, testId('rule-error'))),
testId('rule-part'),
testId('rule-part-and-memo'),
);
}
@ -1652,6 +1724,7 @@ const cssCell = styled('div', `
// Variations on columns of different widths.
const cssCellIcon = styled(cssCell, `flex: none; width: 24px;`);
const cssCellIconWithMargins = styled(cssCellIcon, `margin: 0px 8px;`);
const cssCell1 = styled(cssCell, `flex: 1;`);
const cssCell2 = styled(cssCell, `flex: 2;`);
const cssCell4 = styled(cssCell, `flex: 4;`);
@ -1704,3 +1777,19 @@ const cssRuleProblems = styled('div', `
flex-wrap: wrap;
gap: 8px;
`);
const cssRulePartAndMemo = styled('div', `
display: flex;
flex-direction: column;
row-gap: 4px;
`);
const cssMemoColumnGroup = styled(cssColumnGroup, `
margin-bottom: 8px;
`);
const cssMemoIcon = styled(icon, `
--icon-color: ${theme.accentIcon};
margin-left: 8px;
margin-right: 8px;
`);

View File

@ -98,7 +98,7 @@ function buildNotificationDom(item: Notification, options: IBeaconOpenOptions) {
item.options.actions.map((action) => buildAction(action, item, options))
) : null,
item.options.memos.length ? cssToastMemos(
item.options.memos.map(memo => cssToastMemo(memo))
item.options.memos.map(memo => cssToastMemo(memo, testId('toast-memo')))
) : null,
),
dom.maybe(item.options.canUserClose, () =>

View File

@ -77,6 +77,7 @@ export type IconName = "ChartArea" |
"Lock" |
"Log" |
"Mail" |
"Memo" |
"Message" |
"Minus" |
"MobileChat" |
@ -209,6 +210,7 @@ export const IconList: IconName[] = ["ChartArea",
"Lock",
"Log",
"Mail",
"Memo",
"Message",
"Minus",
"MobileChat",

View File

@ -454,7 +454,7 @@ function readAclRules(docData: DocData, {log, compile, includeHelperCols}: ReadA
origRecord: rule,
aclFormula: String(rule.aclFormula),
matchFunc: rule.aclFormula ? compile?.(aclFormulaParsed) : defaultMatchFunc,
memo: aclFormulaParsed && aclFormulaParsed[0] === 'Comment' && aclFormulaParsed[2],
memo: rule.memo,
permissions: parsePermissions(String(rule.permissionsText)),
permissionsText: String(rule.permissionsText),
});

View File

@ -4,7 +4,7 @@ import { GristObjCode } from "app/plugin/GristData";
// tslint:disable:object-literal-key-quotes
export const SCHEMA_VERSION = 34;
export const SCHEMA_VERSION = 35;
export const schema = {
@ -170,6 +170,7 @@ export const schema = {
permissionsText : "Text",
rulePos : "PositionNumber",
userAttributes : "Text",
memo : "Text",
},
"_grist_ACLResources": {
@ -374,6 +375,7 @@ export interface SchemaTypes {
permissionsText: string;
rulePos: number;
userAttributes: string;
memo: string;
};
"_grist_ACLResources": {

View File

@ -6,7 +6,7 @@ export const GRIST_DOC_SQL = `
PRAGMA foreign_keys=OFF;
BEGIN TRANSACTION;
CREATE TABLE IF NOT EXISTS "_grist_DocInfo" (id INTEGER PRIMARY KEY, "docId" TEXT DEFAULT '', "peers" TEXT DEFAULT '', "basketId" TEXT DEFAULT '', "schemaVersion" INTEGER DEFAULT 0, "timezone" TEXT DEFAULT '', "documentSettings" TEXT DEFAULT '');
INSERT INTO _grist_DocInfo VALUES(1,'','','',34,'','');
INSERT INTO _grist_DocInfo VALUES(1,'','','',35,'','');
CREATE TABLE IF NOT EXISTS "_grist_Tables" (id INTEGER PRIMARY KEY, "tableId" TEXT DEFAULT '', "primaryViewId" INTEGER DEFAULT 0, "summarySourceTable" INTEGER DEFAULT 0, "onDemand" BOOLEAN DEFAULT 0, "rawViewSectionRef" INTEGER DEFAULT 0);
CREATE TABLE IF NOT EXISTS "_grist_Tables_column" (id INTEGER PRIMARY KEY, "parentId" INTEGER DEFAULT 0, "parentPos" NUMERIC DEFAULT 1e999, "colId" TEXT DEFAULT '', "type" TEXT DEFAULT '', "widgetOptions" TEXT DEFAULT '', "isFormula" BOOLEAN DEFAULT 0, "formula" TEXT DEFAULT '', "label" TEXT DEFAULT '', "untieColIdFromLabel" BOOLEAN DEFAULT 0, "summarySourceCol" INTEGER DEFAULT 0, "displayCol" INTEGER DEFAULT 0, "visibleCol" INTEGER DEFAULT 0, "rules" TEXT DEFAULT NULL, "recalcWhen" INTEGER DEFAULT 0, "recalcDeps" TEXT DEFAULT NULL);
CREATE TABLE IF NOT EXISTS "_grist_Imports" (id INTEGER PRIMARY KEY, "tableRef" INTEGER DEFAULT 0, "origFileName" TEXT DEFAULT '', "parseFormula" TEXT DEFAULT '', "delimiter" TEXT DEFAULT '', "doublequote" BOOLEAN DEFAULT 0, "escapechar" TEXT DEFAULT '', "quotechar" TEXT DEFAULT '', "skipinitialspace" BOOLEAN DEFAULT 0, "encoding" TEXT DEFAULT '', "hasHeaders" BOOLEAN DEFAULT 0);
@ -23,8 +23,8 @@ CREATE TABLE IF NOT EXISTS "_grist_Validations" (id INTEGER PRIMARY KEY, "formul
CREATE TABLE IF NOT EXISTS "_grist_REPL_Hist" (id INTEGER PRIMARY KEY, "code" TEXT DEFAULT '', "outputText" TEXT DEFAULT '', "errorText" TEXT DEFAULT '');
CREATE TABLE IF NOT EXISTS "_grist_Attachments" (id INTEGER PRIMARY KEY, "fileIdent" TEXT DEFAULT '', "fileName" TEXT DEFAULT '', "fileType" TEXT DEFAULT '', "fileSize" INTEGER DEFAULT 0, "imageHeight" INTEGER DEFAULT 0, "imageWidth" INTEGER DEFAULT 0, "timeDeleted" DATETIME DEFAULT NULL, "timeUploaded" DATETIME DEFAULT NULL);
CREATE TABLE IF NOT EXISTS "_grist_Triggers" (id INTEGER PRIMARY KEY, "tableRef" INTEGER DEFAULT 0, "eventTypes" TEXT DEFAULT NULL, "isReadyColRef" INTEGER DEFAULT 0, "actions" TEXT DEFAULT '');
CREATE TABLE IF NOT EXISTS "_grist_ACLRules" (id INTEGER PRIMARY KEY, "resource" INTEGER DEFAULT 0, "permissions" INTEGER DEFAULT 0, "principals" TEXT DEFAULT '', "aclFormula" TEXT DEFAULT '', "aclColumn" INTEGER DEFAULT 0, "aclFormulaParsed" TEXT DEFAULT '', "permissionsText" TEXT DEFAULT '', "rulePos" NUMERIC DEFAULT 1e999, "userAttributes" TEXT DEFAULT '');
INSERT INTO _grist_ACLRules VALUES(1,1,63,'[1]','',0,'','',1e999,'');
CREATE TABLE IF NOT EXISTS "_grist_ACLRules" (id INTEGER PRIMARY KEY, "resource" INTEGER DEFAULT 0, "permissions" INTEGER DEFAULT 0, "principals" TEXT DEFAULT '', "aclFormula" TEXT DEFAULT '', "aclColumn" INTEGER DEFAULT 0, "aclFormulaParsed" TEXT DEFAULT '', "permissionsText" TEXT DEFAULT '', "rulePos" NUMERIC DEFAULT 1e999, "userAttributes" TEXT DEFAULT '', "memo" TEXT DEFAULT '');
INSERT INTO _grist_ACLRules VALUES(1,1,63,'[1]','',0,'','',1e999,'','');
CREATE TABLE IF NOT EXISTS "_grist_ACLResources" (id INTEGER PRIMARY KEY, "tableId" TEXT DEFAULT '', "colIds" TEXT DEFAULT '');
INSERT INTO _grist_ACLResources VALUES(1,'','');
CREATE TABLE IF NOT EXISTS "_grist_ACLPrincipals" (id INTEGER PRIMARY KEY, "type" TEXT DEFAULT '', "userEmail" TEXT DEFAULT '', "userName" TEXT DEFAULT '', "groupName" TEXT DEFAULT '', "instanceId" TEXT DEFAULT '');
@ -43,7 +43,7 @@ export const GRIST_DOC_WITH_TABLE1_SQL = `
PRAGMA foreign_keys=OFF;
BEGIN TRANSACTION;
CREATE TABLE IF NOT EXISTS "_grist_DocInfo" (id INTEGER PRIMARY KEY, "docId" TEXT DEFAULT '', "peers" TEXT DEFAULT '', "basketId" TEXT DEFAULT '', "schemaVersion" INTEGER DEFAULT 0, "timezone" TEXT DEFAULT '', "documentSettings" TEXT DEFAULT '');
INSERT INTO _grist_DocInfo VALUES(1,'','','',34,'','');
INSERT INTO _grist_DocInfo VALUES(1,'','','',35,'','');
CREATE TABLE IF NOT EXISTS "_grist_Tables" (id INTEGER PRIMARY KEY, "tableId" TEXT DEFAULT '', "primaryViewId" INTEGER DEFAULT 0, "summarySourceTable" INTEGER DEFAULT 0, "onDemand" BOOLEAN DEFAULT 0, "rawViewSectionRef" INTEGER DEFAULT 0);
INSERT INTO _grist_Tables VALUES(1,'Table1',1,0,0,2);
CREATE TABLE IF NOT EXISTS "_grist_Tables_column" (id INTEGER PRIMARY KEY, "parentId" INTEGER DEFAULT 0, "parentPos" NUMERIC DEFAULT 1e999, "colId" TEXT DEFAULT '', "type" TEXT DEFAULT '', "widgetOptions" TEXT DEFAULT '', "isFormula" BOOLEAN DEFAULT 0, "formula" TEXT DEFAULT '', "label" TEXT DEFAULT '', "untieColIdFromLabel" BOOLEAN DEFAULT 0, "summarySourceCol" INTEGER DEFAULT 0, "displayCol" INTEGER DEFAULT 0, "visibleCol" INTEGER DEFAULT 0, "rules" TEXT DEFAULT NULL, "recalcWhen" INTEGER DEFAULT 0, "recalcDeps" TEXT DEFAULT NULL);
@ -76,8 +76,8 @@ CREATE TABLE IF NOT EXISTS "_grist_Validations" (id INTEGER PRIMARY KEY, "formul
CREATE TABLE IF NOT EXISTS "_grist_REPL_Hist" (id INTEGER PRIMARY KEY, "code" TEXT DEFAULT '', "outputText" TEXT DEFAULT '', "errorText" TEXT DEFAULT '');
CREATE TABLE IF NOT EXISTS "_grist_Attachments" (id INTEGER PRIMARY KEY, "fileIdent" TEXT DEFAULT '', "fileName" TEXT DEFAULT '', "fileType" TEXT DEFAULT '', "fileSize" INTEGER DEFAULT 0, "imageHeight" INTEGER DEFAULT 0, "imageWidth" INTEGER DEFAULT 0, "timeDeleted" DATETIME DEFAULT NULL, "timeUploaded" DATETIME DEFAULT NULL);
CREATE TABLE IF NOT EXISTS "_grist_Triggers" (id INTEGER PRIMARY KEY, "tableRef" INTEGER DEFAULT 0, "eventTypes" TEXT DEFAULT NULL, "isReadyColRef" INTEGER DEFAULT 0, "actions" TEXT DEFAULT '');
CREATE TABLE IF NOT EXISTS "_grist_ACLRules" (id INTEGER PRIMARY KEY, "resource" INTEGER DEFAULT 0, "permissions" INTEGER DEFAULT 0, "principals" TEXT DEFAULT '', "aclFormula" TEXT DEFAULT '', "aclColumn" INTEGER DEFAULT 0, "aclFormulaParsed" TEXT DEFAULT '', "permissionsText" TEXT DEFAULT '', "rulePos" NUMERIC DEFAULT 1e999, "userAttributes" TEXT DEFAULT '');
INSERT INTO _grist_ACLRules VALUES(1,1,63,'[1]','',0,'','',1e999,'');
CREATE TABLE IF NOT EXISTS "_grist_ACLRules" (id INTEGER PRIMARY KEY, "resource" INTEGER DEFAULT 0, "permissions" INTEGER DEFAULT 0, "principals" TEXT DEFAULT '', "aclFormula" TEXT DEFAULT '', "aclColumn" INTEGER DEFAULT 0, "aclFormulaParsed" TEXT DEFAULT '', "permissionsText" TEXT DEFAULT '', "rulePos" NUMERIC DEFAULT 1e999, "userAttributes" TEXT DEFAULT '', "memo" TEXT DEFAULT '');
INSERT INTO _grist_ACLRules VALUES(1,1,63,'[1]','',0,'','',1e999,'','');
CREATE TABLE IF NOT EXISTS "_grist_ACLResources" (id INTEGER PRIMARY KEY, "tableId" TEXT DEFAULT '', "colIds" TEXT DEFAULT '');
INSERT INTO _grist_ACLResources VALUES(1,'','');
CREATE TABLE IF NOT EXISTS "_grist_ACLPrincipals" (id INTEGER PRIMARY KEY, "type" TEXT DEFAULT '', "userEmail" TEXT DEFAULT '', "userName" TEXT DEFAULT '', "groupName" TEXT DEFAULT '', "instanceId" TEXT DEFAULT '');

View File

@ -1115,7 +1115,7 @@ def migration33(tdset):
@migration(schema_version=34)
def migration34(tdset):
""""
"""
Add pinned column to _grist_Filters and populate based on existing sections.
When populating, pinned will be set to true for filters that either belong to
@ -1154,3 +1154,37 @@ def migration34(tdset):
))
return tdset.apply_doc_actions(doc_actions)
@migration(schema_version=35)
def migration35(tdset):
"""
Add memo column to _grist_ACLRules and populate with comments stored in
_grist_ACLRules.aclFormula.
From this version on, comments in _grist_ACLRules.aclFormula will no longer
be used as memos.
"""
doc_actions = [add_column('_grist_ACLRules', 'memo', 'Text')]
acl_rules = list(actions.transpose_bulk_action(tdset.all_tables['_grist_ACLRules']))
# List of (acl_rule_rec, memo) pairs.
acl_rule_updates = []
for acl_rule_rec in acl_rules:
acl_formula = safe_parse(acl_rule_rec.aclFormulaParsed)
if not acl_formula or acl_formula[0] != 'Comment':
continue
acl_rule_updates.append((
acl_rule_rec,
acl_formula[2]
))
if acl_rule_updates:
doc_actions.append(actions.BulkUpdateRecord(
'_grist_ACLRules',
[acl_rule_rec.id for acl_rule_rec, _ in acl_rule_updates],
{'memo': [memo for _, memo in acl_rule_updates]},
))
return tdset.apply_doc_actions(doc_actions)

View File

@ -15,7 +15,7 @@ import six
import actions
SCHEMA_VERSION = 34
SCHEMA_VERSION = 35
def make_column(col_id, col_type, formula='', isFormula=False):
return {
@ -280,6 +280,10 @@ def schema_create_actions():
# becomes available to matchFunc. These rules are processed in order of rulePos,
# which should list them before regular rules.
make_column('userAttributes', 'Text'),
# Text of memo associated with this rule, if any. Prior to version 35, this was
# stored within aclFormula.
make_column('memo', 'Text'),
]),
# Note that the special resource with tableId of '' and colIds of '' should be ignored. It is

View File

@ -78,6 +78,7 @@
--icon-Lock: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGcgZmlsbD0iIzAwMCIgZmlsbC1ydWxlPSJub256ZXJvIj48cGF0aCBkPSJNMTIgNkwxMCA2IDEwIDRDMTAgMi44OTU0MzA1IDkuMTA0NTY5NSAyIDggMiA2Ljg5NTQzMDUgMiA2IDIuODk1NDMwNSA2IDRMNiA2IDQgNiA0IDRDNCAxLjc5MDg2MSA1Ljc5MDg2MSAwIDggMCAxMC4yMDkxMzkgMCAxMiAxLjc5MDg2MSAxMiA0TDEyIDZ6TTE0IDdMMiA3QzEuNDQ3NzE1MjUgNyAxIDcuNDQ3NzE1MjUgMSA4TDEgMTVDMSAxNS41NTIyODQ3IDEuNDQ3NzE1MjUgMTYgMiAxNkwxNCAxNkMxNC41NTIyODQ3IDE2IDE1IDE1LjU1MjI4NDcgMTUgMTVMMTUgOEMxNSA3LjQ0NzcxNTI1IDE0LjU1MjI4NDcgNyAxNCA3ek04IDEzQzYuODk1NDMwNSAxMyA2IDEyLjEwNDU2OTUgNiAxMSA2IDkuODk1NDMwNSA2Ljg5NTQzMDUgOSA4IDkgOS4xMDQ1Njk1IDkgMTAgOS44OTU0MzA1IDEwIDExIDEwIDEyLjEwNDU2OTUgOS4xMDQ1Njk1IDEzIDggMTN6Ii8+PC9nPjwvc3ZnPg==');
--icon-Log: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTcuNSw4LjUgTDYuNSw4LjUgQzYuMjIzODU3NjMsOC41IDYsOC4yNzYxNDIzNyA2LDggQzYsNy43MjM4NTc2MyA2LjIyMzg1NzYzLDcuNSA2LjUsNy41IEw3LjUsNy41IEw3LjUsMC41IEM3LjUsMC4yMjM4NTc2MjUgNy43MjM4NTc2MywwIDgsMCBDOC4yNzYxNDIzNywwIDguNSwwLjIyMzg1NzYyNSA4LjUsMC41IEw4LjUsMyBMOS41LDMgQzkuNzc2MTQyMzcsMyAxMCwzLjIyMzg1NzYzIDEwLDMuNSBDMTAsMy43NzYxNDIzNyA5Ljc3NjE0MjM3LDQgOS41LDQgTDguNSw0IEw4LjUsMTIgTDkuNSwxMiBDOS43NzYxNDIzNywxMiAxMCwxMi4yMjM4NTc2IDEwLDEyLjUgQzEwLDEyLjc3NjE0MjQgOS43NzYxNDIzNywxMyA5LjUsMTMgTDguNSwxMyBMOC41LDE1LjUgQzguNSwxNS43NzYxNDI0IDguMjc2MTQyMzcsMTYgOCwxNiBDNy43MjM4NTc2MywxNiA3LjUsMTUuNzc2MTQyNCA3LjUsMTUuNSBMNy41LDguNSBaIE0xMiwyIEwxMiw1IEwxNSw1IEwxNSwyIEwxMiwyIFogTTExLjUsMSBMMTUuNSwxIEMxNS43NzYxNDI0LDEgMTYsMS4yMjM4NTc2MyAxNiwxLjUgTDE2LDUuNSBDMTYsNS43NzYxNDIzNyAxNS43NzYxNDI0LDYgMTUuNSw2IEwxMS41LDYgQzExLjIyMzg1NzYsNiAxMSw1Ljc3NjE0MjM3IDExLDUuNSBMMTEsMS41IEMxMSwxLjIyMzg1NzYzIDExLjIyMzg1NzYsMSAxMS41LDEgWiBNMTIsMTEgTDEyLDE0IEwxNSwxNCBMMTUsMTEgTDEyLDExIFogTTExLjUsMTAgTDE1LjUsMTAgQzE1Ljc3NjE0MjQsMTAgMTYsMTAuMjIzODU3NiAxNiwxMC41IEwxNiwxNC41IEMxNiwxNC43NzYxNDI0IDE1Ljc3NjE0MjQsMTUgMTUuNSwxNSBMMTEuNSwxNSBDMTEuMjIzODU3NiwxNSAxMSwxNC43NzYxNDI0IDExLDE0LjUgTDExLDEwLjUgQzExLDEwLjIyMzg1NzYgMTEuMjIzODU3NiwxMCAxMS41LDEwIFogTTQsOS41IEw0LDYuNSBMMSw2LjUgTDEsOS41IEw0LDkuNSBaIE00LjUsMTAuNSBMMC41LDEwLjUgQzAuMjIzODU3NjI1LDEwLjUgMCwxMC4yNzYxNDI0IDAsMTAgTDguODgxNzg0MmUtMTYsNiBDOC44ODE3ODQyZS0xNiw1LjcyMzg1NzYzIDAuMjIzODU3NjI1LDUuNSAwLjUsNS41IEw0LjUsNS41IEM0Ljc3NjE0MjM3LDUuNSA1LDUuNzIzODU3NjMgNSw2IEw1LDEwIEM1LDEwLjI3NjE0MjQgNC43NzYxNDIzNywxMC41IDQuNSwxMC41IFoiIGZpbGw9IiMwMDAiIGZpbGwtcnVsZT0ibm9uemVybyIvPjwvc3ZnPg==');
--icon-Mail: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTQiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTE0LjUsMTMgQzE0Ljc3NTg1NzYsMTMgMTUsMTIuNzc1ODU3NiAxNSwxMi41IEwxNSwxLjUgQzE1LDEuMjI0MTQyMzcgMTQuNzc1ODU3NiwxIDE0LjUsMSBMMS41LDEgQzEuMjI0MTQyMzcsMSAxLDEuMjI0MTQyMzcgMSwxLjUgTDEsMTIuNSBDMSwxMi43NzU4NTc2IDEuMjI0MTQyMzcsMTMgMS41LDEzIEwxNC41LDEzIFogTTE0LjUsMTQgTDEuNSwxNCBDMC42NzE4NTc2MjUsMTQgMCwxMy4zMjgxNDI0IDAsMTIuNSBMMCwxLjUgQzAsMC42NzE4NTc2MjUgMC42NzE4NTc2MjUsOC44ODE3ODQyZS0xNiAxLjUsOC44ODE3ODQyZS0xNiBMMTQuNSw4Ljg4MTc4NDJlLTE2IEMxNS4zMjgxNDI0LDguODgxNzg0MmUtMTYgMTYsMC42NzE4NTc2MjUgMTYsMS41IEwxNiwxMi41IEMxNiwxMy4zMjgxNDI0IDE1LjMyODE0MjQsMTQgMTQuNSwxNCBaIE0xMy4xODMzODExLDMuMTEzMDIxMzUgQzEzLjM5NzEwMzUsMi45MzgxNTc1NiAxMy43MTIxMTQ5LDIuOTY5NjU4NyAxMy44ODY5Nzg2LDMuMTgzMzgxMSBDMTQuMDYxODQyNCwzLjM5NzEwMzUxIDE0LjAzMDM0MTMsMy43MTIxMTQ4NiAxMy44MTY2MTg5LDMuODg2OTc4NjUgTDguMzE2NjE4OSw4LjM4Njk3ODY1IEM4LjEzMjQzNTk1LDguNTM3NjczNzggNy44Njc1NjQwNSw4LjUzNzY3Mzc4IDcuNjgzMzgxMSw4LjM4Njk3ODY1IEwyLjE4MzM4MTEsMy44ODY5Nzg2NSBDMS45Njk2NTg3LDMuNzEyMTE0ODYgMS45MzgxNTc1NiwzLjM5NzEwMzUxIDIuMTEzMDIxMzUsMy4xODMzODExIEMyLjI4Nzg4NTE0LDIuOTY5NjU4NyAyLjYwMjg5NjQ5LDIuOTM4MTU3NTYgMi44MTY2MTg5LDMuMTEzMDIxMzUgTDgsNy4zNTM5Njk1MyBMMTMuMTgzMzgxMSwzLjExMzAyMTM1IFoiIGZpbGw9IiMwMDAiIGZpbGwtcnVsZT0ibm9uemVybyIvPjwvc3ZnPg==');
--icon-Memo: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGcgY2xpcC1wYXRoPSJ1cmwoI2NsaXAwXzE4NTFfMTYzMjIpIiBzdHJva2U9IiMwMDkwNTgiIHN0cm9rZS1taXRlcmxpbWl0PSIxMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48cGF0aCBkPSJNMTQuNS41SDEuNUMuOTQ4LjUuNS45NDguNSAxLjVWMTEuNUMuNSAxMi4wNTIuOTQ4IDEyLjUgMS41IDEyLjVINS41TDggMTUuNSAxMC41IDEyLjVIMTQuNUMxNS4wNTIgMTIuNSAxNS41IDEyLjA1MiAxNS41IDExLjVWMS41QzE1LjUuOTQ4IDE1LjA1Mi41IDE0LjUuNXpNMy41IDQuNUgxMi41TTMuNSA4LjVIMTIuNSIvPjwvZz48ZGVmcz48Y2xpcFBhdGggaWQ9ImNsaXAwXzE4NTFfMTYzMjIiPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0wIDBIMTZWMTZIMHoiLz48L2NsaXBQYXRoPjwvZGVmcz48L3N2Zz4=');
--icon-Message: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTE1LjUgN0MxNS41IDMuNDEgMTIuMTQyIDAuNSA4IDAuNUMzLjg1OCAwLjUgMC41IDMuNDEgMC41IDdDMC41IDEwLjU5IDMuODU4IDEzLjUgOCAxMy41QzguNTI1IDEzLjUgOS4wMzcgMTMuNDUyIDkuNTMyIDEzLjM2M0wxMy41IDE1LjVWMTEuNDA5QzE0LjczOCAxMC4yNSAxNS41IDguNzA0IDE1LjUgN1oiIHN0cm9rZT0iIzkyOTI5OSIgc3Ryb2tlLW1pdGVybGltaXQ9IjEwIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz48L3N2Zz4=');
--icon-Minus: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHJlY3QgZmlsbD0iIzAwMCIgZmlsbC1ydWxlPSJub256ZXJvIiB4PSIyIiB5PSI3LjUiIHdpZHRoPSIxMiIgaGVpZ2h0PSIxIiByeD0iLjUiLz48L3N2Zz4=');
--icon-MobileChat: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTExLjUgOC41VjE0LjVDMTEuNSAxNC43NjUyIDExLjM5NDYgMTUuMDE5NiAxMS4yMDcxIDE1LjIwNzFDMTEuMDE5NiAxNS4zOTQ2IDEwLjc2NTIgMTUuNSAxMC41IDE1LjVIMi41QzIuMjM0NzggMTUuNSAxLjk4MDQzIDE1LjM5NDYgMS43OTI4OSAxNS4yMDcxQzEuNjA1MzYgMTUuMDE5NiAxLjUgMTQuNzY1MiAxLjUgMTQuNVYyLjVDMS41IDIuMjM0NzggMS42MDUzNiAxLjk4MDQzIDEuNzkyODkgMS43OTI4OUMxLjk4MDQzIDEuNjA1MzYgMi4yMzQ3OCAxLjUgMi41IDEuNUg0LjUiIHN0cm9rZT0iIzI2MjYzMyIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+PHBhdGggZD0iTTE0LjUgNi41SDkuNUw2LjUgOC41VjEuNUM2LjUgMS4yMzQ3OCA2LjYwNTM2IDAuOTgwNDMgNi43OTI4OSAwLjc5Mjg5M0M2Ljk4MDQzIDAuNjA1MzU3IDcuMjM0NzggMC41IDcuNSAwLjVIMTQuNUMxNC43NjUyIDAuNSAxNS4wMTk2IDAuNjA1MzU3IDE1LjIwNzEgMC43OTI4OTNDMTUuMzk0NiAwLjk4MDQzIDE1LjUgMS4yMzQ3OCAxNS41IDEuNVY1LjVDMTUuNSA1Ljc2NTIyIDE1LjM5NDYgNi4wMTk1NyAxNS4yMDcxIDYuMjA3MTFDMTUuMDE5NiA2LjM5NDY0IDE0Ljc2NTIgNi41IDE0LjUgNi41WiIgc3Ryb2tlPSIjMjYyNjMzIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz48L3N2Zz4=');

View File

@ -587,7 +587,8 @@
"EnterCondition": "Enter Condition",
"RemoveRulesMentioningTable": "Remove {{- tableId }} rules",
"RemoveRulesMentioningColumn": "Remove column {{- colId }} from {{- tableId }} rules",
"RemoveUserAttribute": "Remove {{- name }} user attribute"
"RemoveUserAttribute": "Remove {{- name }} user attribute",
"MemoEditorPlaceholder": "Type a message..."
},
"PermissionsWidget": {
"AllowAll": "Allow All",

View File

@ -0,0 +1,12 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1851_16322)">
<path d="M14.5 0.5H1.5C0.948 0.5 0.5 0.948 0.5 1.5V11.5C0.5 12.052 0.948 12.5 1.5 12.5H5.5L8 15.5L10.5 12.5H14.5C15.052 12.5 15.5 12.052 15.5 11.5V1.5C15.5 0.948 15.052 0.5 14.5 0.5Z" stroke="#009058" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3.5 4.5H12.5" stroke="#009058" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3.5 8.5H12.5" stroke="#009058" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_1851_16322">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 750 B

Binary file not shown.

BIN
test/fixtures/docs/Memos-v34.grist vendored Normal file

Binary file not shown.