mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) add buttons to delete bad rules
Summary: When access rules refer to tables and/or columns that no longer exist, offer convenient buttons to remove these rules. It could alternatively be useful to generate errors when deleting tables or columns that are mentioned in access rules, and refuse to do so unless the access rules are updated first. Test Plan: added and updated tests Reviewers: georgegevoian Reviewed By: georgegevoian Subscribers: jarek Differential Revision: https://phab.getgrist.com/D3718
This commit is contained in:
@@ -26,7 +26,7 @@ import {
|
||||
summarizePermissionSet
|
||||
} from 'app/common/ACLPermissions';
|
||||
import {ACLRuleCollection, SPECIAL_RULES_TABLE_ID} from 'app/common/ACLRuleCollection';
|
||||
import {AclTableDescription, getTableTitle} from 'app/common/ActiveDocAPI';
|
||||
import {AclRuleProblem, AclTableDescription, getTableTitle} from 'app/common/ActiveDocAPI';
|
||||
import {BulkColValues, getColValues, RowRecord, UserAction} from 'app/common/DocActions';
|
||||
import {
|
||||
FormulaProperties,
|
||||
@@ -112,6 +112,9 @@ export class AccessRules extends Disposable {
|
||||
// Error or warning message to show next to Save/Reset buttons if non-empty.
|
||||
private _errorMessage = Observable.create(this, '');
|
||||
|
||||
// Details of rule problems, for offering solutions to the user.
|
||||
private _ruleProblems = this.autoDispose(obsArray<AclRuleProblem>());
|
||||
|
||||
// Map of tableId to basic metadata for all tables in the document.
|
||||
private _aclResources = new Map<string, AclTableDescription>();
|
||||
|
||||
@@ -196,7 +199,8 @@ export class AccessRules extends Disposable {
|
||||
this._updateDocAccessData(),
|
||||
this._gristDoc.docComm.getAclResources(),
|
||||
]);
|
||||
this._aclResources = new Map(Object.entries(aclResources));
|
||||
this._aclResources = new Map(Object.entries(aclResources.tables));
|
||||
this._ruleProblems.set(aclResources.problems);
|
||||
if (this.isDisposed()) { return; }
|
||||
|
||||
this._tableRules.set(
|
||||
@@ -354,6 +358,14 @@ export class AccessRules extends Disposable {
|
||||
dom.text(this._errorMessage),
|
||||
testId('access-rules-error')
|
||||
),
|
||||
|
||||
dom.maybe(use => {
|
||||
const ruleProblems = use(this._ruleProblems);
|
||||
return ruleProblems.length > 0 ? ruleProblems : null;
|
||||
}, ruleProblems =>
|
||||
cssSection(
|
||||
cssRuleProblems(
|
||||
this.buildRuleProblemsDom(ruleProblems)))),
|
||||
shadowScroll(
|
||||
dom.maybe(use => use(this._userAttrRules).length, () =>
|
||||
cssSection(
|
||||
@@ -398,6 +410,26 @@ export class AccessRules extends Disposable {
|
||||
);
|
||||
}
|
||||
|
||||
public buildRuleProblemsDom(ruleProblems: AclRuleProblem[]) {
|
||||
const buttons: Array<HTMLAnchorElement | HTMLButtonElement> = [];
|
||||
for (const problem of ruleProblems) {
|
||||
// Is the problem a missing table?
|
||||
if (problem.tables) {
|
||||
this._addButtonsForMissingTables(buttons, problem.tables.tableIds);
|
||||
}
|
||||
// Is the problem a missing column?
|
||||
if (problem.columns) {
|
||||
this._addButtonsForMissingColumns(buttons, problem.columns.tableId, problem.columns.colIds);
|
||||
}
|
||||
// Is the problem a misconfigured user attribute?
|
||||
if (problem.userAttributes) {
|
||||
const names = problem.userAttributes.names;
|
||||
this._addButtonsForMisconfiguredUserAttributes(buttons, names);
|
||||
}
|
||||
}
|
||||
return buttons.map(button => dom('span', button));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of all rule records, for saving.
|
||||
*/
|
||||
@@ -476,6 +508,63 @@ export class AccessRules extends Disposable {
|
||||
|
||||
this._aclUsersPopup.init(pageModel, permissionData);
|
||||
}
|
||||
|
||||
private _addButtonsForMissingTables(buttons: Array<HTMLAnchorElement | HTMLButtonElement>, tableIds: string[]) {
|
||||
for (const tableId of tableIds) {
|
||||
// We don't know what the table's name was, just its tableId.
|
||||
// Hopefully, the user will understand.
|
||||
const title = t('RemoveRulesMentioningTable', { tableId });
|
||||
const button = bigBasicButton(title, cssRemoveIcon('Remove'), dom.on('click', async () => {
|
||||
await Promise.all(this._tableRules.get()
|
||||
.filter(rules => rules.tableId === tableId)
|
||||
.map(rules => rules.remove()));
|
||||
button.style.display = 'none';
|
||||
}));
|
||||
buttons.push(button);
|
||||
}
|
||||
}
|
||||
|
||||
private _addButtonsForMissingColumns(buttons: Array<HTMLAnchorElement | HTMLButtonElement>,
|
||||
tableId: string, colIds: string[]) {
|
||||
const removeColRules = (rules: TableRules, colId: string) => {
|
||||
for (const rule of rules.columnRuleSets.get()) {
|
||||
const ruleColIds = new Set(rule.getColIdList());
|
||||
if (!ruleColIds.has(colId)) { continue; }
|
||||
if (ruleColIds.size === 1) {
|
||||
rule.remove();
|
||||
} else {
|
||||
rule.removeColId(colId);
|
||||
}
|
||||
}
|
||||
};
|
||||
for (const colId of colIds) {
|
||||
// TODO: we could translate tableId to table name in this case.
|
||||
const title = t('RemoveRulesMentioningColumn', { tableId, colId });
|
||||
const button = bigBasicButton(title, cssRemoveIcon('Remove'), dom.on('click', async () => {
|
||||
await Promise.all(this._tableRules.get()
|
||||
.filter(rules => rules.tableId === tableId)
|
||||
.map(rules => removeColRules(rules, colId)));
|
||||
button.style.display = 'none';
|
||||
}));
|
||||
buttons.push(button);
|
||||
}
|
||||
}
|
||||
|
||||
private _addButtonsForMisconfiguredUserAttributes(
|
||||
buttons: Array<HTMLAnchorElement | HTMLButtonElement>,
|
||||
names: string[]
|
||||
) {
|
||||
for (const name of names) {
|
||||
const title = t('RemoveUserAttribute', {name});
|
||||
const button = bigBasicButton(title, cssRemoveIcon('Remove'), dom.on('click', async () => {
|
||||
await Promise.all(this._userAttrRules.get()
|
||||
.filter(rule => rule.name.get() === name)
|
||||
.map(rule => rule.remove()));
|
||||
button.style.display = 'none';
|
||||
}));
|
||||
buttons.push(button);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Represents all rules for a table.
|
||||
@@ -521,6 +610,14 @@ class TableRules extends Disposable {
|
||||
});
|
||||
}
|
||||
|
||||
public remove() {
|
||||
this._accessRules.removeTableRules(this);
|
||||
}
|
||||
|
||||
public get columnRuleSets() {
|
||||
return this._columnRuleSets;
|
||||
}
|
||||
|
||||
public buildDom() {
|
||||
return cssSection(
|
||||
cssSectionHeading(
|
||||
@@ -530,7 +627,7 @@ class TableRules extends Disposable {
|
||||
menuItemAsync(() => this._addColumnRuleSet(), t('AddColumnRule')),
|
||||
menuItemAsync(() => this._addDefaultRuleSet(), t('AddDefaultRule'),
|
||||
dom.cls('disabled', use => Boolean(use(this._defaultRuleSet)))),
|
||||
menuItemAsync(() => this._accessRules.removeTableRules(this), t('DeleteTableRules')),
|
||||
menuItemAsync(() => this.remove(), t('DeleteTableRules')),
|
||||
]),
|
||||
testId('rule-table-menu-btn'),
|
||||
),
|
||||
@@ -707,6 +804,10 @@ abstract class ObsRuleSet extends Disposable {
|
||||
});
|
||||
}
|
||||
|
||||
public remove() {
|
||||
this._tableRules?.removeRuleSet(this);
|
||||
}
|
||||
|
||||
public getRules(tableId: string): RuleRec[] {
|
||||
// Return every part in the body, tacking on resourceRec to each rule.
|
||||
return this._body.get().map(part => ({
|
||||
@@ -864,6 +965,10 @@ class ColumnObsRuleSet extends ObsRuleSet {
|
||||
return this._colIds.get();
|
||||
}
|
||||
|
||||
public removeColId(colId: string) {
|
||||
this._colIds.set(this._colIds.get().filter(c => (c !== colId)));
|
||||
}
|
||||
|
||||
public getColIds(): string {
|
||||
return this._colIds.get().join(",");
|
||||
}
|
||||
@@ -1031,6 +1136,10 @@ class ObsUserAttributeRule extends Disposable {
|
||||
});
|
||||
}
|
||||
|
||||
public remove() {
|
||||
this._accessRules.removeUserAttributes(this);
|
||||
}
|
||||
|
||||
public get name() { return this._name; }
|
||||
public get tableId() { return this._tableId; }
|
||||
|
||||
@@ -1440,6 +1549,10 @@ const cssDropdownIcon = styled(icon, `
|
||||
margin: -2px -2px 0 4px;
|
||||
`);
|
||||
|
||||
const cssRemoveIcon = styled(icon, `
|
||||
margin: -2px -2px 0 4px;
|
||||
`);
|
||||
|
||||
const cssSection = styled('div', `
|
||||
margin: 16px 16px 24px 16px;
|
||||
`);
|
||||
@@ -1581,3 +1694,13 @@ const cssDefaultLabel = styled('div', `
|
||||
color: ${theme.accessRulesTableBodyFg};
|
||||
font-weight: bold;
|
||||
`);
|
||||
|
||||
const cssRuleProblems = styled('div', `
|
||||
flex: auto;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
`);
|
||||
|
||||
Reference in New Issue
Block a user