From 68916ee8daafb03ad2aff3338e18ca419ce888de Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 6 Oct 2020 12:15:49 -0400
Subject: fixed adding documents to check acl of added document(s) correctly

---
 src/client/views/collections/CollectionView.tsx | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

(limited to 'src/client/views/collections/CollectionView.tsx')

diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 2bdc8e2f3..0ef3dd6cd 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -157,7 +157,7 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus
                     });
                 }
                 else {
-                    added.map(doc => {
+                    added.filter(doc => [AclAdmin, AclEdit].includes(GetEffectiveAcl(doc))).map(doc => {  // only make a pushpin if we have acl's to edit the document
                         const context = Cast(doc.context, Doc, null);
                         if (context && (context.type === DocumentType.VID || context.type === DocumentType.WEB || context.type === DocumentType.PDF || context.type === DocumentType.IMG)) {
                             const pushpin = Docs.Create.FontIconDocument({
@@ -186,9 +186,9 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus
     @action.bound
     removeDocument = (doc: any): boolean => {
         const effectiveAcl = GetEffectiveAcl(this.props.Document[DataSym]);
-        const docAcl = GetEffectiveAcl(doc);
-        if (effectiveAcl === AclEdit || effectiveAcl === AclAdmin || docAcl === AclAdmin) {
-            const docs = doc instanceof Doc ? [doc] : doc as Doc[];
+        const indocs = doc instanceof Doc ? [doc] : doc as Doc[];
+        const docs = indocs.filter(doc => effectiveAcl === AclEdit || effectiveAcl === AclAdmin || GetEffectiveAcl(doc) === AclAdmin);
+        if (docs.length) {
             const targetDataDoc = this.props.Document[DataSym];
             const value = DocListCast(targetDataDoc[this.props.fieldKey]);
             const toRemove = value.filter(v => docs.includes(v));
-- 
cgit v1.2.3-70-g09d2


From e66380d8a99c3ee61faeaaebf6e28395332e1d64 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 6 Oct 2020 14:12:06 -0400
Subject: fixed list additions to be truly incremental to allow undo to work

---
 src/client/views/collections/CollectionView.tsx |  1 -
 src/fields/List.ts                              |  2 +-
 src/fields/util.ts                              | 28 ++++++++++++++++---------
 src/server/websocket.ts                         | 10 +++++----
 4 files changed, 25 insertions(+), 16 deletions(-)

(limited to 'src/client/views/collections/CollectionView.tsx')

diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 0ef3dd6cd..80e9b41ad 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -196,7 +196,6 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus
                 const recent = Cast(Doc.UserDoc().myRecentlyClosedDocs, Doc) as Doc;
                 toRemove.forEach(doc => {
                     const ind = (targetDataDoc[this.props.fieldKey] as List<Doc>).indexOf(doc);
-                    (targetDataDoc[this.props.fieldKey] as List<Doc>).splice(ind, 0);
                     if (ind !== -1) {
                         Doc.RemoveDocFromList(targetDataDoc, this.props.fieldKey, doc);
                         recent && Doc.AddDocToList(recent, "data", doc, undefined, true, true);
diff --git a/src/fields/List.ts b/src/fields/List.ts
index dca8d111c..d9bd54673 100644
--- a/src/fields/List.ts
+++ b/src/fields/List.ts
@@ -43,7 +43,7 @@ const listHandlers: any = {
             }
         }
         const res = list.__fields.push(...items);
-        this[Update]("$addToSet");
+        this[Update]({ op: "$addToSet", items });
         return res;
     }),
     reverse() {
diff --git a/src/fields/util.ts b/src/fields/util.ts
index 90446f531..b6128f5e6 100644
--- a/src/fields/util.ts
+++ b/src/fields/util.ts
@@ -11,6 +11,8 @@ import { ComputedField } from "./ScriptField";
 import { ScriptCast, StrCast } from "./Types";
 import { returnZero } from "../Utils";
 import CursorField from "./CursorField";
+import { undo } from "prosemirror-history";
+import { List } from "./List";
 
 
 function _readOnlySetter(): never {
@@ -371,20 +373,26 @@ export function deleteProperty(target: any, prop: string | number | symbol) {
 export function updateFunction(target: any, prop: any, value: any, receiver: any) {
     let current = ObjectField.MakeCopy(value);
     return (diff?: any) => {
-        if (diff === "$addToSet") {
-            diff = { '$addToSet': { ["fields." + prop]: SerializationHelper.Serialize(value) } };
-        } else {
-            diff = { '$set': { ["fields." + prop]: SerializationHelper.Serialize(value) } };
-        }
+        const op = diff?.op === "$addToSet" ?
+            { '$addToSet': { ["fields." + prop]: SerializationHelper.Serialize(new List<Doc>(diff.items)) } }
+            : { '$set': { ["fields." + prop]: SerializationHelper.Serialize(value) } };
         const oldValue = current;
         const newValue = ObjectField.MakeCopy(value);
         current = newValue;
         if (!(value instanceof CursorField) && !(value?.some?.((v: any) => v instanceof CursorField))) {
-            UndoManager.AddEvent({
-                redo() { receiver[prop] = newValue; },
-                undo() { receiver[prop] = oldValue; }
-            });
+            UndoManager.AddEvent(diff?.op === "$addToSet" ?
+                {
+                    redo: () => receiver[prop].push(...(newValue as List<Doc>)),
+                    undo: action(() => (newValue as List<Doc>).forEach(doc => {
+                        const ind = receiver[prop].indexOf(doc);
+                        ind !== -1 && receiver[prop].splice(ind, 1);
+                    }))
+                }
+                : {
+                    redo: () => receiver[prop] = newValue,
+                    undo: () => receiver[prop] = oldValue
+                });
         }
-        target[Update](diff);
+        target[Update](op);
     };
 }
\ No newline at end of file
diff --git a/src/server/websocket.ts b/src/server/websocket.ts
index 3687ef876..221a01308 100644
--- a/src/server/websocket.ts
+++ b/src/server/websocket.ts
@@ -271,14 +271,16 @@ export namespace WebSocket {
         return typeof value === "string" ? value : value[0];
     }
 
-    function updateListField(socket: Socket, diff: Diff, results?: Transferable): void {
+    function updateListField(socket: Socket, diff: Diff, curListItems?: Transferable): void {
         diff.diff.$set = diff.diff.$addToSet;  // convert add to set to a query of the current fields, and then a set of the composition of the new fields with the old ones
         delete diff.diff.$addToSet;
         const updatefield = Array.from(Object.keys(diff.diff.$set))[0];
-        const list = (results as any).fields?.[updatefield.replace("fields.", "")]?.fields;
+        const newListItems = diff.diff.$set[updatefield].fields;
+        const curList = (curListItems as any).fields?.[updatefield.replace("fields.", "")]?.fields;
         const prelen = diff.diff.$set[updatefield].fields.length;
-        list?.forEach((item: any) => !diff.diff.$set[updatefield].fields.some((x: any) => x.fieldId === item.fieldId) && diff.diff.$set[updatefield].fields.push(item));
-        const sendBack = diff.diff.$set[updatefield].fields.length !== prelen;
+        let insInd = 0;
+        curList?.forEach((curItem: any) => !newListItems.some((newItem: any) => newItem.fieldId === curItem.fieldId) && newListItems.splice(insInd++, 0, curItem));
+        const sendBack = curList.length !== prelen;
         Database.Instance.update(diff.id, diff.diff,
             () => {
                 if (sendBack) {
-- 
cgit v1.2.3-70-g09d2


From 27635402ad810b910557eb1a86c7e85fa281aaee Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 12 Oct 2020 01:05:13 -0400
Subject: switched fonticonbox and colelctionsubview to test GetEffectiveAcl to
 determine if document is available (instead of hack of testing 'author'). 
 made GetEffectiveAcl a computedFn.  No longer create a pushpin when an
 annotation that's linked to text is dragged off a PDF.   fixed undo of
 pushpin navigation (used to call finish() twice).  fixed pushpin navigation
 to conistenly pan & sensibly toggle target

---
 src/client/util/DocumentManager.ts                  |  9 +++++----
 src/client/views/collections/CollectionSubView.tsx  |  5 +++--
 src/client/views/collections/CollectionView.tsx     |  3 ++-
 .../collectionFreeForm/CollectionFreeFormView.tsx   | 21 +++++++++++++--------
 src/client/views/nodes/FontIconBox.tsx              |  5 +++--
 src/client/views/pdf/Annotation.tsx                 |  2 ++
 src/client/views/pdf/PDFViewer.tsx                  |  2 +-
 src/fields/util.ts                                  | 14 ++++++++++----
 8 files changed, 39 insertions(+), 22 deletions(-)

(limited to 'src/client/views/collections/CollectionView.tsx')

diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 178daf5f0..b37181e57 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -164,13 +164,14 @@ export class DocumentManager {
             const sameContext = annotatedDoc && annotatedDoc === originatingDoc?.context;
             if (originatingDoc?.isPushpin) {
                 const hide = !docView.props.Document.hidden;
-                (!hide || !sameContext) && (docView.props.Document.hidden = !docView.props.Document.hidden);
-                docView.props.focus(docView.props.Document, willZoom, undefined, (notfocused: boolean) => { // bcz: Argh! TODO: Need to add a notFocused argument to the after finish callback function that indicates whether the window had to scroll to show the target
-                    notfocused && hide && (docView.props.Document.hidden = true);
+                docView.props.focus(docView.props.Document, willZoom, undefined, (notfocused: boolean) => { // bcz: Argh! TODO: Need to add a notFocused argument to the after finish callback function that indicates whether the window had to scroll to show the target  
+                    if (notfocused || docView.props.Document.hidden) {
+                        docView.props.Document.hidden = !docView.props.Document.hidden;
+                    }
                     return focusAndFinish();
                     // @ts-ignore   bcz: Argh TODO: Need to add a parameter to focus() everywhere for whether focus should center the target's container in the view or not. // here we don't want to focus the container if the source and target are in the same container
                 }, sameContext);
-                finished?.();
+                //finished?.();
             }
             else {
                 docView.select(false);
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 493018093..b282d1e27 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -103,7 +103,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
             const { Document, DataDoc } = this.props;
             const validPairs = this.childDocs.map(doc => Doc.GetLayoutDataDocPair(Document, !this.props.annotationsKey ? DataDoc : undefined, doc)).
                 filter(pair => {  // filter out any documents that have a proto that we don't have permissions to (which we determine by not having any keys
-                    return pair.layout && (!pair.layout.proto || (pair.layout.proto instanceof Doc && Object.keys(pair.layout.proto).length));
+                    return pair.layout && (!pair.layout.proto || (pair.layout.proto instanceof Doc && GetEffectiveAcl(pair.layout.proto) !== AclPrivate));// Object.keys(pair.layout.proto).length));
                 });
             return validPairs.map(({ data, layout }) => ({ data: data as Doc, layout: layout! })); // this mapping is a bit of a hack to coerce types
         }
@@ -134,7 +134,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
                 rawdocs = rootDoc && !this.props.annotationsKey ? [Doc.GetProto(rootDoc)] : [];
             }
 
-            const docs = rawdocs.filter(d => !(d instanceof Promise) && Object.keys(d).length).map(d => d as Doc);
+            const docs = rawdocs.filter(d => !(d instanceof Promise) && GetEffectiveAcl(d) !== AclPrivate).map(d => d as Doc);
             const viewSpecScript = Cast(this.props.Document.viewSpecScript, ScriptField);
             const childDocs = viewSpecScript ? docs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result) : docs;
 
@@ -502,4 +502,5 @@ import { SelectionManager } from "../../util/SelectionManager";
 import { OverlayView } from "../OverlayView";
 import { setTimeout } from "timers";
 import { Hypothesis } from "../../util/HypothesisUtils";
+import { GetEffectiveAcl } from "../../../fields/util";
 
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 80e9b41ad..cfd24545b 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -159,7 +159,8 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus
                 else {
                     added.filter(doc => [AclAdmin, AclEdit].includes(GetEffectiveAcl(doc))).map(doc => {  // only make a pushpin if we have acl's to edit the document
                         const context = Cast(doc.context, Doc, null);
-                        if (context && (context.type === DocumentType.VID || context.type === DocumentType.WEB || context.type === DocumentType.PDF || context.type === DocumentType.IMG)) {
+                        const hasContextAnchor = DocListCast(doc.links).some(l => (l.anchor2 === doc && Cast(l.anchor1, Doc, null)?.annotationOn === context) || (l.anchor1 === doc && Cast(l.anchor2, Doc, null)?.annotationOn === context));
+                        if (context && !hasContextAnchor && (context.type === DocumentType.VID || context.type === DocumentType.WEB || context.type === DocumentType.PDF || context.type === DocumentType.IMG)) {
                             const pushpin = Docs.Create.FontIconDocument({
                                 title: "pushpin", label: "",
                                 icon: "map-pin", x: Cast(doc.x, "number", null), y: Cast(doc.y, "number", null), _backgroundColor: "#0000003d", color: "#ACCEF7",
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 4d9906f93..4df90e8ea 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -893,21 +893,24 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
                 afterFocus && setTimeout(afterFocus, delay);
             } else {
                 const contextHgt = Doc.AreProtosEqual(annotOn, this.props.Document) && this.props.VisibleHeight ? this.props.VisibleHeight() : NumCast(annotOn._height);
-                const offset = annotOn && (contextHgt / 2);
                 const curScroll = NumCast(this.props.Document._scrollTop);
                 let scrollTo = curScroll;
                 if (curScroll + contextHgt < NumCast(doc.y)) {
-                    scrollTo = NumCast(doc.y) + Math.max(NumCast(doc._height), 50) - contextHgt;
+                    scrollTo = NumCast(doc.y) + Math.max(NumCast(doc._height), 100) - contextHgt;
                 } else if (curScroll > NumCast(doc.y)) {
-                    scrollTo = NumCast(doc.y);
+                    scrollTo = Math.max(0, NumCast(doc.y) - 50);
                 }
-                if (curScroll !== scrollTo) {
+                if (curScroll !== scrollTo || this.props.Document._viewTransition) {
                     this.props.Document._scrollPY = this.props.Document._scrollY = scrollTo;
                     delay = Math.abs(scrollTo - curScroll) > 5 ? 1000 : 0;
-                    !dontCenter && delay && this.props.focus(this.props.Document);
+                    !dontCenter && this.props.focus(this.props.Document);
                     afterFocus && setTimeout(afterFocus, delay);
+                } else {
+                    !dontCenter && delay && this.props.focus(this.props.Document);
                     // @ts-ignore
-                } else afterFocus(true);  // bcz: TODO Aragh -- need to add a parameter to afterFocus() functions to indicate whether the focus function didn't need to scroll
+                    afterFocus(true);  // bcz: TODO Aragh -- need to add a parameter to afterFocus() functions to indicate whether the focus function didn't need to scroll
+
+                }
             }
 
         } else {
@@ -929,14 +932,16 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
             willZoom && this.setScaleToZoom(layoutdoc, scale);
             Doc.linkFollowHighlight(doc);
 
+            const notFocused = newPanX === savedState.px && newPanY === savedState.py;
             afterFocus && setTimeout(() => {
-                if (afterFocus?.()) {
+                // @ts-ignore
+                if (afterFocus?.(notFocused)) { // bcz: TODO Aragh -- need to add a parameter to afterFocus() functions to indicate whether the focus function didn't need to scroll
                     this.Document._panX = savedState.px;
                     this.Document._panY = savedState.py;
                     this.Document[this.scaleFieldKey] = savedState.s;
                     this.Document._viewTransition = savedState.pt;
                 }
-            }, 500);
+            }, notFocused ? 0 : 500);
         }
 
     }
diff --git a/src/client/views/nodes/FontIconBox.tsx b/src/client/views/nodes/FontIconBox.tsx
index 156256fe5..276c66bb1 100644
--- a/src/client/views/nodes/FontIconBox.tsx
+++ b/src/client/views/nodes/FontIconBox.tsx
@@ -8,11 +8,12 @@ import { FieldView, FieldViewProps } from './FieldView';
 import { StrCast, Cast, ScriptCast } from '../../../fields/Types';
 import { Utils, setupMoveUpEvents, returnFalse, emptyFunction } from "../../../Utils";
 import { runInAction, observable, reaction, IReactionDisposer } from 'mobx';
-import { Doc, DocListCast } from '../../../fields/Doc';
+import { Doc, DocListCast, AclPrivate } from '../../../fields/Doc';
 import { ContextMenu } from '../ContextMenu';
 import { ScriptField } from '../../../fields/ScriptField';
 import { Tooltip } from '@material-ui/core';
 import { DragManager } from '../../util/DragManager';
+import { GetEffectiveAcl } from '../../../fields/util';
 const FontIconSchema = createSchema({
     icon: "string",
 });
@@ -105,7 +106,7 @@ export class FontIconBadge extends React.Component<FontIconBadgeProps> {
 
     render() {
         if (!(this.props.collection instanceof Doc)) return (null);
-        const length = DocListCast(this.props.collection.data).filter(d => Object.keys(d).length).length; // filter out any documents that we can't read
+        const length = DocListCast(this.props.collection.data).filter(d => GetEffectiveAcl(d) !== AclPrivate).length; //  Object.keys(d).length).length; // filter out any documents that we can't read
         return <div className="fontIconBadge-container" style={{ width: 15, height: 15, top: 12 }} ref={this._notifsRef}>
             <div className="fontIconBadge" style={length > 0 ? { "display": "initial" } : { "display": "none" }}
                 onPointerDown={this.onPointerDown} >
diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx
index e7f901091..a071abd21 100644
--- a/src/client/views/pdf/Annotation.tsx
+++ b/src/client/views/pdf/Annotation.tsx
@@ -71,6 +71,7 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> {
         this._reactionDisposer && this._reactionDisposer();
     }
 
+    @undoBatch
     deleteAnnotation = () => {
         const annotation = DocListCast(this.props.dataDoc[this.props.fieldKey + "-annotations"]);
         const group = FieldValue(Cast(this.props.document.group, Doc));
@@ -86,6 +87,7 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> {
         PDFMenu.Instance.fadeOut(true);
     }
 
+    @undoBatch
     pinToPres = () => {
         const group = FieldValue(Cast(this.props.document.group, Doc));
         const isPinned = group && Doc.isDocPinned(group) ? true : false;
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 78b95b385..d8be3defd 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -194,7 +194,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu
                 if (scrollY !== undefined) {
                     (this._showCover || this._showWaiting) && this.setupPdfJsViewer();
                     if (this.props.renderDepth === -1 && scrollY >= 0) {
-                        if (!this._mainCont.current) setTimeout(() => smoothScroll(1000, this._mainCont.current!, scrollY || 0));
+                        if (!this._mainCont.current) setTimeout(() => this._mainCont.current && smoothScroll(1000, this._mainCont.current, scrollY || 0));
                         else smoothScroll(1000, this._mainCont.current, scrollY || 0);
                         this.Document._scrollPY = undefined;
                     }
diff --git a/src/fields/util.ts b/src/fields/util.ts
index 881f301f3..b68d961b1 100644
--- a/src/fields/util.ts
+++ b/src/fields/util.ts
@@ -13,6 +13,7 @@ import { returnZero } from "../Utils";
 import CursorField from "./CursorField";
 import { List } from "./List";
 import { SnappingManager } from "../client/util/SnappingManager";
+import { computedFn } from "mobx-utils";
 
 function _readOnlySetter(): never {
     throw new Error("Documents can't be modified in read-only mode");
@@ -139,7 +140,7 @@ export function denormalizeEmail(email: string) {
  * 
  * Edit: a user with edit access to a document can remove/edit that document, add/remove/edit annotations (depending on permissions), but not change any access rights to that document.
  * 
- * Add: a user with add access to a document can add documents/annotations to that document but cannot edit or delete anything.
+ * Add: a user with add access to a document can augment documents/annotations to that document but cannot edit or delete anything.
  * 
  * View: a user with view access to a document can only view it - they cannot add/remove/edit anything.
  * 
@@ -148,7 +149,7 @@ export function denormalizeEmail(email: string) {
 export enum SharingPermissions {
     Admin = "Admin",
     Edit = "Can Edit",
-    Add = "Can Add",
+    Add = "Can Augment",
     View = "Can View",
     None = "Not Shared"
 }
@@ -157,6 +158,11 @@ export enum SharingPermissions {
  * Calculates the effective access right to a document for the current user.
  */
 export function GetEffectiveAcl(target: any, in_prop?: string | symbol | number, user?: string): symbol {
+    return computedFn(function (target: any, in_prop?: string | symbol | number, user?: string) {
+        return getEffectiveAcl(target, in_prop, user);
+    }, true)(target, in_prop, user);
+}
+function getEffectiveAcl(target: any, in_prop?: string | symbol | number, user?: string): symbol {
     if (!target) return AclPrivate;
 
     // all changes received fromt the server must be processed as Admin
@@ -219,7 +225,7 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc
     const HierarchyMapping = new Map<string, number>([
         ["Not Shared", 0],
         ["Can View", 1],
-        ["Can Add", 2],
+        ["Can Augment", 2],
         ["Can Edit", 3],
         ["Admin", 4]
     ]);
@@ -281,7 +287,7 @@ export function setter(target: any, in_prop: string | symbol | number, value: an
 
     // if you're trying to change an acl but don't have Admin access / you're trying to change it to something that isn't an acceptable acl, you can't
     if (typeof prop === "string" && prop.startsWith("acl") && (effectiveAcl !== AclAdmin || ![...Object.values(SharingPermissions), undefined, "None"].includes(value))) 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.startsWith("acl") && !["Can Edit", "Can Augment", "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("_")) {
-- 
cgit v1.2.3-70-g09d2


From 04bbff759710d58fa97ce2f0d685ec59b6beb60e Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 12 Oct 2020 12:53:40 -0400
Subject: split getEffectiveAcl code for determining if a field can be modified
 out into getPropAcl.

---
 src/client/util/SharingManager.tsx              |  4 +--
 src/client/views/DocComponent.tsx               |  2 +-
 src/client/views/collections/CollectionView.tsx |  2 +-
 src/fields/util.ts                              | 44 ++++++++++++++-----------
 4 files changed, 28 insertions(+), 24 deletions(-)

(limited to 'src/client/views/collections/CollectionView.tsx')

diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx
index 0c8f19eae..914253e3c 100644
--- a/src/client/util/SharingManager.tsx
+++ b/src/client/util/SharingManager.tsx
@@ -177,7 +177,7 @@ export class SharingManager extends React.Component<{}> {
 
                 users.forEach(({ user, sharingDoc }) => {
                     if (permission !== SharingPermissions.None) Doc.AddDocToList(sharingDoc, storage, doc); // add the doc to the sharingDoc if it hasn't already been added
-                    else GetEffectiveAcl(doc, undefined, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, storage, (doc.aliasOf as Doc || doc)); // remove the doc from the list if it already exists
+                    else GetEffectiveAcl(doc, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, storage, (doc.aliasOf as Doc || doc)); // remove the doc from the list if it already exists
                 });
             }
         });
@@ -273,7 +273,7 @@ export class SharingManager extends React.Component<{}> {
             distributeAcls(acl, permission as SharingPermissions, doc);
 
             if (permission !== SharingPermissions.None) Doc.AddDocToList(sharingDoc, storage, doc);
-            else GetEffectiveAcl(doc, undefined, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, storage, (doc.aliasOf as Doc || doc));
+            else GetEffectiveAcl(doc, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, storage, (doc.aliasOf as Doc || doc));
         });
     }
 
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index 98e888538..a55f4adaf 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -164,7 +164,7 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T
                     return false;
                 }
                 else {
-                    if (this.props.Document[AclSym]) {
+                    if (this.props.Document[AclSym] && Object.keys(this.props.Document[AclSym]).length) {
                         added.forEach(d => {
                             for (const [key, value] of Object.entries(this.props.Document[AclSym])) {
                                 if (d.author === denormalizeEmail(key.substring(4)) && !d.aliasOf) distributeAcls(key, SharingPermissions.Admin, d, true);
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index cfd24545b..a27fa5a66 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -140,7 +140,7 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus
                 return false;
             }
             else {
-                if (this.props.Document[AclSym]) {
+                if (this.props.Document[AclSym] && Object.keys(this.props.Document[AclSym])) {
                     added.forEach(d => {
                         for (const [key, value] of Object.entries(this.props.Document[AclSym])) {
                             if (d.author === denormalizeEmail(key.substring(4)) && !d.aliasOf) distributeAcls(key, SharingPermissions.Admin, d, true);
diff --git a/src/fields/util.ts b/src/fields/util.ts
index 56736028a..dd0444d61 100644
--- a/src/fields/util.ts
+++ b/src/fields/util.ts
@@ -155,32 +155,35 @@ export enum SharingPermissions {
 }
 
 // return acl from cache or cache the acl and return. bcz: Argh!  NOT WORKING ... nothing gets invalidated properly....
-const getEffectiveAclCache = computedFn(function (target: any, playgroundProp: boolean, user?: string) { return getEffectiveAcl(target, playgroundProp, user); }, true);
+const getEffectiveAclCache = computedFn(function (target: any, user?: string) { return getEffectiveAcl(target, user); }, true);
 
 /**
  * Calculates the effective access right to a document for the current user.
  */
-export function GetEffectiveAcl(target: any, in_prop?: string | symbol | number, user?: string): symbol {
-    if (!target) return AclPrivate;
-    if (in_prop === UpdatingFromServer) return AclAdmin;  // requesting the UpdatingFromServer prop must always go through to keep the local DB consistent
-    const playgroundProp = in_prop && DocServer.PlaygroundFields?.includes(in_prop.toString()) ? true : false;
-    return getEffectiveAcl(target, playgroundProp, user);
+export function GetEffectiveAcl(target: any, user?: string): symbol {
+    return target ? getEffectiveAcl(target, user) : AclPrivate;
 }
 
-function getEffectiveAcl(target: any, playgroundProp: boolean, user?: string): symbol {
+function getPropAcl(target: any, prop: string | symbol | number) {
+    if (prop === UpdatingFromServer) return AclAdmin;  // requesting the UpdatingFromServer prop must always go through to keep the local DB consistent
+    if (prop && DocServer.PlaygroundFields?.includes(prop.toString())) return AclEdit; // playground props are always editable
+    return GetEffectiveAcl(target);
+}
+
+let HierarchyMapping: Map<symbol, number> | undefined;
+
+function getEffectiveAcl(target: any, user?: string): symbol {
     if (target[UpdatingFromServer]) return AclAdmin; // all changes received from the server must be processed as Admin
     // if the current user is the author of the document / the current user is a member of the admin group
     const userChecked = user || Doc.CurrentUserEmail;
-    if (userChecked === (target.__fields?.author || target.author)) return AclAdmin;
+    if (userChecked === (target.__fields?.author || target.author)) return AclAdmin; // target may be a Doc of Proxy, so check __fields.author and .author
     if (SnappingManager.GetCachedGroupByName("Admin")) return AclAdmin;
+    const targetAcls = target[AclSym];
 
-    if (target[AclSym] && Object.keys(target[AclSym]).length) {
+    if (targetAcls && Object.keys(targetAcls).length) {
+        if (_overrideAcl) return AclEdit;
 
-        // if the acl is being overriden or the property being modified is one of the playground fields (which can be freely modified)
-        if (_overrideAcl || playgroundProp) return AclEdit;
-
-        let effectiveAcl = AclPrivate;
-        const HierarchyMapping = new Map<symbol, number>([
+        HierarchyMapping = HierarchyMapping || new Map<symbol, number>([
             [AclPrivate, 0],
             [AclReadonly, 1],
             [AclAddonly, 2],
@@ -188,12 +191,13 @@ function getEffectiveAcl(target: any, playgroundProp: boolean, user?: string): s
             [AclAdmin, 4]
         ]);
 
-        for (const [key, value] of Object.entries(target[AclSym])) {
+        let effectiveAcl = AclPrivate;
+        for (const [key, value] of Object.entries(targetAcls)) {
             // there are issues with storing fields with . in the name, so they are replaced with _ during creation
             // as a result we need to restore them again during this comparison.
             const entity = denormalizeEmail(key.substring(4)); // an individual or a group
-            if (SnappingManager.GetCachedGroupByName(entity) || userChecked === entity) {
-                if (HierarchyMapping.get(value as symbol)! > HierarchyMapping.get(effectiveAcl)!) {
+            if (HierarchyMapping.get(value as symbol)! > HierarchyMapping.get(effectiveAcl)!) {
+                if (SnappingManager.GetCachedGroupByName(entity) || userChecked === entity) {
                     effectiveAcl = value as symbol;
                     if (effectiveAcl === AclAdmin) return effectiveAcl;
                 }
@@ -201,8 +205,8 @@ function getEffectiveAcl(target: any, playgroundProp: boolean, user?: string): s
         }
 
         // if there's an overriding acl set through the properties panel or sharing menu, that's what's returned if the user isn't an admin of the document
-        const override = target[AclSym]["acl-Override"];
-        if (override !== AclUnset && override !== undefined) effectiveAcl = target[AclSym]["acl-Override"];
+        const override = targetAcls["acl-Override"];
+        if (override !== AclUnset && override !== undefined) effectiveAcl = override;
 
         // if we're in playground mode, return AclEdit (or AclAdmin if that's the user's effectiveAcl)
         return DocServer?.Control?.isReadOnly?.() && HierarchyMapping.get(effectiveAcl)! < 3 ? AclEdit : effectiveAcl;
@@ -282,7 +286,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;
-    const effectiveAcl = GetEffectiveAcl(target, in_prop);
+    const effectiveAcl = getPropAcl(target, prop);
     if (effectiveAcl !== AclEdit && effectiveAcl !== AclAdmin) return true;
 
     // if you're trying to change an acl but don't have Admin access / you're trying to change it to something that isn't an acceptable acl, you can't
-- 
cgit v1.2.3-70-g09d2

