aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/client/util/GroupManager.tsx5
-rw-r--r--src/client/util/SharingManager.scss62
-rw-r--r--src/client/util/SharingManager.tsx164
-rw-r--r--src/fields/util.ts55
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);
}
});
}