From d7f2f6994ce6f2a450dff67b3595a692be9cb977 Mon Sep 17 00:00:00 2001
From: usodhi <61431818+usodhi@users.noreply.github.com>
Date: Thu, 2 Jul 2020 17:23:31 +0530
Subject: restructuring and simplifying group implementation

---
 src/client/views/nodes/DocumentView.tsx | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

(limited to 'src/client/views/nodes/DocumentView.tsx')

diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 09eeaee36..21b6d8310 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1169,9 +1169,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     }
 
     render() {
-        if (!(this.props.Document instanceof Doc)) return (null);
         if (this.props.Document[AclSym] && this.props.Document[AclSym] === AclPrivate) return (null);
-        if (this.props.Document.hidden) return (null);
+        if (!(this.props.Document instanceof Doc)) return (null);
         const backgroundColor = Doc.UserDoc().renderStyle === "comic" ? undefined : this.props.forcedBackgroundColor?.(this.Document) || StrCast(this.layoutDoc._backgroundColor) || StrCast(this.layoutDoc.backgroundColor) || StrCast(this.Document.backgroundColor) || this.props.backgroundColor?.(this.Document);
         const opacity = Cast(this.layoutDoc._opacity, "number", Cast(this.layoutDoc.opacity, "number", Cast(this.Document.opacity, "number", null)));
         const finalOpacity = this.props.opacity ? this.props.opacity() : opacity;
-- 
cgit v1.2.3-70-g09d2


From f4830de4f8c4794ec98e54be9ba8730e46155c35 Mon Sep 17 00:00:00 2001
From: usodhi <61431818+usodhi@users.noreply.github.com>
Date: Mon, 6 Jul 2020 18:18:17 +0530
Subject: trying first implementation of storing acls

---
 src/client/DocServer.ts                            |  7 ++-
 src/client/util/GroupManager.tsx                   | 60 +++----------------
 src/client/util/SharingManager.tsx                 | 42 +++++++-------
 src/client/views/DocComponent.tsx                  |  7 ++-
 src/client/views/collections/CollectionView.tsx    |  8 ++-
 src/client/views/nodes/DocumentContentsView.tsx    |  6 +-
 src/client/views/nodes/DocumentView.tsx            |  5 +-
 .../views/nodes/formattedText/FormattedTextBox.tsx |  8 ++-
 src/fields/Doc.ts                                  | 67 ++++++++++++++--------
 src/fields/util.ts                                 | 60 +++++++++++++++++--
 10 files changed, 151 insertions(+), 119 deletions(-)

(limited to 'src/client/views/nodes/DocumentView.tsx')

diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts
index eac53bb02..860a8fd92 100644
--- a/src/client/DocServer.ts
+++ b/src/client/DocServer.ts
@@ -39,9 +39,10 @@ export namespace DocServer {
     const docsWithUpdates: { [field: string]: Set<Doc> } = {};
 
     export var PlaygroundFields: string[];
-    export function setPlaygroundFields(livePlayougroundFields: string[]) {
-        DocServer.PlaygroundFields = livePlayougroundFields;
-        livePlayougroundFields.forEach(f => DocServer.setFieldWriteMode(f, DocServer.WriteMode.LivePlayground));
+    export function setPlaygroundFields(livePlaygroundFields: string[]) {
+        console.log("here");
+        DocServer.PlaygroundFields = livePlaygroundFields;
+        livePlaygroundFields.forEach(f => DocServer.setFieldWriteMode(f, DocServer.WriteMode.LivePlayground));
     }
 
     export function setFieldWriteMode(field: string, writeMode: WriteMode) {
diff --git a/src/client/util/GroupManager.tsx b/src/client/util/GroupManager.tsx
index b14dcf55b..83b206f94 100644
--- a/src/client/util/GroupManager.tsx
+++ b/src/client/util/GroupManager.tsx
@@ -33,7 +33,7 @@ export default class GroupManager extends React.Component<{}> {
     @observable private selectedUsers: UserOptions[] | null = null; // list of users selected in the "Select users" dropdown.
     @observable currentGroup: Opt<Doc>; // the currently selected group.
     private inputRef: React.RefObject<HTMLInputElement> = React.createRef(); // the ref for the input box.
-    private currentUserGroups: Doc[] = [];
+    currentUserGroups: string[] = [];
 
     constructor(props: Readonly<{}>) {
         super(props);
@@ -51,7 +51,7 @@ export default class GroupManager extends React.Component<{}> {
         DocListCastAsync(this.GroupManagerDoc?.data).then(groups => {
             groups?.forEach(group => {
                 const members: string[] = JSON.parse(StrCast(group.members));
-                if (members.includes(Doc.CurrentUserEmail)) this.currentUserGroups.push(group);
+                if (members.includes(Doc.CurrentUserEmail)) this.currentUserGroups.push(StrCast(group.groupName));
             });
         })
             .finally(() => console.log(this.currentUserGroups));
@@ -82,11 +82,6 @@ export default class GroupManager extends React.Component<{}> {
         return this.users.map(user => ({ label: user, value: user }));
     }
 
-
-    get groupMemberships() {
-        return this.currentUserGroups;
-    }
-
     /**
      * Makes the GroupManager visible.
      */
@@ -151,6 +146,11 @@ export default class GroupManager extends React.Component<{}> {
         );
     }
 
+    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[];
+    }
+
     /**
      * @returns the members of the admin group.
      */
@@ -279,52 +279,6 @@ export default class GroupManager extends React.Component<{}> {
         this.inputRef.current.value = "";
     }
 
-    /**
-     * A getter that @returns the interface rendered to view an individual group.
-     */
-    // private get editingInterface() {
-    //     const members: string[] = this.currentGroup ? JSON.parse(StrCast(this.currentGroup.members)) : [];
-    //     const options: UserOptions[] = this.currentGroup ? this.options.filter(option => !(JSON.parse(StrCast(this.currentGroup!.members)) as string[]).includes(option.value)) : [];
-    //     return (!this.currentGroup ? null :
-    //         <div className="editing-interface">
-    //             <div className="editing-header">
-    //                 <b>{this.currentGroup.groupName}</b>
-    //                 <div className={"close-button"} onClick={action(() => this.currentGroup = undefined)}>
-    //                     <FontAwesomeIcon icon={fa.faWindowClose} size={"lg"} />
-    //                 </div>
-
-    //                 {this.hasEditAccess(this.currentGroup) ?
-    //                     <div className="group-buttons">
-    //                         <div className="add-member-dropdown">
-    //                             <Select
-    //                                 // isMulti={true}
-    //                                 isSearchable={true}
-    //                                 options={options}
-    //                                 onChange={selectedOption => this.addMemberToGroup(this.currentGroup!, (selectedOption as UserOptions).value)}
-    //                                 placeholder={"Add members"}
-    //                                 value={null}
-    //                                 closeMenuOnSelect={true}
-    //                             />
-    //                         </div>
-    //                         <button onClick={() => this.deleteGroup(this.currentGroup!)}>Delete group</button>
-    //                     </div> :
-    //                     null}
-    //             </div>
-    //             <div className="editing-contents">
-    //                 {members.map(member => (
-    //                     <div className="editing-row">
-    //                         <div className="user-email">
-    //                             {member}
-    //                         </div>
-    //                         {this.hasEditAccess(this.currentGroup!) ? <button onClick={() => this.removeMemberFromGroup(this.currentGroup!, member)}> Remove </button> : null}
-    //                     </div>
-    //                 ))}
-    //             </div>
-    //         </div>
-    //     );
-
-    // }
-
     /**
      * A getter that @returns the main interface for the GroupManager.
      */
diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx
index e74824581..bec6b973b 100644
--- a/src/client/util/SharingManager.tsx
+++ b/src/client/util/SharingManager.tsx
@@ -43,18 +43,18 @@ export enum SharingPermissions {
 //     [SharingPermissions.Edit, "green"]
 // ]);
 
-const HierarchyMapping = new Map<string, string>([
-    [SharingPermissions.None, "0"],
-    [SharingPermissions.View, "1"],
-    [SharingPermissions.Add, "2"],
-    [SharingPermissions.Edit, "3"],
+// export const HierarchyMapping = new Map<string, number>([
+//     [SharingPermissions.None, 0],
+//     [SharingPermissions.View, 1],
+//     [SharingPermissions.Add, 2],
+//     [SharingPermissions.Edit, 3]
 
-    ["0", SharingPermissions.None],
-    ["1", SharingPermissions.View],
-    ["2", SharingPermissions.Add],
-    ["3", SharingPermissions.Edit]
+//     // ["0", SharingPermissions.None],
+//     // ["1", SharingPermissions.View],
+//     // ["2", SharingPermissions.Add],
+//     // ["3", SharingPermissions.Edit]
 
-]);
+// ]);
 
 interface GroupOptions {
     label: string;
@@ -161,14 +161,6 @@ export default class SharingManager extends React.Component<{}> {
         const ACL = `ACL-${StrCast(group.groupName)}`;
 
         target[ACL] = permission;
-        // const sharingDoc = this.sharingDoc!;
-        // if (permission === SharingPermissions.None) {
-        //     const metadata = sharingDoc[StrCast(group.groupName)];
-        //     if (metadata) sharingDoc[StrCast(group.groupName)] = undefined;
-        // }
-        // else {
-        //     sharingDoc[StrCast(group.groupName)] = permission;
-        // }
 
         group.docsShared ? Doc.IndexOf(target, DocListCast(group.docsShared)) === -1 && (group.docsShared as List<Doc>).push(target) : group.docsShared = new List<Doc>([target]);
 
@@ -206,7 +198,7 @@ export default class SharingManager extends React.Component<{}> {
                 const users: ValidatedUser[] = this.users.filter(user => members.includes(user.user.email));
 
                 users.forEach(user => Doc.RemoveDocFromList(user.notificationDoc, storage, doc));
-            })
+            });
         }
     }
 
@@ -224,8 +216,16 @@ export default class SharingManager extends React.Component<{}> {
         target[ACL] = permission;
 
 
-        if (permission !== SharingPermissions.None) Doc.IndexOf(target, DocListCast(notificationDoc[storage])) === -1 && Doc.AddDocToList(notificationDoc, storage, target);
-        else Doc.IndexOf(target, DocListCast(notificationDoc[storage])) !== -1 && Doc.RemoveDocFromList(notificationDoc, storage, target);
+        if (permission !== SharingPermissions.None) {
+            !this.sharedUsers.includes(recipient) && this.sharedUsers.push(recipient);
+
+            Doc.IndexOf(target, DocListCast(notificationDoc[storage])) === -1 && Doc.AddDocToList(notificationDoc, storage, target);
+        }
+        else {
+            const index = this.sharedUsers.findIndex(user => user === recipient);
+            index !== -1 && this.sharedUsers.splice(index, 1);
+            Doc.IndexOf(target, DocListCast(notificationDoc[storage])) !== -1 && Doc.RemoveDocFromList(notificationDoc, storage, target);
+        }
 
     }
 
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index 9b9a28f0f..e8c34d931 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -7,6 +7,7 @@ import { InteractionUtils } from '../util/InteractionUtils';
 import { List } from '../../fields/List';
 import { DateField } from '../../fields/DateField';
 import { ScriptField } from '../../fields/ScriptField';
+import { getEffectiveAcl } from '../../fields/util';
 
 
 ///  DocComponent returns a generic React base class used by views that don't have 'fieldKey' props (e.g.,CollectionFreeFormDocumentView, DocumentView)
@@ -137,10 +138,12 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T
             const targetDataDoc = this.props.Document[DataSym];
             const docList = DocListCast(targetDataDoc[this.annotationKey]);
             const added = docs.filter(d => !docList.includes(d));
+            console.log("here");
+            const effectiveAcl = getEffectiveAcl(this.dataDoc);
             if (added.length) {
-                if (this.dataDoc[AclSym] === AclReadonly) {
+                if (effectiveAcl === AclReadonly) {
                     return false;
-                } else if (this.dataDoc[AclSym] === AclAddonly) {
+                } else if (effectiveAcl === AclAddonly) {
                     added.map(doc => Doc.AddDocToList(targetDataDoc, this.annotationKey, doc));
                 } else {
                     added.map(doc => doc.context = this.props.Document);
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 26abd2529..0dfe9c52a 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 } from '../../../fields/util';
+import { TraceMobx, getEffectiveAcl } 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';
@@ -132,10 +132,12 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus
         const targetDataDoc = this.props.Document[DataSym];
         const docList = DocListCast(targetDataDoc[this.props.fieldKey]);
         const added = docs.filter(d => !docList.includes(d));
+        console.log("here");
+        const effectiveAcl = getEffectiveAcl(this.dataDoc);
         if (added.length) {
-            if (this.dataDoc[AclSym] === AclReadonly) {
+            if (effectiveAcl === AclReadonly) {
                 return false;
-            } else if (this.dataDoc[AclSym] === AclAddonly) {
+            } else if (effectiveAcl === AclAddonly) {
                 added.map(doc => Doc.AddDocToList(targetDataDoc, this.props.fieldKey, doc));
             } else {
                 added.map(doc => {
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index f1438fd54..d480c76d0 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -36,7 +36,7 @@ import { WebBox } from "./WebBox";
 import { InkingStroke } from "../InkingStroke";
 import React = require("react");
 import { RecommendationsBox } from "../RecommendationsBox";
-import { TraceMobx } from "../../../fields/util";
+import { TraceMobx, getEffectiveAcl } from "../../../fields/util";
 import { ScriptField } from "../../../fields/ScriptField";
 import XRegExp = require("xregexp");
 
@@ -184,8 +184,8 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
 
         const bindings = this.CreateBindings(onClick, onInput);
         //  layoutFrame = splits.length > 1 ? splits[0] + splits[1].replace(/{([^{}]|(?R))*}/, replacer4) : ""; // might have been more elegant if javascript supported recursive patterns
-
-        return (this.props.renderDepth > 12 || !layoutFrame || !this.layoutDoc || this.layoutDoc[AclSym] === AclPrivate) ? (null) :
+        console.log("here");
+        return (this.props.renderDepth > 12 || !layoutFrame || !this.layoutDoc || getEffectiveAcl(this.layoutDoc) === AclPrivate) ? (null) :
             <ObserverJsxParser
                 key={42}
                 blacklistedAttrs={[]}
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 297a9f2a5..e277ddc36 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -12,7 +12,7 @@ import { listSpec } from "../../../fields/Schema";
 import { SchemaHeaderField } from '../../../fields/SchemaHeaderField';
 import { ScriptField } from '../../../fields/ScriptField';
 import { BoolCast, Cast, NumCast, StrCast } from "../../../fields/Types";
-import { TraceMobx } from '../../../fields/util';
+import { TraceMobx, getEffectiveAcl } from '../../../fields/util';
 import { GestureUtils } from '../../../pen-gestures/GestureUtils';
 import { emptyFunction, OmitKeys, returnOne, returnTransparent, Utils, emptyPath } from "../../../Utils";
 import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils';
@@ -1188,7 +1188,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     }
 
     render() {
-        if (this.props.Document[AclSym] && this.props.Document[AclSym] === AclPrivate) return (null);
+        console.log("here");
+        if (getEffectiveAcl(this.props.Document) === AclPrivate) return (null);
         if (!(this.props.Document instanceof Doc)) return (null);
         const backgroundColor = Doc.UserDoc().renderStyle === "comic" ? undefined : this.props.forcedBackgroundColor?.(this.Document) || StrCast(this.layoutDoc._backgroundColor) || StrCast(this.layoutDoc.backgroundColor) || StrCast(this.Document.backgroundColor) || this.props.backgroundColor?.(this.Document);
         const opacity = Cast(this.layoutDoc._opacity, "number", Cast(this.layoutDoc.opacity, "number", Cast(this.Document.opacity, "number", null)));
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index fc63dfbf5..a0dbcd980 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -13,7 +13,7 @@ import { EditorState, NodeSelection, Plugin, TextSelection, Transaction } from "
 import { ReplaceStep } from 'prosemirror-transform';
 import { EditorView } from "prosemirror-view";
 import { DateField } from '../../../../fields/DateField';
-import { DataSym, Doc, DocListCast, DocListCastAsync, Field, HeightSym, Opt, WidthSym, AclSym } from "../../../../fields/Doc";
+import { DataSym, Doc, DocListCast, DocListCastAsync, Field, HeightSym, Opt, WidthSym, AclSym, AclEdit } from "../../../../fields/Doc";
 import { documentSchema } from '../../../../fields/documentSchemas';
 import applyDevTools = require("prosemirror-dev-tools");
 import { removeMarkWithAttrs } from "./prosemirrorPatches";
@@ -24,7 +24,7 @@ import { RichTextField } from "../../../../fields/RichTextField";
 import { RichTextUtils } from '../../../../fields/RichTextUtils';
 import { createSchema, makeInterface } from "../../../../fields/Schema";
 import { Cast, DateCast, NumCast, StrCast, ScriptCast } from "../../../../fields/Types";
-import { TraceMobx, OVERRIDE_ACL } from '../../../../fields/util';
+import { TraceMobx, OVERRIDE_ACL, getEffectiveAcl } from '../../../../fields/util';
 import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, numberRange, returnOne, returnZero, Utils, setupMoveUpEvents } from '../../../../Utils';
 import { GoogleApiClientUtils, Pulls, Pushes } from '../../../apis/google_docs/GoogleApiClientUtils';
 import { DocServer } from "../../../DocServer";
@@ -226,7 +226,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
             const curProto = Cast(Cast(this.dataDoc.proto, Doc, null)?.[this.fieldKey], RichTextField, null);              // the default text inherited from a prototype
             const curLayout = this.rootDoc !== this.layoutDoc ? Cast(this.layoutDoc[this.fieldKey], RichTextField, null) : undefined; // the default text stored in a layout template
             const json = JSON.stringify(state.toJSON());
-            if (!this.dataDoc[AclSym]) {
+            // if (!this.dataDoc[AclSym]) { // what?
+            console.log("here");
+            if (getEffectiveAcl(this.dataDoc) === AclEdit) {
                 if (!this._applyingChange && json.replace(/"selection":.*/, "") !== curProto?.Data.replace(/"selection":.*/, "")) {
                     this._applyingChange = true;
                     (curText !== Cast(this.dataDoc[this.fieldKey], RichTextField)?.Text) && (this.dataDoc[this.props.fieldKey + "-lastModified"] = new DateField(new Date(Date.now())));
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index bbc9d388a..c965dc282 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -17,8 +17,9 @@ import { RichTextField } from "./RichTextField";
 import { listSpec } from "./Schema";
 import { ComputedField } from "./ScriptField";
 import { Cast, FieldValue, NumCast, StrCast, ToConstructor } from "./Types";
-import { deleteProperty, getField, getter, makeEditable, makeReadOnly, setter, updateFunction } from "./util";
+import { deleteProperty, getField, getter, makeEditable, makeReadOnly, setter, updateFunction, getEffectiveAcl } from "./util";
 import { LinkManager } from "../client/util/LinkManager";
+import { SharingPermissions } from "../client/util/SharingManager";
 
 export namespace Field {
     export function toKeyValueString(doc: Doc, key: string): string {
@@ -100,23 +101,39 @@ export const AclEdit = Symbol("AclEdit");
 export const UpdatingFromServer = Symbol("UpdatingFromServer");
 const CachedUpdates = Symbol("Cached updates");
 
+const AclMap = new Map<string, symbol>([
+    [SharingPermissions.None, AclPrivate],
+    [SharingPermissions.View, AclReadonly],
+    [SharingPermissions.Add, AclAddonly],
+    [SharingPermissions.Edit, AclEdit]
+]);
 
 export function fetchProto(doc: Doc) {
+    console.log("in fetchproto");
     if (doc.author !== Doc.CurrentUserEmail) { // storing acls for groups needs to be extended here - AclSym should store a datastructure that stores information about permissions
-        const acl = Doc.Get(doc, "ACL", true);
-        switch (acl) {
-            case "ownerOnly":
-                doc[AclSym] = AclPrivate;
-                return undefined;
-            case "readOnly":
-                doc[AclSym] = AclReadonly;
-                break;
-            case "addOnly":
-                doc[AclSym] = AclAddonly;
-                break;
-            case "edit":
-                doc[AclSym] = AclEdit;
-        }
+
+        const permissions: { [key: string]: symbol } = {};
+
+        Object.keys(doc).forEach(key => {
+            if (key.startsWith("ACL")) permissions[key] = AclMap.get(StrCast(doc[key]))!;
+        });
+
+        doc[AclSym] = permissions;
+
+        // const acl = Doc.Get(doc, "ACL", true);
+        // switch (acl) {
+        //     case "ownerOnly":
+        //         doc[AclSym] = AclPrivate;
+        //         return undefined;
+        //     case "readOnly":
+        //         doc[AclSym] = AclReadonly;
+        //         break;
+        //     case "addOnly":
+        //         doc[AclSym] = AclAddonly;
+        //         break;
+        //     // case "edit":
+        //     //     doc[AclSym] = AclEdit;
+        // }
     }
 
     if (doc.proto instanceof Promise) {
@@ -134,10 +151,10 @@ export class Doc extends RefField {
             set: setter,
             get: getter,
             // getPrototypeOf: (target) => Cast(target[SelfProxy].proto, Doc) || null, // TODO this might be able to replace the proto logic in getter
-            has: (target, key) => target[AclSym] !== AclPrivate && key in target.__fields,
+            has: (target, key) => getEffectiveAcl(target) !== AclPrivate && key in target.__fields,
             ownKeys: target => {
                 const obj = {} as any;
-                if (target[AclSym] !== AclPrivate) Object.assign(obj, target.___fields);
+                if (getEffectiveAcl(target) !== AclPrivate) Object.assign(obj, target.___fields);
                 runInAction(() => obj.__LAYOUT__ = target.__LAYOUT__);
                 return Object.keys(obj);
             },
@@ -191,11 +208,11 @@ export class Doc extends RefField {
 
     private [Self] = this;
     private [SelfProxy]: any;
-    public [AclSym]: any = undefined;
+    public [AclSym]: any;
     public [WidthSym] = () => NumCast(this[SelfProxy]._width);
     public [HeightSym] = () => NumCast(this[SelfProxy]._height);
     public [ToScriptString]() { return `DOC-"${this[Self][Id]}"-`; }
-    public [ToString]() { return `Doc(${this[AclSym] === AclPrivate ? "-inaccessible-" : this.title})`; }
+    public [ToString]() { return `Doc(${getEffectiveAcl(this) === AclPrivate ? "-inaccessible-" : this.title})`; }
     public get [LayoutSym]() { return this[SelfProxy].__LAYOUT__; }
     public get [DataSym]() {
         const self = this[SelfProxy];
@@ -823,7 +840,8 @@ export namespace Doc {
     }
     // don't bother memoizing (caching) the result if called from a non-reactive context. (plus this avoids a warning message)
     export function IsBrushedDegreeUnmemoized(doc: Doc) {
-        if (!doc || doc[AclSym] === AclPrivate || Doc.GetProto(doc)[AclSym] === AclPrivate) return 0;
+        console.log("here");
+        if (!doc || getEffectiveAcl(doc) === AclPrivate || getEffectiveAcl(Doc.GetProto(doc)) === AclPrivate) return 0;
         return brushManager.BrushedDoc.has(doc) ? 2 : brushManager.BrushedDoc.has(Doc.GetProto(doc)) ? 1 : 0;
     }
     export function IsBrushedDegree(doc: Doc) {
@@ -832,15 +850,15 @@ export namespace Doc {
         })(doc);
     }
     export function BrushDoc(doc: Doc) {
-
-        if (!doc || doc[AclSym] === AclPrivate || Doc.GetProto(doc)[AclSym] === AclPrivate) return doc;
+        console.log("here");
+        if (!doc || getEffectiveAcl(doc) === AclPrivate || getEffectiveAcl(Doc.GetProto(doc)) === AclPrivate) return doc;
         brushManager.BrushedDoc.set(doc, true);
         brushManager.BrushedDoc.set(Doc.GetProto(doc), true);
         return doc;
     }
     export function UnBrushDoc(doc: Doc) {
 
-        if (!doc || doc[AclSym] === AclPrivate || Doc.GetProto(doc)[AclSym] === AclPrivate) return doc;
+        if (!doc || getEffectiveAcl(doc) === AclPrivate || getEffectiveAcl(Doc.GetProto(doc)) === AclPrivate) return doc;
         brushManager.BrushedDoc.delete(doc);
         brushManager.BrushedDoc.delete(Doc.GetProto(doc));
         return doc;
@@ -870,7 +888,8 @@ export namespace Doc {
     }
     const highlightManager = new HighlightBrush();
     export function IsHighlighted(doc: Doc) {
-        if (!doc || doc[AclSym] === AclPrivate || Doc.GetProto(doc)[AclSym] === AclPrivate) return false;
+        console.log("here");
+        if (!doc || getEffectiveAcl(doc) === AclPrivate || getEffectiveAcl(Doc.GetProto(doc)) === AclPrivate) return false;
         return highlightManager.HighlightedDoc.get(doc) || highlightManager.HighlightedDoc.get(Doc.GetProto(doc));
     }
     export function HighlightDoc(doc: Doc, dataAndDisplayDocs = true) {
diff --git a/src/fields/util.ts b/src/fields/util.ts
index 7bb090a93..a3e7a36f8 100644
--- a/src/fields/util.ts
+++ b/src/fields/util.ts
@@ -1,5 +1,5 @@
 import { UndoManager } from "../client/util/UndoManager";
-import { Doc, Field, FieldResult, UpdatingFromServer, LayoutSym, AclSym, AclPrivate } from "./Doc";
+import { Doc, Field, FieldResult, UpdatingFromServer, LayoutSym, AclSym, AclPrivate, AclEdit, AclReadonly, AclAddonly } from "./Doc";
 import { SerializationHelper } from "../client/util/SerializationHelper";
 import { ProxyField, PrefetchProxy } from "./Proxy";
 import { RefField } from "./RefField";
@@ -9,6 +9,7 @@ import { Parent, OnUpdate, Update, Id, SelfProxy, Self } from "./FieldSymbols";
 import { DocServer } from "../client/DocServer";
 import { ComputedField } from "./ScriptField";
 import { ScriptCast } from "./Types";
+import GroupManager from "../client/util/GroupManager";
 
 
 function _readOnlySetter(): never {
@@ -107,11 +108,57 @@ export function OVERRIDE_ACL(val: boolean) {
     _overrideAcl = val;
 }
 
+const HierarchyMapping = new Map<symbol, number>([
+    [AclPrivate, 0],
+    [AclReadonly, 1],
+    [AclAddonly, 2],
+    [AclEdit, 3]
+]);
+
+export function getEffectiveAcl(target: any): symbol {
+
+    console.log("in getEffectiveAcl");
+
+    if (target[AclSym].ACL) return target[AclSym].ACL;
+
+    let effectiveAcl = AclEdit;
+
+    for (const [key, value] of Object.entries(target[AclSym])) {
+        if (key.startsWith("ACL-")) {
+            if (GroupManager.Instance.currentUserGroups.includes(key.substring(4)) || Doc.CurrentUserEmail === key.substring(4).replace("_", ".")) {
+                if (HierarchyMapping.get(value as symbol)! > HierarchyMapping.get(effectiveAcl)!) {
+                    effectiveAcl = value as symbol;
+                    if (effectiveAcl === AclEdit) break;
+                }
+            }
+        }
+    }
+
+    return effectiveAcl;
+}
+
+function testPermission(target: any, in_prop: string | symbol | number): boolean {
+
+    console.log("here");
+    // if (target[AclSym].ACL !== AclEdit && !_overrideAcl && !DocServer.PlaygroundFields.includes(in_prop.toString())) return false;
+    if (target[AclSym].ACL === AclEdit) return true;
+    for (const [key, value] of Object.entries(target[AclSym])) {
+        if (key.startsWith("ACL-")) {
+            if (GroupManager.Instance.currentUserGroups.includes(key.substring(4))) {
+                if (value === AclEdit) return true;
+            }
+        }
+    }
+    return _overrideAcl || DocServer.PlaygroundFields.includes(in_prop.toString());
+}
+
 const layoutProps = ["panX", "panY", "width", "height", "nativeWidth", "nativeHeight", "fitWidth", "fitToBox",
     "chromeStatus", "viewType", "gridGap", "xMargin", "yMargin", "autoHeight"];
 export function setter(target: any, in_prop: string | symbol | number, value: any, receiver: any): boolean {
+    console.log("in setter")
     let prop = in_prop;
-    if (target[AclSym] && !_overrideAcl && !DocServer.PlaygroundFields.includes(in_prop.toString())) return true; // generalise to a testpermission function
+    // if (target[AclSym] && !_overrideAcl && !DocServer.PlaygroundFields.includes(in_prop.toString())) return true; // generalise to a testpermission function
+    if (!testPermission(target, in_prop)) return true;
     if (typeof prop === "string" && prop !== "__id" && prop !== "__fields" && (prop.startsWith("_") || layoutProps.includes(prop))) {
         if (!prop.startsWith("_")) {
             console.log(prop + " is deprecated - switch to _" + prop);
@@ -129,9 +176,11 @@ export function setter(target: any, in_prop: string | symbol | number, value: an
 }
 
 export function getter(target: any, in_prop: string | symbol | number, receiver: any): any {
+    console.log("in getter")
     let prop = in_prop;
-    if (in_prop === AclSym) return _overrideAcl ? undefined : target[AclSym];
-    if (target[AclSym] === AclPrivate && !_overrideAcl) return undefined;
+    const effectiveAcl = getEffectiveAcl(target);
+    if (in_prop === AclSym) return _overrideAcl ? undefined : effectiveAcl;
+    if (effectiveAcl === AclPrivate && !_overrideAcl) return undefined;
     if (prop === LayoutSym) {
         return target.__LAYOUT__;
     }
@@ -168,7 +217,8 @@ function getFieldImpl(target: any, prop: string | number, receiver: any, ignoreP
     }
     if (field === undefined && !ignoreProto && prop !== "proto") {
         const proto = getFieldImpl(target, "proto", receiver, true);//TODO tfs: instead of receiver we could use target[SelfProxy]... I don't which semantics we want or if it really matters
-        if (proto instanceof Doc && proto[AclSym] !== AclPrivate) {
+        console.log("here");
+        if (proto instanceof Doc && getEffectiveAcl(proto) !== AclPrivate) {
             return getFieldImpl(proto[Self], prop, receiver, ignoreProto);
         }
         return undefined;
-- 
cgit v1.2.3-70-g09d2


From 12285b8c0aff514a2345874508c35b6de1026ef4 Mon Sep 17 00:00:00 2001
From: usodhi <61431818+usodhi@users.noreply.github.com>
Date: Thu, 9 Jul 2020 12:25:46 +0530
Subject: acls now work I think + some cleanup

---
 src/client/DocServer.ts                            |  1 -
 src/client/util/GroupManager.tsx                   | 13 +++-
 src/client/util/GroupMemberView.tsx                |  2 +-
 src/client/util/SharingManager.tsx                 |  2 -
 src/client/views/DocComponent.tsx                  |  1 -
 src/client/views/GlobalKeyHandler.ts               |  2 +
 src/client/views/collections/CollectionView.tsx    | 21 +++---
 src/client/views/nodes/DocumentContentsView.tsx    |  1 -
 src/client/views/nodes/DocumentView.tsx            | 22 +++---
 .../views/nodes/formattedText/FormattedTextBox.tsx |  1 -
 src/fields/Doc.ts                                  | 24 +------
 src/fields/util.ts                                 | 84 +++++++++++-----------
 12 files changed, 84 insertions(+), 90 deletions(-)

(limited to 'src/client/views/nodes/DocumentView.tsx')

diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts
index 860a8fd92..bac324c77 100644
--- a/src/client/DocServer.ts
+++ b/src/client/DocServer.ts
@@ -40,7 +40,6 @@ export namespace DocServer {
 
     export var PlaygroundFields: string[];
     export function setPlaygroundFields(livePlaygroundFields: string[]) {
-        console.log("here");
         DocServer.PlaygroundFields = livePlaygroundFields;
         livePlaygroundFields.forEach(f => DocServer.setFieldWriteMode(f, DocServer.WriteMode.LivePlayground));
     }
diff --git a/src/client/util/GroupManager.tsx b/src/client/util/GroupManager.tsx
index 83b206f94..23bdd248b 100644
--- a/src/client/util/GroupManager.tsx
+++ b/src/client/util/GroupManager.tsx
@@ -14,6 +14,7 @@ import Select from 'react-select';
 import "./GroupManager.scss";
 import { StrCast } from "../../fields/Types";
 import GroupMemberView from "./GroupMemberView";
+import { setGroups } from "../../fields/util";
 
 library.add(fa.faWindowClose);
 
@@ -54,7 +55,7 @@ export default class GroupManager extends React.Component<{}> {
                 if (members.includes(Doc.CurrentUserEmail)) this.currentUserGroups.push(StrCast(group.groupName));
             });
         })
-            .finally(() => console.log(this.currentUserGroups));
+            .finally(() => setGroups(this.currentUserGroups));
 
         // (this.GroupManagerDoc?.data as List<Doc>).forEach(group => {
         //     Promise.resolve(group).then(resolvedGroup => {
@@ -178,6 +179,10 @@ export default class GroupManager extends React.Component<{}> {
         groupDoc.groupName = groupName;
         groupDoc.owners = JSON.stringify([Doc.CurrentUserEmail]);
         groupDoc.members = JSON.stringify(memberEmails);
+        if (memberEmails.includes(Doc.CurrentUserEmail)) {
+            this.currentUserGroups.push(groupName);
+            setGroups(this.currentUserGroups);
+        }
         this.addGroup(groupDoc);
     }
 
@@ -204,6 +209,12 @@ export default class GroupManager extends React.Component<{}> {
                 // SharingManager.Instance.setInternalGroupSharing(group, "Not Shared");
                 Doc.RemoveDocFromList(this.GroupManagerDoc, "data", group);
                 SharingManager.Instance.removeGroup(group);
+                const members: string[] = JSON.parse(StrCast(group.members));
+                if (members.includes(Doc.CurrentUserEmail)) {
+                    const index = this.currentUserGroups.findIndex(groupName => groupName === group.groupName);
+                    index !== -1 && this.currentUserGroups.splice(index, 1);
+                    setGroups(this.currentUserGroups);
+                }
                 if (group === this.currentGroup) {
                     runInAction(() => this.currentGroup = undefined);
                 }
diff --git a/src/client/util/GroupMemberView.tsx b/src/client/util/GroupMemberView.tsx
index cc279b6b2..742caa676 100644
--- a/src/client/util/GroupMemberView.tsx
+++ b/src/client/util/GroupMemberView.tsx
@@ -51,7 +51,7 @@ export default class GroupMemberView extends React.Component<GroupMemberViewProp
                                     }}
                                 />
                             </div>
-                            <button onClick={() => console.log(GroupManager.Instance.deleteGroup(this.props.group))}>Delete group</button>
+                            <button onClick={() => GroupManager.Instance.deleteGroup(this.props.group)}>Delete group</button>
                         </div> :
                         null}
                 </div>
diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx
index bec6b973b..d64302456 100644
--- a/src/client/util/SharingManager.tsx
+++ b/src/client/util/SharingManager.tsx
@@ -359,8 +359,6 @@ export default class SharingManager extends React.Component<{}> {
     share = () => {
         this.selectedUsers?.forEach(user => {
             if (user.value.includes(indType)) {
-                console.log(user);
-                console.log(this.users.find(u => u.user.email === user.label));
                 this.setInternalSharing(this.users.find(u => u.user.email === user.label)!, this.permissions);
             }
             else {
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index e8c34d931..781673e59 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -138,7 +138,6 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T
             const targetDataDoc = this.props.Document[DataSym];
             const docList = DocListCast(targetDataDoc[this.annotationKey]);
             const added = docs.filter(d => !docList.includes(d));
-            console.log("here");
             const effectiveAcl = getEffectiveAcl(this.dataDoc);
             if (added.length) {
                 if (effectiveAcl === AclReadonly) {
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index a3a023164..45d53a5f5 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -22,6 +22,7 @@ import { DocumentView } from "./nodes/DocumentView";
 import { DocumentLinksButton } from "./nodes/DocumentLinksButton";
 import PDFMenu from "./pdf/PDFMenu";
 import { ContextMenu } from "./ContextMenu";
+import GroupManager from "../util/GroupManager";
 
 const modifiers = ["control", "meta", "shift", "alt"];
 type KeyHandler = (keycode: string, e: KeyboardEvent) => KeyControlInfo | Promise<KeyControlInfo>;
@@ -107,6 +108,7 @@ export default class KeyManager {
                 GoogleAuthenticationManager.Instance.cancel();
                 HypothesisAuthenticationManager.Instance.cancel();
                 SharingManager.Instance.close();
+                GroupManager.Instance.close();
                 break;
             case "delete":
             case "backspace":
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 31f0c1df3..6a6a475c8 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -8,7 +8,7 @@ import * as React from 'react';
 import Lightbox from 'react-image-lightbox-with-rotate';
 import 'react-image-lightbox-with-rotate/style.css'; // This only needs to be imported once in your app
 import { DateField } from '../../../fields/DateField';
-import { AclAddonly, AclReadonly, AclSym, DataSym, Doc, DocListCast, Field, Opt } from '../../../fields/Doc';
+import { AclAddonly, AclReadonly, AclSym, DataSym, Doc, DocListCast, Field, Opt, AclEdit } from '../../../fields/Doc';
 import { Id } from '../../../fields/FieldSymbols';
 import { List } from '../../../fields/List';
 import { ObjectField } from '../../../fields/ObjectField';
@@ -132,8 +132,7 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus
         const targetDataDoc = this.props.Document[DataSym];
         const docList = DocListCast(targetDataDoc[this.props.fieldKey]);
         const added = docs.filter(d => !docList.includes(d));
-        console.log("here");
-        const effectiveAcl = getEffectiveAcl(this.dataDoc);
+        const effectiveAcl = getEffectiveAcl(this.props.Document);
         if (added.length) {
             if (effectiveAcl === AclReadonly) {
                 return false;
@@ -167,13 +166,15 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus
 
     @action.bound
     removeDocument = (doc: any): boolean => {
-        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);
-            return true;
+        if (getEffectiveAcl(this.props.Document) === 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);
+                return true;
+            }
         }
         return false;
     }
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index d480c76d0..e34ceb994 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -184,7 +184,6 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
 
         const bindings = this.CreateBindings(onClick, onInput);
         //  layoutFrame = splits.length > 1 ? splits[0] + splits[1].replace(/{([^{}]|(?R))*}/, replacer4) : ""; // might have been more elegant if javascript supported recursive patterns
-        console.log("here");
         return (this.props.renderDepth > 12 || !layoutFrame || !this.layoutDoc || getEffectiveAcl(this.layoutDoc) === AclPrivate) ? (null) :
             <ObserverJsxParser
                 key={42}
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 1d76633be..b9ae8b444 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -26,7 +26,7 @@ import { InteractionUtils } from '../../util/InteractionUtils';
 import { Scripting } from '../../util/Scripting';
 import { SearchUtil } from '../../util/SearchUtil';
 import { SelectionManager } from "../../util/SelectionManager";
-import SharingManager from '../../util/SharingManager';
+import SharingManager, { SharingPermissions } from '../../util/SharingManager';
 import { Transform } from "../../util/Transform";
 import { undoBatch, UndoManager } from "../../util/UndoManager";
 import { CollectionView, CollectionViewType } from '../collections/CollectionView';
@@ -725,7 +725,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
 
     @undoBatch
     @action
-    setAcl = (acl: "readOnly" | "addOnly" | "ownerOnly" | "write") => {
+    setAcl = (acl: SharingPermissions) => {
         this.dataDoc.ACL = this.props.Document.ACL = acl;
         DocListCast(this.dataDoc[Doc.LayoutFieldKey(this.dataDoc)]).map(d => {
             if (d.author === Doc.CurrentUserEmail) d.ACL = acl;
@@ -735,7 +735,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     }
     @undoBatch
     @action
-    testAcl = (acl: "readOnly" | "addOnly" | "ownerOnly" | "write") => {
+    testAcl = (acl: SharingPermissions) => {
         this.dataDoc.author = this.props.Document.author = "ADMIN";
         this.dataDoc.ACL = this.props.Document.ACL = acl;
         DocListCast(this.dataDoc[Doc.LayoutFieldKey(this.dataDoc)]).map(d => {
@@ -845,12 +845,12 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
 
         const existingAcls = cm.findByDescription("Privacy...");
         const aclItems: ContextMenuProps[] = existingAcls && "subitems" in existingAcls ? existingAcls.subitems : [];
-        aclItems.push({ description: "Make Add Only", event: () => this.setAcl("addOnly"), icon: "concierge-bell" });
-        aclItems.push({ description: "Make Read Only", event: () => this.setAcl("readOnly"), icon: "concierge-bell" });
-        aclItems.push({ description: "Make Private", event: () => this.setAcl("ownerOnly"), icon: "concierge-bell" });
-        aclItems.push({ description: "Make Editable", event: () => this.setAcl("write"), icon: "concierge-bell" });
-        aclItems.push({ description: "Test Private", event: () => this.testAcl("ownerOnly"), icon: "concierge-bell" });
-        aclItems.push({ description: "Test Readonly", event: () => this.testAcl("readOnly"), icon: "concierge-bell" });
+        aclItems.push({ description: "Make Add Only", event: () => this.setAcl(SharingPermissions.Add), icon: "concierge-bell" });
+        aclItems.push({ description: "Make Read Only", event: () => this.setAcl(SharingPermissions.View), icon: "concierge-bell" });
+        aclItems.push({ description: "Make Private", event: () => this.setAcl(SharingPermissions.None), icon: "concierge-bell" });
+        aclItems.push({ description: "Make Editable", event: () => this.setAcl(SharingPermissions.Edit), icon: "concierge-bell" });
+        aclItems.push({ description: "Test Private", event: () => this.testAcl(SharingPermissions.None), icon: "concierge-bell" });
+        aclItems.push({ description: "Test Readonly", event: () => this.testAcl(SharingPermissions.View), icon: "concierge-bell" });
         !existingAcls && cm.addItem({ description: "Privacy...", subitems: aclItems, icon: "question" });
 
         // const recommender_subitems: ContextMenuProps[] = [];
@@ -1206,9 +1206,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     }
 
     render() {
-        console.log("here");
-        if (getEffectiveAcl(this.props.Document) === AclPrivate) return (null);
         if (!(this.props.Document instanceof Doc)) return (null);
+        if (getEffectiveAcl(this.props.Document) === AclPrivate) return (null);
+        if (this.props.Document.hidden) return (null);
         const backgroundColor = Doc.UserDoc().renderStyle === "comic" ? undefined : this.props.forcedBackgroundColor?.(this.Document) || StrCast(this.layoutDoc._backgroundColor) || StrCast(this.layoutDoc.backgroundColor) || StrCast(this.Document.backgroundColor) || this.props.backgroundColor?.(this.Document);
         const opacity = Cast(this.layoutDoc._opacity, "number", Cast(this.layoutDoc.opacity, "number", Cast(this.Document.opacity, "number", null)));
         const finalOpacity = this.props.opacity ? this.props.opacity() : opacity;
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index a0dbcd980..ccf83cbf9 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -227,7 +227,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
             const curLayout = this.rootDoc !== this.layoutDoc ? Cast(this.layoutDoc[this.fieldKey], RichTextField, null) : undefined; // the default text stored in a layout template
             const json = JSON.stringify(state.toJSON());
             // if (!this.dataDoc[AclSym]) { // what?
-            console.log("here");
             if (getEffectiveAcl(this.dataDoc) === AclEdit) {
                 if (!this._applyingChange && json.replace(/"selection":.*/, "") !== curProto?.Data.replace(/"selection":.*/, "")) {
                     this._applyingChange = true;
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index c965dc282..27eabf451 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -109,7 +109,6 @@ const AclMap = new Map<string, symbol>([
 ]);
 
 export function fetchProto(doc: Doc) {
-    console.log("in fetchproto");
     if (doc.author !== Doc.CurrentUserEmail) { // storing acls for groups needs to be extended here - AclSym should store a datastructure that stores information about permissions
 
         const permissions: { [key: string]: symbol } = {};
@@ -118,22 +117,8 @@ export function fetchProto(doc: Doc) {
             if (key.startsWith("ACL")) permissions[key] = AclMap.get(StrCast(doc[key]))!;
         });
 
-        doc[AclSym] = permissions;
 
-        // const acl = Doc.Get(doc, "ACL", true);
-        // switch (acl) {
-        //     case "ownerOnly":
-        //         doc[AclSym] = AclPrivate;
-        //         return undefined;
-        //     case "readOnly":
-        //         doc[AclSym] = AclReadonly;
-        //         break;
-        //     case "addOnly":
-        //         doc[AclSym] = AclAddonly;
-        //         break;
-        //     // case "edit":
-        //     //     doc[AclSym] = AclEdit;
-        // }
+        if (Object.keys(permissions).length) doc[AclSym] = permissions;
     }
 
     if (doc.proto instanceof Promise) {
@@ -208,7 +193,7 @@ export class Doc extends RefField {
 
     private [Self] = this;
     private [SelfProxy]: any;
-    public [AclSym]: any;
+    public [AclSym]: { [key: string]: symbol };
     public [WidthSym] = () => NumCast(this[SelfProxy]._width);
     public [HeightSym] = () => NumCast(this[SelfProxy]._height);
     public [ToScriptString]() { return `DOC-"${this[Self][Id]}"-`; }
@@ -232,8 +217,8 @@ export class Doc extends RefField {
             return Cast(this[SelfProxy][renderFieldKey + "-layout[" + templateLayoutDoc[Id] + "]"], Doc, null) || templateLayoutDoc;
         }
         return undefined;
-    }
 
+    }
 
     private [CachedUpdates]: { [key: string]: () => void | Promise<any> } = {};
     public static CurrentUserEmail: string = "";
@@ -840,7 +825,6 @@ export namespace Doc {
     }
     // don't bother memoizing (caching) the result if called from a non-reactive context. (plus this avoids a warning message)
     export function IsBrushedDegreeUnmemoized(doc: Doc) {
-        console.log("here");
         if (!doc || getEffectiveAcl(doc) === AclPrivate || getEffectiveAcl(Doc.GetProto(doc)) === AclPrivate) return 0;
         return brushManager.BrushedDoc.has(doc) ? 2 : brushManager.BrushedDoc.has(Doc.GetProto(doc)) ? 1 : 0;
     }
@@ -850,7 +834,6 @@ export namespace Doc {
         })(doc);
     }
     export function BrushDoc(doc: Doc) {
-        console.log("here");
         if (!doc || getEffectiveAcl(doc) === AclPrivate || getEffectiveAcl(Doc.GetProto(doc)) === AclPrivate) return doc;
         brushManager.BrushedDoc.set(doc, true);
         brushManager.BrushedDoc.set(Doc.GetProto(doc), true);
@@ -888,7 +871,6 @@ export namespace Doc {
     }
     const highlightManager = new HighlightBrush();
     export function IsHighlighted(doc: Doc) {
-        console.log("here");
         if (!doc || getEffectiveAcl(doc) === AclPrivate || getEffectiveAcl(Doc.GetProto(doc)) === AclPrivate) return false;
         return highlightManager.HighlightedDoc.get(doc) || highlightManager.HighlightedDoc.get(Doc.GetProto(doc));
     }
diff --git a/src/fields/util.ts b/src/fields/util.ts
index a3e7a36f8..a1af1d3c5 100644
--- a/src/fields/util.ts
+++ b/src/fields/util.ts
@@ -1,5 +1,5 @@
 import { UndoManager } from "../client/util/UndoManager";
-import { Doc, Field, FieldResult, UpdatingFromServer, LayoutSym, AclSym, AclPrivate, AclEdit, AclReadonly, AclAddonly } from "./Doc";
+import { Doc, FieldResult, UpdatingFromServer, LayoutSym, AclPrivate, AclEdit, AclReadonly, AclAddonly, AclSym, fetchProto } from "./Doc";
 import { SerializationHelper } from "../client/util/SerializationHelper";
 import { ProxyField, PrefetchProxy } from "./Proxy";
 import { RefField } from "./RefField";
@@ -9,7 +9,6 @@ import { Parent, OnUpdate, Update, Id, SelfProxy, Self } from "./FieldSymbols";
 import { DocServer } from "../client/DocServer";
 import { ComputedField } from "./ScriptField";
 import { ScriptCast } from "./Types";
-import GroupManager from "../client/util/GroupManager";
 
 
 function _readOnlySetter(): never {
@@ -108,57 +107,65 @@ export function OVERRIDE_ACL(val: boolean) {
     _overrideAcl = val;
 }
 
-const HierarchyMapping = new Map<symbol, number>([
-    [AclPrivate, 0],
-    [AclReadonly, 1],
-    [AclAddonly, 2],
-    [AclEdit, 3]
-]);
+let currentUserGroups: string[] = [];
+let currentUserEmail: string;// = Doc.CurrentUserEmail;
 
-export function getEffectiveAcl(target: any): symbol {
+export function setGroups(groups: string[]) {
+    currentUserGroups = groups;
+    currentUserEmail = Doc.CurrentUserEmail;
+}
 
-    console.log("in getEffectiveAcl");
 
-    if (target[AclSym].ACL) return target[AclSym].ACL;
+export function getEffectiveAcl(target: any, in_prop?: string | symbol | number): symbol {
 
-    let effectiveAcl = AclEdit;
+    const HierarchyMapping = new Map<symbol, number>([
+        [AclPrivate, 0],
+        [AclReadonly, 1],
+        [AclAddonly, 2],
+        [AclEdit, 3]
+    ]);
 
-    for (const [key, value] of Object.entries(target[AclSym])) {
-        if (key.startsWith("ACL-")) {
-            if (GroupManager.Instance.currentUserGroups.includes(key.substring(4)) || Doc.CurrentUserEmail === key.substring(4).replace("_", ".")) {
-                if (HierarchyMapping.get(value as symbol)! > HierarchyMapping.get(effectiveAcl)!) {
-                    effectiveAcl = value as symbol;
-                    if (effectiveAcl === AclEdit) break;
-                }
-            }
-        }
+    if (!target[AclSym] && target instanceof Doc) {
+        fetchProto(target);
     }
 
-    return effectiveAcl;
-}
+    if (target[AclSym] && Object.keys(target[AclSym]).length) {
+
+        if (target.author === currentUserEmail) return AclEdit;
+
+        if (_overrideAcl || (in_prop && DocServer.PlaygroundFields?.includes(in_prop.toString()))) return AclEdit;
 
-function testPermission(target: any, in_prop: string | symbol | number): boolean {
+        if (target[AclSym].ACL) return target[AclSym].ACL;
 
-    console.log("here");
-    // if (target[AclSym].ACL !== AclEdit && !_overrideAcl && !DocServer.PlaygroundFields.includes(in_prop.toString())) return false;
-    if (target[AclSym].ACL === AclEdit) return true;
-    for (const [key, value] of Object.entries(target[AclSym])) {
-        if (key.startsWith("ACL-")) {
-            if (GroupManager.Instance.currentUserGroups.includes(key.substring(4))) {
-                if (value === AclEdit) return true;
+        let effectiveAcl = AclPrivate;
+        let aclPresent = false;
+
+        for (const [key, value] of Object.entries(target[AclSym])) {
+            if (key.startsWith("ACL-")) {
+                if (currentUserGroups.includes(key.substring(4)) || currentUserEmail === key.substring(4).replace("_", ".")) {
+                    if (HierarchyMapping.get(value as symbol)! >= HierarchyMapping.get(effectiveAcl)!) {
+                        aclPresent = true;
+                        effectiveAcl = value as symbol;
+                        if (effectiveAcl === AclEdit) break;
+                    }
+                }
             }
         }
+        return aclPresent ? effectiveAcl : AclEdit;
+    }
+    else {
+        return AclEdit;
     }
-    return _overrideAcl || DocServer.PlaygroundFields.includes(in_prop.toString());
 }
 
+
 const layoutProps = ["panX", "panY", "width", "height", "nativeWidth", "nativeHeight", "fitWidth", "fitToBox",
     "chromeStatus", "viewType", "gridGap", "xMargin", "yMargin", "autoHeight"];
 export function setter(target: any, in_prop: string | symbol | number, value: any, receiver: any): boolean {
-    console.log("in setter")
     let prop = in_prop;
-    // if (target[AclSym] && !_overrideAcl && !DocServer.PlaygroundFields.includes(in_prop.toString())) return true; // generalise to a testpermission function
-    if (!testPermission(target, in_prop)) return true;
+    if (getEffectiveAcl(target, in_prop) !== AclEdit) {
+        return true;
+    }
     if (typeof prop === "string" && prop !== "__id" && prop !== "__fields" && (prop.startsWith("_") || layoutProps.includes(prop))) {
         if (!prop.startsWith("_")) {
             console.log(prop + " is deprecated - switch to _" + prop);
@@ -176,11 +183,9 @@ export function setter(target: any, in_prop: string | symbol | number, value: an
 }
 
 export function getter(target: any, in_prop: string | symbol | number, receiver: any): any {
-    console.log("in getter")
     let prop = in_prop;
-    const effectiveAcl = getEffectiveAcl(target);
-    if (in_prop === AclSym) return _overrideAcl ? undefined : effectiveAcl;
-    if (effectiveAcl === AclPrivate && !_overrideAcl) return undefined;
+    if (in_prop === AclSym) return _overrideAcl ? undefined : target[AclSym];
+    if (getEffectiveAcl(target) === AclPrivate && !_overrideAcl) return undefined;
     if (prop === LayoutSym) {
         return target.__LAYOUT__;
     }
@@ -217,7 +222,6 @@ function getFieldImpl(target: any, prop: string | number, receiver: any, ignoreP
     }
     if (field === undefined && !ignoreProto && prop !== "proto") {
         const proto = getFieldImpl(target, "proto", receiver, true);//TODO tfs: instead of receiver we could use target[SelfProxy]... I don't which semantics we want or if it really matters
-        console.log("here");
         if (proto instanceof Doc && getEffectiveAcl(proto) !== AclPrivate) {
             return getFieldImpl(proto[Self], prop, receiver, ignoreProto);
         }
-- 
cgit v1.2.3-70-g09d2


From d70c9004215aea00514030be4137ccc2247b541a Mon Sep 17 00:00:00 2001
From: usodhi <61431818+usodhi@users.noreply.github.com>
Date: Thu, 9 Jul 2020 12:42:20 +0530
Subject: removed some unnecessary imports

---
 src/client/util/SharingManager.tsx                        | 2 +-
 src/client/views/DocComponent.tsx                         | 2 +-
 src/client/views/collections/CollectionView.tsx           | 2 +-
 src/client/views/nodes/DocumentContentsView.tsx           | 2 +-
 src/client/views/nodes/DocumentView.tsx                   | 2 +-
 src/client/views/nodes/formattedText/FormattedTextBox.tsx | 3 +--
 6 files changed, 6 insertions(+), 7 deletions(-)

(limited to 'src/client/views/nodes/DocumentView.tsx')

diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx
index d64302456..fd3b2dd04 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, DocCastAsync, DocListCast } from "../../fields/Doc";
+import { Doc, Opt, DocListCast } from "../../fields/Doc";
 import { DocServer } from "../DocServer";
 import { Cast, StrCast } from "../../fields/Types";
 import * as RequestPromise from "request-promise";
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index 781673e59..43ffe225f 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -1,4 +1,4 @@
-import { Doc, Opt, DataSym, DocListCast, AclSym, AclReadonly, AclAddonly } from '../../fields/Doc';
+import { Doc, Opt, DataSym, DocListCast, AclReadonly, AclAddonly } from '../../fields/Doc';
 import { Touchable } from './Touchable';
 import { computed, action, observable } from 'mobx';
 import { Cast, BoolCast, ScriptCast } from '../../fields/Types';
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 6a6a475c8..032012b2d 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -8,7 +8,7 @@ import * as React from 'react';
 import Lightbox from 'react-image-lightbox-with-rotate';
 import 'react-image-lightbox-with-rotate/style.css'; // This only needs to be imported once in your app
 import { DateField } from '../../../fields/DateField';
-import { AclAddonly, AclReadonly, AclSym, DataSym, Doc, DocListCast, Field, Opt, AclEdit } from '../../../fields/Doc';
+import { AclAddonly, AclReadonly, DataSym, Doc, DocListCast, Field, Opt, AclEdit } from '../../../fields/Doc';
 import { Id } from '../../../fields/FieldSymbols';
 import { List } from '../../../fields/List';
 import { ObjectField } from '../../../fields/ObjectField';
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index e34ceb994..1e6966c05 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -1,6 +1,6 @@
 import { computed } from "mobx";
 import { observer } from "mobx-react";
-import { Doc, Opt, Field, AclSym, AclPrivate } from "../../../fields/Doc";
+import { Doc, Opt, Field, AclPrivate } from "../../../fields/Doc";
 import { Cast, StrCast, NumCast } from "../../../fields/Types";
 import { OmitKeys, Without, emptyPath } from "../../../Utils";
 import { DirectoryImportBox } from "../../util/Import & Export/DirectoryImportBox";
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index b9ae8b444..9dc30c683 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -4,7 +4,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { action, computed, observable, runInAction } from "mobx";
 import { observer } from "mobx-react";
 import * as rp from "request-promise";
-import { Doc, DocListCast, HeightSym, Opt, WidthSym, DataSym, AclSym, AclReadonly, AclPrivate } from "../../../fields/Doc";
+import { Doc, DocListCast, HeightSym, Opt, WidthSym, DataSym, AclPrivate } from "../../../fields/Doc";
 import { Document } from '../../../fields/documentSchemas';
 import { Id } from '../../../fields/FieldSymbols';
 import { InkTool } from '../../../fields/InkField';
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index ccf83cbf9..aa89b4a10 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -13,7 +13,7 @@ import { EditorState, NodeSelection, Plugin, TextSelection, Transaction } from "
 import { ReplaceStep } from 'prosemirror-transform';
 import { EditorView } from "prosemirror-view";
 import { DateField } from '../../../../fields/DateField';
-import { DataSym, Doc, DocListCast, DocListCastAsync, Field, HeightSym, Opt, WidthSym, AclSym, AclEdit } from "../../../../fields/Doc";
+import { DataSym, Doc, DocListCast, DocListCastAsync, Field, HeightSym, Opt, WidthSym, AclEdit } from "../../../../fields/Doc";
 import { documentSchema } from '../../../../fields/documentSchemas';
 import applyDevTools = require("prosemirror-dev-tools");
 import { removeMarkWithAttrs } from "./prosemirrorPatches";
@@ -226,7 +226,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
             const curProto = Cast(Cast(this.dataDoc.proto, Doc, null)?.[this.fieldKey], RichTextField, null);              // the default text inherited from a prototype
             const curLayout = this.rootDoc !== this.layoutDoc ? Cast(this.layoutDoc[this.fieldKey], RichTextField, null) : undefined; // the default text stored in a layout template
             const json = JSON.stringify(state.toJSON());
-            // if (!this.dataDoc[AclSym]) { // what?
             if (getEffectiveAcl(this.dataDoc) === AclEdit) {
                 if (!this._applyingChange && json.replace(/"selection":.*/, "") !== curProto?.Data.replace(/"selection":.*/, "")) {
                     this._applyingChange = true;
-- 
cgit v1.2.3-70-g09d2


From bc0d6410ac42af595cea1fb242e10e464da321ae Mon Sep 17 00:00:00 2001
From: usodhi <61431818+usodhi@users.noreply.github.com>
Date: Thu, 9 Jul 2020 20:41:28 +0530
Subject: change DocListCast to async in SharingManager + prevented textbox
 from showing up on click

---
 src/client/util/SharingManager.tsx                 | 71 +++++++++++-----------
 src/client/views/DocComponent.tsx                  |  4 +-
 src/client/views/collections/CollectionView.tsx    |  6 +-
 .../collections/collectionFreeForm/MarqueeView.tsx |  6 +-
 src/client/views/nodes/DocumentContentsView.tsx    |  4 +-
 src/client/views/nodes/DocumentView.tsx            |  4 +-
 .../views/nodes/formattedText/FormattedTextBox.tsx |  4 +-
 src/fields/Doc.ts                                  | 16 ++---
 src/fields/util.ts                                 | 24 +++-----
 9 files changed, 70 insertions(+), 69 deletions(-)

(limited to 'src/client/views/nodes/DocumentView.tsx')

diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx
index fd3b2dd04..af68edab6 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, DocListCast } from "../../fields/Doc";
+import { Doc, Opt, DocListCastAsync } from "../../fields/Doc";
 import { DocServer } from "../DocServer";
 import { Cast, StrCast } from "../../fields/Types";
 import * as RequestPromise from "request-promise";
@@ -43,19 +43,6 @@ export enum SharingPermissions {
 //     [SharingPermissions.Edit, "green"]
 // ]);
 
-// export const HierarchyMapping = new Map<string, number>([
-//     [SharingPermissions.None, 0],
-//     [SharingPermissions.View, 1],
-//     [SharingPermissions.Add, 2],
-//     [SharingPermissions.Edit, 3]
-
-//     // ["0", SharingPermissions.None],
-//     // ["1", SharingPermissions.View],
-//     // ["2", SharingPermissions.Add],
-//     // ["3", SharingPermissions.Edit]
-
-// ]);
-
 interface GroupOptions {
     label: string;
     options: UserOptions[];
@@ -88,8 +75,6 @@ export default class SharingManager extends React.Component<{}> {
     @observable private overlayOpacity = 0.4;
     @observable private selectedUsers: UserOptions[] | null = null;
     @observable private permissions: SharingPermissions = SharingPermissions.Edit;
-    @observable private sharedUsers: ValidatedUser[] = [];
-    @observable private sharedGroups: Doc[] = [];
 
     // private get linkVisible() {
     //     return this.sharingDoc ? this.sharingDoc[PublicKey] !== SharingPermissions.None : false;
@@ -162,13 +147,16 @@ export default class SharingManager extends React.Component<{}> {
 
         target[ACL] = permission;
 
-        group.docsShared ? Doc.IndexOf(target, DocListCast(group.docsShared)) === -1 && (group.docsShared as List<Doc>).push(target) : group.docsShared = new List<Doc>([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]);
+        // group.docsShared ? Doc.IndexOf(target, DocListCast(group.docsShared)) === -1 && (group.docsShared as List<Doc>).push(target) : group.docsShared = new List<Doc>([target]);
 
         users.forEach(({ notificationDoc }) => {
 
-            if (permission !== SharingPermissions.None) Doc.IndexOf(target, DocListCast(notificationDoc[storage])) === -1 && Doc.AddDocToList(notificationDoc, storage, target);
-            else Doc.IndexOf(target, DocListCast(notificationDoc[storage])) !== -1 && Doc.RemoveDocFromList(notificationDoc, storage, target);
-
+            DocListCastAsync(notificationDoc[storage]).then(res => console.log(res));
+            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);
+            });
         });
     }
 
@@ -176,7 +164,12 @@ export default class SharingManager extends React.Component<{}> {
         const user: ValidatedUser = this.users.find(user => user.user.email === email)!;
 
         if (group.docsShared) {
-            DocListCast(group.docsShared).forEach(doc => Doc.IndexOf(doc, DocListCast(user.notificationDoc[storage])) === -1 && Doc.AddDocToList(user.notificationDoc, storage, doc));
+            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));
+                });
+            });
+            // DocListCast(group.docsShared).forEach(doc => Doc.IndexOf(doc, DocListCast(user.notificationDoc[storage])) === -1 && Doc.AddDocToList(user.notificationDoc, storage, doc));
         }
     }
 
@@ -184,20 +177,28 @@ export default class SharingManager extends React.Component<{}> {
         const user: ValidatedUser = this.users.find(user => user.user.email === email)!;
 
         if (group.docsShared) {
-            DocListCast(group.docsShared).forEach(doc => Doc.IndexOf(doc, DocListCast(user.notificationDoc[storage])) !== -1 && Doc.RemoveDocFromList(user.notificationDoc, storage, doc));
+            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));
+                });
+            });
+            // DocListCast(group.docsShared).forEach(doc => Doc.IndexOf(doc, DocListCast(user.notificationDoc[storage])) === -1 && Doc.AddDocToList(user.notificationDoc, storage, doc));
         }
     }
 
     removeGroup = (group: Doc) => {
         if (group.docsShared) {
-            DocListCast(group.docsShared).forEach(doc => {
-                const ACL = `ACL-${StrCast(group.groupName)}`;
-                doc[ACL] = "Not Shared";
+            DocListCastAsync(group.docsShared).then(resolved => {
+                resolved?.forEach(doc => {
+                    const ACL = `ACL-${StrCast(group.groupName)}`;
+                    doc[ACL] = "Not Shared";
+
+                    const members: string[] = JSON.parse(StrCast(group.members));
+                    const users: ValidatedUser[] = this.users.filter(user => members.includes(user.user.email));
 
-                const members: string[] = JSON.parse(StrCast(group.members));
-                const users: ValidatedUser[] = this.users.filter(user => members.includes(user.user.email));
+                    users.forEach(user => Doc.RemoveDocFromList(user.notificationDoc, storage, doc));
+                });
 
-                users.forEach(user => Doc.RemoveDocFromList(user.notificationDoc, storage, doc));
             });
         }
     }
@@ -217,14 +218,16 @@ export default class SharingManager extends React.Component<{}> {
 
 
         if (permission !== SharingPermissions.None) {
-            !this.sharedUsers.includes(recipient) && this.sharedUsers.push(recipient);
-
-            Doc.IndexOf(target, DocListCast(notificationDoc[storage])) === -1 && Doc.AddDocToList(notificationDoc, storage, target);
+            console.log(target);
+            console.log(notificationDoc);
+            DocListCastAsync(notificationDoc[storage]).then(resolved => {
+                Doc.IndexOf(target, resolved!) === -1 && Doc.AddDocToList(notificationDoc, storage, target);
+            });
         }
         else {
-            const index = this.sharedUsers.findIndex(user => user === recipient);
-            index !== -1 && this.sharedUsers.splice(index, 1);
-            Doc.IndexOf(target, DocListCast(notificationDoc[storage])) !== -1 && Doc.RemoveDocFromList(notificationDoc, storage, target);
+            DocListCastAsync(notificationDoc[storage]).then(resolved => {
+                Doc.IndexOf(target, resolved!) === -1 && Doc.RemoveDocFromList(notificationDoc, storage, target);
+            });
         }
 
     }
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index 43ffe225f..eb58d8a3e 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 } from '../../fields/util';
+import { GetEffectiveAcl } from '../../fields/util';
 
 
 ///  DocComponent returns a generic React base class used by views that don't have 'fieldKey' props (e.g.,CollectionFreeFormDocumentView, DocumentView)
@@ -138,7 +138,7 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T
             const targetDataDoc = this.props.Document[DataSym];
             const docList = DocListCast(targetDataDoc[this.annotationKey]);
             const added = docs.filter(d => !docList.includes(d));
-            const effectiveAcl = getEffectiveAcl(this.dataDoc);
+            const effectiveAcl = GetEffectiveAcl(this.dataDoc);
             if (added.length) {
                 if (effectiveAcl === AclReadonly) {
                     return false;
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 032012b2d..7448ae002 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 } from '../../../fields/util';
+import { TraceMobx, GetEffectiveAcl } 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';
@@ -132,7 +132,7 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus
         const targetDataDoc = this.props.Document[DataSym];
         const docList = DocListCast(targetDataDoc[this.props.fieldKey]);
         const added = docs.filter(d => !docList.includes(d));
-        const effectiveAcl = getEffectiveAcl(this.props.Document);
+        const effectiveAcl = GetEffectiveAcl(this.props.Document);
         if (added.length) {
             if (effectiveAcl === AclReadonly) {
                 return false;
@@ -166,7 +166,7 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus
 
     @action.bound
     removeDocument = (doc: any): boolean => {
-        if (getEffectiveAcl(this.props.Document) === AclEdit) {
+        if (GetEffectiveAcl(this.props.Document) === AclEdit) {
             const docs = doc instanceof Doc ? [doc] : doc as Doc[];
             const targetDataDoc = this.props.Document[DataSym];
             const value = DocListCast(targetDataDoc[this.props.fieldKey]);
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index b47236bea..971c501ca 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -1,6 +1,7 @@
 import { action, computed, observable } from "mobx";
 import { observer } from "mobx-react";
-import { Doc, Opt, DocListCast, DataSym } from "../../../../fields/Doc";
+import { Doc, Opt, DocListCast, DataSym, AclEdit, AclAddonly } from "../../../../fields/Doc";
+import { GetEffectiveAcl } from "../../../../fields/util";
 import { InkData, InkField, InkTool } from "../../../../fields/InkField";
 import { List } from "../../../../fields/List";
 import { RichTextField } from "../../../../fields/RichTextField";
@@ -276,7 +277,8 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
         } else {
             this._downX = x;
             this._downY = y;
-            PreviewCursor.Show(x, y, this.onKeyPress, this.props.addLiveTextDocument, this.props.getTransform, this.props.addDocument, this.props.nudge);
+            const effectiveAcl = GetEffectiveAcl(this.props.Document);
+            if (effectiveAcl === AclEdit || effectiveAcl === AclAddonly) 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/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index 1e6966c05..f2f8ada68 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -36,7 +36,7 @@ import { WebBox } from "./WebBox";
 import { InkingStroke } from "../InkingStroke";
 import React = require("react");
 import { RecommendationsBox } from "../RecommendationsBox";
-import { TraceMobx, getEffectiveAcl } from "../../../fields/util";
+import { TraceMobx, GetEffectiveAcl } from "../../../fields/util";
 import { ScriptField } from "../../../fields/ScriptField";
 import XRegExp = require("xregexp");
 
@@ -184,7 +184,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
 
         const bindings = this.CreateBindings(onClick, onInput);
         //  layoutFrame = splits.length > 1 ? splits[0] + splits[1].replace(/{([^{}]|(?R))*}/, replacer4) : ""; // might have been more elegant if javascript supported recursive patterns
-        return (this.props.renderDepth > 12 || !layoutFrame || !this.layoutDoc || getEffectiveAcl(this.layoutDoc) === AclPrivate) ? (null) :
+        return (this.props.renderDepth > 12 || !layoutFrame || !this.layoutDoc || GetEffectiveAcl(this.layoutDoc) === AclPrivate) ? (null) :
             <ObserverJsxParser
                 key={42}
                 blacklistedAttrs={[]}
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 9dc30c683..7c0bdc6c1 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -12,7 +12,7 @@ import { listSpec } from "../../../fields/Schema";
 import { SchemaHeaderField } from '../../../fields/SchemaHeaderField';
 import { ScriptField } from '../../../fields/ScriptField';
 import { BoolCast, Cast, NumCast, StrCast } from "../../../fields/Types";
-import { TraceMobx, getEffectiveAcl } from '../../../fields/util';
+import { TraceMobx, GetEffectiveAcl } from '../../../fields/util';
 import { GestureUtils } from '../../../pen-gestures/GestureUtils';
 import { emptyFunction, OmitKeys, returnOne, returnTransparent, Utils, emptyPath } from "../../../Utils";
 import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils';
@@ -1207,7 +1207,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
 
     render() {
         if (!(this.props.Document instanceof Doc)) return (null);
-        if (getEffectiveAcl(this.props.Document) === AclPrivate) return (null);
+        if (GetEffectiveAcl(this.props.Document) === AclPrivate) return (null);
         if (this.props.Document.hidden) return (null);
         const backgroundColor = Doc.UserDoc().renderStyle === "comic" ? undefined : this.props.forcedBackgroundColor?.(this.Document) || StrCast(this.layoutDoc._backgroundColor) || StrCast(this.layoutDoc.backgroundColor) || StrCast(this.Document.backgroundColor) || this.props.backgroundColor?.(this.Document);
         const opacity = Cast(this.layoutDoc._opacity, "number", Cast(this.layoutDoc.opacity, "number", Cast(this.Document.opacity, "number", null)));
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index aa89b4a10..0583c7f39 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -24,7 +24,7 @@ import { RichTextField } from "../../../../fields/RichTextField";
 import { RichTextUtils } from '../../../../fields/RichTextUtils';
 import { createSchema, makeInterface } from "../../../../fields/Schema";
 import { Cast, DateCast, NumCast, StrCast, ScriptCast } from "../../../../fields/Types";
-import { TraceMobx, OVERRIDE_ACL, getEffectiveAcl } from '../../../../fields/util';
+import { TraceMobx, OVERRIDE_ACL, GetEffectiveAcl } from '../../../../fields/util';
 import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, numberRange, returnOne, returnZero, Utils, setupMoveUpEvents } from '../../../../Utils';
 import { GoogleApiClientUtils, Pulls, Pushes } from '../../../apis/google_docs/GoogleApiClientUtils';
 import { DocServer } from "../../../DocServer";
@@ -226,7 +226,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
             const curProto = Cast(Cast(this.dataDoc.proto, Doc, null)?.[this.fieldKey], RichTextField, null);              // the default text inherited from a prototype
             const curLayout = this.rootDoc !== this.layoutDoc ? Cast(this.layoutDoc[this.fieldKey], RichTextField, null) : undefined; // the default text stored in a layout template
             const json = JSON.stringify(state.toJSON());
-            if (getEffectiveAcl(this.dataDoc) === AclEdit) {
+            if (GetEffectiveAcl(this.dataDoc) === AclEdit) {
                 if (!this._applyingChange && json.replace(/"selection":.*/, "") !== curProto?.Data.replace(/"selection":.*/, "")) {
                     this._applyingChange = true;
                     (curText !== Cast(this.dataDoc[this.fieldKey], RichTextField)?.Text) && (this.dataDoc[this.props.fieldKey + "-lastModified"] = new DateField(new Date(Date.now())));
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 27eabf451..e8dca5fb6 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -17,7 +17,7 @@ import { RichTextField } from "./RichTextField";
 import { listSpec } from "./Schema";
 import { ComputedField } from "./ScriptField";
 import { Cast, FieldValue, NumCast, StrCast, ToConstructor } from "./Types";
-import { deleteProperty, getField, getter, makeEditable, makeReadOnly, setter, updateFunction, getEffectiveAcl } from "./util";
+import { deleteProperty, getField, getter, makeEditable, makeReadOnly, setter, updateFunction, GetEffectiveAcl } from "./util";
 import { LinkManager } from "../client/util/LinkManager";
 import { SharingPermissions } from "../client/util/SharingManager";
 
@@ -136,10 +136,10 @@ export class Doc extends RefField {
             set: setter,
             get: getter,
             // getPrototypeOf: (target) => Cast(target[SelfProxy].proto, Doc) || null, // TODO this might be able to replace the proto logic in getter
-            has: (target, key) => getEffectiveAcl(target) !== AclPrivate && key in target.__fields,
+            has: (target, key) => GetEffectiveAcl(target) !== AclPrivate && key in target.__fields,
             ownKeys: target => {
                 const obj = {} as any;
-                if (getEffectiveAcl(target) !== AclPrivate) Object.assign(obj, target.___fields);
+                if (GetEffectiveAcl(target) !== AclPrivate) Object.assign(obj, target.___fields);
                 runInAction(() => obj.__LAYOUT__ = target.__LAYOUT__);
                 return Object.keys(obj);
             },
@@ -197,7 +197,7 @@ export class Doc extends RefField {
     public [WidthSym] = () => NumCast(this[SelfProxy]._width);
     public [HeightSym] = () => NumCast(this[SelfProxy]._height);
     public [ToScriptString]() { return `DOC-"${this[Self][Id]}"-`; }
-    public [ToString]() { return `Doc(${getEffectiveAcl(this) === AclPrivate ? "-inaccessible-" : this.title})`; }
+    public [ToString]() { return `Doc(${GetEffectiveAcl(this) === AclPrivate ? "-inaccessible-" : this.title})`; }
     public get [LayoutSym]() { return this[SelfProxy].__LAYOUT__; }
     public get [DataSym]() {
         const self = this[SelfProxy];
@@ -825,7 +825,7 @@ export namespace Doc {
     }
     // don't bother memoizing (caching) the result if called from a non-reactive context. (plus this avoids a warning message)
     export function IsBrushedDegreeUnmemoized(doc: Doc) {
-        if (!doc || getEffectiveAcl(doc) === AclPrivate || getEffectiveAcl(Doc.GetProto(doc)) === AclPrivate) return 0;
+        if (!doc || GetEffectiveAcl(doc) === AclPrivate || GetEffectiveAcl(Doc.GetProto(doc)) === AclPrivate) return 0;
         return brushManager.BrushedDoc.has(doc) ? 2 : brushManager.BrushedDoc.has(Doc.GetProto(doc)) ? 1 : 0;
     }
     export function IsBrushedDegree(doc: Doc) {
@@ -834,14 +834,14 @@ export namespace Doc {
         })(doc);
     }
     export function BrushDoc(doc: Doc) {
-        if (!doc || getEffectiveAcl(doc) === AclPrivate || getEffectiveAcl(Doc.GetProto(doc)) === AclPrivate) return doc;
+        if (!doc || GetEffectiveAcl(doc) === AclPrivate || GetEffectiveAcl(Doc.GetProto(doc)) === AclPrivate) return doc;
         brushManager.BrushedDoc.set(doc, true);
         brushManager.BrushedDoc.set(Doc.GetProto(doc), true);
         return doc;
     }
     export function UnBrushDoc(doc: Doc) {
 
-        if (!doc || getEffectiveAcl(doc) === AclPrivate || getEffectiveAcl(Doc.GetProto(doc)) === AclPrivate) return doc;
+        if (!doc || GetEffectiveAcl(doc) === AclPrivate || GetEffectiveAcl(Doc.GetProto(doc)) === AclPrivate) return doc;
         brushManager.BrushedDoc.delete(doc);
         brushManager.BrushedDoc.delete(Doc.GetProto(doc));
         return doc;
@@ -871,7 +871,7 @@ export namespace Doc {
     }
     const highlightManager = new HighlightBrush();
     export function IsHighlighted(doc: Doc) {
-        if (!doc || getEffectiveAcl(doc) === AclPrivate || getEffectiveAcl(Doc.GetProto(doc)) === AclPrivate) return false;
+        if (!doc || GetEffectiveAcl(doc) === AclPrivate || GetEffectiveAcl(Doc.GetProto(doc)) === AclPrivate) return false;
         return highlightManager.HighlightedDoc.get(doc) || highlightManager.HighlightedDoc.get(Doc.GetProto(doc));
     }
     export function HighlightDoc(doc: Doc, dataAndDisplayDocs = true) {
diff --git a/src/fields/util.ts b/src/fields/util.ts
index a1af1d3c5..f82ea26e0 100644
--- a/src/fields/util.ts
+++ b/src/fields/util.ts
@@ -108,15 +108,13 @@ export function OVERRIDE_ACL(val: boolean) {
 }
 
 let currentUserGroups: string[] = [];
-let currentUserEmail: string;// = Doc.CurrentUserEmail;
 
 export function setGroups(groups: string[]) {
     currentUserGroups = groups;
-    currentUserEmail = Doc.CurrentUserEmail;
 }
 
 
-export function getEffectiveAcl(target: any, in_prop?: string | symbol | number): symbol {
+export function GetEffectiveAcl(target: any, in_prop?: string | symbol | number): symbol {
 
     const HierarchyMapping = new Map<symbol, number>([
         [AclPrivate, 0],
@@ -131,7 +129,7 @@ export function getEffectiveAcl(target: any, in_prop?: string | symbol | number)
 
     if (target[AclSym] && Object.keys(target[AclSym]).length) {
 
-        if (target.author === currentUserEmail) return AclEdit;
+        if (target.author === Doc.CurrentUserEmail || currentUserGroups.includes("admin")) return AclEdit;
 
         if (_overrideAcl || (in_prop && DocServer.PlaygroundFields?.includes(in_prop.toString()))) return AclEdit;
 
@@ -141,13 +139,11 @@ export function getEffectiveAcl(target: any, in_prop?: string | symbol | number)
         let aclPresent = false;
 
         for (const [key, value] of Object.entries(target[AclSym])) {
-            if (key.startsWith("ACL-")) {
-                if (currentUserGroups.includes(key.substring(4)) || currentUserEmail === key.substring(4).replace("_", ".")) {
-                    if (HierarchyMapping.get(value as symbol)! >= HierarchyMapping.get(effectiveAcl)!) {
-                        aclPresent = true;
-                        effectiveAcl = value as symbol;
-                        if (effectiveAcl === AclEdit) break;
-                    }
+            if (currentUserGroups.includes(key.substring(4)) || Doc.CurrentUserEmail === key.substring(4).replace("_", ".")) {
+                if (HierarchyMapping.get(value as symbol)! >= HierarchyMapping.get(effectiveAcl)!) {
+                    aclPresent = true;
+                    effectiveAcl = value as symbol;
+                    if (effectiveAcl === AclEdit) break;
                 }
             }
         }
@@ -163,7 +159,7 @@ const layoutProps = ["panX", "panY", "width", "height", "nativeWidth", "nativeHe
     "chromeStatus", "viewType", "gridGap", "xMargin", "yMargin", "autoHeight"];
 export function setter(target: any, in_prop: string | symbol | number, value: any, receiver: any): boolean {
     let prop = in_prop;
-    if (getEffectiveAcl(target, in_prop) !== AclEdit) {
+    if (GetEffectiveAcl(target, in_prop) !== AclEdit) {
         return true;
     }
     if (typeof prop === "string" && prop !== "__id" && prop !== "__fields" && (prop.startsWith("_") || layoutProps.includes(prop))) {
@@ -185,7 +181,7 @@ export function setter(target: any, in_prop: string | symbol | number, value: an
 export function getter(target: any, in_prop: string | symbol | number, receiver: any): any {
     let prop = in_prop;
     if (in_prop === AclSym) return _overrideAcl ? undefined : target[AclSym];
-    if (getEffectiveAcl(target) === AclPrivate && !_overrideAcl) return undefined;
+    if (GetEffectiveAcl(target) === AclPrivate && !_overrideAcl) return undefined;
     if (prop === LayoutSym) {
         return target.__LAYOUT__;
     }
@@ -222,7 +218,7 @@ function getFieldImpl(target: any, prop: string | number, receiver: any, ignoreP
     }
     if (field === undefined && !ignoreProto && prop !== "proto") {
         const proto = getFieldImpl(target, "proto", receiver, true);//TODO tfs: instead of receiver we could use target[SelfProxy]... I don't which semantics we want or if it really matters
-        if (proto instanceof Doc && getEffectiveAcl(proto) !== AclPrivate) {
+        if (proto instanceof Doc && GetEffectiveAcl(proto) !== AclPrivate) {
             return getFieldImpl(proto[Self], prop, receiver, ignoreProto);
         }
         return undefined;
-- 
cgit v1.2.3-70-g09d2


From 085829cec2b11fe23d0424cc4e3e84785f8b7f51 Mon Sep 17 00:00:00 2001
From: usodhi <61431818+usodhi@users.noreply.github.com>
Date: Fri, 10 Jul 2020 11:51:20 +0530
Subject: set up playground mode

---
 src/client/views/collections/CollectionView.tsx         |  8 ++++----
 .../collections/collectionFreeForm/MarqueeView.tsx      |  4 ++--
 src/client/views/nodes/DocumentView.tsx                 |  6 ++++--
 src/fields/util.ts                                      | 17 +++++++++++++++--
 4 files changed, 25 insertions(+), 10 deletions(-)

(limited to 'src/client/views/nodes/DocumentView.tsx')

diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 7448ae002..34d67905e 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 } from '../../../fields/util';
+import { TraceMobx, GetEffectiveAcl, getPlaygroundMode } 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';
@@ -134,9 +134,9 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus
         const added = docs.filter(d => !docList.includes(d));
         const effectiveAcl = GetEffectiveAcl(this.props.Document);
         if (added.length) {
-            if (effectiveAcl === AclReadonly) {
+            if (effectiveAcl === AclReadonly && !getPlaygroundMode()) {
                 return false;
-            } else if (effectiveAcl === AclAddonly) {
+            } else if (effectiveAcl === AclAddonly && !getPlaygroundMode()) {
                 added.map(doc => Doc.AddDocToList(targetDataDoc, this.props.fieldKey, doc));
             } else {
                 added.map(doc => {
@@ -166,7 +166,7 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus
 
     @action.bound
     removeDocument = (doc: any): boolean => {
-        if (GetEffectiveAcl(this.props.Document) === AclEdit) {
+        if (GetEffectiveAcl(this.props.Document) === AclEdit || getPlaygroundMode()) {
             const docs = doc instanceof Doc ? [doc] : doc as Doc[];
             const targetDataDoc = this.props.Document[DataSym];
             const value = DocListCast(targetDataDoc[this.props.fieldKey]);
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 971c501ca..8ce2dc9a4 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 } from "../../../../fields/Doc";
-import { GetEffectiveAcl } from "../../../../fields/util";
+import { GetEffectiveAcl, getPlaygroundMode } from "../../../../fields/util";
 import { InkData, InkField, InkTool } from "../../../../fields/InkField";
 import { List } from "../../../../fields/List";
 import { RichTextField } from "../../../../fields/RichTextField";
@@ -278,7 +278,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
             this._downX = x;
             this._downY = y;
             const effectiveAcl = GetEffectiveAcl(this.props.Document);
-            if (effectiveAcl === AclEdit || effectiveAcl === AclAddonly) PreviewCursor.Show(x, y, this.onKeyPress, this.props.addLiveTextDocument, this.props.getTransform, this.props.addDocument, this.props.nudge);
+            if (effectiveAcl === AclEdit || effectiveAcl === AclAddonly || getPlaygroundMode()) 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/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 7c0bdc6c1..1a9ff1ba8 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -4,7 +4,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { action, computed, observable, runInAction } from "mobx";
 import { observer } from "mobx-react";
 import * as rp from "request-promise";
-import { Doc, DocListCast, HeightSym, Opt, WidthSym, DataSym, AclPrivate } from "../../../fields/Doc";
+import { Doc, DocListCast, HeightSym, Opt, WidthSym, DataSym, AclPrivate, AclReadonly } from "../../../fields/Doc";
 import { Document } from '../../../fields/documentSchemas';
 import { Id } from '../../../fields/FieldSymbols';
 import { InkTool } from '../../../fields/InkField';
@@ -12,7 +12,7 @@ import { listSpec } from "../../../fields/Schema";
 import { SchemaHeaderField } from '../../../fields/SchemaHeaderField';
 import { ScriptField } from '../../../fields/ScriptField';
 import { BoolCast, Cast, NumCast, StrCast } from "../../../fields/Types";
-import { TraceMobx, GetEffectiveAcl } from '../../../fields/util';
+import { TraceMobx, GetEffectiveAcl, getPlaygroundMode, togglePlaygroundMode } from '../../../fields/util';
 import { GestureUtils } from '../../../pen-gestures/GestureUtils';
 import { emptyFunction, OmitKeys, returnOne, returnTransparent, Utils, emptyPath } from "../../../Utils";
 import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils';
@@ -853,6 +853,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         aclItems.push({ description: "Test Readonly", event: () => this.testAcl(SharingPermissions.View), icon: "concierge-bell" });
         !existingAcls && cm.addItem({ description: "Privacy...", subitems: aclItems, icon: "question" });
 
+        cm.addItem({ description: `${getPlaygroundMode() ? "Disable" : "Enable"} playground mode`, event: togglePlaygroundMode, icon: "concierge-bell" });
+
         // const recommender_subitems: ContextMenuProps[] = [];
 
         // recommender_subitems.push({
diff --git a/src/fields/util.ts b/src/fields/util.ts
index f82ea26e0..77855887e 100644
--- a/src/fields/util.ts
+++ b/src/fields/util.ts
@@ -71,7 +71,8 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number
     const fromServer = target[UpdatingFromServer];
     const sameAuthor = fromServer || (receiver.author === Doc.CurrentUserEmail);
     const writeToDoc = sameAuthor || (writeMode !== DocServer.WriteMode.LiveReadonly);
-    const writeToServer = sameAuthor || (writeMode === DocServer.WriteMode.Default);
+    const writeToServer = (sameAuthor || (writeMode === DocServer.WriteMode.Default)) && !playgroundMode;
+
     if (writeToDoc) {
         if (value === undefined) {
             delete target.__fields[prop];
@@ -79,10 +80,13 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number
             target.__fields[prop] = value;
         }
         //if (typeof value === "object" && !(value instanceof ObjectField)) debugger;
+
+        // remove below lines to prevent saving changes to server
         if (writeToServer) {
             if (value === undefined) target[Update]({ '$unset': { ["fields." + prop]: "" } });
             else target[Update]({ '$set': { ["fields." + prop]: value instanceof ObjectField ? SerializationHelper.Serialize(value) : (value === undefined ? null : value) } });
         } else {
+            console.log("not writing to server")
             DocServer.registerDocWithCachedUpdate(receiver, prop as string, curValue);
         }
         UndoManager.AddEvent({
@@ -107,13 +111,22 @@ export function OVERRIDE_ACL(val: boolean) {
     _overrideAcl = val;
 }
 
+let playgroundMode = false;
+
+export function togglePlaygroundMode() {
+    playgroundMode = !playgroundMode;
+}
+
+export function getPlaygroundMode() {
+    return playgroundMode;
+}
+
 let currentUserGroups: string[] = [];
 
 export function setGroups(groups: string[]) {
     currentUserGroups = groups;
 }
 
-
 export function GetEffectiveAcl(target: any, in_prop?: string | symbol | number): symbol {
 
     const HierarchyMapping = new Map<symbol, number>([
-- 
cgit v1.2.3-70-g09d2


From 3ad593cc8865d3fa1dc22bf403ad7cfaf1a751e6 Mon Sep 17 00:00:00 2001
From: usodhi <61431818+usodhi@users.noreply.github.com>
Date: Wed, 15 Jul 2020 21:52:23 +0530
Subject: bugfix + cleanup

---
 src/client/util/GroupManager.scss       |  25 --------
 src/client/util/GroupManager.tsx        |   4 --
 src/client/util/GroupMemberView.scss    |  10 ---
 src/client/util/GroupMemberView.tsx     |   3 -
 src/client/util/SharingManager.scss     |  30 ---------
 src/client/util/SharingManager.tsx      | 106 +++++---------------------------
 src/client/views/nodes/DocumentView.tsx |  22 +++----
 src/fields/Doc.ts                       |   2 +-
 8 files changed, 28 insertions(+), 174 deletions(-)

(limited to 'src/client/views/nodes/DocumentView.tsx')

diff --git a/src/client/util/GroupManager.scss b/src/client/util/GroupManager.scss
index 8a2c616b1..34d4f40f8 100644
--- a/src/client/util/GroupManager.scss
+++ b/src/client/util/GroupManager.scss
@@ -1,8 +1,4 @@
-// @import "../views/globalCssVariables";
-
 .group-interface {
-    // background-color: whitesmoke !important;
-    // color: grey;
     width: 550px;
     height: 300px;
 
@@ -12,21 +8,17 @@
             flex-direction: column;
             height: 90%;
             justify-content: space-between;
-            // flex-basis: 30%;
             margin-left: 5px;
 
             input {
                 border-radius: 5px;
-                // border: none;
                 padding: 8px;
                 min-width: 100%;
-                // margin: 4px 0 4px 0;
                 border: 1px solid hsl(0, 0%, 80%);
                 outline: none;
                 height: 30;
 
                 &:focus {
-                    // border: unset;
                     border: 2.5px solid #2684FF;
                 }
             }
@@ -43,18 +35,12 @@
         }
     }
 
-    //     .dialogue-box {
-    //     width: 450;
-    //     height: 300;
-    // }
 
     button {
-        // background: $lighter-alt-accent;
         align-self: center;
         outline: none;
         border-radius: 5px;
         border: 0px;
-        // color: #fcfbf7;
         text-transform: none;
         letter-spacing: 2px;
         font-size: 75%;
@@ -94,10 +80,8 @@
         p {
             font-size: 20px;
             text-align: left;
-            // margin: 0 0 20px 0;
             margin-right: 15px;
             color: black;
-            // width: 60%;
         }
     }
 
@@ -112,14 +96,10 @@
         }
 
         .group-body {
-            // display: flex;
             justify-content: space-between;
-            // max-height: 80%;
             height: 220;
             background-color: #e8e8e8;
-            // flex-direction: column;
 
-            // padding-left: 1em;
             padding-right: 1em;
             justify-content: space-around;
             text-align: left;
@@ -129,17 +109,12 @@
 
             .group-row {
                 display: flex;
-                // position: relative;
                 margin-bottom: 5px;
                 min-height: 30px;
-                // border: 1px solid;
-                // border-radius: 10px;
                 align-items: center;
 
                 .group-name {
-                    // position: relative;
                     max-width: 65%;
-                    // left: 10;
                     margin: 0 10;
                     color: black;
                 }
diff --git a/src/client/util/GroupManager.tsx b/src/client/util/GroupManager.tsx
index 2d8930660..12951f2ab 100644
--- a/src/client/util/GroupManager.tsx
+++ b/src/client/util/GroupManager.tsx
@@ -286,7 +286,6 @@ export default class GroupManager extends React.Component<{}> {
      */
     @action
     createGroup = () => {
-        // this.createGroupModalOpen = true;
         if (!this.inputRef.current?.value) {
             alert("Please enter a group name");
             return;
@@ -410,9 +409,6 @@ export default class GroupManager extends React.Component<{}> {
                                 <div className="group-info" onClick={action(() => this.currentGroup = group)}>
                                     <FontAwesomeIcon icon={fa.faInfoCircle} color={"#e8e8e8"} size={"sm"} style={{ backgroundColor: "#1e89d7", borderRadius: "100%", border: "1px solid #1e89d7" }} />
                                 </div>
-                                {/* <button onClick={action(() => this.currentGroup = group)}>
-                                {this.hasEditAccess(group) ? "Edit" : "View"}
-                            </button> */}
                             </div>
                         )}
                     </div>
diff --git a/src/client/util/GroupMemberView.scss b/src/client/util/GroupMemberView.scss
index a34e5b989..c609c5c7b 100644
--- a/src/client/util/GroupMemberView.scss
+++ b/src/client/util/GroupMemberView.scss
@@ -1,18 +1,12 @@
-// @import "../views/globalCssVariables";
-
 .editing-interface {
-    // background-color: whitesmoke !important;
-    // color: grey;
     width: 100%;
     height: 100%;
-    // color: black;
 
     hr {
         margin-top: 20;
     }
 
     button {
-        // background: $darker-alt-accent;
         outline: none;
         border-radius: 5px;
         border: 0px;
@@ -78,7 +72,6 @@
 
     .editing-contents {
         overflow-y: auto;
-        // max-height: 67%;
         height: 65%;
         width: 100%;
         color: black;
@@ -89,11 +82,8 @@
             align-items: center;
             margin-bottom: 10px;
             position: relative;
-            // border: 1px solid;
-            // border-radius: 10px;
 
             .user-email {
-                // position: relative;
                 min-width: 65%;
                 word-break: break-all;
                 padding: 0 5;
diff --git a/src/client/util/GroupMemberView.tsx b/src/client/util/GroupMemberView.tsx
index ebe9830ba..f20670c4e 100644
--- a/src/client/util/GroupMemberView.tsx
+++ b/src/client/util/GroupMemberView.tsx
@@ -28,9 +28,6 @@ export default class GroupMemberView extends React.Component<GroupMemberViewProp
         members = this.memberSort === "ascending" ? members.sort() : this.memberSort === "descending" ? members.sort().reverse() : members;
 
         const options: UserOptions[] = this.props.group ? GroupManager.Instance.options.filter(option => !(JSON.parse(StrCast(this.props.group.members)) as string[]).includes(option.value)) : [];
-        console.log(this.props.group, options);
-        console.log(GroupManager.Instance.options);
-
 
         return (!this.props.group ? null :
             <div className="editing-interface">
diff --git a/src/client/util/SharingManager.scss b/src/client/util/SharingManager.scss
index 572b94ffb..130785672 100644
--- a/src/client/util/SharingManager.scss
+++ b/src/client/util/SharingManager.scss
@@ -1,16 +1,7 @@
-// @import "../views/globalCssVariables";
-
 .sharing-interface {
-    // display: flex;
-    // flex-direction: column;
     width: 600px;
     height: 360px;
 
-    // .dialogue-box {
-    //     width: 450;
-    //     height: 300;
-    // }
-
     .overlay {
         transform: translate(-20px, -20px);
     }
@@ -87,7 +78,6 @@
                 .users-list {
                     font-style: italic;
                     background: #e8e8e8;
-                    // border: 1px solid black;
                     padding-left: 10px;
                     padding-right: 10px;
                     overflow-y: scroll;
@@ -98,7 +88,6 @@
                     align-items: center;
                     text-align: center;
                     justify-content: center;
-                    // color: red;
                     color: black;
                     height: 250px;
                     margin: 0 2;
@@ -113,7 +102,6 @@
         }
 
         button {
-            // background: $darker-alt-accent;
             outline: none;
             border-radius: 5px;
             border: 0px;
@@ -135,8 +123,6 @@
     p {
         font-size: 20px;
         text-align: left;
-        // font-style: italic;
-        // padding: 0;
         margin: 0 0 20px 0;
         color: black;
     }
@@ -174,8 +160,6 @@
         -ms-user-select: none;
         user-select: none;
         width: 100%;
-        // min-width: 700px;
-        // max-width: 700px;
         text-align: left;
         font-style: normal;
         font-size: 14;
@@ -249,18 +233,4 @@
             padding-top: 12px;
         }
     }
-
-    // .close-button {
-    //     border-radius: 5px;
-    //     margin-top: 20px;
-    //     padding: 10px 0;
-    //     background: aliceblue;
-    //     transition: 0.5s ease all;
-    //     border: 1px solid;
-    //     border-color: aliceblue;
-    // }
-
-    // .close-button:hover {
-    //     border-color: black;
-    // }
 }
\ No newline at end of file
diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx
index be86b183f..8d4e508ac 100644
--- a/src/client/util/SharingManager.tsx
+++ b/src/client/util/SharingManager.tsx
@@ -36,13 +36,6 @@ export enum SharingPermissions {
     None = "Not Shared"
 }
 
-// const ColorMapping = new Map<string, string>([
-//     [SharingPermissions.None, "red"],
-//     [SharingPermissions.View, "maroon"],
-//     [SharingPermissions.Add, "blue"],
-//     [SharingPermissions.Edit, "green"]
-// ]);
-
 interface GroupOptions {
     label: string;
     options: UserOptions[];
@@ -67,10 +60,9 @@ export default class SharingManager extends React.Component<{}> {
     public static Instance: SharingManager;
     @observable private isOpen = false;
     @observable private users: ValidatedUser[] = [];
-    // @observable private groups: Doc[] = [];
     @observable private targetDoc: Doc | undefined;
     @observable private targetDocView: DocumentView | undefined;
-    @observable private copied = false;
+    // @observable private copied = false;
     @observable private dialogueBoxOpacity = 1;
     @observable private overlayOpacity = 0.4;
     @observable private selectedUsers: UserOptions[] | null = null;
@@ -90,32 +82,20 @@ export default class SharingManager extends React.Component<{}> {
             this.targetDoc = target.props.Document;
             DictationOverlay.Instance.hasActiveModal = true;
             this.isOpen = true;
-            // if (!this.sharingDoc) {
-            //     this.sharingDoc = new Doc;
-            // }
         }));
 
-        // runInAction(() => this.groups = GroupManager.Instance.getAllGroups());
     }
 
     public close = action(() => {
         this.isOpen = false;
         this.users = [];
         setTimeout(action(() => {
-            this.copied = false;
+            // this.copied = false;
             DictationOverlay.Instance.hasActiveModal = false;
             this.targetDoc = undefined;
         }), 500);
     });
 
-    // private get sharingDoc() {
-    //     return this.targetDoc ? Cast(this.targetDoc[SharingKey], Doc) as Doc : undefined;
-    // }
-
-    // private set sharingDoc(value: Doc | undefined) {
-    //     this.targetDoc && (this.targetDoc[SharingKey] = value);
-    // }
-
     constructor(props: {}) {
         super(props);
         SharingManager.Instance = this;
@@ -152,7 +132,6 @@ export default class SharingManager extends React.Component<{}> {
         Doc.GetProto(target)[ACL] = permission;
 
         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]);
-        // group.docsShared ? Doc.IndexOf(target, DocListCast(group.docsShared)) === -1 && (group.docsShared as List<Doc>).push(target) : group.docsShared = new List<Doc>([target]);
 
         users.forEach(({ notificationDoc }) => {
             DocListCastAsync(notificationDoc[storage]).then(resolved => {
@@ -171,7 +150,6 @@ export default class SharingManager extends React.Component<{}> {
                     DocListCastAsync(user.notificationDoc[storage]).then(resolved => Doc.IndexOf(doc, resolved!) === -1 && Doc.AddDocToList(user.notificationDoc, storage, doc));
                 });
             });
-            // DocListCast(group.docsShared).forEach(doc => Doc.IndexOf(doc, DocListCast(user.notificationDoc[storage])) === -1 && Doc.AddDocToList(user.notificationDoc, storage, doc));
         }
     }
 
@@ -184,7 +162,6 @@ export default class SharingManager extends React.Component<{}> {
                     DocListCastAsync(user.notificationDoc[storage]).then(resolved => Doc.IndexOf(doc, resolved!) !== -1 && Doc.RemoveDocFromList(user.notificationDoc, storage, doc));
                 });
             });
-            // DocListCast(group.docsShared).forEach(doc => Doc.IndexOf(doc, DocListCast(user.notificationDoc[storage])) === -1 && Doc.AddDocToList(user.notificationDoc, storage, doc));
         }
     }
 
@@ -208,14 +185,10 @@ export default class SharingManager extends React.Component<{}> {
     setInternalSharing = (recipient: ValidatedUser, permission: string) => {
         const { user, notificationDoc } = recipient;
         const target = this.targetDoc!;
-        // const manager = this.sharingDoc!;
         const key = user.email.replace('.', '_');
-        // const key = user.userDocumentId;
 
         const ACL = `ACL-${key}`;
 
-        // const permissions: { [key: string]: number } = target[ACL] ? JSON.parse(StrCast(target[ACL])) : {};
-
         target[ACL] = permission;
         Doc.GetProto(target)[ACL] = permission;
 
@@ -228,47 +201,12 @@ export default class SharingManager extends React.Component<{}> {
         }
         else {
             DocListCastAsync(notificationDoc[storage]).then(resolved => {
-                Doc.IndexOf(target, resolved!) === -1 && Doc.RemoveDocFromList(notificationDoc, storage, target);
+                Doc.IndexOf(target, resolved!) !== -1 && Doc.RemoveDocFromList(notificationDoc, storage, target);
             });
         }
 
     }
 
-
-    // let metadata = await DocCastAsync(manager[key]);
-    // const permissions: { [key: string]: number } = metadata?.permissions ? JSON.parse(StrCast(metadata.permissions)) : {};
-    // permissions[StrCast(group ? group.groupName : Doc.CurrentUserEmail)] = parseInt(HierarchyMapping.get(permission)!);
-    // const max = Math.max(...Object.values(permissions));
-
-    // switch (max) {
-    //     case 0:
-    //         // if (metadata) {
-    //         //     const sharedAlias = (await DocCastAsync(metadata.sharedAlias))!;
-    //         //     Doc.RemoveDocFromList(notificationDoc, storage, sharedAlias);
-    //         //     manager[key] = undefined;
-    //         // }
-    //         Doc.RemoveDocFromList(notificationDoc, storage, target);
-    //         break;
-
-    //     case 1: case 2: case 3:
-
-    //         Doc.AddDocToList(notificationDoc, storage, target);
-
-    //         if (!metadata) {
-    //             metadata = new Doc;
-    //             const sharedAlias = Doc.MakeAlias(target);
-    //             Doc.AddDocToList(notificationDoc, storage, target);
-    //             metadata.sharedAlias = sharedAlias;
-    //             manager[key] = metadata;
-    //         }
-    //         metadata.permissions = JSON.stringify(permissions);
-    //         // metadata.usersShared = JSON.stringify(keys);
-    //         break;
-    // }
-
-    // if (metadata) metadata.maxPermission = HierarchyMapping.get(`${max}`);
-
-
     // private setExternalSharing = (permission: string) => {
     //     const sharingDoc = this.sharingDoc;
     //     if (!sharingDoc) {
@@ -277,20 +215,20 @@ export default class SharingManager extends React.Component<{}> {
     //     sharingDoc[PublicKey] = permission;
     // }
 
-    private get sharingUrl() {
-        if (!this.targetDoc) {
-            return undefined;
-        }
-        const baseUrl = Utils.prepend("/doc/" + this.targetDoc[Id]);
-        return `${baseUrl}?sharing=true`;
-    }
+    // private get sharingUrl() {
+    //     if (!this.targetDoc) {
+    //         return undefined;
+    //     }
+    //     const baseUrl = Utils.prepend("/doc/" + this.targetDoc[Id]);
+    //     return `${baseUrl}?sharing=true`;
+    // }
 
-    copy = action(() => {
-        if (this.sharingUrl) {
-            Utils.CopyText(this.sharingUrl);
-            this.copied = true;
-        }
-    });
+    // copy = action(() => {
+    //     if (this.sharingUrl) {
+    //         Utils.CopyText(this.sharingUrl);
+    //         this.copied = true;
+    //     }
+    // });
 
     private get sharingOptions() {
         return Object.values(SharingPermissions).map(permission => {
@@ -394,14 +332,7 @@ export default class SharingManager extends React.Component<{}> {
 
         const userListContents: (JSX.Element | null)[] = users.map(({ user, notificationDoc }) => { // can't use async here
             const userKey = user.email.replace('.', '_');
-            // const userKey = user.userDocumentId;
             const permissions = StrCast(this.targetDoc?.[`ACL-${userKey}`], SharingPermissions.None);
-            // const color = ColorMapping.get(permissions);
-
-            // console.log(manager);
-            // const metadata = manager[userKey] as Doc;
-            // const usersShared = StrCast(metadata?.usersShared, "");
-            // console.log(usersShared)
 
             return permissions === SharingPermissions.None || user.email === this.targetDoc?.author ? null : (
                 <div
@@ -409,12 +340,10 @@ export default class SharingManager extends React.Component<{}> {
                     className={"container"}
                 >
                     <span className={"padding"}>{user.email}</span>
-                    {/* <div className={"shared-by"}>{usersShared}</div> */}
                     <div className="edit-actions">
                         <select
                             className={"permissions-dropdown"}
                             value={permissions}
-                            // style={{ color, borderColor: color }}
                             onChange={e => this.setInternalSharing({ user, notificationDoc }, e.currentTarget.value)}
                         >
                             {this.sharingOptions}
@@ -442,7 +371,6 @@ export default class SharingManager extends React.Component<{}> {
 
         const groupListContents = groups.map(group => {
             const permissions = StrCast(this.targetDoc?.[`ACL-${StrCast(group.groupName)}`], SharingPermissions.None);
-            // const color = ColorMapping.get(permissions);
 
             return permissions === SharingPermissions.None ? null : (
                 <div
@@ -457,7 +385,6 @@ export default class SharingManager extends React.Component<{}> {
                         <select
                             className={"permissions-dropdown"}
                             value={permissions}
-                            // style={{ color, borderColor: color }}
                             onChange={e => this.setInternalGroupSharing(group, e.currentTarget.value)}
                         >
                             {this.sharingOptions}
@@ -582,7 +509,6 @@ export default class SharingManager extends React.Component<{}> {
     }
 
     render() {
-        // console.log(this.sharingDoc);
         return (
             <MainViewModal
                 contents={this.sharingInterface}
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 0015e4bc7..8ef367a7c 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -831,17 +831,17 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         !Doc.UserDoc().novice && helpItems.push({ description: "Show Fields ", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), "onRight"), icon: "layer-group" });
         cm.addItem({ description: "Help...", noexpand: true, subitems: helpItems, icon: "question" });
 
-        const existingAcls = cm.findByDescription("Privacy...");
-        const aclItems: ContextMenuProps[] = existingAcls && "subitems" in existingAcls ? existingAcls.subitems : [];
-        aclItems.push({ description: "Make Add Only", event: () => this.setAcl("addOnly"), icon: "concierge-bell" });
-        aclItems.push({ description: "Make Read Only", event: () => this.setAcl("readOnly"), icon: "concierge-bell" });
-        aclItems.push({ description: "Make Private", event: () => this.setAcl("ownerOnly"), icon: "concierge-bell" });
-        aclItems.push({ description: "Make Editable", event: () => this.setAcl("write"), icon: "concierge-bell" });
-        aclItems.push({ description: "Test Private", event: () => this.testAcl("ownerOnly"), icon: "concierge-bell" });
-        aclItems.push({ description: "Test Readonly", event: () => this.testAcl("readOnly"), icon: "concierge-bell" });
-        !existingAcls && cm.addItem({ description: "Privacy...", subitems: aclItems, icon: "question" });
-
-        cm.addItem({ description: `${getPlaygroundMode() ? "Disable" : "Enable"} playground mode`, event: togglePlaygroundMode, icon: "concierge-bell" });
+        // const existingAcls = cm.findByDescription("Privacy...");
+        // const aclItems: ContextMenuProps[] = existingAcls && "subitems" in existingAcls ? existingAcls.subitems : [];
+        // aclItems.push({ description: "Make Add Only", event: () => this.setAcl(SharingPermissions.Add), icon: "concierge-bell" });
+        // aclItems.push({ description: "Make Read Only", event: () => this.setAcl(SharingPermissions.View), icon: "concierge-bell" });
+        // aclItems.push({ description: "Make Private", event: () => this.setAcl(SharingPermissions.None), icon: "concierge-bell" });
+        // aclItems.push({ description: "Make Editable", event: () => this.setAcl(SharingPermissions.Edit), icon: "concierge-bell" });
+        // aclItems.push({ description: "Test Private", event: () => this.testAcl(SharingPermissions.None), icon: "concierge-bell" });
+        // aclItems.push({ description: "Test Readonly", event: () => this.testAcl(SharingPermissions.View), icon: "concierge-bell" });
+        // !existingAcls && cm.addItem({ description: "Privacy...", subitems: aclItems, icon: "question" });
+
+        // cm.addItem({ description: `${getPlaygroundMode() ? "Disable" : "Enable"} playground mode`, event: togglePlaygroundMode, icon: "concierge-bell" });
 
         // const recommender_subitems: ContextMenuProps[] = [];
 
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 8ab4735a7..5dfc14a4a 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -109,7 +109,7 @@ const AclMap = new Map<string, symbol>([
 ]);
 
 export function fetchProto(doc: Doc) {
-    if (doc.author !== Doc.CurrentUserEmail) { // storing acls for groups needs to be extended here - AclSym should store a datastructure that stores information about permissions
+    if (doc.author !== Doc.CurrentUserEmail) {
         untracked(() => {
             const permissions: { [key: string]: symbol } = {};
 
-- 
cgit v1.2.3-70-g09d2


From 7543ca061700fda8286e6dd3f4374a877ccf929c Mon Sep 17 00:00:00 2001
From: usodhi <61431818+usodhi@users.noreply.github.com>
Date: Thu, 16 Jul 2020 13:00:12 +0530
Subject: can't change acl field to non-acl (through kvp) now + added
 playground mode button to settings

---
 src/client/util/SettingsManager.scss    |  2 ++
 src/client/util/SettingsManager.tsx     | 13 +++++++++++--
 src/client/util/SharingManager.tsx      |  2 --
 src/client/views/nodes/DocumentView.tsx |  2 +-
 src/fields/util.ts                      |  3 +++
 5 files changed, 17 insertions(+), 5 deletions(-)

(limited to 'src/client/views/nodes/DocumentView.tsx')

diff --git a/src/client/util/SettingsManager.scss b/src/client/util/SettingsManager.scss
index 6d394a38d..c1627e69f 100644
--- a/src/client/util/SettingsManager.scss
+++ b/src/client/util/SettingsManager.scss
@@ -52,6 +52,7 @@
     .settings-body {
         display: flex;
         justify-content: space-between;
+        margin-top: -10;
 
         .settings-type {
             display: flex;
@@ -105,6 +106,7 @@
         text-transform: uppercase;
         letter-spacing: 2px;
         font-size: 120%;
+        margin-top: 0;
     }
 
     .container {
diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx
index d54a39943..9d91568cf 100644
--- a/src/client/util/SettingsManager.tsx
+++ b/src/client/util/SettingsManager.tsx
@@ -14,8 +14,9 @@ import { Doc } from "../../fields/Doc";
 import GroupManager from "./GroupManager";
 import HypothesisAuthenticationManager from "../apis/HypothesisAuthenticationManager";
 import GoogleAuthenticationManager from "../apis/GoogleAuthenticationManager";
+import { togglePlaygroundMode } from "../../fields/util";
 
-library.add(fa.faWindowClose);
+library.add(fa.faTimes);
 
 @observer
 export default class SettingsManager extends React.Component<{}> {
@@ -26,6 +27,7 @@ export default class SettingsManager extends React.Component<{}> {
     @observable private settingsContent = "password";
     @observable private errorText = "";
     @observable private successText = "";
+    @observable private playgroundMode = false;
     private curr_password_ref = React.createRef<HTMLInputElement>();
     private new_password_ref = React.createRef<HTMLInputElement>();
     private new_confirm_ref = React.createRef<HTMLInputElement>();
@@ -95,19 +97,26 @@ export default class SettingsManager extends React.Component<{}> {
         HypothesisAuthenticationManager.Instance.fetchAccessToken(true)
     }
 
+    @action
+    togglePlaygroundMode = () => {
+        togglePlaygroundMode();
+        this.playgroundMode = !this.playgroundMode;
+    }
+
     private get settingsInterface() {
         return (
             <div className={"settings-interface"}>
                 <div className="settings-heading">
                     <h1>settings</h1>
                     <div className={"close-button"} onClick={this.close}>
-                        <FontAwesomeIcon icon={fa.faWindowClose} size={"lg"} />
+                        <FontAwesomeIcon icon={fa.faTimes} color="black" size={"lg"} />
                     </div>
                 </div>
                 <div className="settings-body">
                     <div className="settings-type">
                         <button onClick={this.onClick} value="password">reset password</button>
                         <button onClick={this.noviceToggle} value="data">{`Set ${Doc.UserDoc().noviceMode ? "developer" : "novice"} mode`}</button>
+                        <button onClick={this.togglePlaygroundMode}>{`${this.playgroundMode ? "Disable" : "Enable"} playground mode`}</button>
                         <button onClick={this.googleAuthorize} value="data">{`Link to Google`}</button>
                         <button onClick={this.hypothesisAuthorize} value="data">{`Link to Hypothes.is`}</button>
                         <button onClick={() => window.location.assign(Utils.prepend("/logout"))}>
diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx
index 8d4e508ac..fe7324d5c 100644
--- a/src/client/util/SharingManager.tsx
+++ b/src/client/util/SharingManager.tsx
@@ -193,8 +193,6 @@ export default class SharingManager extends React.Component<{}> {
         Doc.GetProto(target)[ACL] = permission;
 
         if (permission !== SharingPermissions.None) {
-            console.log(target);
-            console.log(notificationDoc);
             DocListCastAsync(notificationDoc[storage]).then(resolved => {
                 Doc.IndexOf(target, resolved!) === -1 && Doc.AddDocToList(notificationDoc, storage, target);
             });
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 8ef367a7c..ac62a67d7 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -12,7 +12,7 @@ import { listSpec } from "../../../fields/Schema";
 import { SchemaHeaderField } from '../../../fields/SchemaHeaderField';
 import { ScriptField } from '../../../fields/ScriptField';
 import { BoolCast, Cast, NumCast, StrCast } from "../../../fields/Types";
-import { TraceMobx, GetEffectiveAcl, getPlaygroundMode, togglePlaygroundMode } from '../../../fields/util';
+import { TraceMobx, GetEffectiveAcl } from '../../../fields/util';
 import { GestureUtils } from '../../../pen-gestures/GestureUtils';
 import { emptyFunction, OmitKeys, returnOne, returnTransparent, Utils, emptyPath } from "../../../Utils";
 import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils';
diff --git a/src/fields/util.ts b/src/fields/util.ts
index ea4966861..6d2d715bd 100644
--- a/src/fields/util.ts
+++ b/src/fields/util.ts
@@ -173,6 +173,9 @@ export function setter(target: any, in_prop: string | symbol | number, value: an
     if (GetEffectiveAcl(target, in_prop) !== AclEdit) {
         return true;
     }
+
+    if (typeof prop === "string" && prop.startsWith("ACL") && !["Can Edit", "Can Add", "Can View", "Not Shared", undefined].includes(value)) return true;
+
     if (typeof prop === "string" && prop !== "__id" && prop !== "__fields" && (prop.startsWith("_") || layoutProps.includes(prop))) {
         if (!prop.startsWith("_")) {
             console.log(prop + " is deprecated - switch to _" + prop);
-- 
cgit v1.2.3-70-g09d2


From 3d06cdd362d58dfbc8d6efdcd9dc59250ab003a4 Mon Sep 17 00:00:00 2001
From: usodhi <61431818+usodhi@users.noreply.github.com>
Date: Tue, 21 Jul 2020 23:16:49 +0530
Subject: distributeAcls only changes if container is more restrictive

---
 src/client/util/SharingManager.tsx              | 14 +++++---------
 src/client/views/DocComponent.tsx               |  2 +-
 src/client/views/collections/CollectionView.tsx |  6 +++---
 src/client/views/nodes/DocumentView.tsx         |  5 +++--
 src/fields/util.ts                              | 18 +++++++-----------
 5 files changed, 19 insertions(+), 26 deletions(-)

(limited to 'src/client/views/nodes/DocumentView.tsx')

diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx
index d3bc84770..9c857a7c0 100644
--- a/src/client/util/SharingManager.tsx
+++ b/src/client/util/SharingManager.tsx
@@ -1,7 +1,7 @@
-import { observable, runInAction, action, computed } from "mobx";
+import { observable, runInAction, action } from "mobx";
 import * as React from "react";
 import MainViewModal from "../views/MainViewModal";
-import { Doc, Opt, DocListCastAsync, DataSym, DocListCast } from "../../fields/Doc";
+import { Doc, Opt, DocListCastAsync } from "../../fields/Doc";
 import { DocServer } from "../DocServer";
 import { Cast, StrCast } from "../../fields/Types";
 import * as RequestPromise from "request-promise";
@@ -41,9 +41,9 @@ interface GroupOptions {
     options: UserOptions[];
 }
 
-const SharingKey = "sharingPermissions";
-const PublicKey = "publicLinkPermissions";
-const DefaultColor = "black";
+// const SharingKey = "sharingPermissions";
+// const PublicKey = "publicLinkPermissions";
+// const DefaultColor = "black";
 
 const groupType = "!groupType/";
 const indType = "!indType/";
@@ -192,12 +192,8 @@ export default class SharingManager extends React.Component<{}> {
         const { user, notificationDoc } = recipient;
         const target = this.targetDoc!;
         const key = user.email.replace('.', '_');
-
         const ACL = `ACL-${key}`;
 
-        // target[ACL] = permission;
-        // Doc.GetProto(target)[ACL] = permission;
-
         distributeAcls(ACL, permission as SharingPermissions, this.targetDoc!);
 
         if (permission !== SharingPermissions.None) {
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index 655be80ef..95c1bcda8 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -165,7 +165,7 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T
                         });
                     }
                     if (effectiveAcl === AclAddonly) {
-                        added.map(doc => console.log(Doc.AddDocToList(targetDataDoc, this.annotationKey, doc)));
+                        added.map(doc => Doc.AddDocToList(targetDataDoc, this.annotationKey, doc));
                     }
                     else {
                         added.map(doc => doc.context = this.props.Document);
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 5cef6c44e..9b04deff5 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -151,11 +151,11 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus
                 if (this.props.Document[AclSym]) {
                     // change so it only adds if more restrictive
                     added.forEach(d => {
-                        const dataDoc = d[DataSym];
+                        // const dataDoc = d[DataSym];
                         for (const [key, value] of Object.entries(this.props.Document[AclSym])) {
-                            distributeAcls(key, this.AclMap.get(value) as SharingPermissions, d);
+                            distributeAcls(key, this.AclMap.get(value) as SharingPermissions, d, true);
                         }
-                        dataDoc[AclSym] = d[AclSym] = this.props.Document[AclSym];
+                        // dataDoc[AclSym] = d[AclSym] = this.props.Document[AclSym];
                     });
                 }
 
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 803720417..0b5bd707b 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -4,7 +4,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { action, computed, observable, runInAction } from "mobx";
 import { observer } from "mobx-react";
 import * as rp from "request-promise";
-import { Doc, DocListCast, HeightSym, Opt, WidthSym, DataSym, AclPrivate, AclReadonly } from "../../../fields/Doc";
+import { Doc, DocListCast, HeightSym, Opt, WidthSym, DataSym, AclPrivate, AclEdit } from "../../../fields/Doc";
 import { Document } from '../../../fields/documentSchemas';
 import { Id } from '../../../fields/FieldSymbols';
 import { InkTool } from '../../../fields/InkField';
@@ -707,6 +707,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
             if (data && data.author === Doc.CurrentUserEmail) data.ACL = acl;
         });
     }
+
     @undoBatch
     @action
     testAcl = (acl: SharingPermissions) => {
@@ -806,7 +807,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
             });
             moreItems.push({ description: "Copy ID", event: () => Utils.CopyText(Utils.prepend("/doc/" + this.props.Document[Id])), icon: "fingerprint" });
         }
-        moreItems.push({ description: "Delete", event: this.deleteClicked, icon: "trash" });
+        GetEffectiveAcl(this.props.Document) === AclEdit && moreItems.push({ description: "Delete", event: this.deleteClicked, icon: "trash" });
         moreItems.push({ description: "Share", event: () => SharingManager.Instance.open(this), icon: "external-link-alt" });
         !more && cm.addItem({ description: "More...", subitems: moreItems, icon: "hand-point-right" });
         cm.moveAfter(cm.findByDescription("More...")!, cm.findByDescription("OnClick...")!);
diff --git a/src/fields/util.ts b/src/fields/util.ts
index a714b01e3..81ccbf6d9 100644
--- a/src/fields/util.ts
+++ b/src/fields/util.ts
@@ -137,14 +137,10 @@ export function GetEffectiveAcl(target: any, in_prop?: string | symbol | number)
 
     if (target[AclSym] && Object.keys(target[AclSym]).length) {
 
-        // console.log(target[AclSym]);
-
         if (target.__fields?.author === Doc.CurrentUserEmail || target.author === Doc.CurrentUserEmail || currentUserGroups.includes("admin")) return AclEdit;
 
         if (_overrideAcl || (in_prop && DocServer.PlaygroundFields?.includes(in_prop.toString()))) return AclEdit;
 
-        // if (target[AclSym].ACL) return target[AclSym].ACL;
-
         let effectiveAcl = AclPrivate;
         let aclPresent = false;
 
@@ -169,7 +165,7 @@ export function GetEffectiveAcl(target: any, in_prop?: string | symbol | number)
     return AclEdit;
 }
 
-export function distributeAcls(key: string, acl: SharingPermissions, target: Doc) {
+export function distributeAcls(key: string, acl: SharingPermissions, target: Doc, inheritingFromCollection?: boolean) {
 
     const HierarchyMapping = new Map<string, number>([
         ["Not Shared", 0],
@@ -180,30 +176,30 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc
 
     const dataDoc = target[DataSym];
 
-    if (!target[key] || HierarchyMapping.get(StrCast(target[key]))! < HierarchyMapping.get(acl)!) target[key] = acl;
+    if (!inheritingFromCollection || !target[key] || HierarchyMapping.get(StrCast(target[key]))! > HierarchyMapping.get(acl)!) target[key] = acl;
 
-    if (dataDoc && (!dataDoc[key] || HierarchyMapping.get(StrCast(dataDoc[key]))! < HierarchyMapping.get(acl)!)) {
+    if (dataDoc && (!inheritingFromCollection || !dataDoc[key] || HierarchyMapping.get(StrCast(dataDoc[key]))! > HierarchyMapping.get(acl)!)) {
         dataDoc[key] = acl;
 
         DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc)]).map(d => {
-            if (d.author === Doc.CurrentUserEmail && d[key] && HierarchyMapping.get(StrCast(d[key]))! < HierarchyMapping.get(acl)!) {
+            if (d.author === Doc.CurrentUserEmail && (!inheritingFromCollection || !d[key] || HierarchyMapping.get(StrCast(d[key]))! > HierarchyMapping.get(acl)!)) {
                 distributeAcls(key, acl, d);
                 d[key] = acl;
             }
             const data = d[DataSym];
-            if (data && data.author === Doc.CurrentUserEmail && data[key] && HierarchyMapping.get(StrCast(data[key]))! < HierarchyMapping.get(acl)!) {
+            if (data && data.author === Doc.CurrentUserEmail && (!inheritingFromCollection || !data[key] || HierarchyMapping.get(StrCast(data[key]))! > HierarchyMapping.get(acl)!)) {
                 distributeAcls(key, acl, data);
                 data[key] = acl;
             }
         });
 
         DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc) + "-annotations"]).map(d => {
-            if (d.author === Doc.CurrentUserEmail && d[key] && HierarchyMapping.get(StrCast(d[key]))! < HierarchyMapping.get(acl)!) {
+            if (d.author === Doc.CurrentUserEmail && (!inheritingFromCollection || !d[key] || HierarchyMapping.get(StrCast(d[key]))! > HierarchyMapping.get(acl)!)) {
                 distributeAcls(key, acl, d);
                 d[key] = acl;
             }
             const data = d[DataSym];
-            if (data && data.author === Doc.CurrentUserEmail && data[key] && HierarchyMapping.get(StrCast(data[key]))! < HierarchyMapping.get(acl)!) {
+            if (data && data.author === Doc.CurrentUserEmail && (!inheritingFromCollection || !data[key] || HierarchyMapping.get(StrCast(data[key]))! > HierarchyMapping.get(acl)!)) {
                 distributeAcls(key, acl, data);
                 data[key] = acl;
             }
-- 
cgit v1.2.3-70-g09d2


From 828a28e112db3357f52d3923987070129fd64b21 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Wed, 22 Jul 2020 11:33:28 -0400
Subject: restored download document.  fixed clone() to clone to be async and
 handles lists properly.

---
 src/client/util/CurrentUserUtils.ts                |  6 +++++
 src/client/util/LinkManager.ts                     | 12 ++-------
 src/client/util/Scripting.ts                       | 10 +++-----
 src/client/views/GlobalKeyHandler.ts               |  4 +--
 src/client/views/PreviewCursor.tsx                 |  4 +--
 .../collections/collectionFreeForm/MarqueeView.tsx |  8 +++---
 src/client/views/linking/LinkEditor.tsx            |  4 +--
 src/client/views/linking/LinkMenuItem.tsx          |  3 ++-
 src/client/views/nodes/DocumentView.tsx            | 14 ++++------
 src/client/views/nodes/LinkDocPreview.tsx          |  9 +------
 .../formattedText/FormattedTextBoxComment.tsx      |  2 ++
 .../views/nodes/formattedText/RichTextMenu.tsx     |  4 ++-
 src/fields/Doc.ts                                  | 30 +++++++++++-----------
 src/server/ApiManagers/DownloadManager.ts          | 12 +++------
 src/server/ApiManagers/UploadManager.ts            | 14 +++-------
 15 files changed, 56 insertions(+), 80 deletions(-)

(limited to 'src/client/views/nodes/DocumentView.tsx')

diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 03a75381a..23b8f09de 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -22,6 +22,7 @@ import { DocumentType } from "../documents/DocumentTypes";
 import { SchemaHeaderField } from "../../fields/SchemaHeaderField";
 import { DimUnit } from "../views/collections/collectionMulticolumn/CollectionMulticolumnView";
 import { LabelBox } from "../views/nodes/LabelBox";
+import { LinkManager } from "./LinkManager";
 
 export class CurrentUserUtils {
     private static curr_id: string;
@@ -888,3 +889,8 @@ export class CurrentUserUtils {
 
 Scripting.addGlobal(function createNewWorkspace() { return MainView.Instance.createNewWorkspace(); },
     "creates a new workspace when called");
+
+Scripting.addGlobal(function links(doc: any) { return new List(LinkManager.Instance.getAllRelatedLinks(doc)); },
+    "returns all the links to the document or its annotations", "(doc: any)");
+Scripting.addGlobal(function directLinks(doc: any) { return new List(LinkManager.Instance.getAllDirectLinks(doc)); },
+    "returns all the links directly to the document", "(doc: any)");
diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index 974744344..223f0e7ef 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -2,8 +2,6 @@ import { Doc, DocListCast, Opt } from "../../fields/Doc";
 import { List } from "../../fields/List";
 import { listSpec } from "../../fields/Schema";
 import { Cast, StrCast } from "../../fields/Types";
-import { Scripting } from "./Scripting";
-import { undoBatch } from "./UndoManager";
 
 /* 
  * link doc: 
@@ -25,12 +23,12 @@ export class LinkManager {
 
     private static _instance: LinkManager;
 
-
     public static currentLink: Opt<Doc>;
 
     public static get Instance(): LinkManager {
         return this._instance || (this._instance = new this());
     }
+
     private constructor() {
     }
 
@@ -53,7 +51,6 @@ export class LinkManager {
         return false;
     }
 
-    @undoBatch
     public deleteLink(linkDoc: Doc): boolean {
         if (LinkManager.Instance.LinkManagerDoc && linkDoc instanceof Doc) {
             Doc.RemoveDocFromList(LinkManager.Instance.LinkManagerDoc, "data", linkDoc);
@@ -210,9 +207,4 @@ export class LinkManager {
         if (Doc.AreProtosEqual(anchor, a2)) return a1;
         if (Doc.AreProtosEqual(anchor, linkDoc)) return linkDoc;
     }
-}
-
-Scripting.addGlobal(function links(doc: any) { return new List(LinkManager.Instance.getAllRelatedLinks(doc)); },
-    "returns all the links to the document or its annotations", "(doc: any)");
-Scripting.addGlobal(function directLinks(doc: any) { return new List(LinkManager.Instance.getAllDirectLinks(doc)); },
-    "returns all the links directly to the document", "(doc: any)");
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts
index e6cf50de3..f1e6155d2 100644
--- a/src/client/util/Scripting.ts
+++ b/src/client/util/Scripting.ts
@@ -10,8 +10,6 @@ export { ts };
 // @ts-ignore
 import * as typescriptlib from '!!raw-loader!./type_decls.d';
 import { Doc, Field } from '../../fields/Doc';
-import { Cast } from "../../fields/Types";
-import { listSpec } from "../../fields/Schema";
 
 export interface ScriptSucccess {
     success: true;
@@ -95,10 +93,10 @@ export namespace Scripting {
     export function removeGlobal(name: string) {
         if (getGlobals().includes(name)) {
             delete _scriptingGlobals[name];
-            if (_scriptingDescriptions[name]){
+            if (_scriptingDescriptions[name]) {
                 delete _scriptingDescriptions[name];
             }
-            if (_scriptingParams[name]){
+            if (_scriptingParams[name]) {
                 delete _scriptingParams[name];
             }
             return true;
@@ -123,11 +121,11 @@ export namespace Scripting {
         return _scriptingGlobals;
     }
 
-    export function getDescriptions(){
+    export function getDescriptions() {
         return _scriptingDescriptions;
     }
 
-    export function getParameters(){
+    export function getParameters() {
         return _scriptingParams;
     }
 }
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index 4dfa7aec8..b63537b5f 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -309,13 +309,13 @@ export default class KeyManager {
                 const targetDataDoc = Doc.GetProto(first.props.Document);
                 const fieldKey = Doc.LayoutFieldKey(first.props.Document);
                 const docList = DocListCast(targetDataDoc[fieldKey]);
-                docids.map((did, i) => i && DocServer.GetRefField(did).then(doc => {
+                docids.map((did, i) => i && DocServer.GetRefField(did).then(async doc => {
                     count++;
                     if (doc instanceof Doc) {
                         list.push(doc);
                     }
                     if (count === docids.length) {
-                        const added = list.filter(d => !docList.includes(d)).map(d => clone ? Doc.MakeClone(d) : d);
+                        const added = await Promise.all(list.filter(d => !docList.includes(d)).map(async d => clone ? await Doc.MakeClone(d) : d));
                         if (added.length) {
                             added.map(doc => doc.context = targetDataDoc);
                             undoBatch(() => {
diff --git a/src/client/views/PreviewCursor.tsx b/src/client/views/PreviewCursor.tsx
index 6583589f3..2d51403d7 100644
--- a/src/client/views/PreviewCursor.tsx
+++ b/src/client/views/PreviewCursor.tsx
@@ -69,11 +69,11 @@ export class PreviewCursor extends React.Component<{}> {
                     const list: Doc[] = [];
 
                     let first: Doc | undefined;
-                    docids.map((did, i) => i && DocServer.GetRefField(did).then(doc => {
+                    docids.map((did, i) => i && DocServer.GetRefField(did).then(async doc => {
                         count++;
                         if (doc instanceof Doc) {
                             i === 1 && (first = doc);
-                            const alias = clone ? Doc.MakeClone(doc) : doc;
+                            const alias = clone ? await Doc.MakeClone(doc) : doc;
                             const deltaX = NumCast(doc.x) - NumCast(first!.x) - ptx;
                             const deltaY = NumCast(doc.y) - NumCast(first!.y) - pty;
                             alias.x = newPoint[0] + deltaX;
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 6db8c8992..84719b2c9 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -189,8 +189,8 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
     onPointerDown = (e: React.PointerEvent): void => {
         this._downX = this._lastX = e.clientX;
         this._downY = this._lastY = e.clientY;
-        if (!(e as any).marqueeHit) {
-            (e as any).marqueeHit = true;
+        if (!(e.nativeEvent as any).marqueeHit) {
+            (e.nativeEvent as any).marqueeHit = true;
             // allow marquee if right click OR alt+left click OR space bar + left click
             if (e.button === 2 || (e.button === 0 && (e.altKey || (MarqueeView.DragMarquee && this.props.active(true))))) {
                 // if (e.altKey || (MarqueeView.DragMarquee && this.props.active(true))) {
@@ -291,8 +291,8 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
         if (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD &&
             Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD) {
             if (Doc.GetSelectedTool() === InkTool.None) {
-                if (!(e as any).marqueeHit) {
-                    (e as any).marqueeHit = true;
+                if (!(e.nativeEvent as any).marqueeHit) {
+                    (e.nativeEvent as any).marqueeHit = true;
                     !(e.nativeEvent as any).formattedHandled && this.setPreviewCursor(e.clientX, e.clientY, false);
                 }
             }
diff --git a/src/client/views/linking/LinkEditor.tsx b/src/client/views/linking/LinkEditor.tsx
index ed2bea722..04329182e 100644
--- a/src/client/views/linking/LinkEditor.tsx
+++ b/src/client/views/linking/LinkEditor.tsx
@@ -14,6 +14,7 @@ import { DocumentLinksButton } from "../nodes/DocumentLinksButton";
 import { EditableView } from "../EditableView";
 import { RefObject } from "react";
 import { Tooltip } from "@material-ui/core";
+import { undoBatch } from "../../util/UndoManager";
 
 library.add(faArrowLeft, faEllipsisV, faTable, faTrash, faCog, faExchangeAlt, faTimes, faPlus);
 
@@ -295,6 +296,7 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
 
     //@observable description = this.props.linkDoc.description ? StrCast(this.props.linkDoc.description) : "DESCRIPTION";
 
+    @undoBatch
     @action
     deleteLink = (): void => {
         LinkManager.Instance.deleteLink(this.props.linkDoc);
@@ -422,8 +424,6 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
                     </Tooltip>
                     <p className="linkEditor-linkedTo">Editing Link to: <b>{
                         destination.proto?.title ?? destination.title ?? "untitled"}</b></p>
-                    {/* <button className="linkEditor-button" onPointerDown={() => this.deleteLink()} title="Delete link">
-                        <FontAwesomeIcon icon="trash" size="sm" /></button> */}
                     <Tooltip title={<><div className="dash-tooltip">Show more link information</div></>} placement="top">
                         <div className="linkEditor-downArrow"><FontAwesomeIcon className="button" icon={this.infoIcon} size="lg" onPointerDown={this.changeInfo} /></div>
                     </Tooltip>
diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx
index 0e847632b..d8ba39f09 100644
--- a/src/client/views/linking/LinkMenuItem.tsx
+++ b/src/client/views/linking/LinkMenuItem.tsx
@@ -17,6 +17,7 @@ import { DocumentLinksButton } from '../nodes/DocumentLinksButton';
 import { LinkDocPreview } from '../nodes/LinkDocPreview';
 import { Tooltip } from '@material-ui/core';
 import { DocumentType } from '../../documents/DocumentTypes';
+import { undoBatch } from '../../util/UndoManager';
 library.add(faEye, faEdit, faTimes, faArrowRight, faChevronDown, faChevronUp, faPencilAlt, faEyeSlash);
 
 
@@ -163,10 +164,10 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
         }
     }
 
+    @undoBatch
     @action
     deleteLink = (): void => {
         LinkManager.Instance.deleteLink(this.props.linkDoc);
-        //this.props.showLinks();
         LinkDocPreview.LinkInfo = undefined;
         DocumentLinksButton.EditLink = undefined;
     }
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 0b5bd707b..12d9890c9 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -794,16 +794,12 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
             }
             moreItems.push({
                 description: "Download document", icon: "download", event: async () => {
-                    const response = await rp.get(Utils.CorsProxy("http://localhost:8983/solr/dash/select"), {
-                        qs: { q: 'world', fq: 'NOT baseProto_b:true AND NOT deleted:true', start: '0', rows: '100', hl: true, 'hl.fl': '*' }
-                    });
-                    console.log(response ? JSON.parse(response) : undefined);
+                    const a = document.createElement("a");
+                    const url = Utils.prepend(`/downloadId/${this.props.Document[Id]}`);
+                    a.href = url;
+                    a.download = `DocExport-${this.props.Document[Id]}.zip`;
+                    a.click();
                 }
-                // const a = document.createElement("a");
-                // const url = Utils.prepend(`/downloadId/${this.props.Document[Id]}`);
-                // a.href = url;
-                // a.download = `DocExport-${this.props.Document[Id]}.zip`;
-                // a.click();
             });
             moreItems.push({ description: "Copy ID", event: () => Utils.CopyText(Utils.prepend("/doc/" + this.props.Document[Id])), icon: "fingerprint" });
         }
diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx
index 1caa82380..ebb916307 100644
--- a/src/client/views/nodes/LinkDocPreview.tsx
+++ b/src/client/views/nodes/LinkDocPreview.tsx
@@ -15,6 +15,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { LinkManager } from '../../util/LinkManager';
 import { DocumentLinksButton } from './DocumentLinksButton';
 import { ContextMenu } from '../ContextMenu';
+import { undoBatch } from '../../util/UndoManager';
 
 interface Props {
     linkDoc?: Doc;
@@ -31,14 +32,6 @@ export class LinkDocPreview extends React.Component<Props> {
     @observable _toolTipText = "";
     _editRef = React.createRef<HTMLDivElement>();
 
-    @action
-    deleteLink = (): void => {
-        this.props.linkDoc ? LinkManager.Instance.deleteLink(this.props.linkDoc) : null;
-        //this.props.showLinks();
-        LinkDocPreview.LinkInfo = undefined;
-        DocumentLinksButton.EditLink = undefined;
-    }
-
     @action
     onContextMenu = (e: React.MouseEvent) => {
         DocumentLinksButton.EditLink = undefined;
diff --git a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx
index 3687d5513..6f3984f39 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx
@@ -22,6 +22,7 @@ import { LinkManager } from "../../../util/LinkManager";
 import { LinkDocPreview } from "../LinkDocPreview";
 import { DocumentLinksButton } from "../DocumentLinksButton";
 import { Tooltip } from "@material-ui/core";
+import { undoBatch } from "../../../util/UndoManager";
 
 export let formattedTextBoxCommentPlugin = new Plugin({
     view(editorView) { return new FormattedTextBoxComment(editorView); }
@@ -143,6 +144,7 @@ export class FormattedTextBoxComment {
         }
     }
 
+    @undoBatch
     @action
     deleteLink = () => {
         FormattedTextBoxComment.linkDoc ? LinkManager.Instance.deleteLink(FormattedTextBoxComment.linkDoc) : null;
diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx
index 2e0b0e659..47a4911b8 100644
--- a/src/client/views/nodes/formattedText/RichTextMenu.tsx
+++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx
@@ -23,7 +23,7 @@ import { updateBullets } from "./ProsemirrorExampleTransfer";
 import "./RichTextMenu.scss";
 import { schema } from "./schema_rts";
 import { TraceMobx } from "../../../../fields/util";
-import { UndoManager } from "../../../util/UndoManager";
+import { UndoManager, undoBatch } from "../../../util/UndoManager";
 import { Tooltip } from "@material-ui/core";
 const { toggleMark } = require("prosemirror-commands");
 
@@ -831,6 +831,8 @@ export default class RichTextMenu extends AntimodeMenu {
         ((this.view as any)?.TextView as FormattedTextBox).makeLinkToSelection("", target, "onRight", "", target);
     }
 
+    @undoBatch
+    @action
     deleteLink = () => {
         if (this.view) {
             const link = this.view.state.selection.$from.nodeAfter?.marks.find(m => m.type === this.view!.state.schema.marks.linkAnchor);
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 2f3b7025e..16ade5912 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -483,25 +483,25 @@ export namespace Doc {
         return alias;
     }
 
-
-
-    export function makeClone(doc: Doc, cloneMap: Map<string, Doc>, rtfs: { copy: Doc, key: string, field: RichTextField }[]): Doc {
+    export async function makeClone(doc: Doc, cloneMap: Map<string, Doc>, rtfs: { copy: Doc, key: string, field: RichTextField }[], exclusions: string[]): Promise<Doc> {
         if (Doc.IsBaseProto(doc)) return doc;
         if (cloneMap.get(doc[Id])) return cloneMap.get(doc[Id])!;
         const copy = new Doc(undefined, true);
         cloneMap.set(doc[Id], copy);
         if (LinkManager.Instance.getAllLinks().includes(doc) && LinkManager.Instance.getAllLinks().indexOf(copy) === -1) LinkManager.Instance.addLink(copy);
-        const exclude = Cast(doc.excludeFields, listSpec("string"), []);
-        Object.keys(doc).forEach(key => {
-            if (exclude.includes(key)) return;
+        const filter = Cast(doc.cloneFieldFilter, listSpec("string"), exclusions);
+        Object.keys(doc).forEach(async key => {
+            if (filter.includes(key)) return;
             const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key]));
             const field = ProxyField.WithoutProxy(() => doc[key]);
-            const copyObjectField = (field: ObjectField) => {
-                const list = Cast(doc[key], listSpec(Doc));
-                if (list !== undefined && !(list instanceof Promise)) {
-                    copy[key] = new List<Doc>(list.filter(d => d instanceof Doc).map(d => Doc.makeClone(d as Doc, cloneMap, rtfs)));
+            const copyObjectField = async (field: ObjectField) => {
+                const list = await Cast(doc[key], listSpec(Doc));
+                const docs = list && (await DocListCastAsync(list))?.filter(d => d instanceof Doc);
+                if (docs !== undefined && docs.length) {
+                    const clones = docs.map(async d => await Doc.makeClone(d as Doc, cloneMap, rtfs, exclusions));
+                    copy[key] = new List<Doc>(await Promise.all(clones));
                 } else if (doc[key] instanceof Doc) {
-                    copy[key] = key.includes("layout[") ? undefined : Doc.makeClone(doc[key] as Doc, cloneMap, rtfs); // reference documents except copy documents that are expanded teplate fields 
+                    copy[key] = key.includes("layout[") ? undefined : key.startsWith("layout") ? doc[key] as Doc : await Doc.makeClone(doc[key] as Doc, cloneMap, rtfs, exclusions); // reference documents except copy documents that are expanded teplate fields 
                 } else {
                     copy[key] = ObjectField.MakeCopy(field);
                     if (field instanceof RichTextField) {
@@ -513,7 +513,7 @@ export namespace Doc {
             };
             if (key === "proto") {
                 if (doc[key] instanceof Doc) {
-                    copy[key] = Doc.makeClone(doc[key]!, cloneMap, rtfs);
+                    copy[key] = await Doc.makeClone(doc[key]!, cloneMap, rtfs, exclusions);
                 }
             } else {
                 if (field instanceof RefField) {
@@ -535,10 +535,10 @@ export namespace Doc {
         cloneMap.set(doc[Id], copy);
         return copy;
     }
-    export function MakeClone(doc: Doc): Doc {
+    export async function MakeClone(doc: Doc): Promise<Doc> {
         const cloneMap = new Map<string, Doc>();
         const rtfMap: { copy: Doc, key: string, field: RichTextField }[] = [];
-        const copy = Doc.makeClone(doc, cloneMap, rtfMap);
+        const copy = await Doc.makeClone(doc, cloneMap, rtfMap, ["context", "annotationOn", "cloneOf"]);
         rtfMap.map(({ copy, key, field }) => {
             const replacer = (match: any, attr: string, id: string, offset: any, string: any) => {
                 const mapped = cloneMap.get(id);
@@ -657,7 +657,7 @@ export namespace Doc {
 
     export function MakeCopy(doc: Doc, copyProto: boolean = false, copyProtoId?: string): Doc {
         const copy = new Doc(copyProtoId, true);
-        const exclude = Cast(doc.excludeFields, listSpec("string"), []);
+        const exclude = Cast(doc.cloneFieldFilter, listSpec("string"), []);
         Object.keys(doc).forEach(key => {
             if (exclude.includes(key)) return;
             const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key]));
diff --git a/src/server/ApiManagers/DownloadManager.ts b/src/server/ApiManagers/DownloadManager.ts
index c5f3ca717..0d4472fdc 100644
--- a/src/server/ApiManagers/DownloadManager.ts
+++ b/src/server/ApiManagers/DownloadManager.ts
@@ -80,20 +80,14 @@ async function getDocs(id: string) {
         }
         const ids: string[] = [];
         for (const key in doc.fields) {
-            if (!doc.fields.hasOwnProperty(key)) {
-                continue;
-            }
+            if (!doc.fields.hasOwnProperty(key)) { continue; }
             const field = doc.fields[key];
-            if (field === undefined || field === null) {
-                continue;
-            }
+            if (field === undefined || field === null) { continue; }
 
             if (field.__type === "proxy" || field.__type === "prefetch_proxy") {
                 ids.push(field.fieldId);
             } else if (field.__type === "script" || field.__type === "computed") {
-                if (field.captures) {
-                    ids.push(field.captures.fieldId);
-                }
+                field.captures && ids.push(field.captures.fieldId);
             } else if (field.__type === "list") {
                 ids.push(...fn(field));
             } else if (typeof field === "string") {
diff --git a/src/server/ApiManagers/UploadManager.ts b/src/server/ApiManagers/UploadManager.ts
index 55ceab9fb..0b9e999ac 100644
--- a/src/server/ApiManagers/UploadManager.ts
+++ b/src/server/ApiManagers/UploadManager.ts
@@ -139,13 +139,9 @@ export default class UploadManager extends ApiManager {
                         doc.id = getId(doc.id);
                     }
                     for (const key in doc.fields) {
-                        if (!doc.fields.hasOwnProperty(key)) {
-                            continue;
-                        }
+                        if (!doc.fields.hasOwnProperty(key)) { continue; }
                         const field = doc.fields[key];
-                        if (field === undefined || field === null) {
-                            continue;
-                        }
+                        if (field === undefined || field === null) { continue; }
 
                         if (field.__type === "proxy" || field.__type === "prefetch_proxy") {
                             field.fieldId = getId(field.fieldId);
@@ -208,11 +204,7 @@ export default class UploadManager extends ApiManager {
                                 } catch (e) { console.log(e); }
                                 unlink(path_2, () => { });
                             }
-                            if (id) {
-                                res.send(JSON.stringify(getId(id)));
-                            } else {
-                                res.send(JSON.stringify("error"));
-                            }
+                            res.send(JSON.stringify(id ? getId(id) : "error"));
                         } catch (e) { console.log(e); }
                         resolve();
                     });
-- 
cgit v1.2.3-70-g09d2


From 8f95d52ed2b93930716ab6ed0ba9aab5c438876c Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Wed, 22 Jul 2020 20:16:53 -0400
Subject: fixed document download/import to capture links as well.

---
 package-lock.json                                  |  56 ++++++++++++
 package.json                                       |   3 +
 src/client/views/GlobalKeyHandler.ts               |   2 +-
 src/client/views/PreviewCursor.tsx                 |   2 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |   7 ++
 src/client/views/nodes/DocHolderBox.tsx            |   2 +-
 src/client/views/nodes/DocumentView.tsx            |  12 +--
 src/fields/Doc.ts                                  | 101 ++++++++++++++++-----
 src/server/ApiManagers/UploadManager.ts            |   2 +
 9 files changed, 155 insertions(+), 32 deletions(-)

(limited to 'src/client/views/nodes/DocumentView.tsx')

diff --git a/package-lock.json b/package-lock.json
index 1b39905cf..698bd60cc 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -711,6 +711,12 @@
                 "express-validator": "*"
             }
         },
+        "@types/file-saver": {
+            "version": "2.0.1",
+            "resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.1.tgz",
+            "integrity": "sha512-g1QUuhYVVAamfCifK7oB7G3aIl4BbOyzDOqVyUfEr4tfBKrXfeH+M+Tg7HKCXSrbzxYdhyCP7z9WbKo0R2hBCw==",
+            "dev": true
+        },
         "@types/formidable": {
             "version": "1.0.31",
             "resolved": "https://registry.npmjs.org/@types/formidable/-/formidable-1.0.31.tgz",
@@ -6069,6 +6075,11 @@
                 }
             }
         },
+        "file-saver": {
+            "version": "2.0.2",
+            "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.2.tgz",
+            "integrity": "sha512-Wz3c3XQ5xroCxd1G8b7yL0Ehkf0TC9oYC6buPFkNnU9EnaPlifeAFCyCh+iewXTyFRcg0a6j3J7FmJsIhlhBdw=="
+        },
         "file-uri-to-path": {
             "version": "1.0.0",
             "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
@@ -7280,6 +7291,11 @@
                 }
             }
         },
+        "immediate": {
+            "version": "3.0.6",
+            "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
+            "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps="
+        },
         "import-fresh": {
             "version": "3.2.1",
             "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz",
@@ -8160,6 +8176,33 @@
                 "promise": "^7.0.1"
             }
         },
+        "jszip": {
+            "version": "3.5.0",
+            "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.5.0.tgz",
+            "integrity": "sha512-WRtu7TPCmYePR1nazfrtuF216cIVon/3GWOvHS9QR5bIwSbnxtdpma6un3jyGGNhHsKCSzn5Ypk+EkDRvTGiFA==",
+            "requires": {
+                "lie": "~3.3.0",
+                "pako": "~1.0.2",
+                "readable-stream": "~2.3.6",
+                "set-immediate-shim": "~1.0.1"
+            },
+            "dependencies": {
+                "readable-stream": {
+                    "version": "2.3.7",
+                    "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
+                    "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
+                    "requires": {
+                        "core-util-is": "~1.0.0",
+                        "inherits": "~2.0.3",
+                        "isarray": "~1.0.0",
+                        "process-nextick-args": "~2.0.0",
+                        "safe-buffer": "~5.1.1",
+                        "string_decoder": "~1.1.1",
+                        "util-deprecate": "~1.0.1"
+                    }
+                }
+            }
+        },
         "jwa": {
             "version": "1.4.1",
             "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
@@ -8300,6 +8343,14 @@
                 }
             }
         },
+        "lie": {
+            "version": "3.3.0",
+            "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
+            "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
+            "requires": {
+                "immediate": "~3.0.5"
+            }
+        },
         "lines-and-columns": {
             "version": "1.1.6",
             "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz",
@@ -15142,6 +15193,11 @@
             "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
             "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc="
         },
+        "set-immediate-shim": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz",
+            "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E="
+        },
         "set-value": {
             "version": "2.0.1",
             "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz",
diff --git a/package.json b/package.json
index cb083020f..6c466825e 100644
--- a/package.json
+++ b/package.json
@@ -37,6 +37,7 @@
         "@types/express-flash": "0.0.0",
         "@types/express-session": "^1.15.16",
         "@types/express-validator": "^3.0.0",
+        "@types/file-saver": "^2.0.1",
         "@types/formidable": "^1.0.31",
         "@types/google-maps-react": "^2.0.5",
         "@types/jquery": "^3.5.0",
@@ -154,6 +155,7 @@
         "express-session": "^1.17.0",
         "express-validator": "^5.3.1",
         "expressjs": "^1.0.1",
+        "file-saver": "^2.0.2",
         "find-in-files": "^0.5.0",
         "fit-curve": "^0.1.7",
         "flexlayout-react": "^0.3.11",
@@ -172,6 +174,7 @@
         "image-size-stream": "^1.1.0",
         "js-datepicker": "^4.6.6",
         "jsonschema": "^1.2.5",
+        "jszip": "^3.5.0",
         "libxmljs": "^0.19.7",
         "lodash": "^4.17.15",
         "material-ui": "^0.20.2",
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index b63537b5f..086085db5 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -315,7 +315,7 @@ export default class KeyManager {
                         list.push(doc);
                     }
                     if (count === docids.length) {
-                        const added = await Promise.all(list.filter(d => !docList.includes(d)).map(async d => clone ? await Doc.MakeClone(d) : d));
+                        const added = await Promise.all(list.filter(d => !docList.includes(d)).map(async d => clone ? (await Doc.MakeClone(d)).clone : d));
                         if (added.length) {
                             added.map(doc => doc.context = targetDataDoc);
                             undoBatch(() => {
diff --git a/src/client/views/PreviewCursor.tsx b/src/client/views/PreviewCursor.tsx
index 2d51403d7..b4116e980 100644
--- a/src/client/views/PreviewCursor.tsx
+++ b/src/client/views/PreviewCursor.tsx
@@ -73,7 +73,7 @@ export class PreviewCursor extends React.Component<{}> {
                         count++;
                         if (doc instanceof Doc) {
                             i === 1 && (first = doc);
-                            const alias = clone ? await Doc.MakeClone(doc) : doc;
+                            const alias = clone ? (await Doc.MakeClone(doc)).clone : doc;
                             const deltaX = NumCast(doc.x) - NumCast(first!.x) - ptx;
                             const deltaY = NumCast(doc.y) - NumCast(first!.y) - pty;
                             alias.x = newPoint[0] + deltaX;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 412f91417..bee9e7009 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -46,6 +46,8 @@ import "./CollectionFreeFormView.scss";
 import MarqueeOptionsMenu from "./MarqueeOptionsMenu";
 import { MarqueeView } from "./MarqueeView";
 import React = require("react");
+import { SearchUtil } from "../../../util/SearchUtil";
+import { LinkManager } from "../../../util/LinkManager";
 
 library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload, faBraille, faChalkboard, faFileUpload);
 
@@ -1276,6 +1278,11 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
                                     const [xx, yy] = this.props.ScreenToLocalTransform().transformPoint(x, y);
                                     doc.x = xx, doc.y = yy;
                                     this.props.addDocument?.(doc);
+                                    setTimeout(() => {
+                                        SearchUtil.Search(`{!join from=id to=proto_i}id:link*`, true, {}).then(docs => {
+                                            docs.docs.forEach(d => LinkManager.Instance.addLink(d));
+                                        })
+                                    }, 2000); // need to give solr some time to update so that this query will find any link docs we've added.
                                 }
                             }
                         }
diff --git a/src/client/views/nodes/DocHolderBox.tsx b/src/client/views/nodes/DocHolderBox.tsx
index 0cf5505cc..0c4242172 100644
--- a/src/client/views/nodes/DocHolderBox.tsx
+++ b/src/client/views/nodes/DocHolderBox.tsx
@@ -180,7 +180,7 @@ export class DocHolderBox extends ViewBoxAnnotatableComponent<FieldViewProps, Do
     render() {
         const containedDoc = Cast(this.dataDoc[this.fieldKey], Doc, null);
         TraceMobx();
-        return <div className="documentBox-container" ref={this._contRef}
+        return !containedDoc ? (null) : <div className="documentBox-container" ref={this._contRef}
             onContextMenu={this.specificContextMenu}
             onPointerDown={this.onPointerDown} onClick={this.onClick}
             style={{
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 12d9890c9..3b46b70ea 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -3,7 +3,6 @@ import * as fa from '@fortawesome/free-solid-svg-icons';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { action, computed, observable, runInAction } from "mobx";
 import { observer } from "mobx-react";
-import * as rp from "request-promise";
 import { Doc, DocListCast, HeightSym, Opt, WidthSym, DataSym, AclPrivate, AclEdit } from "../../../fields/Doc";
 import { Document } from '../../../fields/documentSchemas';
 import { Id } from '../../../fields/FieldSymbols';
@@ -794,11 +793,12 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
             }
             moreItems.push({
                 description: "Download document", icon: "download", event: async () => {
-                    const a = document.createElement("a");
-                    const url = Utils.prepend(`/downloadId/${this.props.Document[Id]}`);
-                    a.href = url;
-                    a.download = `DocExport-${this.props.Document[Id]}.zip`;
-                    a.click();
+                    Doc.Zip(this.props.Document);
+                    // const a = document.createElement("a");
+                    // const url = Utils.prepend(`/downloadId/${this.props.Document[Id]}`);
+                    // a.href = url;
+                    // a.download = `DocExport-${this.props.Document[Id]}.zip`;
+                    // a.click();
                 }
             });
             moreItems.push({ description: "Copy ID", event: () => Utils.CopyText(Utils.prepend("/doc/" + this.props.Document[Id])), icon: "fingerprint" });
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 16ade5912..917a6853c 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -1,6 +1,6 @@
 import { action, computed, observable, ObservableMap, runInAction, untracked } from "mobx";
 import { computedFn } from "mobx-utils";
-import { alias, map, serializable } from "serializr";
+import { alias, map, serializable, list } from "serializr";
 import { DocServer } from "../client/DocServer";
 import { DocumentType } from "../client/documents/DocumentTypes";
 import { Scripting, scriptingGlobal } from "../client/util/Scripting";
@@ -14,12 +14,16 @@ import { ObjectField } from "./ObjectField";
 import { PrefetchProxy, ProxyField } from "./Proxy";
 import { FieldId, RefField } from "./RefField";
 import { RichTextField } from "./RichTextField";
+import { ImageField, VideoField, WebField, AudioField, PdfField } from "./URLField";
+import { DateField } from "./DateField";
 import { listSpec } from "./Schema";
 import { ComputedField } from "./ScriptField";
 import { Cast, FieldValue, NumCast, StrCast, ToConstructor } from "./Types";
 import { deleteProperty, getField, getter, makeEditable, makeReadOnly, setter, updateFunction, GetEffectiveAcl } from "./util";
 import { LinkManager } from "../client/util/LinkManager";
 import { SharingPermissions } from "../client/util/SharingManager";
+import JSZip = require("jszip");
+import { saveAs } from "file-saver";
 
 export namespace Field {
     export function toKeyValueString(doc: Doc, key: string): string {
@@ -93,6 +97,7 @@ export const WidthSym = Symbol("Width");
 export const HeightSym = Symbol("Height");
 export const DataSym = Symbol("Data");
 export const LayoutSym = Symbol("Layout");
+export const FieldsSym = Symbol("Fields");
 export const AclSym = Symbol("Acl");
 export const AclPrivate = Symbol("AclOwnerOnly");
 export const AclReadonly = Symbol("AclReadOnly");
@@ -180,7 +185,6 @@ export class Doc extends RefField {
     }
 
     @observable
-    //{ [key: string]: Field | FieldWaiting | undefined }
     private ___fields: any = {};
 
     private [UpdatingFromServer]: boolean = false;
@@ -191,6 +195,7 @@ export class Doc extends RefField {
 
     private [Self] = this;
     private [SelfProxy]: any;
+    public [FieldsSym] = () => this.___fields;
     public [AclSym]: { [key: string]: symbol };
     public [WidthSym] = () => NumCast(this[SelfProxy]._width);
     public [HeightSym] = () => NumCast(this[SelfProxy]._height);
@@ -483,27 +488,28 @@ export namespace Doc {
         return alias;
     }
 
-    export async function makeClone(doc: Doc, cloneMap: Map<string, Doc>, rtfs: { copy: Doc, key: string, field: RichTextField }[], exclusions: string[]): Promise<Doc> {
+    export async function makeClone(doc: Doc, cloneMap: Map<string, Doc>, rtfs: { copy: Doc, key: string, field: RichTextField }[], exclusions: string[], dontCreate: boolean): Promise<Doc> {
         if (Doc.IsBaseProto(doc)) return doc;
         if (cloneMap.get(doc[Id])) return cloneMap.get(doc[Id])!;
-        const copy = new Doc(undefined, true);
+        const copy = dontCreate ? doc : new Doc(undefined, true);
         cloneMap.set(doc[Id], copy);
         if (LinkManager.Instance.getAllLinks().includes(doc) && LinkManager.Instance.getAllLinks().indexOf(copy) === -1) LinkManager.Instance.addLink(copy);
         const filter = Cast(doc.cloneFieldFilter, listSpec("string"), exclusions);
-        Object.keys(doc).forEach(async key => {
+        await Promise.all(Object.keys(doc).map(async key => {
             if (filter.includes(key)) return;
+            const assignKey = (val: any) => !dontCreate && (copy[key] = val);
             const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key]));
             const field = ProxyField.WithoutProxy(() => doc[key]);
             const copyObjectField = async (field: ObjectField) => {
                 const list = await Cast(doc[key], listSpec(Doc));
                 const docs = list && (await DocListCastAsync(list))?.filter(d => d instanceof Doc);
                 if (docs !== undefined && docs.length) {
-                    const clones = docs.map(async d => await Doc.makeClone(d as Doc, cloneMap, rtfs, exclusions));
-                    copy[key] = new List<Doc>(await Promise.all(clones));
+                    const clones = await Promise.all(docs.map(async d => await Doc.makeClone(d as Doc, cloneMap, rtfs, exclusions, dontCreate)));
+                    !dontCreate && assignKey(new List<Doc>(clones));
                 } else if (doc[key] instanceof Doc) {
-                    copy[key] = key.includes("layout[") ? undefined : key.startsWith("layout") ? doc[key] as Doc : await Doc.makeClone(doc[key] as Doc, cloneMap, rtfs, exclusions); // reference documents except copy documents that are expanded teplate fields 
+                    assignKey(key.includes("layout[") ? undefined : key.startsWith("layout") ? doc[key] as Doc : await Doc.makeClone(doc[key] as Doc, cloneMap, rtfs, exclusions, dontCreate)); // reference documents except copy documents that are expanded teplate fields 
                 } else {
-                    copy[key] = ObjectField.MakeCopy(field);
+                    assignKey(ObjectField.MakeCopy(field));
                     if (field instanceof RichTextField) {
                         if (field.Data.includes('"docid":') || field.Data.includes('"targetId":') || field.Data.includes('"linkId":')) {
                             rtfs.push({ copy, key, field });
@@ -513,32 +519,34 @@ export namespace Doc {
             };
             if (key === "proto") {
                 if (doc[key] instanceof Doc) {
-                    copy[key] = await Doc.makeClone(doc[key]!, cloneMap, rtfs, exclusions);
+                    assignKey(await Doc.makeClone(doc[key]!, cloneMap, rtfs, exclusions, dontCreate));
                 }
             } else {
                 if (field instanceof RefField) {
-                    copy[key] = field;
+                    assignKey(field);
                 } else if (cfield instanceof ComputedField) {
-                    copy[key] = ComputedField.MakeFunction(cfield.script.originalScript);
-                    (key === "links" && field instanceof ObjectField) && copyObjectField(field);
+                    !dontCreate && assignKey(ComputedField.MakeFunction(cfield.script.originalScript));
+                    (key === "links" && field instanceof ObjectField) && await copyObjectField(field);
                 } else if (field instanceof ObjectField) {
-                    copyObjectField(field);
+                    await copyObjectField(field);
                 } else if (field instanceof Promise) {
                     debugger; //This shouldn't happend...
                 } else {
-                    copy[key] = field;
+                    assignKey(field);
                 }
             }
-        });
-        Doc.SetInPlace(copy, "title", "CLONE: " + doc.title, true);
-        copy.cloneOf = doc;
-        cloneMap.set(doc[Id], copy);
+        }));
+        if (!dontCreate) {
+            Doc.SetInPlace(copy, "title", "CLONE: " + doc.title, true);
+            copy.cloneOf = doc;
+            cloneMap.set(doc[Id], copy);
+        }
         return copy;
     }
-    export async function MakeClone(doc: Doc): Promise<Doc> {
+    export async function MakeClone(doc: Doc, dontCreate: boolean = false) {
         const cloneMap = new Map<string, Doc>();
         const rtfMap: { copy: Doc, key: string, field: RichTextField }[] = [];
-        const copy = await Doc.makeClone(doc, cloneMap, rtfMap, ["context", "annotationOn", "cloneOf"]);
+        const copy = await Doc.makeClone(doc, cloneMap, rtfMap, ["context", "annotationOn", "cloneOf"], dontCreate);
         rtfMap.map(({ copy, key, field }) => {
             const replacer = (match: any, attr: string, id: string, offset: any, string: any) => {
                 const mapped = cloneMap.get(id);
@@ -552,9 +560,56 @@ export namespace Doc {
             const re = new RegExp(regex, "g");
             copy[key] = new RichTextField(field.Data.replace(/("docid":|"targetId":|"linkId":)"([^"]+)"/g, replacer).replace(re, replacer2), field.Text);
         });
-        return copy;
-    }
+        return { clone: copy, map: cloneMap };
+    }
+
+    export async function Zip(doc: Doc) {
+        const { clone, map } = await Doc.MakeClone(doc, true);
+        function replacer(key: any, value: any) {
+            console.log("Checkin: " + key);
+            if (["cloneOf", "context", "cursors"].includes(key)) return undefined;
+            else if (value instanceof Doc) {
+                if (key !== "field" && Number.isNaN(Number(key))) {
+                    const __fields = value[FieldsSym]();
+                    return { id: value[Id], __type: "Doc", fields: __fields };
+                } else {
+                    return { fieldId: value[Id], __type: "proxy" };
+                }
+            }
+            else if (value instanceof RichTextField) return { Data: value.Data, Text: value.Text, __type: "RichTextField" };
+            else if (value instanceof ImageField) return { url: value.url.href, __type: "image" };
+            else if (value instanceof PdfField) return { url: value.url.href, __type: "pdf" };
+            else if (value instanceof AudioField) return { url: value.url.href, __type: "audio" };
+            else if (value instanceof VideoField) return { url: value.url.href, __type: "video" };
+            else if (value instanceof WebField) return { url: value.url.href, __type: "web" };
+            else if (value instanceof DateField) return { date: value.toString(), __type: "date" };
+            else if (value instanceof ProxyField) return { fieldId: value.fieldId, __type: "proxy" };
+            else if (value instanceof Array && key !== "fields") return { fields: value, __type: "list" };
+            else if (value instanceof ComputedField) return { script: value.script, __type: "computed" };
+            else return value;
+        }
+
+        const docs: { [id: string]: any } = {};
+        Array.from(map.entries()).forEach(f => docs[f[0]] = f[1]);
+        const docString = JSON.stringify({ id: doc[Id], docs }, replacer);
+
+        var zip = new JSZip();
 
+        zip.file("doc.json", docString);
+
+        // // Generate a directory within the Zip file structure
+        // var img = zip.folder("images");
+
+        // // Add a file to the directory, in this case an image with data URI as contents
+        // img.file("smile.gif", imgData, {base64: true});
+
+        // Generate the zip file asynchronously
+        zip.generateAsync({ type: "blob" })
+            .then((content: any) => {
+                // Force down of the Zip file
+                saveAs(content, "download.zip");
+            });
+    }
     //
     // Determines whether the layout needs to be expanded (as a template).
     // template expansion is rquired when the layout is a template doc/field and there's a datadoc which isn't equal to the layout template
diff --git a/src/server/ApiManagers/UploadManager.ts b/src/server/ApiManagers/UploadManager.ts
index 0b9e999ac..4455d11eb 100644
--- a/src/server/ApiManagers/UploadManager.ts
+++ b/src/server/ApiManagers/UploadManager.ts
@@ -16,6 +16,7 @@ const imageDataUri = require('image-data-uri');
 import { isWebUri } from "valid-url";
 import { launch } from "puppeteer";
 import { Opt } from "../../fields/Doc";
+import { SolrManager } from "./SearchManager";
 
 export enum Directory {
     parsed_files = "parsed_files",
@@ -204,6 +205,7 @@ export default class UploadManager extends ApiManager {
                                 } catch (e) { console.log(e); }
                                 unlink(path_2, () => { });
                             }
+                            SolrManager.update();
                             res.send(JSON.stringify(id ? getId(id) : "error"));
                         } catch (e) { console.log(e); }
                         resolve();
-- 
cgit v1.2.3-70-g09d2


From 49e1dccee78af835ef48723bb708f1c2b47c9228 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Wed, 22 Jul 2020 21:09:20 -0400
Subject: fixed highlighting of text based on author.  made download /import
 document novice menu items.

---
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 69 +++++++++++-----------
 src/client/views/nodes/DocumentView.tsx            | 20 +++----
 .../views/nodes/formattedText/FormattedTextBox.tsx | 15 ++---
 src/client/views/nodes/formattedText/marks_rts.ts  | 10 ++--
 4 files changed, 58 insertions(+), 56 deletions(-)

(limited to 'src/client/views/nodes/DocumentView.tsx')

diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index bee9e7009..dc32ecb07 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1258,41 +1258,44 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
         if (!Doc.UserDoc().noviceMode) {
             optionItems.push({ description: (!this.layoutDoc._nativeWidth || !this.layoutDoc._nativeHeight ? "Freeze" : "Unfreeze") + " Aspect", event: this.toggleNativeDimensions, icon: "snowflake" });
             optionItems.push({ description: `${this.Document._freeformLOD ? "Enable LOD" : "Disable LOD"}`, event: () => this.Document._freeformLOD = !this.Document._freeformLOD, icon: "table" });
-            optionItems.push({
-                description: "Import document", icon: "upload", event: ({ x, y }) => {
-                    const input = document.createElement("input");
-                    input.type = "file";
-                    input.accept = ".zip";
-                    input.onchange = async _e => {
-                        const upload = Utils.prepend("/uploadDoc");
-                        const formData = new FormData();
-                        const file = input.files && input.files[0];
-                        if (file) {
-                            formData.append('file', file);
-                            formData.append('remap', "true");
-                            const response = await fetch(upload, { method: "POST", body: formData });
-                            const json = await response.json();
-                            if (json !== "error") {
-                                const doc = await DocServer.GetRefField(json);
-                                if (doc instanceof Doc) {
-                                    const [xx, yy] = this.props.ScreenToLocalTransform().transformPoint(x, y);
-                                    doc.x = xx, doc.y = yy;
-                                    this.props.addDocument?.(doc);
-                                    setTimeout(() => {
-                                        SearchUtil.Search(`{!join from=id to=proto_i}id:link*`, true, {}).then(docs => {
-                                            docs.docs.forEach(d => LinkManager.Instance.addLink(d));
-                                        })
-                                    }, 2000); // need to give solr some time to update so that this query will find any link docs we've added.
-                                }
-                            }
-                        }
-                    };
-                    input.click();
-                }
-            });
+
         }
         !options && ContextMenu.Instance.addItem({ description: "Options...", subitems: optionItems, icon: "eye" });
-
+        const mores = ContextMenu.Instance.findByDescription("More...");
+        const moreItems = mores && "subitems" in mores ? mores.subitems : [];
+        moreItems.push({
+            description: "Import document", icon: "upload", event: ({ x, y }) => {
+                const input = document.createElement("input");
+                input.type = "file";
+                input.accept = ".zip";
+                input.onchange = async _e => {
+                    const upload = Utils.prepend("/uploadDoc");
+                    const formData = new FormData();
+                    const file = input.files && input.files[0];
+                    if (file) {
+                        formData.append('file', file);
+                        formData.append('remap', "true");
+                        const response = await fetch(upload, { method: "POST", body: formData });
+                        const json = await response.json();
+                        if (json !== "error") {
+                            const doc = await DocServer.GetRefField(json);
+                            if (doc instanceof Doc) {
+                                const [xx, yy] = this.props.ScreenToLocalTransform().transformPoint(x, y);
+                                doc.x = xx, doc.y = yy;
+                                this.props.addDocument?.(doc);
+                                setTimeout(() => {
+                                    SearchUtil.Search(`{!join from=id to=proto_i}id:link*`, true, {}).then(docs => {
+                                        docs.docs.forEach(d => LinkManager.Instance.addLink(d));
+                                    })
+                                }, 2000); // need to give solr some time to update so that this query will find any link docs we've added.
+                            }
+                        }
+                    }
+                };
+                input.click();
+            }
+        });
+        !mores && ContextMenu.Instance.addItem({ description: "More...", subitems: moreItems, icon: "eye" });
     }
     @observable showTimeline = false;
 
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 3b46b70ea..748938699 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -782,6 +782,16 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
 
         const more = cm.findByDescription("More...");
         const moreItems = more && "subitems" in more ? more.subitems : [];
+        moreItems.push({
+            description: "Download document", icon: "download", event: async () => {
+                Doc.Zip(this.props.Document);
+                // const a = document.createElement("a");
+                // const url = Utils.prepend(`/downloadId/${this.props.Document[Id]}`);
+                // a.href = url;
+                // a.download = `DocExport-${this.props.Document[Id]}.zip`;
+                // a.click();
+            }
+        });
         if (!Doc.UserDoc().noviceMode) {
             moreItems.push({ description: "Make View of Metadata Field", event: () => Doc.MakeMetadataFieldTemplate(this.props.Document, this.props.DataDoc), icon: "concierge-bell" });
             moreItems.push({ description: `${this.Document._chromeStatus !== "disabled" ? "Hide" : "Show"} Chrome`, event: () => this.Document._chromeStatus = (this.Document._chromeStatus !== "disabled" ? "disabled" : "enabled"), icon: "project-diagram" });
@@ -791,16 +801,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                 moreItems.push({ description: "Tag Child Images via Google Photos", event: () => GooglePhotos.Query.TagChildImages(this.props.Document), icon: "caret-square-right" });
                 moreItems.push({ description: "Write Back Link to Album", event: () => GooglePhotos.Transactions.AddTextEnrichment(this.props.Document), icon: "caret-square-right" });
             }
-            moreItems.push({
-                description: "Download document", icon: "download", event: async () => {
-                    Doc.Zip(this.props.Document);
-                    // const a = document.createElement("a");
-                    // const url = Utils.prepend(`/downloadId/${this.props.Document[Id]}`);
-                    // a.href = url;
-                    // a.download = `DocExport-${this.props.Document[Id]}.zip`;
-                    // a.click();
-                }
-            });
             moreItems.push({ description: "Copy ID", event: () => Utils.CopyText(Utils.prepend("/doc/" + this.props.Document[Id])), icon: "fingerprint" });
         }
         GetEffectiveAcl(this.props.Document) === AclEdit && moreItems.push({ description: "Delete", event: this.deleteClicked, icon: "trash" });
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 7906e2533..e703a81e2 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -425,16 +425,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
             addStyleSheetRule(FormattedTextBox._userStyleSheet, "UM-" + Doc.CurrentUserEmail.replace(".", "").replace("@", ""), { background: "moccasin" });
         }
         if (FormattedTextBox._highlights.indexOf("Todo Items") !== -1) {
-            addStyleSheetRule(FormattedTextBox._userStyleSheet, "userTag-" + "todo", { outline: "black solid 1px" });
+            addStyleSheetRule(FormattedTextBox._userStyleSheet, "UT-" + "todo", { outline: "black solid 1px" });
         }
         if (FormattedTextBox._highlights.indexOf("Important Items") !== -1) {
-            addStyleSheetRule(FormattedTextBox._userStyleSheet, "userTag-" + "important", { "font-size": "larger" });
+            addStyleSheetRule(FormattedTextBox._userStyleSheet, "UT-" + "important", { "font-size": "larger" });
         }
         if (FormattedTextBox._highlights.indexOf("Disagree Items") !== -1) {
-            addStyleSheetRule(FormattedTextBox._userStyleSheet, "userTag-" + "disagree", { "text-decoration": "line-through" });
+            addStyleSheetRule(FormattedTextBox._userStyleSheet, "UT-" + "disagree", { "text-decoration": "line-through" });
         }
         if (FormattedTextBox._highlights.indexOf("Ignore Items") !== -1) {
-            addStyleSheetRule(FormattedTextBox._userStyleSheet, "userTag-" + "ignore", { "font-size": "1" });
+            addStyleSheetRule(FormattedTextBox._userStyleSheet, "UT-" + "ignore", { "font-size": "1" });
         }
         if (FormattedTextBox._highlights.indexOf("By Recent Minute") !== -1) {
             addStyleSheetRule(FormattedTextBox._userStyleSheet, "UM-" + Doc.CurrentUserEmail.replace(".", "").replace("@", ""), { opacity: "0.1" });
@@ -1303,9 +1303,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
         if (e.key === "Tab" || e.key === "Enter") {
             e.preventDefault();
         }
-        const mark = e.key !== " " && this._lastTimedMark ? this._lastTimedMark : schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) });
-        this._lastTimedMark = mark;
-        // this._editorView!.dispatch(this._editorView!.state.tr.removeStoredMark(schema.marks.user_mark.create({})).addStoredMark(mark));
+        if (e.key === " " || this._lastTimedMark?.attrs.userid !== Doc.CurrentUserEmail) {
+            const mark = schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) });
+            this._editorView!.dispatch(this._editorView!.state.tr.removeStoredMark(schema.marks.user_mark.create({})).addStoredMark(mark));
+        }
 
         if (!this._undoTyping) {
             this.startUndoTypingBatch();
diff --git a/src/client/views/nodes/formattedText/marks_rts.ts b/src/client/views/nodes/formattedText/marks_rts.ts
index 3d7d71b14..f95f46104 100644
--- a/src/client/views/nodes/formattedText/marks_rts.ts
+++ b/src/client/views/nodes/formattedText/marks_rts.ts
@@ -258,9 +258,7 @@ export const marks: { [index: string]: MarkSpec } = {
         },
         parseDOM: [{ style: 'background: yellow' }],
         toDOM(node: any) {
-            return ['span', {
-                style: `background: ${node.attrs.selected ? "orange" : "yellow"}`
-            }];
+            return ['span', { style: `background: ${node.attrs.selected ? "orange" : "yellow"}` }];
         }
     },
 
@@ -277,8 +275,8 @@ export const marks: { [index: string]: MarkSpec } = {
             const min = Math.round(node.attrs.modified / 12);
             const hr = Math.round(min / 60);
             const day = Math.round(hr / 60 / 24);
-            const remote = node.attrs.userid !== Doc.CurrentUserEmail ? " userMark-remote" : "";
-            return ['span', { class: "userMark-" + uid + remote + " userMark-min-" + min + " userMark-hr-" + hr + " userMark-day-" + day }, 0];
+            const remote = node.attrs.userid !== Doc.CurrentUserEmail ? " UM-remote" : "";
+            return ['span', { class: "UM-" + uid + remote + " UM-min-" + min + " UM-hr-" + hr + " UM-day-" + day }, 0];
         }
     },
     // the id of the user who entered the text
@@ -292,7 +290,7 @@ export const marks: { [index: string]: MarkSpec } = {
         inclusive: false,
         toDOM(node: any) {
             const uid = node.attrs.userid.replace(".", "").replace("@", "");
-            return ['span', { class: "userTag-" + uid + " userTag-" + node.attrs.tag }, 0];
+            return ['span', { class: "UT-" + uid + " UT-" + node.attrs.tag }, 0];
         }
     },
 
-- 
cgit v1.2.3-70-g09d2

