@ -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 , I PopupOptions, setPopupToCreateDom } from 'popweasel' ;
import { cssMenu , cssMenuWrap , defaultMenuOptions , I MenuOptions, I PopupOptions, 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 24 px ;
margin - bottom : 4px ;
font - weight : 700 ;
text - transform : uppercase ;
font - size : $ { vars . xsmallFontSize } ;
color : $ { theme . darkText } ;
` );