2021-01-22 14:01:20 +00:00
|
|
|
import {copyToClipboard} from 'app/client/lib/copyToClipboard';
|
|
|
|
import {DocPageModel} from 'app/client/models/DocPageModel';
|
|
|
|
import {urlState} from 'app/client/models/gristUrlState';
|
|
|
|
import {createUserImage} from 'app/client/ui/UserImage';
|
2021-10-01 14:24:23 +00:00
|
|
|
import {cssMemberImage, cssMemberListItem, cssMemberPrimary,
|
|
|
|
cssMemberSecondary, cssMemberText} from 'app/client/ui/UserItem';
|
2021-01-22 14:01:20 +00:00
|
|
|
import {basicButton, basicButtonLink} from 'app/client/ui2018/buttons';
|
|
|
|
import {colors, testId} from 'app/client/ui2018/cssVars';
|
|
|
|
import {icon} from 'app/client/ui2018/icons';
|
2021-10-08 14:56:33 +00:00
|
|
|
import {menuCssClass, menuDivider} from 'app/client/ui2018/menus';
|
|
|
|
import {PermissionDataWithExtraUsers} from 'app/common/ActiveDocAPI';
|
2021-03-08 21:08:13 +00:00
|
|
|
import {userOverrideParams} from 'app/common/gristUrls';
|
2021-01-22 14:01:20 +00:00
|
|
|
import {FullUser} from 'app/common/LoginSessionAPI';
|
|
|
|
import * as roles from 'app/common/roles';
|
2021-03-25 23:15:34 +00:00
|
|
|
import {ANONYMOUS_USER_EMAIL, EVERYONE_EMAIL} from 'app/common/UserAPI';
|
2021-10-08 14:56:33 +00:00
|
|
|
import {getRealAccess, UserAccessData} from 'app/common/UserAPI';
|
2021-01-22 14:01:20 +00:00
|
|
|
import {Disposable, dom, Observable, styled} from 'grainjs';
|
|
|
|
import {cssMenu, cssMenuWrap, defaultMenuOptions, IOpenController, setPopupToCreateDom} from 'popweasel';
|
|
|
|
|
|
|
|
const roleNames: {[role: string]: string} = {
|
|
|
|
[roles.OWNER]: 'Owner',
|
|
|
|
[roles.EDITOR]: 'Editor',
|
|
|
|
[roles.VIEWER]: 'Viewer',
|
|
|
|
};
|
|
|
|
|
|
|
|
function buildUserRow(user: UserAccessData, currentUser: FullUser|null, ctl: IOpenController) {
|
|
|
|
const isCurrentUser = Boolean(currentUser && user.id === currentUser.id);
|
|
|
|
return cssUserItem(
|
2021-10-01 14:24:23 +00:00
|
|
|
cssMemberImage(
|
2021-01-22 14:01:20 +00:00
|
|
|
createUserImage(user, 'large')
|
|
|
|
),
|
2021-10-01 14:24:23 +00:00
|
|
|
cssMemberText(
|
|
|
|
cssMemberPrimary(user.name || dom('span', user.email),
|
2021-10-08 14:56:33 +00:00
|
|
|
cssRole('(', roleNames[user.access!] || user.access || 'no access', ')', testId('acl-user-access')),
|
2021-01-22 14:01:20 +00:00
|
|
|
),
|
2021-10-01 14:24:23 +00:00
|
|
|
user.name ? cssMemberSecondary(user.email) : null
|
2021-01-22 14:01:20 +00:00
|
|
|
),
|
|
|
|
basicButton(cssUserButton.cls(''), icon('Copy'), 'Copy Email',
|
|
|
|
testId('acl-user-copy'),
|
|
|
|
dom.on('click', async (ev, elem) => { await copyToClipboard(user.email); ctl.close(); }),
|
|
|
|
),
|
|
|
|
basicButtonLink(cssUserButton.cls(''), cssUserButton.cls('-disabled', isCurrentUser),
|
|
|
|
testId('acl-user-view-as'),
|
2021-03-08 21:08:13 +00:00
|
|
|
icon('FieldLink'), 'View As',
|
|
|
|
urlState().setHref(userOverrideParams(user.email, {docPage: undefined})),
|
|
|
|
),
|
2021-01-22 14:01:20 +00:00
|
|
|
testId('acl-user-item'),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
function isSpecialEmail(email: string) {
|
|
|
|
return email === ANONYMOUS_USER_EMAIL || email === EVERYONE_EMAIL;
|
|
|
|
}
|
|
|
|
|
|
|
|
export class ACLUsersPopup extends Disposable {
|
|
|
|
public readonly isInitialized = Observable.create(this, false);
|
2021-10-08 14:56:33 +00:00
|
|
|
private _shareUsers: UserAccessData[] = []; // Users doc is shared with.
|
|
|
|
private _attributeTableUsers: UserAccessData[] = []; // Users mentioned in attribute tables.
|
|
|
|
private _exampleUsers: UserAccessData[] = []; // Example users.
|
2021-01-22 14:01:20 +00:00
|
|
|
private _currentUser: FullUser|null = null;
|
|
|
|
|
2021-10-08 14:56:33 +00:00
|
|
|
public init(pageModel: DocPageModel, permissionData: PermissionDataWithExtraUsers|null) {
|
2021-01-22 14:01:20 +00:00
|
|
|
this._currentUser = pageModel.userOverride.get()?.user || pageModel.appModel.currentValidUser;
|
2021-03-25 23:15:34 +00:00
|
|
|
if (permissionData) {
|
2021-10-08 14:56:33 +00:00
|
|
|
this._shareUsers = permissionData.users.map(user => ({
|
2021-01-22 14:01:20 +00:00
|
|
|
...user,
|
|
|
|
access: getRealAccess(user, permissionData),
|
|
|
|
}))
|
|
|
|
.filter(user => user.access && !isSpecialEmail(user.email));
|
2021-10-08 14:56:33 +00:00
|
|
|
this._attributeTableUsers = permissionData.attributeTableUsers;
|
|
|
|
this._exampleUsers = permissionData.exampleUsers;
|
2021-01-22 14:01:20 +00:00
|
|
|
this.isInitialized.set(true);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public attachPopup(elem: Element) {
|
2021-10-08 14:56:33 +00:00
|
|
|
setPopupToCreateDom(elem, (ctl) => {
|
|
|
|
const buildRow = (user: UserAccessData) => buildUserRow(user, this._currentUser, ctl);
|
|
|
|
return cssMenuWrap(cssMenu(
|
2021-01-22 14:01:20 +00:00
|
|
|
dom.cls(menuCssClass),
|
|
|
|
cssUsers.cls(''),
|
2021-10-08 14:56:33 +00:00
|
|
|
dom.forEach(this._shareUsers, buildRow),
|
|
|
|
// Add a divider between users-from-shares and users from attribute tables.
|
|
|
|
(this._attributeTableUsers.length > 0) ? menuDivider() : null,
|
|
|
|
dom.forEach(this._attributeTableUsers, buildRow),
|
|
|
|
// 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._exampleUsers.length > 0) ? menuDivider() : null,
|
|
|
|
dom.forEach(this._exampleUsers, buildRow)
|
|
|
|
] : null,
|
2021-01-22 14:01:20 +00:00
|
|
|
(el) => { setTimeout(() => el.focus(), 0); },
|
|
|
|
dom.onKeyDown({Escape: () => ctl.close()}),
|
2021-10-08 14:56:33 +00:00
|
|
|
));
|
|
|
|
}, {...defaultMenuOptions, placement: 'bottom-end'});
|
2021-01-22 14:01:20 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const cssUsers = styled('div', `
|
|
|
|
max-width: unset;
|
|
|
|
`);
|
|
|
|
|
2021-10-01 14:24:23 +00:00
|
|
|
const cssUserItem = styled(cssMemberListItem, `
|
2021-01-22 14:01:20 +00:00
|
|
|
width: auto;
|
|
|
|
padding: 8px 16px;
|
|
|
|
align-items: center;
|
|
|
|
&:hover {
|
|
|
|
background-color: ${colors.lightGrey};
|
|
|
|
}
|
|
|
|
`);
|
|
|
|
|
|
|
|
const cssRole = styled('span', `
|
|
|
|
margin: 0 8px;
|
|
|
|
font-weight: normal;
|
|
|
|
`);
|
|
|
|
|
|
|
|
const cssUserButton = styled('div', `
|
|
|
|
margin: 0 8px;
|
|
|
|
border: none;
|
|
|
|
display: inline-flex;
|
|
|
|
white-space: nowrap;
|
|
|
|
gap: 4px;
|
|
|
|
&:hover {
|
|
|
|
background-color: ${colors.darkGrey};
|
|
|
|
}
|
|
|
|
&-disabled {
|
|
|
|
visibility: hidden;
|
|
|
|
}
|
|
|
|
`);
|