diff options
-rw-r--r-- | src/client/util/GroupManager.tsx | 5 | ||||
-rw-r--r-- | src/client/util/SharingManager.scss | 62 | ||||
-rw-r--r-- | src/client/util/SharingManager.tsx | 164 | ||||
-rw-r--r-- | src/fields/util.ts | 55 |
4 files changed, 183 insertions, 103 deletions
diff --git a/src/client/util/GroupManager.tsx b/src/client/util/GroupManager.tsx index 72fba5c1b..551216fa4 100644 --- a/src/client/util/GroupManager.tsx +++ b/src/client/util/GroupManager.tsx @@ -20,6 +20,9 @@ import { TaskCompletionBox } from "../views/nodes/TaskCompletedBox"; library.add(fa.faPlus, fa.faTimes, fa.faInfoCircle); +/** + * Interface for options for the react-select component + */ export interface UserOptions { label: string; value: string; @@ -30,8 +33,6 @@ export default class GroupManager extends React.Component<{}> { static Instance: GroupManager; @observable isOpen: boolean = false; // whether the GroupManager is to be displayed or not. - @observable private dialogueBoxOpacity: number = 1; // opacity of the dialogue box div of the MainViewModal. - @observable private overlayOpacity: number = 0.4; // opacity of the overlay div of the MainViewModal. @observable private users: string[] = []; // list of users populated from the database. @observable private selectedUsers: UserOptions[] | null = null; // list of users selected in the "Select users" dropdown. @observable currentGroup: Opt<Doc>; // the currently selected group. diff --git a/src/client/util/SharingManager.scss b/src/client/util/SharingManager.scss index d71ff0cf6..2ce9f232c 100644 --- a/src/client/util/SharingManager.scss +++ b/src/client/util/SharingManager.scss @@ -23,33 +23,51 @@ z-index: 999; } - .share-setup { - display: flex; - margin-bottom: 20px; - align-items: center; - height: 36; + .share-container { + .share-setup { + display: flex; + margin-bottom: 20px; + align-items: center; + height: 36; - .user-search { - width: 90%; + .user-search { + width: 90%; - input { - height: 30; + input { + height: 30; + } + } + + .permissions-select { + z-index: 1; + margin-left: -100; + border: none; + outline: none; + text-align: justify; // for Edge + text-align-last: end; } - } - .permissions-select { - z-index: 1; - margin-left: -100; - border: none; - outline: none; - text-align: justify; // for Edge - text-align-last: end; + .share-button { + height: 105%; + margin-left: 2%; + background-color: #979797; + } } - .share-button { - height: 105%; - margin-left: 2%; - background-color: #979797; + .sort-checkboxes { + float: left; + margin-top: -17px; + margin-bottom: 10px; + font-size: 10px; + + input { + height: 10px; + } + + label { + font-weight: normal; + font-style: italic; + } } } @@ -92,10 +110,8 @@ height: 250px; margin: 0 2; - .none { font-style: italic; - } } } diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx index 9fbdfa8e5..ec04bdd81 100644 --- a/src/client/util/SharingManager.tsx +++ b/src/client/util/SharingManager.tsx @@ -30,7 +30,10 @@ export interface User { userDocumentId: string; } -interface GroupOptions { +/** + * Interface for grouped options for the react-select component. + */ +interface GroupedOptions { label: string; options: UserOptions[]; } @@ -39,9 +42,13 @@ interface GroupOptions { // const PublicKey = "publicLinkPermissions"; // const DefaultColor = "black"; -const groupType = "!groupType/"; +// used to differentiate between individuals and groups when sharing const indType = "!indType/"; +const groupType = "!groupType/"; +/** + * A user who also has a notificationDoc. + */ interface ValidatedUser { user: User; notificationDoc: Doc; @@ -52,18 +59,21 @@ const storage = "data"; @observer export default class SharingManager extends React.Component<{}> { public static Instance: SharingManager; - @observable private isOpen = false; - @observable private users: ValidatedUser[] = []; - @observable private targetDoc: Doc | undefined; - @observable private targetDocView: DocumentView | undefined; + @observable private isOpen = false; // whether the SharingManager modal is open or not + @observable private users: ValidatedUser[] = []; // the list of users with notificationDocs + @observable private targetDoc: Doc | undefined; // the document being shared + @observable private targetDocView: DocumentView | undefined; // the DocumentView of the document being shared // @observable private copied = false; - @observable private dialogueBoxOpacity = 1; - @observable private overlayOpacity = 0.4; - @observable private selectedUsers: UserOptions[] | null = null; - @observable private permissions: SharingPermissions = SharingPermissions.Edit; - @observable private individualSort: "ascending" | "descending" | "none" = "none"; - @observable private groupSort: "ascending" | "descending" | "none" = "none"; - private shareDocumentButtonRef: React.RefObject<HTMLButtonElement> = React.createRef(); + @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) @@ -85,7 +95,7 @@ export default class SharingManager extends React.Component<{}> { public close = action(() => { this.isOpen = false; - this.users = []; + this.users = []; // resets the list of users and seleected users (in the react-select component) this.selectedUsers = null; setTimeout(action(() => { @@ -100,6 +110,9 @@ export default class SharingManager extends React.Component<{}> { SharingManager.Instance = this; } + /** + * Populates the list of validated users (this.users) by adding registered users which have a rightSidebarCollection. + */ populateUsers = async () => { const userList = await RequestPromise.get(Utils.prepend("/getUsers")); const raw = JSON.parse(userList) as User[]; @@ -120,58 +133,74 @@ export default class SharingManager extends React.Component<{}> { return Promise.all(evaluating); } + /** + * Sets the permission on the target for the group. + * @param group + * @param permission + */ setInternalGroupSharing = (group: Doc, permission: string) => { const members: string[] = JSON.parse(StrCast(group.members)); const users: ValidatedUser[] = this.users.filter(({ user: { email } }) => members.includes(email)); const target = this.targetDoc!; const ACL = `ACL-${StrCast(group.groupName)}`; - // fix this - not needed (here and setinternalsharing and removegroup) - // target[ACL] = permission; - // Doc.GetProto(target)[ACL] = permission; - distributeAcls(ACL, permission as SharingPermissions, this.targetDoc!); + distributeAcls(ACL, permission as SharingPermissions, target); + // if documents have been shared, add the target to that list if it doesn't already exist, otherwise create a new list with the target group.docsShared ? DocListCastAsync(group.docsShared).then(resolved => Doc.IndexOf(target, resolved!) === -1 && (group.docsShared as List<Doc>).push(target)) : group.docsShared = new List<Doc>([target]); users.forEach(({ notificationDoc }) => { DocListCastAsync(notificationDoc[storage]).then(resolved => { - if (permission !== SharingPermissions.None) Doc.IndexOf(target, resolved!) === -1 && Doc.AddDocToList(notificationDoc, storage, target); - else Doc.IndexOf(target, resolved!) !== -1 && Doc.RemoveDocFromList(notificationDoc, storage, target); + if (permission !== SharingPermissions.None) Doc.IndexOf(target, resolved!) === -1 && Doc.AddDocToList(notificationDoc, storage, target); // add the target to the notificationDoc if it hasn't already been added + else Doc.IndexOf(target, resolved!) !== -1 && Doc.RemoveDocFromList(notificationDoc, storage, target); // remove the target from the list if it already exists }); }); } + /** + * 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) => { const user: ValidatedUser = this.users.find(({ user: { email } }) => email === emailId)!; if (group.docsShared) { DocListCastAsync(group.docsShared).then(docsShared => { docsShared?.forEach(doc => { - DocListCastAsync(user.notificationDoc[storage]).then(resolved => Doc.IndexOf(doc, resolved!) === -1 && Doc.AddDocToList(user.notificationDoc, storage, doc)); + DocListCastAsync(user.notificationDoc[storage]).then(resolved => Doc.IndexOf(doc, resolved!) === -1 && Doc.AddDocToList(user.notificationDoc, storage, doc)); // add the doc if it isn't already in the list }); }); } } + /** + * 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) { DocListCastAsync(group.docsShared).then(docsShared => { docsShared?.forEach(doc => { - DocListCastAsync(user.notificationDoc[storage]).then(resolved => Doc.IndexOf(doc, resolved!) !== -1 && Doc.RemoveDocFromList(user.notificationDoc, storage, doc)); + DocListCastAsync(user.notificationDoc[storage]).then(resolved => Doc.IndexOf(doc, resolved!) !== -1 && Doc.RemoveDocFromList(user.notificationDoc, storage, doc)); // remove the doc only if it is in the list }); }); } } + /** + * Removes a group's permissions from documents that have been shared with it. + * @param group + */ removeGroup = (group: Doc) => { if (group.docsShared) { DocListCastAsync(group.docsShared).then(resolved => { resolved?.forEach(doc => { const ACL = `ACL-${StrCast(group.groupName)}`; - // doc[ACL] = doc[DataSym][ACL] = "Not Shared"; distributeAcls(ACL, SharingPermissions.None, doc); @@ -185,7 +214,6 @@ export default class SharingManager extends React.Component<{}> { } } - // @action setInternalSharing = (recipient: ValidatedUser, permission: string) => { const { user, notificationDoc } = recipient; const target = this.targetDoc!; @@ -323,18 +351,32 @@ export default class SharingManager extends React.Component<{}> { const sortedGroups = groupList.sort(this.sortGroups) .map(({ groupName }) => ({ label: StrCast(groupName), value: groupType + StrCast(groupName) })); - const options: GroupOptions[] = GroupManager.Instance ? - [ - { + const options: GroupedOptions[] = []; + + if (GroupManager.Instance) { + if ((this.showUserOptions && this.showGroupOptions) || (!this.showUserOptions && !this.showGroupOptions)) { + options.push({ label: 'Individuals', options: sortedUsers }, - { + { + label: 'Groups', + options: sortedGroups + }); + } + else if (this.showUserOptions) { + options.push({ + label: 'Individuals', + options: sortedUsers + }); + } + else { + options.push({ label: 'Groups', options: sortedGroups - } - ] - : []; + }); + } + } const users = this.individualSort === "ascending" ? this.users.sort(this.sortUsers) : this.individualSort === "descending" ? this.users.sort(this.sortUsers).reverse() : this.users; const groups = this.groupSort === "ascending" ? groupList.sort(this.sortGroups) : this.groupSort === "descending" ? groupList.sort(this.sortGroups).reverse() : groupList; @@ -403,7 +445,6 @@ export default class SharingManager extends React.Component<{}> { ); }); - const displayUserList = !userListContents?.every(user => user === null); const displayGroupList = !groupListContents?.every(group => group === null); return ( @@ -451,27 +492,33 @@ export default class SharingManager extends React.Component<{}> { </div> {this.targetDoc?.author !== Doc.CurrentUserEmail ? null : - <div className="share-setup"> - <Select - className={"user-search"} - placeholder={"Enter user or group name..."} - isMulti - closeMenuOnSelect={false} - options={options} - onChange={this.handleUsersChange} - value={this.selectedUsers} - styles={{ - indicatorSeparator: () => ({ - visibility: "hidden" - }) - }} - /> - <select className="permissions-select" onChange={this.handlePermissionsChange}> - {this.sharingOptions} - </select> - <button ref={this.shareDocumentButtonRef} className="share-button" onClick={this.share}> - Share + <div className="share-container"> + <div className="share-setup"> + <Select + className={"user-search"} + placeholder={"Enter user or group name..."} + isMulti + closeMenuOnSelect={false} + options={options} + onChange={this.handleUsersChange} + value={this.selectedUsers} + styles={{ + indicatorSeparator: () => ({ + visibility: "hidden" + }) + }} + /> + <select className="permissions-select" onChange={this.handlePermissionsChange}> + {this.sharingOptions} + </select> + <button ref={this.shareDocumentButtonRef} className="share-button" onClick={this.share}> + Share </button> + </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> + </div> </div> } <div className="main-container"> @@ -481,17 +528,8 @@ export default class SharingManager extends React.Component<{}> { onClick={action(() => this.individualSort = this.individualSort === "ascending" ? "descending" : this.individualSort === "descending" ? "none" : "ascending")}> Individuals {this.individualSort === "ascending" ? "↑" : this.individualSort === "descending" ? "↓" : ""} {/* → */} </div> - <div className={"users-list"} style={{ display: !displayUserList ? "flex" : "block" }}>{/*200*/} - { - !displayUserList ? - <div - className={"none"} - > - There are no users this document has been shared with. - </div> - : - userListContents - } + <div className={"users-list"} style={{ display: "block" }}>{/*200*/} + {userListContents} </div> </div> <div className={"group-container"}> diff --git a/src/fields/util.ts b/src/fields/util.ts index a62795e64..cf8e730fd 100644 --- a/src/fields/util.ts +++ b/src/fields/util.ts @@ -115,6 +115,7 @@ export function OVERRIDE_ACL(val: boolean) { _overrideAcl = val; } +// playground mode allows the user to add/delete documents or make layout changes without them saving to the server let playgroundMode = false; export function togglePlaygroundMode() { @@ -125,12 +126,27 @@ export function getPlaygroundMode() { return playgroundMode; } +// the list of groups that the current user is a member of let currentUserGroups: string[] = []; +// called from GroupManager once the groups have been fetched from the server export function setGroups(groups: string[]) { currentUserGroups = groups; } +/** + * These are the various levels of access a user can have to a document. + * + * Admin: a user with admin access to a document can remove/edit that document, add/remove/edit annotations (depending on permissions), as well as change others' access rights to that document. + * + * Edit: a user with edit access to a document can remove/edit that document, add/remove/edit annotations (depending on permissions), but not change any access rights to that document. + * + * Add: a user with add access to a document can add documents/annotations to that document but cannot edit or delete anything. + * + * View: a user with view access to a document can only view it - they cannot add/remove/edit anything. + * + * None: the document is not shared with that user. + */ export enum SharingPermissions { Admin = "Admin", Edit = "Can Edit", @@ -139,18 +155,21 @@ export enum SharingPermissions { None = "Not Shared" } +/** + * Calculates the effective access right to a document for the current user. + */ export function GetEffectiveAcl(target: any, in_prop?: string | symbol | number): symbol { if (in_prop === UpdatingFromServer || target[UpdatingFromServer]) return AclAdmin; if (target[AclSym] && Object.keys(target[AclSym]).length) { + // if the current user is the author of the document / the current user is a member of the admin group if (target.__fields?.author === Doc.CurrentUserEmail || target.author === Doc.CurrentUserEmail || currentUserGroups.includes("admin")) return AclAdmin; + // if the ACL is being overriden or the property being modified is one of the playground fields (which can be freely modified) if (_overrideAcl || (in_prop && DocServer.PlaygroundFields?.includes(in_prop.toString()))) return AclEdit; let effectiveAcl = AclPrivate; - let aclPresent = false; - const HierarchyMapping = new Map<symbol, number>([ [AclPrivate, 0], [AclReadonly, 1], @@ -160,19 +179,26 @@ export function GetEffectiveAcl(target: any, in_prop?: string | symbol | number) ]); for (const [key, value] of Object.entries(target[AclSym])) { + // there are issues with storing fields with . in the name, so they are replaced with _ during creation + // as a result we need to restore them again during this comparison. if (currentUserGroups.includes(key.substring(4)) || Doc.CurrentUserEmail === key.substring(4).replace("_", ".")) { - if (HierarchyMapping.get(value as symbol)! >= HierarchyMapping.get(effectiveAcl)!) { - aclPresent = true; + if (HierarchyMapping.get(value as symbol)! > HierarchyMapping.get(effectiveAcl)!) { effectiveAcl = value as symbol; - if (effectiveAcl === AclEdit) break; + if (effectiveAcl === AclAdmin) break; } } } - return aclPresent ? effectiveAcl : AclEdit; + return effectiveAcl; } return AclAdmin; } - +/** + * Recursively distributes the access right for a user across the children of a document and its annotations. + * @param key the key storing the access right (e.g. ACL-groupname) + * @param acl the access right being stored (e.g. "Can Edit") + * @param target the document on which this access right is being set + * @param inheritingFromCollection whether the target is being assigned rights after being dragged into a collection (and so is inheriting the ACLs from the collection) + */ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc, inheritingFromCollection?: boolean) { const HierarchyMapping = new Map<string, number>([ @@ -185,32 +211,31 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc const dataDoc = target[DataSym]; + // if it is inheriting from a collection, it only inherits if A) the key doesn't already exist or B) the right being inherited is more restrictive if (!inheritingFromCollection || !target[key] || HierarchyMapping.get(StrCast(target[key]))! > HierarchyMapping.get(acl)!) target[key] = acl; if (dataDoc && (!inheritingFromCollection || !dataDoc[key] || HierarchyMapping.get(StrCast(dataDoc[key]))! > HierarchyMapping.get(acl)!)) { dataDoc[key] = acl; + // maps over the children of the document DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc)]).map(d => { if (d.author === Doc.CurrentUserEmail && (!inheritingFromCollection || !d[key] || HierarchyMapping.get(StrCast(d[key]))! > HierarchyMapping.get(acl)!)) { - distributeAcls(key, acl, d); - d[key] = acl; + distributeAcls(key, acl, d, inheritingFromCollection); } const data = d[DataSym]; if (data && data.author === Doc.CurrentUserEmail && (!inheritingFromCollection || !data[key] || HierarchyMapping.get(StrCast(data[key]))! > HierarchyMapping.get(acl)!)) { - distributeAcls(key, acl, data); - data[key] = acl; + distributeAcls(key, acl, data, inheritingFromCollection); } }); + // maps over the annotations of the document DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc) + "-annotations"]).map(d => { if (d.author === Doc.CurrentUserEmail && (!inheritingFromCollection || !d[key] || HierarchyMapping.get(StrCast(d[key]))! > HierarchyMapping.get(acl)!)) { - distributeAcls(key, acl, d); - d[key] = acl; + distributeAcls(key, acl, d, inheritingFromCollection); } const data = d[DataSym]; if (data && data.author === Doc.CurrentUserEmail && (!inheritingFromCollection || !data[key] || HierarchyMapping.get(StrCast(data[key]))! > HierarchyMapping.get(acl)!)) { - distributeAcls(key, acl, data); - data[key] = acl; + distributeAcls(key, acl, data, inheritingFromCollection); } }); } |