mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) One more phase of ACL UI revision.
Summary: - Add ACLColumnList widget for a list of column IDs. - Replace autocomplete widgets with simpler dropdowns. - Add select dropdown for the Attribute of UserAttribute rules. - Switch formula to use ACE editor. - Factor out customized completion logic from AceEditor.js into a separate file. - Implement completions for ACL formulas. - Collect ACL UI files in app/client/aclui Test Plan: Updated test case, some behavior (like formula autocomplete) only tested manually. Reviewers: paulfitz Reviewed By: paulfitz Differential Revision: https://phab.getgrist.com/D2697
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,175 +0,0 @@
|
||||
/**
|
||||
* Implements a widget showing 3-state boxes for permissions
|
||||
* (for Allow / Deny / Pass-Through).
|
||||
*/
|
||||
import {colors, testId} from 'app/client/ui2018/cssVars';
|
||||
import {cssIconButton, icon} from 'app/client/ui2018/icons';
|
||||
import {menu, menuIcon, menuItem} from 'app/client/ui2018/menus';
|
||||
import {PartialPermissionSet, PartialPermissionValue} from 'app/common/ACLPermissions';
|
||||
import {ALL_PERMISSION_PROPS, emptyPermissionSet} from 'app/common/ACLPermissions';
|
||||
import {capitalize} from 'app/common/gutil';
|
||||
import {dom, DomElementArg, Observable, styled} from 'grainjs';
|
||||
import isEqual = require('lodash/isEqual');
|
||||
|
||||
// One of the strings 'read', 'update', etc.
|
||||
export type PermissionKey = keyof PartialPermissionSet;
|
||||
|
||||
/**
|
||||
* Renders a box for each of availableBits, and a dropdown with a description and some shortcuts.
|
||||
*/
|
||||
export function permissionsWidget(
|
||||
availableBits: PermissionKey[],
|
||||
pset: Observable<PartialPermissionSet>,
|
||||
options: {disabled: boolean},
|
||||
...args: DomElementArg[]
|
||||
) {
|
||||
// These are the permission sets available to set via the dropdown.
|
||||
const empty: PartialPermissionSet = emptyPermissionSet();
|
||||
const allowAll: PartialPermissionSet = makePermissionSet(availableBits, () => 'allow');
|
||||
const denyAll: PartialPermissionSet = makePermissionSet(availableBits, () => 'deny');
|
||||
const readOnly: PartialPermissionSet = makePermissionSet(availableBits, (b) => b === 'read' ? 'allow' : 'deny');
|
||||
|
||||
return cssPermissions(
|
||||
dom.forEach(availableBits, (bit) => {
|
||||
return cssBit(
|
||||
bit.slice(0, 1).toUpperCase(), // Show the first letter of the property (e.g. "R" for "read")
|
||||
cssBit.cls((use) => '-' + use(pset)[bit]), // -allow, -deny class suffixes.
|
||||
dom.attr('title', (use) => capitalize(`${use(pset)[bit]} ${bit}`.trim())), // Explanation on hover
|
||||
dom.cls('disabled', options.disabled),
|
||||
// Cycle the bit's value on click, unless disabled.
|
||||
(options.disabled ? null :
|
||||
dom.on('click', () => pset.set({...pset.get(), [bit]: next(pset.get()[bit])}))
|
||||
)
|
||||
);
|
||||
}),
|
||||
cssIconButton(icon('Dropdown'), testId('permissions-dropdown'), menu(() => {
|
||||
// Show a disabled "Custom" menu item if the permission set isn't a recognized one, for
|
||||
// information purposes.
|
||||
const isCustom = [allowAll, denyAll, readOnly, empty].every(ps => !isEqual(ps, pset.get()));
|
||||
return [
|
||||
(isCustom ?
|
||||
cssMenuItem(() => null, dom.cls('disabled'), menuIcon('Tick'),
|
||||
cssMenuItemContent(
|
||||
'Custom',
|
||||
cssMenuItemDetails(dom.text((use) => psetDescription(use(pset))))
|
||||
),
|
||||
) :
|
||||
null
|
||||
),
|
||||
// If the set matches any recognized pattern, mark that item with a tick (checkmark).
|
||||
cssMenuItem(() => pset.set(allowAll), tick(isEqual(pset.get(), allowAll)), 'Allow All',
|
||||
dom.cls('disabled', options.disabled)
|
||||
),
|
||||
cssMenuItem(() => pset.set(denyAll), tick(isEqual(pset.get(), denyAll)), 'Deny All',
|
||||
dom.cls('disabled', options.disabled)
|
||||
),
|
||||
cssMenuItem(() => pset.set(readOnly), tick(isEqual(pset.get(), readOnly)), 'Read Only',
|
||||
dom.cls('disabled', options.disabled)
|
||||
),
|
||||
cssMenuItem(() => pset.set(empty),
|
||||
// For the empty permission set, it seems clearer to describe it as "No Effect", but to
|
||||
// all it "Clear" when offering to the user as the action.
|
||||
isEqual(pset.get(), empty) ? [tick(true), 'No Effect'] : [tick(false), 'Clear'],
|
||||
dom.cls('disabled', options.disabled),
|
||||
),
|
||||
];
|
||||
})),
|
||||
...args
|
||||
);
|
||||
}
|
||||
|
||||
function next(pvalue: PartialPermissionValue): PartialPermissionValue {
|
||||
switch (pvalue) {
|
||||
case 'allow': return '';
|
||||
case 'deny': return 'allow';
|
||||
}
|
||||
return 'deny';
|
||||
}
|
||||
|
||||
// Helper to build up permission sets.
|
||||
function makePermissionSet(bits: PermissionKey[], makeValue: (bit: PermissionKey) => PartialPermissionValue) {
|
||||
const pset = emptyPermissionSet();
|
||||
for (const bit of bits) {
|
||||
pset[bit] = makeValue(bit);
|
||||
}
|
||||
return pset;
|
||||
}
|
||||
|
||||
// Helper for a tick (checkmark) icon, replacing it with an equialent space when not shown.
|
||||
function tick(show: boolean) {
|
||||
return show ? menuIcon('Tick') : cssMenuIconSpace();
|
||||
}
|
||||
|
||||
// Human-readable summary of the permission set. E.g. "Allow Read. Deny Update, Create.".
|
||||
function psetDescription(permissionSet: PartialPermissionSet): string {
|
||||
const allow: string[] = [];
|
||||
const deny: string[] = [];
|
||||
for (const prop of ALL_PERMISSION_PROPS) {
|
||||
const value = permissionSet[prop];
|
||||
if (value === "allow") {
|
||||
allow.push(capitalize(prop));
|
||||
} else if (value === "deny") {
|
||||
deny.push(capitalize(prop));
|
||||
}
|
||||
}
|
||||
const parts: string[] = [];
|
||||
if (allow.length) { parts.push(`Allow ${allow.join(", ")}.`); }
|
||||
if (deny.length) { parts.push(`Deny ${deny.join(", ")}.`); }
|
||||
return parts.join(' ');
|
||||
}
|
||||
|
||||
const cssPermissions = styled('div', `
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
`);
|
||||
|
||||
const cssBit = styled('div', `
|
||||
flex: none;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
border-radius: 2px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
border: 1px dashed ${colors.darkGrey};
|
||||
color: ${colors.darkGrey};
|
||||
cursor: pointer;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&-allow {
|
||||
background-color: ${colors.lightGreen};
|
||||
border: 1px solid ${colors.lightGreen};
|
||||
color: white;
|
||||
}
|
||||
&-deny {
|
||||
background-image: linear-gradient(-45deg, ${colors.error} 14px, white 15px 16px, ${colors.error} 16px);
|
||||
border: 1px solid ${colors.error};
|
||||
color: white;
|
||||
}
|
||||
&.disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
`);
|
||||
|
||||
const cssMenuIconSpace = styled('div', `
|
||||
width: 24px;
|
||||
`);
|
||||
|
||||
// Don't make disabled item too hard to see here.
|
||||
const cssMenuItem = styled(menuItem, `
|
||||
align-items: start;
|
||||
&.disabled {
|
||||
opacity: unset;
|
||||
}
|
||||
`);
|
||||
|
||||
const cssMenuItemContent = styled('div', `
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`);
|
||||
|
||||
const cssMenuItemDetails = styled('div', `
|
||||
font-size: 12px;
|
||||
`);
|
||||
Reference in New Issue
Block a user