aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/client/DocServer.ts3
-rw-r--r--src/client/util/GroupManager.tsx44
-rw-r--r--src/client/util/GroupMemberView.scss3
-rw-r--r--src/client/util/GroupMemberView.tsx11
-rw-r--r--src/client/util/SettingsManager.tsx2
-rw-r--r--src/client/util/SharingManager.scss64
-rw-r--r--src/client/util/SharingManager.tsx199
-rw-r--r--src/client/views/DocComponent.tsx24
-rw-r--r--src/client/views/DocumentDecorations.tsx27
-rw-r--r--src/client/views/MainViewModal.tsx2
-rw-r--r--src/client/views/collections/CollectionView.tsx39
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.tsx4
-rw-r--r--src/client/views/nodes/TaskCompletedBox.tsx2
-rw-r--r--src/client/views/nodes/WebBox.tsx14
-rw-r--r--src/client/views/pdf/PDFMenu.tsx2
-rw-r--r--src/client/views/pdf/PDFViewer.tsx14
-rw-r--r--src/fields/Doc.ts4
-rw-r--r--src/fields/util.ts94
18 files changed, 348 insertions, 204 deletions
diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts
index f5ee6da3c..6fa8cf909 100644
--- a/src/client/DocServer.ts
+++ b/src/client/DocServer.ts
@@ -9,7 +9,6 @@ import GestureOverlay from './views/GestureOverlay';
import MobileInkOverlay from '../mobile/MobileInkOverlay';
import { runInAction } from 'mobx';
import { ObjectField } from '../fields/ObjectField';
-import { getPlaygroundMode } from '../fields/util';
/**
* This class encapsulates the transfer and cross-client synchronization of
@@ -452,7 +451,7 @@ export namespace DocServer {
}
function _UpdateFieldImpl(id: string, diff: any) {
- (!getPlaygroundMode()) && Utils.Emit(_socket, MessageStore.UpdateField, { id, diff });
+ (!DocServer.Control.isReadOnly()) && Utils.Emit(_socket, MessageStore.UpdateField, { id, diff });
}
let _UpdateField: (id: string, diff: any) => void = errorFunc;
diff --git a/src/client/util/GroupManager.tsx b/src/client/util/GroupManager.tsx
index 72fba5c1b..5215ea35f 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,15 +33,13 @@ 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.
@observable private createGroupModalOpen: boolean = false;
private inputRef: React.RefObject<HTMLInputElement> = React.createRef(); // the ref for the input box.
- private createGroupButtonRef: React.RefObject<HTMLButtonElement> = React.createRef();
- private currentUserGroups: string[] = [];
+ private createGroupButtonRef: React.RefObject<HTMLButtonElement> = React.createRef(); // the ref for the group creation button
+ private currentUserGroups: string[] = []; // the list of groups the current user is a member of
@observable private buttonColour: "#979797" | "black" = "#979797";
@observable private groupSort: "ascending" | "descending" | "none" = "none";
@@ -49,6 +50,9 @@ export default class GroupManager extends React.Component<{}> {
GroupManager.Instance = this;
}
+ /**
+ * Populates the list of users and groups.
+ */
componentDidMount() {
this.populateUsers();
this.populateGroups();
@@ -62,8 +66,6 @@ export default class GroupManager extends React.Component<{}> {
const userList = await RequestPromise.get(Utils.prepend("/getUsers"));
const raw = JSON.parse(userList) as User[];
const evaluating = raw.map(async user => {
- // const isCandidate = user.email !== Doc.CurrentUserEmail;
- // if (isCandidate) {
const userDocument = await DocServer.GetRefField(user.userDocumentId);
if (userDocument instanceof Doc) {
const notificationDoc = await Cast(userDocument.rightSidebarCollection, Doc);
@@ -73,11 +75,13 @@ export default class GroupManager extends React.Component<{}> {
}
});
}
- // }
});
return Promise.all(evaluating);
}
+ /**
+ * Populates the list of groups the current user is a member of and sets this list to be used in the GetEffectiveAcl in util.ts
+ */
populateGroups = () => {
DocListCastAsync(this.GroupManagerDoc?.data).then(groups => {
groups?.forEach(group => {
@@ -101,7 +105,7 @@ export default class GroupManager extends React.Component<{}> {
*/
@action
open = () => {
- SelectionManager.DeselectAll();
+ // SelectionManager.DeselectAll();
this.isOpen = true;
this.populateUsers();
this.populateGroups();
@@ -145,25 +149,8 @@ export default class GroupManager extends React.Component<{}> {
}
/**
- * @returns a readonly copy of a single group document
+ * Returns an array of the list of members of a given group.
*/
- getGroupCopy(groupName: string): Doc | undefined {
- const groupDoc = this.getGroup(groupName);
- if (groupDoc) {
- const { members, owners } = groupDoc;
- return Doc.assign(new Doc, { groupName, members: StrCast(members), owners: StrCast(owners) });
- }
- return undefined;
- }
- /**
- * @returns a readonly copy of the list of group documents
- */
- getAllGroupsCopy(): Doc[] {
- return this.getAllGroups().map(({ groupName, owners, members }) =>
- Doc.assign(new Doc, { groupName: (StrCast(groupName)), owners: (StrCast(owners)), members: (StrCast(members)) })
- );
- }
-
getGroupMembers(group: string | Doc): string[] {
if (group instanceof Doc) return JSON.parse(StrCast(group.members)) as string[];
else return JSON.parse(StrCast(this.getGroup(group)!.members)) as string[];
@@ -316,6 +303,9 @@ export default class GroupManager extends React.Component<{}> {
}
+ /**
+ * @returns the MainViewModal which allows the user to create groups.
+ */
private get groupCreationModal() {
const contents = (
<div className="group-create">
@@ -415,7 +405,7 @@ export default class GroupManager extends React.Component<{}> {
<div
className="sort-groups"
onClick={action(() => this.groupSort = this.groupSort === "ascending" ? "descending" : this.groupSort === "descending" ? "none" : "ascending")}>
- Name {this.groupSort === "ascending" ? "↑" : this.groupSort === "descending" ? "↓" : ""} {/* → */}
+ Name {this.groupSort === "ascending" ? "↑" : this.groupSort === "descending" ? "↓" : ""}
</div>
<div className="group-body">
{groups.map(group =>
diff --git a/src/client/util/GroupMemberView.scss b/src/client/util/GroupMemberView.scss
index c609c5c7b..2fc27ed03 100644
--- a/src/client/util/GroupMemberView.scss
+++ b/src/client/util/GroupMemberView.scss
@@ -41,6 +41,7 @@
margin-top: -5;
height: 20;
text-overflow: ellipsis;
+ background: none;
&:hover {
text-overflow: visible;
@@ -72,7 +73,7 @@
.editing-contents {
overflow-y: auto;
- height: 65%;
+ height: 62%;
width: 100%;
color: black;
margin-top: -15px;
diff --git a/src/client/util/GroupMemberView.tsx b/src/client/util/GroupMemberView.tsx
index f20670c4e..531ef988a 100644
--- a/src/client/util/GroupMemberView.tsx
+++ b/src/client/util/GroupMemberView.tsx
@@ -29,13 +29,17 @@ export default class GroupMemberView extends React.Component<GroupMemberViewProp
const options: UserOptions[] = this.props.group ? GroupManager.Instance.options.filter(option => !(JSON.parse(StrCast(this.props.group.members)) as string[]).includes(option.value)) : [];
+ const hasEditAccess = GroupManager.Instance.hasEditAccess(this.props.group);
+
return (!this.props.group ? null :
<div className="editing-interface">
<div className="editing-header">
<input
className="group-title"
+ style={{ marginLeft: !hasEditAccess ? "-14%" : 0 }}
value={StrCast(this.props.group.groupName)}
onChange={e => this.props.group.groupName = e.currentTarget.value}
+ disabled={!hasEditAccess}
>
</input>
<div className={"memberView-closeButton"} onClick={action(this.props.onCloseButtonClick)}>
@@ -65,12 +69,15 @@ export default class GroupMemberView extends React.Component<GroupMemberViewProp
null}
<div
className="sort-emails"
+ style={{ paddingTop: hasEditAccess ? 0 : 35 }}
onClick={action(() => this.memberSort = this.memberSort === "ascending" ? "descending" : this.memberSort === "descending" ? "none" : "ascending")}>
Emails {this.memberSort === "ascending" ? "↑" : this.memberSort === "descending" ? "↓" : ""} {/* → */}
</div>
</div>
<hr />
- <div className="editing-contents">
+ <div className="editing-contents"
+ style={{ height: hasEditAccess ? "62%" : "85%" }}
+ >
{members.map(member => (
<div
className="editing-row"
@@ -79,7 +86,7 @@ export default class GroupMemberView extends React.Component<GroupMemberViewProp
<div className="user-email">
{member}
</div>
- {GroupManager.Instance.hasEditAccess(this.props.group) ?
+ {hasEditAccess ?
<div className={"remove-button"} onClick={() => GroupManager.Instance.removeMemberFromGroup(this.props.group, member)}>
<FontAwesomeIcon icon={fa.faTrashAlt} size={"sm"} />
</div>
diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx
index fbdceb43e..207c78964 100644
--- a/src/client/util/SettingsManager.tsx
+++ b/src/client/util/SettingsManager.tsx
@@ -14,7 +14,6 @@ import { Doc } from "../../fields/Doc";
import GroupManager from "./GroupManager";
import HypothesisAuthenticationManager from "../apis/HypothesisAuthenticationManager";
import GoogleAuthenticationManager from "../apis/GoogleAuthenticationManager";
-import { togglePlaygroundMode } from "../../fields/util";
import { DocServer } from "../DocServer";
library.add(fa.faTimes);
@@ -101,7 +100,6 @@ export default class SettingsManager extends React.Component<{}> {
@action
togglePlaygroundMode = () => {
- //togglePlaygroundMode();
this.playgroundMode = !this.playgroundMode;
if (this.playgroundMode) DocServer.Control.makeReadOnly();
else DocServer.Control.makeEditable();
diff --git a/src/client/util/SharingManager.scss b/src/client/util/SharingManager.scss
index 130785672..8da80ef52 100644
--- a/src/client/util/SharingManager.scss
+++ b/src/client/util/SharingManager.scss
@@ -1,6 +1,6 @@
.sharing-interface {
width: 600px;
- height: 360px;
+ // height: 360px;
.overlay {
transform: translate(-20px, -20px);
@@ -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: black;
+ }
}
- .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 0d8b33fbe..196182b77 100644
--- a/src/client/util/SharingManager.tsx
+++ b/src/client/util/SharingManager.tsx
@@ -1,7 +1,7 @@
import { observable, runInAction, action } from "mobx";
import * as React from "react";
import MainViewModal from "../views/MainViewModal";
-import { Doc, Opt, DocListCastAsync } from "../../fields/Doc";
+import { Doc, Opt, DocListCastAsync, AclAdmin, DataSym, AclPrivate } from "../../fields/Doc";
import { DocServer } from "../DocServer";
import { Cast, StrCast } from "../../fields/Types";
import * as RequestPromise from "request-promise";
@@ -19,7 +19,7 @@ import GroupMemberView from "./GroupMemberView";
import Select from "react-select";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { List } from "../../fields/List";
-import { distributeAcls, SharingPermissions } from "../../fields/util";
+import { distributeAcls, SharingPermissions, GetEffectiveAcl } from "../../fields/util";
import { TaskCompletionBox } from "../views/nodes/TaskCompletedBox";
export interface User {
@@ -27,7 +27,10 @@ export interface User {
userDocumentId: string;
}
-interface GroupOptions {
+/**
+ * Interface for grouped options for the react-select component.
+ */
+interface GroupedOptions {
label: string;
options: UserOptions[];
}
@@ -36,9 +39,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;
@@ -49,18 +56,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)
@@ -69,21 +79,23 @@ export default class SharingManager extends React.Component<{}> {
// }
public open = (target: DocumentView) => {
- SelectionManager.DeselectAll();
- this.populateUsers().then(action(() => {
+ runInAction(() => this.users = []);
+ // SelectionManager.DeselectAll();
+ this.populateUsers();
+ runInAction(() => {
this.targetDocView = target;
this.targetDoc = target.props.Document;
DictationOverlay.Instance.hasActiveModal = true;
this.isOpen = true;
this.permissions = SharingPermissions.Edit;
- }));
+ });
}
public close = action(() => {
this.isOpen = false;
this.users = [];
- this.selectedUsers = null;
+ this.selectedUsers = null; // resets the list of users and seleected users (in the react-select component)
setTimeout(action(() => {
// this.copied = false;
@@ -97,7 +109,18 @@ export default class SharingManager extends React.Component<{}> {
SharingManager.Instance = this;
}
+ /**
+ * Populates the list of users.
+ */
+ componentDidMount() {
+ this.populateUsers();
+ }
+
+ /**
+ * Populates the list of validated users (this.users) by adding registered users which have a rightSidebarCollection.
+ */
populateUsers = async () => {
+ runInAction(() => this.users = []);
const userList = await RequestPromise.get(Utils.prepend("/getUsers"));
const raw = JSON.parse(userList) as User[];
const evaluating = raw.map(async user => {
@@ -117,58 +140,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!);
+ target.author === Doc.CurrentUserEmail && 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);
@@ -182,14 +221,13 @@ export default class SharingManager extends React.Component<{}> {
}
}
- // @action
setInternalSharing = (recipient: ValidatedUser, permission: string) => {
const { user, notificationDoc } = recipient;
const target = this.targetDoc!;
const key = user.email.replace('.', '_');
const ACL = `ACL-${key}`;
- distributeAcls(ACL, permission as SharingPermissions, this.targetDoc!);
+ target.author === Doc.CurrentUserEmail && distributeAcls(ACL, permission as SharingPermissions, target);
if (permission !== SharingPermissions.None) {
DocListCastAsync(notificationDoc[storage]).then(resolved => {
@@ -291,7 +329,7 @@ export default class SharingManager extends React.Component<{}> {
const { left, width, top, height } = this.shareDocumentButtonRef.current!.getBoundingClientRect();
TaskCompletionBox.popupX = left - 1.5 * width;
- TaskCompletionBox.popupY = top - height;
+ TaskCompletionBox.popupY = top - 1.5 * height;
TaskCompletionBox.textDisplayed = "Document shared!";
TaskCompletionBox.taskCompleted = true;
setTimeout(action(() => TaskCompletionBox.taskCompleted = false), 2000);
@@ -320,40 +358,62 @@ 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;
+ const effectiveAcl = this.targetDoc ? GetEffectiveAcl(this.targetDoc) : AclPrivate;
+
const userListContents: (JSX.Element | null)[] = users.map(({ user, notificationDoc }) => {
const userKey = user.email.replace('.', '_');
- const permissions = StrCast(this.targetDoc?.[`ACL-${userKey}`], SharingPermissions.None);
+ const permissions = StrCast(this.targetDoc?.[`ACL-${userKey}`]);
- return permissions === SharingPermissions.None || user.email === this.targetDoc?.author ? null : (
+ return !permissions || user.email === this.targetDoc?.author ? null : (
<div
key={userKey}
className={"container"}
>
<span className={"padding"}>{user.email}</span>
<div className="edit-actions">
- <select
- className={"permissions-dropdown"}
- value={permissions}
- onChange={e => this.setInternalSharing({ user, notificationDoc }, e.currentTarget.value)}
- >
- {this.sharingOptions}
- </select>
+ {effectiveAcl === AclAdmin ? (
+ <select
+ className={"permissions-dropdown"}
+ value={permissions}
+ onChange={e => this.setInternalSharing({ user, notificationDoc }, e.currentTarget.value)}
+ >
+ {this.sharingOptions}
+ </select>
+ ) : (
+ <div className={"permissions-dropdown"}>
+ {permissions}
+ </div>
+ )}
</div>
</div>
);
@@ -365,14 +425,28 @@ export default class SharingManager extends React.Component<{}> {
key={"owner"}
className={"container"}
>
- <span className={"padding"}>{this.targetDoc?.author}</span>
+ <span className={"padding"}>{this.targetDoc?.author === Doc.CurrentUserEmail ? "Me" : this.targetDoc?.author}</span>
<div className="edit-actions">
<div className={"permissions-dropdown"}>
Owner
</div>
</div>
</div>
- )
+ ),
+ this.targetDoc?.author !== Doc.CurrentUserEmail ?
+ (
+ <div
+ key={"me"}
+ className={"container"}
+ >
+ <span className={"padding"}>Me</span>
+ <div className="edit-actions">
+ <div className={"permissions-dropdown"}>
+ {this.targetDoc?.[`ACL-${Doc.CurrentUserEmail.replace(".", "_")}`]}
+ </div>
+ </div>
+ </div>
+ ) : null
);
const groupListContents = groups.map(group => {
@@ -400,7 +474,6 @@ export default class SharingManager extends React.Component<{}> {
);
});
- const displayUserList = !userListContents?.every(user => user === null);
const displayGroupList = !groupListContents?.every(group => group === null);
return (
@@ -446,8 +519,7 @@ export default class SharingManager extends React.Component<{}> {
<div className={"close-button"} onClick={this.close}>
<FontAwesomeIcon icon={"times"} color={"black"} size={"lg"} />
</div>
- {this.targetDoc?.author !== Doc.CurrentUserEmail ? null
- :
+ {<div className="share-container">
<div className="share-setup">
<Select
className={"user-search"}
@@ -457,6 +529,11 @@ export default class SharingManager extends React.Component<{}> {
options={options}
onChange={this.handleUsersChange}
value={this.selectedUsers}
+ styles={{
+ indicatorSeparator: () => ({
+ visibility: "hidden"
+ })
+ }}
/>
<select className="permissions-select" onChange={this.handlePermissionsChange}>
{this.sharingOptions}
@@ -465,6 +542,11 @@ export default class SharingManager extends React.Component<{}> {
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">
<div className={"individual-container"}>
@@ -473,17 +555,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/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index 4c82149e2..804c7a8d4 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -7,7 +7,7 @@ import { InteractionUtils } from '../util/InteractionUtils';
import { List } from '../../fields/List';
import { DateField } from '../../fields/DateField';
import { ScriptField } from '../../fields/ScriptField';
-import { GetEffectiveAcl, getPlaygroundMode, SharingPermissions } from '../../fields/util';
+import { GetEffectiveAcl, SharingPermissions } from '../../fields/util';
/// DocComponent returns a generic React base class used by views that don't have 'fieldKey' props (e.g.,CollectionFreeFormDocumentView, DocumentView)
@@ -150,25 +150,25 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T
const effectiveAcl = GetEffectiveAcl(this.dataDoc);
if (added.length) {
- if (effectiveAcl === AclPrivate || (effectiveAcl === AclReadonly && !getPlaygroundMode())) {
+ if (effectiveAcl === AclPrivate || effectiveAcl === AclReadonly) {
return false;
}
else {
- if (this.props.Document[AclSym]) {
- added.forEach(d => {
- const dataDoc = d[DataSym];
- dataDoc[AclSym] = d[AclSym] = this.props.Document[AclSym];
- for (const [key, value] of Object.entries(this.props.Document[AclSym])) {
- dataDoc[key] = d[key] = this.AclMap.get(value);
- }
- });
- }
+ // if (this.props.Document[AclSym]) {
+ // added.forEach(d => {
+ // const dataDoc = d[DataSym];
+ // dataDoc[AclSym] = d[AclSym] = this.props.Document[AclSym];
+ // for (const [key, value] of Object.entries(this.props.Document[AclSym])) {
+ // dataDoc[key] = d[key] = this.AclMap.get(value);
+ // }
+ // });
+ // }
if (effectiveAcl === AclAddonly) {
added.map(doc => Doc.AddDocToList(targetDataDoc, this.annotationKey, doc));
}
else {
added.map(doc => doc.context = this.props.Document);
- targetDataDoc[this.annotationKey] = new List<Doc>([...docList, ...added]);
+ (targetDataDoc[this.annotationKey] as List<Doc>).push(...added);
targetDataDoc[this.annotationKey + "-lastModified"] = new DateField(new Date(Date.now()));
}
}
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 51325ae1b..6e6ffc893 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -1,9 +1,9 @@
import { IconProp, library } from '@fortawesome/fontawesome-svg-core';
import { faCaretUp, faFilePdf, faFilm, faImage, faObjectGroup, faStickyNote, faTextHeight, faArrowAltCircleDown, faArrowAltCircleUp, faCheckCircle, faCloudUploadAlt, faLink, faShare, faStopCircle, faSyncAlt, faTag, faTimes, faAngleLeft, faAngleRight, faAngleDoubleLeft, faAngleDoubleRight, faPause } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { action, computed, observable, reaction, runInAction } from "mobx";
+import { action, computed, observable, reaction, runInAction, get } from "mobx";
import { observer } from "mobx-react";
-import { Doc, DataSym, Field, WidthSym, HeightSym } from "../../fields/Doc";
+import { Doc, DataSym, Field, WidthSym, HeightSym, AclEdit, AclAdmin } from "../../fields/Doc";
import { Document } from '../../fields/documentSchemas';
import { ScriptField } from '../../fields/ScriptField';
import { Cast, StrCast, NumCast } from "../../fields/Types";
@@ -23,6 +23,9 @@ import { SnappingManager } from '../util/SnappingManager';
import { HtmlField } from '../../fields/HtmlField';
import { InkField } from "../../fields/InkField";
import { Tooltip } from '@material-ui/core';
+import { GetEffectiveAcl } from '../../fields/util';
+import { DocumentIcon } from './nodes/DocumentIcon';
+import { render } from 'react-dom';
library.add(faCaretUp);
library.add(faObjectGroup);
@@ -194,8 +197,11 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
SelectionManager.DeselectAll();
selected.map(dv => {
- recent && Doc.AddDocToList(recent, "data", dv.props.Document, undefined, true, true);
- dv.props.removeDocument?.(dv.props.Document);
+ const effectiveAcl = GetEffectiveAcl(dv.props.Document);
+ if (effectiveAcl === AclEdit || effectiveAcl === AclAdmin) { // deletes whatever you have the right to delete
+ recent && Doc.AddDocToList(recent, "data", dv.props.Document, undefined, true, true);
+ dv.props.removeDocument?.(dv.props.Document);
+ }
});
}
}
@@ -580,17 +586,18 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
if (SnappingManager.GetIsDragging() || bounds.r - bounds.x < 2 || bounds.x === Number.MAX_VALUE || !seldoc || this._hidden || isNaN(bounds.r) || isNaN(bounds.b) || isNaN(bounds.x) || isNaN(bounds.y)) {
return (null);
}
+ const canDelete = SelectionManager.SelectedDocuments().map(docView => GetEffectiveAcl(docView.props.Document)).some(permission => permission === AclAdmin || permission === AclEdit);
const minimal = bounds.r - bounds.x < 100 ? true : false;
const maximizeIcon = minimal ? (
<Tooltip title={<><div className="dash-tooltip">Show context menu</div></>} placement="top">
<div className="documentDecorations-contextMenu" onPointerDown={this.onSettingsDown}>
<FontAwesomeIcon size="lg" icon="cog" />
- </div></Tooltip>) : (
- <Tooltip title={<><div className="dash-tooltip">Delete</div></>} placement="top">
- <div className="documentDecorations-closeButton" onClick={this.onCloseClick}>
- {/* Currently, this is set to be enabled if there is no ink selected. It might be interesting to think about minimizing ink if it's useful? -syip2*/}
- <FontAwesomeIcon className="documentdecorations-times" icon={faTimes} size="lg" />
- </div></Tooltip>);
+ </div></Tooltip>) : canDelete ? (
+ <Tooltip title={<><div className="dash-tooltip">Delete</div></>} placement="top">
+ <div className="documentDecorations-closeButton" onClick={this.onCloseClick}>
+ {/* Currently, this is set to be enabled if there is no ink selected. It might be interesting to think about minimizing ink if it's useful? -syip2*/}
+ <FontAwesomeIcon className="documentdecorations-times" icon={faTimes} size="lg" />
+ </div></Tooltip>) : null;
const titleArea = this._edtingTitle ?
<>
diff --git a/src/client/views/MainViewModal.tsx b/src/client/views/MainViewModal.tsx
index 249715511..66ea2dbf8 100644
--- a/src/client/views/MainViewModal.tsx
+++ b/src/client/views/MainViewModal.tsx
@@ -10,7 +10,7 @@ export interface MainViewOverlayProps {
overlayStyle?: React.CSSProperties;
dialogueBoxDisplayedOpacity?: number;
overlayDisplayedOpacity?: number;
- closeOnExternalClick?: () => void;
+ closeOnExternalClick?: () => void; // the close method of a MainViewModal, triggered if there is a click on the overlay (closing the modal)
}
@observer
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 42d320308..7e7ea6786 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -17,7 +17,7 @@ import { listSpec } from '../../../fields/Schema';
import { ComputedField, ScriptField } from '../../../fields/ScriptField';
import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
import { ImageField } from '../../../fields/URLField';
-import { TraceMobx, GetEffectiveAcl, getPlaygroundMode, distributeAcls, SharingPermissions } from '../../../fields/util';
+import { TraceMobx, GetEffectiveAcl, SharingPermissions } from '../../../fields/util';
import { emptyFunction, emptyPath, returnEmptyFilter, returnFalse, returnOne, returnZero, setupMoveUpEvents, Utils } from '../../../Utils';
import { Docs, DocUtils } from '../../documents/Documents';
import { DocumentType } from '../../documents/DocumentTypes';
@@ -142,20 +142,20 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus
const effectiveAcl = GetEffectiveAcl(this.props.Document);
if (added.length) {
- if (effectiveAcl === AclPrivate || (effectiveAcl === AclReadonly && !getPlaygroundMode())) {
+ if (effectiveAcl === AclPrivate || effectiveAcl === AclReadonly) {
return false;
}
else {
- if (this.props.Document[AclSym]) {
- // change so it only adds if more restrictive
- added.forEach(d => {
- // const dataDoc = d[DataSym];
- for (const [key, value] of Object.entries(this.props.Document[AclSym])) {
- distributeAcls(key, this.AclMap.get(value) as SharingPermissions, d, true);
- }
- // dataDoc[AclSym] = d[AclSym] = this.props.Document[AclSym];
- });
- }
+ // if (this.props.Document[AclSym]) {
+ // // change so it only adds if more restrictive
+ // added.forEach(d => {
+ // // const dataDoc = d[DataSym];
+ // for (const [key, value] of Object.entries(this.props.Document[AclSym])) {
+ // // key.substring(4).replace("_", ".") !== Doc.CurrentUserEmail && distributeAcls(key, this.AclMap.get(value) as SharingPermissions, d, true);
+ // distributeAcls(key, this.AclMap.get(value) as SharingPermissions, d, true);
+ // }
+ // });
+ // }
if (effectiveAcl === AclAddonly) {
added.map(doc => Doc.AddDocToList(targetDataDoc, this.props.fieldKey, doc));
@@ -179,7 +179,8 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus
doc.context = this.props.Document;
});
added.map(add => Doc.AddDocToList(Cast(Doc.UserDoc().myCatalog, Doc, null), "data", add));
- targetDataDoc[this.props.fieldKey] = new List<Doc>([...docList, ...added]);
+ // targetDataDoc[this.props.fieldKey] = new List<Doc>([...docList, ...added]);
+ (targetDataDoc[this.props.fieldKey] as List<Doc>).push(...added);
targetDataDoc[this.props.fieldKey + "-lastModified"] = new DateField(new Date(Date.now()));
}
}
@@ -189,14 +190,16 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus
@action.bound
removeDocument = (doc: any): boolean => {
- const effectiveAcl = GetEffectiveAcl(this.props.Document);
- if (effectiveAcl === AclEdit || effectiveAcl === AclAdmin || getPlaygroundMode()) {
+ const collectionEffectiveAcl = GetEffectiveAcl(this.props.Document);
+ const docEffectiveAcl = GetEffectiveAcl(doc);
+ // you can remove the document if you either have Admin/Edit access to the collection or to the specific document
+ if (collectionEffectiveAcl === AclEdit || collectionEffectiveAcl === AclAdmin || docEffectiveAcl === AclAdmin || docEffectiveAcl === AclEdit) {
const docs = doc instanceof Doc ? [doc] : doc as Doc[];
const targetDataDoc = this.props.Document[DataSym];
const value = DocListCast(targetDataDoc[this.props.fieldKey]);
- const result = value.filter(v => !docs.includes(v));
- if (result.length !== value.length) {
- targetDataDoc[this.props.fieldKey] = new List<Doc>(result);
+ const toRemove = value.filter(v => docs.includes(v));
+ if (toRemove.length !== 0) {
+ toRemove.forEach(doc => Doc.RemoveDocFromList(targetDataDoc, this.props.fieldKey, doc));
return true;
}
}
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 764758eee..a32c8b363 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -1,7 +1,7 @@
import { action, computed, observable } from "mobx";
import { observer } from "mobx-react";
import { Doc, Opt, DocListCast, DataSym, AclEdit, AclAddonly, AclAdmin } from "../../../../fields/Doc";
-import { GetEffectiveAcl, getPlaygroundMode } from "../../../../fields/util";
+import { GetEffectiveAcl } from "../../../../fields/util";
import { InkData, InkField, InkTool } from "../../../../fields/InkField";
import { List } from "../../../../fields/List";
import { RichTextField } from "../../../../fields/RichTextField";
@@ -281,7 +281,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
this._downX = x;
this._downY = y;
const effectiveAcl = GetEffectiveAcl(this.props.Document);
- if ([AclAdmin, AclEdit, AclAddonly].includes(effectiveAcl) || getPlaygroundMode()) PreviewCursor.Show(x, y, this.onKeyPress, this.props.addLiveTextDocument, this.props.getTransform, this.props.addDocument, this.props.nudge);
+ if ([AclAdmin, AclEdit, AclAddonly].includes(effectiveAcl)) PreviewCursor.Show(x, y, this.onKeyPress, this.props.addLiveTextDocument, this.props.getTransform, this.props.addDocument, this.props.nudge);
this.clearSelection();
}
});
diff --git a/src/client/views/nodes/TaskCompletedBox.tsx b/src/client/views/nodes/TaskCompletedBox.tsx
index 89602f219..2a3dd8d2d 100644
--- a/src/client/views/nodes/TaskCompletedBox.tsx
+++ b/src/client/views/nodes/TaskCompletedBox.tsx
@@ -1,7 +1,5 @@
import React = require("react");
import { observer } from "mobx-react";
-import { documentSchema } from "../../../fields/documentSchemas";
-import { makeInterface } from "../../../fields/Schema";
import "./TaskCompletedBox.scss";
import { observable, action } from "mobx";
import { Fade } from "@material-ui/core";
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index d30f1499e..3fbebe3e6 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -4,7 +4,7 @@ import { action, computed, IReactionDisposer, observable, reaction, runInAction
import { observer } from "mobx-react";
import { Dictionary } from "typescript-collections";
import * as WebRequest from 'web-request';
-import { Doc, DocListCast, Opt } from "../../../fields/Doc";
+import { Doc, DocListCast, Opt, AclAddonly, AclEdit, AclAdmin } from "../../../fields/Doc";
import { documentSchema } from "../../../fields/documentSchemas";
import { Id } from "../../../fields/FieldSymbols";
import { HtmlField } from "../../../fields/HtmlField";
@@ -13,7 +13,7 @@ import { List } from "../../../fields/List";
import { listSpec, makeInterface } from "../../../fields/Schema";
import { Cast, NumCast, StrCast } from "../../../fields/Types";
import { WebField } from "../../../fields/URLField";
-import { TraceMobx } from "../../../fields/util";
+import { TraceMobx, GetEffectiveAcl } from "../../../fields/util";
import { addStyleSheet, clearStyleSheetRules, emptyFunction, returnOne, returnZero, Utils, returnTrue } from "../../../Utils";
import { Docs, DocUtils } from "../../documents/Documents";
import { DragManager } from "../../util/DragManager";
@@ -535,9 +535,13 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum
@action
highlight = (color: string) => {
// creates annotation documents for current highlights
- const annotationDoc = this.makeAnnotationDocument(color);
- annotationDoc && Doc.AddDocToList(this.props.Document, this.annotationKey, annotationDoc);
- return annotationDoc;
+ const effectiveAcl = GetEffectiveAcl(this.props.Document);
+ if ([AclAddonly, AclEdit, AclAdmin].includes(effectiveAcl)) {
+ const annotationDoc = this.makeAnnotationDocument(color);
+ annotationDoc && Doc.AddDocToList(this.props.Document, this.annotationKey, annotationDoc);
+ return annotationDoc;
+ }
+ return undefined;
}
/**
* This is temporary for creating annotations from highlights. It will
diff --git a/src/client/views/pdf/PDFMenu.tsx b/src/client/views/pdf/PDFMenu.tsx
index c3e1ae22f..7bea8d01b 100644
--- a/src/client/views/pdf/PDFMenu.tsx
+++ b/src/client/views/pdf/PDFMenu.tsx
@@ -93,7 +93,7 @@ export default class PDFMenu extends AntimodeMenu {
@computed get highlighter() {
const button =
- <button className="antimodeMenu-button color-preview-button" title="" key="highilghter-button" onPointerDown={this.highlightClicked}>
+ <button className="antimodeMenu-button color-preview-button" title="" key="highlighter-button" onPointerDown={this.highlightClicked}>
<FontAwesomeIcon icon="highlighter" size="lg" style={{ transition: "transform 0.1s", transform: this.Highlighting ? "" : "rotate(-45deg)" }} />
<div className="color-preview" style={{ backgroundColor: this.highlightColor }}></div>
</button>;
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index c792df882..59fa6b08c 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -4,7 +4,7 @@ const pdfjs = require('pdfjs-dist/es5/build/pdf.js');
import * as Pdfjs from "pdfjs-dist";
import "pdfjs-dist/web/pdf_viewer.css";
import { Dictionary } from "typescript-collections";
-import { Doc, DocListCast, FieldResult, HeightSym, Opt, WidthSym } from "../../../fields/Doc";
+import { Doc, DocListCast, FieldResult, HeightSym, Opt, WidthSym, AclAddonly, AclEdit, AclAdmin } from "../../../fields/Doc";
import { documentSchema } from "../../../fields/documentSchemas";
import { Id } from "../../../fields/FieldSymbols";
import { InkTool } from "../../../fields/InkField";
@@ -13,7 +13,7 @@ import { createSchema, makeInterface } from "../../../fields/Schema";
import { ScriptField } from "../../../fields/ScriptField";
import { Cast, NumCast } from "../../../fields/Types";
import { PdfField } from "../../../fields/URLField";
-import { TraceMobx } from "../../../fields/util";
+import { TraceMobx, GetEffectiveAcl } from "../../../fields/util";
import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, emptyPath, intersectRect, returnZero, smoothScroll, Utils } from "../../../Utils";
import { Docs, DocUtils } from "../../documents/Documents";
import { DocumentType } from "../../documents/DocumentTypes";
@@ -565,9 +565,13 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu
@action
highlight = (color: string) => {
// creates annotation documents for current highlights
- const annotationDoc = this.makeAnnotationDocument(color);
- annotationDoc && this.props.addDocument?.(annotationDoc);
- return annotationDoc;
+ const effectiveAcl = GetEffectiveAcl(this.props.Document);
+ if ([AclAddonly, AclEdit, AclAdmin].includes(effectiveAcl)) {
+ const annotationDoc = this.makeAnnotationDocument(color);
+ annotationDoc && Doc.AddDocToList(this.props.Document, this.annotationKey, annotationDoc);
+ return annotationDoc;
+ }
+ return undefined;
}
/**
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 267defa4b..a1acc7060 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -508,6 +508,10 @@ export namespace Doc {
alias.aliasOf = doc;
alias.title = ComputedField.MakeFunction(`renameAlias(this, ${Doc.GetProto(doc).aliasNumber = NumCast(Doc.GetProto(doc).aliasNumber) + 1})`);
alias.author = Doc.CurrentUserEmail;
+
+ if (!doc.aliases) doc.aliases = new List<Doc>([alias]);
+ else Doc.AddDocToList(doc, "aliases", alias);
+
return alias;
}
diff --git a/src/fields/util.ts b/src/fields/util.ts
index a62795e64..d6d9718f2 100644
--- a/src/fields/util.ts
+++ b/src/fields/util.ts
@@ -1,5 +1,5 @@
import { UndoManager } from "../client/util/UndoManager";
-import { Doc, FieldResult, UpdatingFromServer, LayoutSym, AclPrivate, AclEdit, AclReadonly, AclAddonly, AclSym, CachedUpdates, DataSym, DocListCast, AclAdmin, FieldsSym, HeightSym, WidthSym } from "./Doc";
+import { Doc, FieldResult, UpdatingFromServer, LayoutSym, AclPrivate, AclEdit, AclReadonly, AclAddonly, AclSym, CachedUpdates, DataSym, DocListCast, AclAdmin, FieldsSym, HeightSym, WidthSym, fetchProto } from "./Doc";
import { SerializationHelper } from "../client/util/SerializationHelper";
import { ProxyField, PrefetchProxy } from "./Proxy";
import { RefField } from "./RefField";
@@ -74,7 +74,7 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number
const fromServer = target[UpdatingFromServer];
const sameAuthor = fromServer || (receiver.author === Doc.CurrentUserEmail);
const writeToDoc = sameAuthor || effectiveAcl === AclEdit || effectiveAcl === AclAdmin || (writeMode !== DocServer.WriteMode.LiveReadonly);
- const writeToServer = (sameAuthor || effectiveAcl === AclEdit || effectiveAcl === AclAdmin || writeMode === DocServer.WriteMode.Default) && !playgroundMode;
+ const writeToServer = (sameAuthor || effectiveAcl === AclEdit || effectiveAcl === AclAdmin || writeMode === DocServer.WriteMode.Default) && !DocServer.Control.isReadOnly();// && !playgroundMode;
if (writeToDoc) {
if (value === undefined) {
@@ -115,22 +115,34 @@ export function OVERRIDE_ACL(val: boolean) {
_overrideAcl = val;
}
-let playgroundMode = false;
+// 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() {
- playgroundMode = !playgroundMode;
-}
-
-export function getPlaygroundMode() {
- return playgroundMode;
-}
+// export function togglePlaygroundMode() {
+// playgroundMode = !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 +151,22 @@ 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 (target.__fields?.author === Doc.CurrentUserEmail || target.author === Doc.CurrentUserEmail || currentUserGroups.includes("admin")) return AclAdmin;
+ // if the current user is the author of the document / the current user is a member of the admin group
+ // but not if the doc in question is an alias - the current user will be the author of their alias rather than the original author
+ if ((Doc.CurrentUserEmail === (target.__fields?.author || target.author) && !(target.aliasOf || target.__fields?.aliasOf)) || 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 +176,28 @@ 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;
+ // if we're in playground mode, return AclEdit (or AclAdmin if that's the user's effectiveAcl)
+ return DocServer?.Control?.isReadOnly?.() && HierarchyMapping.get(effectiveAcl)! < 3 ? AclEdit : 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)
+ * inheritingFromCollection is not currently being used but could be used if ACL assignment defaults change
+ */
export function distributeAcls(key: string, acl: SharingPermissions, target: Doc, inheritingFromCollection?: boolean) {
const HierarchyMapping = new Map<string, number>([
@@ -183,37 +208,51 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc
["Admin", 4]
]);
+ let changed = false; // determines whether fetchProto should be called or not (i.e. is there a change that should be reflected in target[AclSym])
const dataDoc = target[DataSym];
- if (!inheritingFromCollection || !target[key] || HierarchyMapping.get(StrCast(target[key]))! > HierarchyMapping.get(acl)!) target[key] = acl;
+ // 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;
+ changed = true;
+
+ // maps over the aliases of the document
+ if (target.aliases) {
+ DocListCast(target.aliases).map(alias => {
+ distributeAcls(key, acl, alias);
+ });
+ }
+
+ }
if (dataDoc && (!inheritingFromCollection || !dataDoc[key] || HierarchyMapping.get(StrCast(dataDoc[key]))! > HierarchyMapping.get(acl)!)) {
dataDoc[key] = acl;
+ changed = true;
+ // 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);
}
});
}
+
+ changed && fetchProto(target); // updates target[AclSym] when changes to acls have been made
}
const layoutProps = ["panX", "panY", "width", "height", "nativeWidth", "nativeHeight", "fitWidth", "fitToBox",
@@ -223,6 +262,7 @@ export function setter(target: any, in_prop: string | symbol | number, value: an
const effectiveAcl = GetEffectiveAcl(target, in_prop);
if (effectiveAcl !== AclEdit && effectiveAcl !== AclAdmin) return true;
+ // if you're trying to change an acl but don't have Admin access / you're trying to change it to something that isn't an acceptable acl, you can't
if (typeof prop === "string" && prop.startsWith("ACL") && (effectiveAcl !== AclAdmin || ![...Object.values(SharingPermissions), undefined].includes(value))) return true;
// if (typeof prop === "string" && prop.startsWith("ACL") && !["Can Edit", "Can Add", "Can View", "Not Shared", undefined].includes(value)) return true;