diff options
author | bobzel <zzzman@gmail.com> | 2024-05-19 00:05:18 -0400 |
---|---|---|
committer | bobzel <zzzman@gmail.com> | 2024-05-19 00:05:18 -0400 |
commit | 38742d5f491ed5232a381da63e126b609cf14aad (patch) | |
tree | a723b0c9dfffa717b70df383d21fb3b14567308f /src/client/util/SharingManager.tsx | |
parent | a3784cd3ab990d8016b1168eb0cbf7e9a2f22301 (diff) | |
parent | 0b451af28e5aef6b749da61e8a9fcd0a840789ac (diff) |
Merge branch 'restoringEslint' into aisosa-starter
Diffstat (limited to 'src/client/util/SharingManager.tsx')
-rw-r--r-- | src/client/util/SharingManager.tsx | 751 |
1 files changed, 407 insertions, 344 deletions
diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx index fddf735e3..c2a52cae9 100644 --- a/src/client/util/SharingManager.tsx +++ b/src/client/util/SharingManager.tsx @@ -1,3 +1,6 @@ +/* eslint-disable jsx-a11y/label-has-associated-control */ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Button, IconButton, Size, Type } from 'browndash-components'; import { concat, intersection } from 'lodash'; @@ -6,25 +9,23 @@ import { observer } from 'mobx-react'; import * as React from 'react'; import Select from 'react-select'; import * as RequestPromise from 'request-promise'; +import { ClientUtils } from '../../ClientUtils'; +import { Utils } from '../../Utils'; import { Doc, DocListCast, DocListCastAsync, HierarchyMapping, ReverseHierarchyMap } from '../../fields/Doc'; import { AclAdmin, AclPrivate, DocAcl, DocData } from '../../fields/DocSymbols'; import { FieldLoader } from '../../fields/FieldLoader'; import { Id } from '../../fields/FieldSymbols'; import { StrCast } from '../../fields/Types'; -import { distributeAcls, GetEffectiveAcl, normalizeEmail, SharingPermissions, TraceMobx } from '../../fields/util'; -import { Utils } from '../../Utils'; +import { GetEffectiveAcl, SharingPermissions, TraceMobx, distributeAcls, normalizeEmail } from '../../fields/util'; import { DocServer } from '../DocServer'; -import { DictationOverlay } from '../views/DictationOverlay'; import { MainViewModal } from '../views/MainViewModal'; import { DocumentView } from '../views/nodes/DocumentView'; import { TaskCompletionBox } from '../views/nodes/TaskCompletedBox'; -import { DocumentManager } from './DocumentManager'; import { GroupManager, UserOptions } from './GroupManager'; import { GroupMemberView } from './GroupMemberView'; import { SearchUtil } from './SearchUtil'; -import { SelectionManager } from './SelectionManager'; -import { SettingsManager } from './SettingsManager'; import './SharingManager.scss'; +import { SnappingManager } from './SnappingManager'; import { undoable } from './UndoManager'; export interface User { @@ -64,63 +65,36 @@ interface ValidatedUser { @observer export class SharingManager extends React.Component<{}> { + // eslint-disable-next-line no-use-before-define public static Instance: SharingManager; + private shareDocumentButtonRef: React.RefObject<HTMLButtonElement> = React.createRef(); // ref for the share button, used for the position of the popup + private populating: boolean = false; // whether the list of users is populating or not @observable private isOpen = false; // whether the SharingManager modal is open or not @observable public users: ValidatedUser[] = []; // the list of users with sharing docs @observable private targetDoc: Doc | undefined = undefined; // the document being shared @observable private targetDocView: DocumentView | undefined = undefined; // the DocumentView of the document being shared // @observable private copied = false; @observable private dialogueBoxOpacity = 1; // for the modal - @observable private overlayOpacity = 0.4; // for the modal @observable private selectedUsers: UserOptions[] | null = null; // users (individuals/groups) selected to share with @observable private permissions: SharingPermissions = SharingPermissions.Edit; // the permission with which to share with other users @observable private individualSort: 'ascending' | 'descending' | 'none' = 'none'; // sorting options for the list of individuals @observable private groupSort: 'ascending' | 'descending' | 'none' = 'none'; // sorting options for the list of groups - private shareDocumentButtonRef: React.RefObject<HTMLButtonElement> = React.createRef(); // ref for the share button, used for the position of the popup // if both showUserOptions and showGroupOptions are false then both are displayed @observable private showUserOptions: boolean = false; // whether to show individuals as options when sharing (in the react-select component) @observable private showGroupOptions: boolean = false; // // whether to show groups as options when sharing (in the react-select component) - private populating: boolean = false; // whether the list of users is populating or not @observable private upgradeNested: boolean = false; // whether child docs in a collection/dashboard should be changed to be less private - initially selected so default is upgrade all @observable private layoutDocAcls: boolean = false; // whether the layout doc or data doc's acls are to be used @observable private myDocAcls: boolean = false; // whether the My Docs checkbox is selected or not - @observable private _buttonDown = false; // private get linkVisible() { - // return this.targetDoc ? this.targetDoc["acl-" + PublicKey] !== SharingPermissions.None : false; + // return this.targetDoc ? this.targetDoc['acl_' + PublicKey] !== SharingPermissions.None : false; // } - public open = (target?: DocumentView, target_doc?: Doc) => { - this.populateUsers(); - runInAction(() => { - this.targetDocView = target; - this.targetDoc = target_doc || target?.Document; - DictationOverlay.Instance.hasActiveModal = true; - this.isOpen = this.targetDoc !== undefined; - this.permissions = SharingPermissions.Augment; - this.upgradeNested = true; - }); - }; - - public close = action(() => { - this.isOpen = false; - this.selectedUsers = null; // resets the list of users and selected users (in the react-select component) - TaskCompletionBox.taskCompleted = false; - setTimeout( - action(() => { - // this.copied = false; - DictationOverlay.Instance.hasActiveModal = false; - this.targetDoc = undefined; - }), - 500 - ); - this.layoutDocAcls = false; - }); - constructor(props: {}) { super(props); makeObservable(this); SharingManager.Instance = this; + DocumentView.ShareOpen = this.open; } /** @@ -131,223 +105,6 @@ export class SharingManager extends React.Component<{}> { } /** - * Populates the list of validated users (this.users) by adding registered users which have a sharingDocument. - */ - populateUsers = async () => { - if (!this.populating && Doc.UserDoc()[Id] !== Utils.GuestID()) { - this.populating = true; - const userList = await RequestPromise.get(Utils.prepend('/getUsers')); - const raw = (JSON.parse(userList) as User[]).filter(user => user.email !== 'guest' && user.email !== Doc.CurrentUserEmail); - runInAction(() => (FieldLoader.ServerLoadStatus.message = 'users')); - const docs = await DocServer.GetRefFields(raw.reduce((list, user) => [...list, user.sharingDocumentId, user.linkDatabaseId], [] as string[])); - raw.map( - action((newUser: User) => { - const sharingDoc = docs[newUser.sharingDocumentId]; - const linkDatabase = docs[newUser.linkDatabaseId]; - if (sharingDoc instanceof Doc && linkDatabase instanceof Doc) { - if (!this.users.find(user => user.user.email === newUser.email)) { - this.users.push({ user: newUser, sharingDoc, linkDatabase, userColor: StrCast(sharingDoc.userColor) }); - //LinkManager.addLinkDB(linkDatabase); - } - } - }) - ); - this.populating = false; - } - }; - - /** - * Shares the document with a user. - */ - setInternalSharing = undoable((recipient: ValidatedUser, permission: string, targetDoc: Doc | undefined) => { - const { user, sharingDoc } = recipient; - const target = targetDoc || this.targetDoc!; - const acl = `acl-${normalizeEmail(user.email)}`; - const docs = SelectionManager.Views.length < 2 ? [target] : SelectionManager.Views.map(docView => docView.Document); - docs.map(doc => (this.layoutDocAcls || doc.dockingConfig ? doc : Doc.GetProto(doc))).forEach(doc => { - distributeAcls(acl, permission as SharingPermissions, doc, undefined, this.upgradeNested ? true : undefined); - if (permission !== SharingPermissions.None) { - Doc.AddDocToList(sharingDoc, doc.dockingConfig ? dashStorage : storage, doc); - } else GetEffectiveAcl(doc, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, ((doc.createdFrom as Doc) || doc).dockingConfig ? dashStorage : storage, (doc.createdFrom as Doc) || doc); - }); - }, 'set Doc permissions'); - - /** - * Sets the permission on the target for the group. - * @param group - * @param permission - */ - setInternalGroupSharing = undoable((group: Doc | { title: string }, permission: string, targetDoc?: Doc) => { - const target = targetDoc || this.targetDoc!; - const acl = `acl-${normalizeEmail(StrCast(group.title))}`; - - const docs = SelectionManager.Views.length < 2 ? [target] : SelectionManager.Views.map(docView => docView.Document); - docs.map(doc => (this.layoutDocAcls || doc.dockingConfig ? doc : Doc.GetProto(doc))).forEach(doc => { - distributeAcls(acl, permission as SharingPermissions, doc, undefined, this.upgradeNested ? true : undefined); - - if (group instanceof Doc) { - Doc.AddDocToList(group, 'docsShared', doc); - - this.users - .filter(({ user: { email } }) => JSON.parse(StrCast(group.members)).includes(email)) - .forEach(({ user, sharingDoc }) => { - if (permission !== SharingPermissions.None) Doc.AddDocToList(sharingDoc, doc.dockingConfig ? dashStorage : storage, doc); // add the doc to the sharingDoc if it hasn't already been added - else GetEffectiveAcl(doc, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, ((doc.createdFrom as Doc) || doc).dockingConfig ? dashStorage : storage, (doc.createdFrom as Doc) || doc); // remove the doc from the list if it already exists - }); - } - }); - }, 'set group permissions'); - - /** - * Shares the documents shared with a group with a new user who has been added to that group. - * @param group - * @param emailId - */ - shareWithAddedMember = (group: Doc, emailId: string, retry: boolean = true) => { - const user = this.users.find(({ user: { email } }) => email === emailId)!; - const self = this; - if (group.docsShared) { - if (!user) retry && this.populateUsers().then(() => self.shareWithAddedMember(group, emailId, false)); - else { - DocListCastAsync(user.sharingDoc[storage]).then(userdocs => - DocListCastAsync(group.docsShared).then(dl => { - const filtered = dl?.filter(doc => !doc.dockingConfig && !userdocs?.includes(doc)); - filtered && userdocs?.push(...filtered); - }) - ); - DocListCastAsync(user.sharingDoc[dashStorage]).then(userdocs => - DocListCastAsync(group.docsShared).then(dl => { - const filtered = dl?.filter(doc => doc.dockingConfig && !userdocs?.includes(doc)); - filtered && userdocs?.push(...filtered); - }) - ); - } - } - }; - - /** - * Called from the properties sidebar to change permissions of a user. - */ - shareFromPropertiesSidebar = undoable((shareWith: string, permission: SharingPermissions, docs: Doc[], layout: boolean) => { - if (layout) this.layoutDocAcls = true; - if (shareWith !== 'Guest') { - const user = this.users.find(({ user: { email } }) => email === (shareWith === 'Me' ? Doc.CurrentUserEmail : shareWith)); - docs.forEach(doc => { - if (user) this.setInternalSharing(user, permission, doc); - else this.setInternalGroupSharing(GroupManager.Instance.getGroup(shareWith)!, permission, doc, undefined, true); - }); - } else { - docs.forEach(doc => { - if (GetEffectiveAcl(doc) === AclAdmin) { - distributeAcls(`acl-${shareWith}`, permission, doc, undefined); - } - }); - } - this.layoutDocAcls = false; - }, 'sidebar set permissions'); - - /** - * Removes the documents shared with a user through a group when the user is removed from the group. - * @param group - * @param emailId - */ - removeMember = (group: Doc, emailId: string) => { - const user: ValidatedUser = this.users.find(({ user: { email } }) => email === emailId)!; - - if (group.docsShared && user) { - DocListCastAsync(user.sharingDoc[storage]).then(userdocs => - DocListCastAsync(group.docsShared).then(dl => { - const remaining = userdocs?.filter(doc => !dl?.includes(doc)) || []; - userdocs?.splice(0, userdocs.length, ...remaining); - }) - ); - DocListCastAsync(user.sharingDoc[dashStorage]).then(userdocs => - DocListCastAsync(group.docsShared).then(dl => { - const remaining = userdocs?.filter(doc => !dl?.includes(doc)) || []; - userdocs?.splice(0, userdocs.length, ...remaining); - }) - ); - } - }; - - /** - * Removes a group's permissions from documents that have been shared with it. - * @param group - */ - removeGroup = (group: Doc) => { - if (group.docsShared) { - DocListCast(group.docsShared).forEach(doc => { - const acl = `acl-${StrCast(group.title)}`; - distributeAcls(acl, SharingPermissions.None, doc); - - const members: string[] = JSON.parse(StrCast(group.members)); - const users: ValidatedUser[] = this.users.filter(({ user: { email } }) => members.includes(email)); - - users.forEach(({ sharingDoc }) => Doc.RemoveDocFromList(sharingDoc, storage, doc)); - }); - } - }; - - // private setExternalSharing = (permission: string) => { - // const targetDoc = this.targetDoc; - // if (!targetDoc) { - // return; - // } - // targetDoc["acl-" + PublicKey] = permission; - // }s - - /** - * Copies the Public sharing url to the user's clipboard. - */ - private copyURL = (e: any) => { - Utils.CopyText(Utils.shareUrl(this.targetDoc![Id])); - }; - - /** - * Returns the SharingPermissions (Admin, Can Edit etc) access that's used to share - */ - private sharingOptions(uniform: boolean, showGuestOptions?: boolean) { - const dropdownValues: string[] = showGuestOptions ? [SharingPermissions.None, SharingPermissions.View] : Object.values(SharingPermissions); - if (!uniform) dropdownValues.unshift('-multiple-'); - return dropdownValues.map(permission => ( - <option key={permission} value={permission}> - {concat(ReverseHierarchyMap.get(permission)?.image, ' ', permission)} - </option> - )); - } - - private focusOn = (contents: string) => { - const title = this.targetDoc ? StrCast(this.targetDoc.title) : ''; - const docs = SelectionManager.Views.length > 1 ? SelectionManager.Views.map(docView => docView.props.Document) : [this.targetDoc]; - return ( - <span - className="focus-span" - title={title} - onClick={() => { - if (this.targetDoc && this.targetDocView && docs.length === 1) { - DocumentManager.Instance.showDocument(this.targetDoc, { willZoomCentered: true }); - } - }} - onPointerEnter={action(() => { - if (docs.length) { - docs.forEach(doc => doc && Doc.BrushDoc(doc)); - this.dialogueBoxOpacity = 0.1; - this.overlayOpacity = 0.1; - } - })} - onPointerLeave={action(() => { - if (docs.length) { - docs.forEach(doc => doc && Doc.UnBrushDoc(doc)); - this.dialogueBoxOpacity = 1; - this.overlayOpacity = 0.4; - } - })}> - {contents} - </span> - ); - }; - - /** * Handles changes in the users selected in react-select */ @action @@ -366,57 +123,6 @@ export class SharingManager extends React.Component<{}> { ); /** - * Calls the relevant method for sharing, displays the popup, and resets the relevant variables. - */ - share = undoable( - action(() => { - if (this.selectedUsers) { - this.selectedUsers.forEach(user => { - if (user.value.includes(indType)) { - this.setInternalSharing(this.users.find(u => u.user.email === user.label)!, this.permissions, undefined); - } else { - this.setInternalGroupSharing(GroupManager.Instance.getGroup(user.label)!, this.permissions); - } - }); - - if (this.shareDocumentButtonRef.current) { - const { left, width, top, height } = this.shareDocumentButtonRef.current.getBoundingClientRect(); - TaskCompletionBox.popupX = left - 1.5 * width; - TaskCompletionBox.popupY = top - 1.5 * height; - TaskCompletionBox.textDisplayed = 'Document shared!'; - TaskCompletionBox.taskCompleted = true; - setTimeout( - action(() => (TaskCompletionBox.taskCompleted = false)), - 2000 - ); - } - - this.layoutDocAcls = false; - this.selectedUsers = null; - } - }), - 'share Doc' - ); - - /** - * Sorting algorithm to sort users. - */ - sortUsers = (u1: ValidatedUser, u2: ValidatedUser) => { - const { email: e1 } = u1.user; - const { email: e2 } = u2.user; - return e1 < e2 ? -1 : e1 === e2 ? 0 : 1; - }; - - /** - * Sorting algorithm to sort groups. - */ - sortGroups = (group1: Doc, group2: Doc) => { - const g1 = StrCast(group1.title); - const g2 = StrCast(group2.title); - return g1 < g2 ? -1 : g1 === g2 ? 0 : 1; - }; - - /** * @returns the main interface of the SharingManager. */ @computed get sharingInterface() { @@ -445,7 +151,7 @@ export class SharingManager extends React.Component<{}> { const users = this.individualSort === 'ascending' ? this.users.slice().sort(this.sortUsers) : this.individualSort === 'descending' ? this.users.slice().sort(this.sortUsers).reverse() : this.users; const groups = this.groupSort === 'ascending' ? groupList.slice().sort(this.sortGroups) : this.groupSort === 'descending' ? groupList.slice().sort(this.sortGroups).reverse() : groupList; - let docs = SelectionManager.Views.length < 2 ? [this.targetDoc] : SelectionManager.Views.map(docView => docView.Document); + let docs = DocumentView.Selected().length < 2 ? [this.targetDoc] : DocumentView.Selected().map(docView => docView.Document); if (this.myDocAcls) { const newDocs: Doc[] = []; @@ -464,18 +170,18 @@ export class SharingManager extends React.Component<{}> { // the list of users shared with const userListContents = users - // .filter(({ user }) => (docs.length > 1 ? commonKeys.includes(`acl-${normalizeEmail(user.email)}`) : docs[0]?.author !== user.email)) + // .filter(({ user }) => (docs.length > 1 ? commonKeys.includes(`acl_${normalizeEmail(user.email)}`) : docs[0]?.author !== user.email)) .filter(({ user }) => docs[0]?.author !== user.email) .map(({ user, linkDatabase, sharingDoc, userColor }) => { - const userKey = `acl-${normalizeEmail(user.email)}`; + const userKey = `acl_${normalizeEmail(user.email)}`; const uniform = docs.every(doc => doc?.[DocAcl]?.[userKey] === docs[0]?.[DocAcl]?.[userKey]); // const permissions = uniform ? StrCast(targetDoc?.[userKey]) : '-multiple-'; let permissions = targetDoc[DocAcl][userKey] ? HierarchyMapping.get(targetDoc[DocAcl][userKey])?.name : StrCast(targetDoc[userKey]); permissions = uniform ? StrCast(targetDoc?.[userKey]) : '-multiple-'; return !permissions ? null : ( - <div key={userKey} className={'container'}> - <span className={'padding'}>{user.email}</span> + <div key={userKey} className="container"> + <span className="padding">{user.email}</span> <div className="edit-actions"> {admin || this.myDocAcls ? ( <select className={`permissions-dropdown-${permissions}`} value={permissions} onChange={e => this.setInternalSharing({ user, linkDatabase, sharingDoc, userColor }, e.currentTarget.value, undefined)}> @@ -496,21 +202,21 @@ export class SharingManager extends React.Component<{}> { const sameAuthor = docs.every(doc => doc?.author === docs[0]?.author); // the owner of the doc and the current user are placed at the top of the user list. - const userKey = `acl-${normalizeEmail(Doc.CurrentUserEmail)}`; + const userKey = `acl_${normalizeEmail(ClientUtils.CurrentUserEmail())}`; const curUserPermission = StrCast(targetDoc[userKey]); // const curUserPermission = HierarchyMapping.get(effectiveAcls[0])!.name userListContents.unshift( sameAuthor ? ( - <div key={'owner'} className={'container'}> - <span className="padding">{targetDoc?.author === Doc.CurrentUserEmail ? 'Me' : StrCast(targetDoc?.author)}</span> + <div key="owner" className="container"> + <span className="padding">{targetDoc?.author === ClientUtils.CurrentUserEmail() ? 'Me' : StrCast(targetDoc?.author)}</span> <div className="edit-actions"> - <div className={'permissions-dropdown'}>Owner</div> + <div className="permissions-dropdown">Owner</div> </div> </div> ) : null, - sameAuthor && targetDoc?.author !== Doc.CurrentUserEmail ? ( - <div key={'me'} className={'container'}> - <span className={'padding'}>Me</span> + sameAuthor && targetDoc?.author !== ClientUtils.CurrentUserEmail() ? ( + <div key="me" className="container"> + <span className="padding">Me</span> <div className="edit-actions"> <div className={`permissions-dropdown-${curUserPermission}`}> {effectiveAcls.every(acl => acl === effectiveAcls[0]) ? concat(ReverseHierarchyMap.get(curUserPermission!)?.image, ' ', curUserPermission) : '-multiple-'} @@ -522,19 +228,28 @@ export class SharingManager extends React.Component<{}> { ); // the list of groups shared with - const groupListMap: (Doc | { title: string })[] = groups.filter(({ title }) => (docs.length > 1 ? commonKeys.includes(`acl-${normalizeEmail(StrCast(title))}`) : true)); - groupListMap.unshift({ title: 'Guest' }); //, { title: "ALL" }); + const groupListMap: (Doc | { title: string })[] = groups.filter(({ title }) => (docs.length > 1 ? commonKeys.includes(`acl_${normalizeEmail(StrCast(title))}`) : true)); + groupListMap.unshift({ title: 'Guest' }); // , { title: "ALL" }); const groupListContents = groupListMap.map(group => { - let groupKey = `acl-${StrCast(group.title)}`; + const groupKey = `acl_${StrCast(group.title)}`; const uniform = docs.every(doc => doc?.[DocAcl]?.[groupKey] === docs[0]?.[DocAcl]?.[groupKey]); const permissions = uniform ? StrCast(targetDoc?.[groupKey]) : '-multiple-'; return !permissions ? null : ( - <div key={groupKey} className={'container'} style={{ background: SettingsManager.userBackgroundColor, color: SettingsManager.userColor }}> - <div className={'padding'}>{StrCast(group.title)}</div> + <div key={groupKey} className="container" style={{ background: SnappingManager.userBackgroundColor, color: SnappingManager.userColor }}> + <div className="padding">{StrCast(group.title)}</div> - {group instanceof Doc ? <IconButton icon={<FontAwesomeIcon icon={'info-circle'} />} size={Size.XSMALL} color={SettingsManager.userColor} onClick={action(() => (GroupManager.Instance.currentGroup = group))} /> : null} - <div className={'edit-actions'}> + {group instanceof Doc ? ( + <IconButton + icon={<FontAwesomeIcon icon="info-circle" />} + size={Size.XSMALL} + color={SnappingManager.userColor} + onClick={action(() => { + GroupManager.Instance.currentGroup = group; + })} + /> + ) : null} + <div className="edit-actions"> {admin || this.myDocAcls ? ( <select className={`permissions-dropdown-${permissions}`} value={permissions} onChange={e => this.setInternalGroupSharing(group, e.currentTarget.value)}> {this.sharingOptions(uniform, group.title === 'Guest')} @@ -551,25 +266,32 @@ export class SharingManager extends React.Component<{}> { }); return ( <div className="sharing-interface"> - {GroupManager.Instance?.currentGroup ? <GroupMemberView group={GroupManager.Instance.currentGroup} onCloseButtonClick={action(() => (GroupManager.Instance.currentGroup = undefined))} /> : null} + {GroupManager.Instance?.currentGroup ? ( + <GroupMemberView + group={GroupManager.Instance.currentGroup} + onCloseButtonClick={action(() => { + GroupManager.Instance.currentGroup = undefined; + })} + /> + ) : null} <div className="sharing-contents" style={{ - background: SettingsManager.userBackgroundColor, + background: SnappingManager.userBackgroundColor, color: StrCast(Doc.UserDoc().userColor), }}> - <p className="share-title" style={{ color: SettingsManager.userColor }}> + <p className="share-title" style={{ color: SnappingManager.userColor }}> <div className="share-info" onClick={() => window.open('https://brown-dash.github.io/Dash-Documentation/features/collaboration/', '_blank')}> - <FontAwesomeIcon icon={'question-circle'} size={'sm'} onClick={() => window.open('https://brown-dash.github.io/Dash-Documentation/features/collaboration/', '_blank')} /> + <FontAwesomeIcon icon="question-circle" size="sm" onClick={() => window.open('https://brown-dash.github.io/Dash-Documentation/features/collaboration/', '_blank')} /> </div> <b>Share </b> {this.focusOn(docs.length < 2 ? StrCast(targetDoc?.title, 'this document') : '-multiple-')} </p> <div className="share-copy-link"> - <Button type={Type.TERT} color={SettingsManager.userColor} icon={<FontAwesomeIcon icon={'copy'} size="sm" />} iconPlacement={'left'} text={'Copy Guest URL'} onClick={this.copyURL} /> + <Button type={Type.TERT} color={SnappingManager.userColor} icon={<FontAwesomeIcon icon="copy" size="sm" />} iconPlacement="left" text="Copy Guest URL" onClick={this.copyURL} /> </div> <div className="close-button"> - <Button icon={<FontAwesomeIcon icon={'times'} size={'lg'} />} onClick={this.close} color={SettingsManager.userColor} /> + <Button icon={<FontAwesomeIcon icon="times" size="lg" />} onClick={this.close} color={SnappingManager.userColor} /> </div> {admin ? ( <div className="share-container"> @@ -611,19 +333,45 @@ export class SharingManager extends React.Component<{}> { </select> </div> <div className="share-button"> - <Button text={'SHARE'} type={Type.TERT} color={SettingsManager.userColor} onClick={this.share} /> + <Button text="SHARE" type={Type.TERT} color={SnappingManager.userColor} onClick={this.share} /> </div> </div> <div className="sort-checkboxes"> - <input type="checkbox" onChange={action(() => (this.showUserOptions = !this.showUserOptions))} /> <label style={{ marginRight: 10 }}>Individuals</label> - <input type="checkbox" onChange={action(() => (this.showGroupOptions = !this.showGroupOptions))} /> <label>Groups</label> + <input + type="checkbox" + onChange={action(() => { + this.showUserOptions = !this.showUserOptions; + })} + />{' '} + <label style={{ marginRight: 10 }}>Individuals</label> + <input + type="checkbox" + onChange={action(() => { + this.showGroupOptions = !this.showGroupOptions; + })} + />{' '} + <label>Groups</label> </div> <div className="acl-container"> {Doc.noviceMode ? null : ( <div className="layoutDoc-acls"> - <input type="checkbox" onChange={action(() => (this.upgradeNested = !this.upgradeNested))} checked={this.upgradeNested} /> <label>Upgrade Nested </label> - <input type="checkbox" onChange={action(() => (this.layoutDocAcls = !this.layoutDocAcls))} checked={this.layoutDocAcls} /> <label>Layout</label> + <input + type="checkbox" + onChange={action(() => { + this.upgradeNested = !this.upgradeNested; + })} + checked={this.upgradeNested} + />{' '} + <label>Upgrade Nested </label> + <input + type="checkbox" + onChange={action(() => { + this.layoutDocAcls = !this.layoutDocAcls; + })} + checked={this.layoutDocAcls} + />{' '} + <label>Layout</label> </div> )} </div> @@ -632,14 +380,25 @@ export class SharingManager extends React.Component<{}> { <div className="share-container"> <div className="acl-container"> <div className="layoutDoc-acls"> - <input type="checkbox" onChange={action(() => (this.layoutDocAcls = !this.layoutDocAcls))} checked={this.layoutDocAcls} /> <label>Layout</label> + <input + type="checkbox" + onChange={action(() => { + this.layoutDocAcls = !this.layoutDocAcls; + })} + checked={this.layoutDocAcls} + />{' '} + <label>Layout</label> </div> </div> </div> )} <div className="main-container" style={{ color: StrCast(Doc.UserDoc().userColor), border: StrCast(Doc.UserDoc().userColor) }}> - <div className={'individual-container'}> - <div className="user-sort" onClick={action(() => (this.individualSort = this.individualSort === 'ascending' ? 'descending' : this.individualSort === 'descending' ? 'none' : 'ascending'))}> + <div className="individual-container"> + <div + className="user-sort" + onClick={action(() => { + this.individualSort = this.individualSort === 'ascending' ? 'descending' : this.individualSort === 'descending' ? 'none' : 'ascending'; + })}> <div className="title-individual"> Individuals <IconButton @@ -651,11 +410,15 @@ export class SharingManager extends React.Component<{}> { </div> <div className="users-list">{userListContents}</div> </div> - <div className={'group-container'}> - <div className="user-sort" onClick={action(() => (this.groupSort = this.groupSort === 'ascending' ? 'descending' : this.groupSort === 'descending' ? 'none' : 'ascending'))}> + <div className="group-container"> + <div + className="user-sort" + onClick={action(() => { + this.groupSort = this.groupSort === 'ascending' ? 'descending' : this.groupSort === 'descending' ? 'none' : 'ascending'; + })}> <div className="title-group"> Groups - <IconButton icon={<FontAwesomeIcon icon={'info-circle'} />} size={Size.XSMALL} color={StrCast(Doc.UserDoc().userColor)} onClick={action(() => GroupManager.Instance.open())} /> + <IconButton icon={<FontAwesomeIcon icon="info-circle" />} size={Size.XSMALL} color={StrCast(Doc.UserDoc().userColor)} onClick={action(() => GroupManager.Instance.open())} /> <IconButton icon={<FontAwesomeIcon icon={this.groupSort === 'ascending' ? 'caret-up' : this.groupSort === 'descending' ? 'caret-down' : 'caret-right'} />} size={Size.XSMALL} @@ -663,7 +426,7 @@ export class SharingManager extends React.Component<{}> { /> </div> </div> - <div className={'groups-list'}>{groupListContents}</div> + <div className="groups-list">{groupListContents}</div> </div> </div> </div> @@ -671,7 +434,307 @@ export class SharingManager extends React.Component<{}> { ); } + /** + * Shares the document with a user. + */ + setInternalSharing = undoable((recipient: ValidatedUser, permission: string, targetDoc: Doc | undefined) => { + const { user, sharingDoc } = recipient; + const target = targetDoc || this.targetDoc!; + const acl = `acl_${normalizeEmail(user.email)}`; + const docs = DocumentView.Selected().length < 2 ? [target] : DocumentView.Selected().map(docView => docView.Document); + docs.map(doc => (this.layoutDocAcls || doc.dockingConfig ? doc : Doc.GetProto(doc))).forEach(doc => { + distributeAcls(acl, permission as SharingPermissions, doc, undefined, this.upgradeNested ? true : undefined); + if (permission !== SharingPermissions.None) { + Doc.AddDocToList(sharingDoc, doc.dockingConfig ? dashStorage : storage, doc); + } else GetEffectiveAcl(doc, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, ((doc.createdFrom as Doc) || doc).dockingConfig ? dashStorage : storage, (doc.createdFrom as Doc) || doc); + }); + }, 'set Doc permissions'); + + /** + * Sets the permission on the target for the group. + * @param group + * @param permission + */ + setInternalGroupSharing = undoable((group: Doc | { title: string }, permission: string, targetDoc?: Doc) => { + const target = targetDoc || this.targetDoc!; + const acl = `acl_${normalizeEmail(StrCast(group.title))}`; + + const docs = DocumentView.Selected().length < 2 ? [target] : DocumentView.Selected().map(docView => docView.Document); + docs.map(doc => (this.layoutDocAcls || doc.dockingConfig ? doc : Doc.GetProto(doc))).forEach(doc => { + distributeAcls(acl, permission as SharingPermissions, doc, undefined, this.upgradeNested ? true : undefined); + + if (group instanceof Doc) { + Doc.AddDocToList(group, 'docsShared', doc); + + this.users + .filter(({ user: { email } }) => JSON.parse(StrCast(group.members)).includes(email)) + .forEach(({ user, sharingDoc }) => { + if (permission !== SharingPermissions.None) + Doc.AddDocToList(sharingDoc, doc.dockingConfig ? dashStorage : storage, doc); // add the doc to the sharingDoc if it hasn't already been added + else GetEffectiveAcl(doc, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, ((doc.createdFrom as Doc) || doc).dockingConfig ? dashStorage : storage, (doc.createdFrom as Doc) || doc); // remove the doc from the list if it already exists + }); + } + }); + }, 'set group permissions'); + /** + * Populates the list of validated users (this.users) by adding registered users which have a sharingDocument. + */ + populateUsers = async () => { + if (!this.populating && Doc.UserDoc()[Id] !== Utils.GuestID()) { + this.populating = true; + const userList = await RequestPromise.get(ClientUtils.prepend('/getUsers')); + const raw = (JSON.parse(userList) as User[]).filter(user => user.email !== 'guest' && user.email !== ClientUtils.CurrentUserEmail()); + runInAction(() => { + FieldLoader.ServerLoadStatus.message = 'users'; + }); + const docs = await DocServer.GetRefFields(raw.reduce((list, user) => [...list, user.sharingDocumentId, user.linkDatabaseId], [] as string[])); + raw.map( + action((newUser: User) => { + const sharingDoc = docs[newUser.sharingDocumentId]; + const linkDatabase = docs[newUser.linkDatabaseId]; + if (sharingDoc instanceof Doc && linkDatabase instanceof Doc) { + if (!this.users.find(user => user.user.email === newUser.email)) { + this.users.push({ user: newUser, sharingDoc, linkDatabase, userColor: StrCast(sharingDoc.userColor) }); + // LinkManager.addLinkDB(linkDatabase); + } + } + }) + ); + this.populating = false; + } + }; + + // eslint-disable-next-line react/sort-comp + public close = action(() => { + this.isOpen = false; + this.selectedUsers = null; // resets the list of users and selected users (in the react-select component) + TaskCompletionBox.taskCompleted = false; + setTimeout( + action(() => { + // this.copied = false; + this.targetDoc = undefined; + }), + 500 + ); + this.layoutDocAcls = false; + }); + + // eslint-disable-next-line react/no-unused-class-component-methods + public open = (target?: DocumentView, targetDoc?: Doc) => { + this.populateUsers(); + runInAction(() => { + this.targetDocView = target; + this.targetDoc = targetDoc || target?.Document; + this.isOpen = this.targetDoc !== undefined; + this.permissions = SharingPermissions.Augment; + this.upgradeNested = true; + }); + }; + + /** + * Shares the documents shared with a group with a new user who has been added to that group. + * @param group + * @param emailId + */ + // eslint-disable-next-line react/no-unused-class-component-methods + shareWithAddedMember = (group: Doc, emailId: string, retry: boolean = true) => { + const user = this.users.find(({ user: { email } }) => email === emailId)!; + const self = this; + if (group.docsShared) { + if (!user) retry && this.populateUsers().then(() => self.shareWithAddedMember(group, emailId, false)); + else { + DocListCastAsync(user.sharingDoc[storage]).then(userdocs => + DocListCastAsync(group.docsShared).then(dl => { + const filtered = dl?.filter(doc => !doc.dockingConfig && !userdocs?.includes(doc)); + filtered && userdocs?.push(...filtered); + }) + ); + DocListCastAsync(user.sharingDoc[dashStorage]).then(userdocs => + DocListCastAsync(group.docsShared).then(dl => { + const filtered = dl?.filter(doc => doc.dockingConfig && !userdocs?.includes(doc)); + filtered && userdocs?.push(...filtered); + }) + ); + } + } + }; + + /** + * Called from the properties sidebar to change permissions of a user. + */ + // eslint-disable-next-line react/no-unused-class-component-methods + shareFromPropertiesSidebar = undoable((shareWith: string, permission: SharingPermissions, docs: Doc[], layout: boolean) => { + if (layout) this.layoutDocAcls = true; + if (shareWith !== 'Guest') { + const user = this.users.find(({ user: { email } }) => email === (shareWith === 'Me' ? ClientUtils.CurrentUserEmail() : shareWith)); + docs.forEach(doc => { + if (user) this.setInternalSharing(user, permission, doc); + else this.setInternalGroupSharing(GroupManager.Instance.getGroup(shareWith)!, permission, doc, undefined, true); + }); + } else { + docs.forEach(doc => { + if (GetEffectiveAcl(doc) === AclAdmin) { + distributeAcls(`acl_${shareWith}`, permission, doc, undefined); + } + }); + } + this.layoutDocAcls = false; + }, 'sidebar set permissions'); + + /** + * Removes the documents shared with a user through a group when the user is removed from the group. + * @param group + * @param emailId + */ + // eslint-disable-next-line react/no-unused-class-component-methods + removeMember = (group: Doc, emailId: string) => { + const user: ValidatedUser = this.users.find(({ user: { email } }) => email === emailId)!; + + if (group.docsShared && user) { + DocListCastAsync(user.sharingDoc[storage]).then(userdocs => + DocListCastAsync(group.docsShared).then(dl => { + const remaining = userdocs?.filter(doc => !dl?.includes(doc)) || []; + userdocs?.splice(0, userdocs.length, ...remaining); + }) + ); + DocListCastAsync(user.sharingDoc[dashStorage]).then(userdocs => + DocListCastAsync(group.docsShared).then(dl => { + const remaining = userdocs?.filter(doc => !dl?.includes(doc)) || []; + userdocs?.splice(0, userdocs.length, ...remaining); + }) + ); + } + }; + + /** + * Removes a group's permissions from documents that have been shared with it. + * @param group + */ + // eslint-disable-next-line react/no-unused-class-component-methods + removeGroup = (group: Doc) => { + if (group.docsShared) { + DocListCast(group.docsShared).forEach(doc => { + const acl = `acl_${StrCast(group.title)}`; + distributeAcls(acl, SharingPermissions.None, doc); + + const members: string[] = JSON.parse(StrCast(group.members)); + const users: ValidatedUser[] = this.users.filter(({ user: { email } }) => members.includes(email)); + + users.forEach(({ sharingDoc }) => Doc.RemoveDocFromList(sharingDoc, storage, doc)); + }); + } + }; + + // private setExternalSharing = (permission: string) => { + // const targetDoc = this.targetDoc; + // if (!targetDoc) { + // return; + // } + // targetDoc['acl_' + PublicKey] = permission; + // }s + + /** + * Copies the Public sharing url to the user's clipboard. + */ + private copyURL = () => { + ClientUtils.CopyText(ClientUtils.shareUrl(this.targetDoc![Id])); + }; + + private focusOn = (contents: string) => { + const title = this.targetDoc ? StrCast(this.targetDoc.title) : ''; + const docs = DocumentView.Selected().length > 1 ? DocumentView.Selected().map(docView => docView.props.Document) : [this.targetDoc]; + return ( + <span + className="focus-span" + title={title} + onClick={() => { + if (this.targetDoc && this.targetDocView && docs.length === 1) { + DocumentView.showDocument(this.targetDoc, { willZoomCentered: true }); + } + }} + onPointerEnter={action(() => { + if (docs.length) { + docs.forEach(doc => doc && Doc.BrushDoc(doc)); + this.dialogueBoxOpacity = 0.1; + } + })} + onPointerLeave={action(() => { + if (docs.length) { + docs.forEach(doc => doc && Doc.UnBrushDoc(doc)); + this.dialogueBoxOpacity = 1; + } + })}> + {contents} + </span> + ); + }; + + /** + * Calls the relevant method for sharing, displays the popup, and resets the relevant variables. + */ + share = undoable( + action(() => { + if (this.selectedUsers) { + this.selectedUsers.forEach(user => { + if (user.value.includes(indType)) { + this.setInternalSharing(this.users.find(u => u.user.email === user.label)!, this.permissions, undefined); + } else { + this.setInternalGroupSharing(GroupManager.Instance.getGroup(user.label)!, this.permissions); + } + }); + + if (this.shareDocumentButtonRef.current) { + const { left, width, top, height } = this.shareDocumentButtonRef.current.getBoundingClientRect(); + TaskCompletionBox.popupX = left - 1.5 * width; + TaskCompletionBox.popupY = top - 1.5 * height; + TaskCompletionBox.textDisplayed = 'Document shared!'; + TaskCompletionBox.taskCompleted = true; + setTimeout( + action(() => { + TaskCompletionBox.taskCompleted = false; + }), + 2000 + ); + } + + this.layoutDocAcls = false; + this.selectedUsers = null; + } + }), + 'share Doc' + ); + + /** + * Sorting algorithm to sort users. + */ + sortUsers = (u1: ValidatedUser, u2: ValidatedUser) => { + const { email: e1 } = u1.user; + const { email: e2 } = u2.user; + return e1 < e2 ? -1 : e1 === e2 ? 0 : 1; + }; + + /** + * Sorting algorithm to sort groups. + */ + sortGroups = (group1: Doc, group2: Doc) => { + const g1 = StrCast(group1.title); + const g2 = StrCast(group2.title); + return g1 < g2 ? -1 : g1 === g2 ? 0 : 1; + }; + /** + * Returns the SharingPermissions (Admin, Can Edit etc) access that's used to share + */ + private sharingOptions(uniform: boolean, showGuestOptions?: boolean) { + const dropdownValues: string[] = showGuestOptions ? [SharingPermissions.None, SharingPermissions.View] : Object.values(SharingPermissions); + if (!uniform) dropdownValues.unshift('-multiple-'); + return dropdownValues.map(permission => ( + <option key={permission} value={permission}> + {concat(ReverseHierarchyMap.get(permission)?.image, ' ', permission)} + </option> + )); + } + render() { - return <MainViewModal contents={this.sharingInterface} isDisplayed={this.isOpen} interactive={true} dialogueBoxDisplayedOpacity={this.dialogueBoxOpacity} overlayDisplayedOpacity={this.overlayOpacity} closeOnExternalClick={this.close} />; + return <MainViewModal contents={this.sharingInterface} isDisplayed={this.isOpen} interactive dialogueBoxDisplayedOpacity={this.dialogueBoxOpacity} closeOnExternalClick={this.close} />; } } |