mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) Adds dots menu to access rules page item
Summary: In Access rules page item, adds “…” buttons that shows a menu of users to view-as: Test Plan: Include new nbrowser test Reviewers: jarek Reviewed By: jarek Differential Revision: https://phab.getgrist.com/D3751
This commit is contained in:
@@ -4,18 +4,20 @@ import {createUserImage} from 'app/client/ui/UserImage';
|
||||
import {cssMemberImage, cssMemberListItem, cssMemberPrimary,
|
||||
cssMemberSecondary, cssMemberText} from 'app/client/ui/UserItem';
|
||||
import {testId, theme, vars} from 'app/client/ui2018/cssVars';
|
||||
import {menuCssClass} from 'app/client/ui2018/menus';
|
||||
import {PermissionDataWithExtraUsers} from 'app/common/ActiveDocAPI';
|
||||
import {menu, menuCssClass, menuItemLink} from 'app/client/ui2018/menus';
|
||||
import {userOverrideParams} from 'app/common/gristUrls';
|
||||
import {FullUser} from 'app/common/LoginSessionAPI';
|
||||
import {ANONYMOUS_USER_EMAIL, EVERYONE_EMAIL} from 'app/common/UserAPI';
|
||||
import {getRealAccess, UserAccessData} from 'app/common/UserAPI';
|
||||
import {Disposable, dom, Observable, styled} from 'grainjs';
|
||||
import {cssMenu, cssMenuWrap, defaultMenuOptions, IPopupOptions, setPopupToCreateDom} from 'popweasel';
|
||||
import {cssMenu, cssMenuWrap, defaultMenuOptions, IMenuOptions, IPopupOptions, setPopupToCreateDom} from 'popweasel';
|
||||
import {getUserRoleText} from 'app/common/UserAPI';
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
import {waitGrainObs} from 'app/common/gutil';
|
||||
import noop from 'lodash/noop';
|
||||
|
||||
const t = makeT("ACLUsers");
|
||||
const t = makeT("ViewAsDropdown");
|
||||
|
||||
function isSpecialEmail(email: string) {
|
||||
return email === ANONYMOUS_USER_EMAIL || email === EVERYONE_EMAIL;
|
||||
@@ -23,15 +25,33 @@ function isSpecialEmail(email: string) {
|
||||
|
||||
export class ACLUsersPopup extends Disposable {
|
||||
public readonly isInitialized = Observable.create(this, false);
|
||||
public readonly allUsers = Observable.create<UserAccessData[]>(this, []);
|
||||
private _shareUsers: UserAccessData[] = []; // Users doc is shared with.
|
||||
private _attributeTableUsers: UserAccessData[] = []; // Users mentioned in attribute tables.
|
||||
private _exampleUsers: UserAccessData[] = []; // Example users.
|
||||
private _currentUser: FullUser|null = null;
|
||||
private _pageModel: DocPageModel|null = null;
|
||||
|
||||
public init(pageModel: DocPageModel, permissionData: PermissionDataWithExtraUsers|null) {
|
||||
constructor(public pageModel: DocPageModel,
|
||||
public fetch: () => Promise<PermissionDataWithExtraUsers|null> = () => this._fetchData()) {
|
||||
super();
|
||||
}
|
||||
|
||||
public async load() {
|
||||
const permissionData = await this.fetch();
|
||||
if (this.isDisposed()) { return; }
|
||||
this.init(permissionData);
|
||||
}
|
||||
|
||||
public getUsers() {
|
||||
const users = [...this._shareUsers, ...this._attributeTableUsers];
|
||||
if (this._showExampleUsers()) { users.push(...this._exampleUsers); }
|
||||
return users;
|
||||
}
|
||||
|
||||
public init(permissionData: PermissionDataWithExtraUsers|null) {
|
||||
const pageModel = this.pageModel;
|
||||
this._currentUser = pageModel.userOverride.get()?.user || pageModel.appModel.currentValidUser;
|
||||
this._pageModel = pageModel;
|
||||
|
||||
if (permissionData) {
|
||||
this._shareUsers = permissionData.users.map(user => ({
|
||||
...user,
|
||||
@@ -41,6 +61,7 @@ export class ACLUsersPopup extends Disposable {
|
||||
.filter(user => this._currentUser?.id !== user.id);
|
||||
this._attributeTableUsers = permissionData.attributeTableUsers;
|
||||
this._exampleUsers = permissionData.exampleUsers;
|
||||
this.allUsers.set(this.getUsers());
|
||||
this.isInitialized.set(true);
|
||||
}
|
||||
}
|
||||
@@ -61,7 +82,7 @@ export class ACLUsersPopup extends Disposable {
|
||||
// Include example users only if there are not many "real" users.
|
||||
// It might be better to have an expandable section with these users, collapsed
|
||||
// by default, but that's beyond my UI ken.
|
||||
(this._shareUsers.length + this._attributeTableUsers.length < 5) ? [
|
||||
this._showExampleUsers() ? [
|
||||
(this._exampleUsers.length > 0) ? cssHeader(t("Example Users")) : null,
|
||||
dom.forEach(this._exampleUsers, buildExampleUserRow)
|
||||
] : null,
|
||||
@@ -71,6 +92,30 @@ export class ACLUsersPopup extends Disposable {
|
||||
}, {...defaultMenuOptions, ...options});
|
||||
}
|
||||
|
||||
public menu(options: IMenuOptions) {
|
||||
return menu(() => {
|
||||
this.load().catch(noop);
|
||||
return [
|
||||
cssMenuHeader('view as'),
|
||||
dom.forEach(this.allUsers, user => menuItemLink(
|
||||
`${user.name || user.email} (${getUserRoleText(user)})`,
|
||||
testId('acl-user-access'),
|
||||
this._viewAs(user),
|
||||
)),
|
||||
];
|
||||
}, options);
|
||||
}
|
||||
|
||||
private async _fetchData() {
|
||||
const doc = this.pageModel.currentDoc.get();
|
||||
const gristDoc = await waitGrainObs(this.pageModel.gristDoc);
|
||||
return doc && gristDoc.docComm.getUsersForViewAs();
|
||||
}
|
||||
|
||||
private _showExampleUsers() {
|
||||
return this._shareUsers.length + this._attributeTableUsers.length < 5;
|
||||
}
|
||||
|
||||
private _buildUserRow(user: UserAccessData, opt: {isExampleUser?: boolean} = {}) {
|
||||
return dom('a',
|
||||
{class: cssMemberListItem.className + ' ' + cssUserItem.className},
|
||||
@@ -89,15 +134,15 @@ export class ACLUsersPopup extends Disposable {
|
||||
}
|
||||
|
||||
private _viewAs(user: UserAccessData) {
|
||||
if (this._pageModel?.isPrefork.get() &&
|
||||
this._pageModel?.currentDoc.get()?.access !== 'owners') {
|
||||
if (this.pageModel?.isPrefork.get() &&
|
||||
this.pageModel?.currentDoc.get()?.access !== 'owners') {
|
||||
// "View As" is restricted to document owners on the back-end. Non-owners can be
|
||||
// permitted to pretend to be owners of a pre-forked document, but if they want
|
||||
// to do "View As", that would be layering pretence over pretense. Better to just
|
||||
// go ahead and create the fork, so the user becomes a genuine owner, so the
|
||||
// back-end doesn't have to become too metaphysical (and maybe hard to review).
|
||||
return dom.on('click', async () => {
|
||||
const forkResult = await this._pageModel?.gristDoc.get()?.docComm.fork();
|
||||
const forkResult = await this.pageModel?.gristDoc.get()?.docComm.fork();
|
||||
if (!forkResult) { throw new Error('Failed to create fork'); }
|
||||
window.location.assign(urlState().makeUrl(userOverrideParams(user.email,
|
||||
{doc: forkResult.urlId,
|
||||
@@ -139,3 +184,12 @@ const cssHeader = styled('div', `
|
||||
font-size: ${vars.xsmallFontSize};
|
||||
color: ${theme.darkText};
|
||||
`);
|
||||
|
||||
const cssMenuHeader = styled('div', `
|
||||
margin: 8px 24px;
|
||||
margin-bottom: 4px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
font-size: ${vars.xsmallFontSize};
|
||||
color: ${theme.darkText};
|
||||
`);
|
||||
|
||||
@@ -122,7 +122,7 @@ export class AccessRules extends Disposable {
|
||||
// Map of tableId to basic metadata for all tables in the document.
|
||||
private _aclResources = new Map<string, AclTableDescription>();
|
||||
|
||||
private _aclUsersPopup = ACLUsersPopup.create(this);
|
||||
private _aclUsersPopup = ACLUsersPopup.create(this, this._gristDoc.docPageModel);
|
||||
|
||||
constructor(private _gristDoc: GristDoc) {
|
||||
super();
|
||||
@@ -531,13 +531,7 @@ export class AccessRules extends Disposable {
|
||||
}
|
||||
|
||||
private async _updateDocAccessData() {
|
||||
const pageModel = this._gristDoc.docPageModel;
|
||||
const doc = pageModel.currentDoc.get();
|
||||
|
||||
const permissionData = doc && await this._gristDoc.docComm.getUsersForViewAs();
|
||||
if (this.isDisposed()) { return; }
|
||||
|
||||
this._aclUsersPopup.init(pageModel, permissionData);
|
||||
await this._aclUsersPopup.load();
|
||||
}
|
||||
|
||||
private _addButtonsForMissingTables(buttons: Array<HTMLAnchorElement | HTMLButtonElement>, tableIds: string[]) {
|
||||
@@ -625,7 +619,7 @@ class TableRules extends Disposable {
|
||||
} else if (this._defRuleSet) {
|
||||
DefaultObsRuleSet.create(this._defaultRuleSet, this._accessRules, this, this._haveColumnRules,
|
||||
this._defRuleSet);
|
||||
}
|
||||
}
|
||||
|
||||
this.ruleStatus = Computed.create(this, (use) => {
|
||||
const columnRuleSets = use(this._columnRuleSets);
|
||||
|
||||
Reference in New Issue
Block a user