From 150d1b3ea263dd0d6d8189d8bf6fd87c5505cfc4 Mon Sep 17 00:00:00 2001
From: usodhi <61431818+usodhi@users.noreply.github.com>
Date: Fri, 24 Jul 2020 19:15:52 +0530
Subject: fixed instances of AclEdit to include AclAdmin

---
 src/client/views/nodes/formattedText/FormattedTextBox.tsx | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

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

diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 38fa66d65..2ac53ce6a 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, AclEdit } from "../../../../fields/Doc";
+import { DataSym, Doc, DocListCast, DocListCastAsync, Field, HeightSym, Opt, WidthSym, AclEdit, AclAdmin } from "../../../../fields/Doc";
 import { documentSchema } from '../../../../fields/documentSchemas';
 import applyDevTools = require("prosemirror-dev-tools");
 import { removeMarkWithAttrs } from "./prosemirrorPatches";
@@ -233,7 +233,8 @@ 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());
             let unchanged = true;
-            if (GetEffectiveAcl(this.dataDoc) === AclEdit) {
+            const effectiveAcl = GetEffectiveAcl(this.dataDoc);
+            if (effectiveAcl === AclEdit || effectiveAcl === AclAdmin) {
                 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())));
-- 
cgit v1.2.3-70-g09d2


From 760c51c493ef190aa5283944b083b55f7c057e6c Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Fri, 24 Jul 2020 23:59:09 -0400
Subject: several siggnificant changes to how acls work with makin documents
 updated interactively when permissions chane.

---
 src/client/DocServer.ts                            | 80 +++++++++++++---------
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  5 +-
 .../views/nodes/formattedText/FormattedTextBox.tsx |  2 +-
 src/fields/Doc.ts                                  | 48 +++++++++----
 src/fields/Schema.ts                               |  2 +-
 src/fields/util.ts                                 | 10 ++-
 6 files changed, 95 insertions(+), 52 deletions(-)

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

diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts
index bac324c77..8ded43468 100644
--- a/src/client/DocServer.ts
+++ b/src/client/DocServer.ts
@@ -1,13 +1,14 @@
 import * as io from 'socket.io-client';
 import { MessageStore, YoutubeQueryTypes, GestureContent, MobileInkOverlayContent, UpdateMobileInkOverlayPositionContent, MobileDocumentUploadContent } from "./../server/Message";
-import { Opt, Doc, fetchProto } from '../fields/Doc';
+import { Opt, Doc, fetchProto, FieldsSym } from '../fields/Doc';
 import { Utils, emptyFunction } from '../Utils';
 import { SerializationHelper } from './util/SerializationHelper';
 import { RefField } from '../fields/RefField';
-import { Id, HandleUpdate } from '../fields/FieldSymbols';
+import { Id, HandleUpdate, Parent } from '../fields/FieldSymbols';
 import GestureOverlay from './views/GestureOverlay';
 import MobileInkOverlay from '../mobile/MobileInkOverlay';
 import { runInAction } from 'mobx';
+import { ObjectField } from '../fields/ObjectField';
 
 /**
  * This class encapsulates the transfer and cross-client synchronization of
@@ -207,12 +208,12 @@ export namespace DocServer {
      * the server if the document has not been cached.
      * @param id the id of the requested document
      */
-    const _GetRefFieldImpl = (id: string): Promise<Opt<RefField>> => {
+    const _GetRefFieldImpl = (id: string, force: boolean = false): Promise<Opt<RefField>> => {
         // an initial pass through the cache to determine whether the document needs to be fetched,
         // is already in the process of being fetched or already exists in the
         // cache
         const cached = _cache[id];
-        if (cached === undefined) {
+        if (cached === undefined || force) {
             // NOT CACHED => we'll have to send a request to the server
 
             // synchronously, we emit a single callback to the server requesting the serialized (i.e. represented by a string)
@@ -226,7 +227,16 @@ export namespace DocServer {
             const deserializeField = getSerializedField.then(async fieldJson => {
                 // deserialize
                 const field = await SerializationHelper.Deserialize(fieldJson);
-                if (field !== undefined) {
+                if (force && field instanceof Doc && cached instanceof Doc) {
+                    Array.from(Object.keys(field)).forEach(key => {
+                        const fieldval = field[key];
+                        if (fieldval instanceof ObjectField) {
+                            fieldval[Parent] = undefined;
+                        }
+                        cached[key] = field[key];
+                    });
+                }
+                else if (field !== undefined) {
                     _cache[id] = field;
                 } else {
                     delete _cache[id];
@@ -238,8 +248,8 @@ export namespace DocServer {
             });
             // here, indicate that the document associated with this id is currently
             // being retrieved and cached
-            _cache[id] = deserializeField;
-            return deserializeField;
+            !force && (_cache[id] = deserializeField);
+            return force ? cached as any : deserializeField;
         } else if (cached instanceof Promise) {
             // BEING RETRIEVED AND CACHED => some other caller previously (likely recently) called GetRefField(s),
             // and requested the document I'm looking for. Shouldn't fetch again, just
@@ -260,11 +270,11 @@ export namespace DocServer {
         }
     };
 
-    let _GetRefField: (id: string) => Promise<Opt<RefField>> = errorFunc;
+    let _GetRefField: (id: string, force: boolean) => Promise<Opt<RefField>> = errorFunc;
     let _GetCachedRefField: (id: string) => Opt<RefField> = errorFunc;
 
-    export function GetRefField(id: string): Promise<Opt<RefField>> {
-        return _GetRefField(id);
+    export function GetRefField(id: string, force = false): Promise<Opt<RefField>> {
+        return _GetRefField(id, force);
     }
     export function GetCachedRefField(id: string): Opt<RefField> {
         return _GetCachedRefField(id);
@@ -330,29 +340,35 @@ export namespace DocServer {
             const proms: Promise<void>[] = [];
             runInAction(() => {
                 for (const field of fields) {
-                    if (field !== undefined && field !== null) {
+                    if (field !== undefined && field !== null && !_cache[field.id]) {
                         // deserialize
-                        const prom = SerializationHelper.Deserialize(field).then(deserialized => {
-                            fieldMap[field.id] = deserialized;
-
-                            //overwrite or delete any promises (that we inserted as flags
-                            // to indicate that the field was in the process of being fetched). Now everything
-                            // should be an actual value within or entirely absent from the cache.
-                            if (deserialized !== undefined) {
-                                _cache[field.id] = deserialized;
-                            } else {
-                                delete _cache[field.id];
-                            }
-                            return deserialized;
-                        });
-                        // 4) here, for each of the documents we've requested *ourselves* (i.e. weren't promises or found in the cache)
-                        // we set the value at the field's id to a promise that will resolve to the field. 
-                        // When we find that promises exist at keys in the cache, THIS is where they were set, just by some other caller (method).
-                        // The mapping in the .then call ensures that when other callers await these promises, they'll
-                        // get the resolved field
-                        _cache[field.id] = prom;
-                        // adds to a list of promises that will be awaited asynchronously
-                        proms.push(prom);
+                        const cached = _cache[field.id];
+                        if (!cached) {
+                            const prom = SerializationHelper.Deserialize(field).then(deserialized => {
+                                fieldMap[field.id] = deserialized;
+
+                                //overwrite or delete any promises (that we inserted as flags
+                                // to indicate that the field was in the process of being fetched). Now everything
+                                // should be an actual value within or entirely absent from the cache.
+                                if (deserialized !== undefined) {
+                                    _cache[field.id] = deserialized;
+                                } else {
+                                    delete _cache[field.id];
+                                }
+                                return deserialized;
+                            });
+                            // 4) here, for each of the documents we've requested *ourselves* (i.e. weren't promises or found in the cache)
+                            // we set the value at the field's id to a promise that will resolve to the field. 
+                            // When we find that promises exist at keys in the cache, THIS is where they were set, just by some other caller (method).
+                            // The mapping in the .then call ensures that when other callers await these promises, they'll
+                            // get the resolved field
+                            _cache[field.id] = prom;
+
+                            // adds to a list of promises that will be awaited asynchronously
+                            proms.push(prom);
+                        } else if (cached instanceof Promise) {
+                            proms.push(cached as any);
+                        }
                     }
                 }
             });
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 979b21321..38c1edde2 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -944,7 +944,8 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
     parentActive = (outsideReaction: boolean) => this.props.active(outsideReaction) || this.backgroundActive ? true : false;
     getChildDocumentViewProps(childLayout: Doc, childData?: Doc): DocumentViewProps {
         return {
-            ...this.props,
+            pinToPres: this.props.pinToPres,
+            whenActiveChanged: this.props.whenActiveChanged,
             NativeHeight: returnZero,
             NativeWidth: returnZero,
             fitToBox: false,
@@ -1145,7 +1146,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
     @action
     componentDidMount() {
         super.componentDidMount?.();
-        this._layoutComputeReaction = reaction(() => this.doLayoutComputation,
+        this._layoutComputeReaction = reaction(() => { TraceMobx(); return this.doLayoutComputation },
             (elements) => this._layoutElements = elements || [],
             { fireImmediately: true, name: "doLayout" });
 
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 2ac53ce6a..53234d819 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -690,7 +690,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
         );
         this._disposers.editorState = reaction(
             () => {
-                if (this.dataDoc[this.props.fieldKey + "-noTemplate"] || !this.layoutDoc[this.props.fieldKey + "-textTemplate"]) {
+                if (this.dataDoc?.[this.props.fieldKey + "-noTemplate"] || !this.layoutDoc[this.props.fieldKey + "-textTemplate"]) {
                     return Cast(this.dataDoc[this.props.fieldKey], RichTextField, null)?.Data;
                 }
                 return Cast(this.layoutDoc[this.props.fieldKey + "-textTemplate"], RichTextField, null)?.Data;
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index c9231753f..9020dcc84 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -104,7 +104,7 @@ export const AclAddonly = Symbol("AclAddonly");
 export const AclEdit = Symbol("AclEdit");
 export const AclAdmin = Symbol("AclAdmin");
 export const UpdatingFromServer = Symbol("UpdatingFromServer");
-const CachedUpdates = Symbol("Cached updates");
+export const CachedUpdates = Symbol("Cached updates");
 
 const AclMap = new Map<string, symbol>([
     [SharingPermissions.None, AclPrivate],
@@ -115,15 +115,15 @@ const AclMap = new Map<string, symbol>([
 ]);
 
 export function fetchProto(doc: Doc) {
-    // if (doc.author !== Doc.CurrentUserEmail) {
-    untracked(() => {
-        const permissions: { [key: string]: symbol } = {};
+    const permissions: { [key: string]: symbol } = {};
 
-        Object.keys(doc).filter(key => key.startsWith("ACL")).forEach(key => permissions[key] = AclMap.get(StrCast(doc[key]))!);
+    Object.keys(doc).filter(key => key.startsWith("ACL")).forEach(key => permissions[key] = AclMap.get(StrCast(doc[key]))!);
 
-        if (Object.keys(permissions).length) doc[AclSym] = permissions;
-    });
-    // }
+    if (Object.keys(permissions).length) doc[AclSym] = permissions;
+
+    if (GetEffectiveAcl(doc) === AclPrivate) {
+        runInAction(() => doc[FieldsSym](true));
+    }
 
     if (doc.proto instanceof Promise) {
         doc.proto.then(fetchProto);
@@ -143,7 +143,7 @@ export class Doc extends RefField {
             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.___fieldKeys);
                 runInAction(() => obj.__LAYOUT__ = target.__LAYOUT__);
                 return Object.keys(obj);
             },
@@ -151,11 +151,11 @@ export class Doc extends RefField {
                 if (prop.toString() === "__LAYOUT__") {
                     return Reflect.getOwnPropertyDescriptor(target, prop);
                 }
-                if (prop in target.__fields) {
+                if (prop in target.__fieldKeys) {
                     return {
                         configurable: true,//TODO Should configurable be true?
                         enumerable: true,
-                        value: target.__fields[prop]
+                        value: 0//() => target.__fields[prop])
                     };
                 }
                 return Reflect.getOwnPropertyDescriptor(target, prop);
@@ -179,15 +179,23 @@ export class Doc extends RefField {
         this.___fields = value;
         for (const key in value) {
             const field = value[key];
+            field && (this.__fieldKeys[key] = true);
             if (!(field instanceof ObjectField)) continue;
             field[Parent] = this[Self];
             field[OnUpdate] = updateFunction(this[Self], key, field, this[SelfProxy]);
         }
     }
+    private get __fieldKeys() { return this.___fieldKeys; }
+    private set __fieldKeys(value) {
+        this.___fieldKeys = value;
+    }
 
     @observable
     private ___fields: any = {};
 
+    @observable
+    private ___fieldKeys: any = {};
+
     private [UpdatingFromServer]: boolean = false;
 
     private [Update] = (diff: any) => {
@@ -196,7 +204,14 @@ export class Doc extends RefField {
 
     private [Self] = this;
     private [SelfProxy]: any;
-    public [FieldsSym] = () => this.___fields;
+    public [FieldsSym] = (clear?: boolean) => {
+        if (clear) {
+            this.___fields = {};
+            this.___fieldKeys = {};
+        }
+        return this.___fields;
+    }
+    @observable
     public [AclSym]: { [key: string]: symbol };
     public [WidthSym] = () => NumCast(this[SelfProxy]._width);
     public [HeightSym] = () => NumCast(this[SelfProxy]._height);
@@ -237,11 +252,18 @@ export class Doc extends RefField {
                 const fKey = key.substring(7);
                 const fn = async () => {
                     const value = await SerializationHelper.Deserialize(set[key]);
+                    const prev = GetEffectiveAcl(this);
                     this[UpdatingFromServer] = true;
                     this[fKey] = value;
+                    if (fKey.startsWith("ACL")) {
+                        fetchProto(this);
+                    }
                     this[UpdatingFromServer] = false;
+                    if (prev === AclPrivate && GetEffectiveAcl(this) !== AclPrivate) {
+                        DocServer.GetRefField(this[Id], true);
+                    }
                 };
-                if (sameAuthor || DocServer.getFieldWriteMode(fKey) !== DocServer.WriteMode.Playground) {
+                if (sameAuthor || fKey.startsWith("ACL") || DocServer.getFieldWriteMode(fKey) !== DocServer.WriteMode.Playground) {
                     delete this[CachedUpdates][fKey];
                     await fn();
                 } else {
diff --git a/src/fields/Schema.ts b/src/fields/Schema.ts
index 98ef3e087..c6a8d6ae9 100644
--- a/src/fields/Schema.ts
+++ b/src/fields/Schema.ts
@@ -31,7 +31,7 @@ export function makeInterface<T extends Interface[]>(...schemas: T): InterfaceFu
     }
     const proto = new Proxy({}, {
         get(target: any, prop, receiver) {
-            const field = receiver.doc[prop];
+            const field = receiver.doc?.[prop];
             if (prop in schema) {
                 const desc = prop === "proto" ? Doc : (schema as any)[prop]; // bcz: proto doesn't appear in schemas ... maybe it should?
                 if (typeof desc === "object" && "defaultVal" in desc && "type" in desc) {//defaultSpec
diff --git a/src/fields/util.ts b/src/fields/util.ts
index 2b970d342..e0b1843a1 100644
--- a/src/fields/util.ts
+++ b/src/fields/util.ts
@@ -1,11 +1,11 @@
 import { UndoManager } from "../client/util/UndoManager";
-import { Doc, FieldResult, UpdatingFromServer, LayoutSym, AclPrivate, AclEdit, AclReadonly, AclAddonly, AclSym, fetchProto, DataSym, DocListCast, AclAdmin } from "./Doc";
+import { Doc, FieldResult, UpdatingFromServer, LayoutSym, AclPrivate, AclEdit, AclReadonly, AclAddonly, AclSym, CachedUpdates, DataSym, DocListCast, AclAdmin, FieldsSym } from "./Doc";
 import { SerializationHelper } from "../client/util/SerializationHelper";
 import { ProxyField, PrefetchProxy } from "./Proxy";
 import { RefField } from "./RefField";
 import { ObjectField } from "./ObjectField";
 import { action, trace } from "mobx";
-import { Parent, OnUpdate, Update, Id, SelfProxy, Self } from "./FieldSymbols";
+import { Parent, OnUpdate, Update, Id, SelfProxy, Self, HandleUpdate } from "./FieldSymbols";
 import { DocServer } from "../client/DocServer";
 import { ComputedField } from "./ScriptField";
 import { ScriptCast, StrCast } from "./Types";
@@ -77,8 +77,10 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number
 
     if (writeToDoc) {
         if (value === undefined) {
+            delete target.__fieldKeys[prop];
             delete target.__fields[prop];
         } else {
+            target.__fieldKeys[prop] = true;
             target.__fields[prop] = value;
         }
         //if (typeof value === "object" && !(value instanceof ObjectField)) debugger;
@@ -137,7 +139,7 @@ export enum SharingPermissions {
 }
 
 export function GetEffectiveAcl(target: any, in_prop?: string | symbol | number): symbol {
-    if (in_prop === UpdatingFromServer || target[UpdatingFromServer]) return AclEdit;
+    if (in_prop === UpdatingFromServer || target[UpdatingFromServer]) return AclAdmin;
 
     if (target[AclSym] && Object.keys(target[AclSym]).length) {
 
@@ -241,6 +243,8 @@ 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 === FieldsSym || in_prop === Id || in_prop === HandleUpdate || in_prop === CachedUpdates) return target.__fields[prop] || target[prop];
     if (in_prop === AclSym) return _overrideAcl ? undefined : target[AclSym];
     if (GetEffectiveAcl(target) === AclPrivate && !_overrideAcl) return undefined;
     if (prop === LayoutSym) {
-- 
cgit v1.2.3-70-g09d2


From 99e0b01dbe41e56cfdb748b53f03fe71d73a80b7 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Sat, 25 Jul 2020 10:54:12 -0400
Subject: pushpin annotations can follow links to multple targets at once. 
 chaned text boxes so that they can be made isBackround .fixed addin/remoin
 documents from freeform collections.

---
 src/client/util/DocumentManager.ts                 | 44 +++++++++++-----------
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  3 ++
 .../views/nodes/formattedText/FormattedTextBox.tsx |  2 +-
 src/fields/Doc.ts                                  |  1 -
 4 files changed, 27 insertions(+), 23 deletions(-)

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

diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 51b50878d..523dbfca0 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -152,18 +152,18 @@ export class DocumentManager {
             const first = getFirstDocView(annotatedDoc);
             if (first) {
                 annotatedDoc = first.props.Document;
-                if (docView) {
-                    docView.props.focus(annotatedDoc, false);
-                }
+                docView?.props.focus(annotatedDoc, false);
             }
         }
         if (docView) {  // we have a docView already and aren't forced to create a new one ... just focus on the document.  TODO move into view if necessary otherwise just highlight?
-            if (originatingDoc?.isPushpin) docView.props.Document.hidden = !docView.props.Document.hidden;
+            if (originatingDoc?.isPushpin) {
+                docView.props.Document.hidden = !docView.props.Document.hidden;
+            }
             else {
                 docView.props.Document.hidden && (docView.props.Document.hidden = undefined);
                 docView.props.focus(docView.props.Document, willZoom, undefined, focusAndFinish);
+                highlight();
             }
-            highlight();
         } else {
             const contextDocs = docContext ? await DocListCastAsync(docContext.data) : undefined;
             const contextDoc = contextDocs?.find(doc => Doc.AreProtosEqual(doc, targetDoc)) ? docContext : undefined;
@@ -218,25 +218,27 @@ export class DocumentManager {
         const backLinkWithoutTargetView = secondDocs.find(d => DocumentManager.Instance.getDocumentViews(d.anchor1 as Doc).length === 0);
         const linkWithoutTargetDoc = traverseBacklink === undefined ? fwdLinkWithoutTargetView || backLinkWithoutTargetView : traverseBacklink ? backLinkWithoutTargetView : fwdLinkWithoutTargetView;
         const linkDocList = linkWithoutTargetDoc ? [linkWithoutTargetDoc] : (traverseBacklink === undefined ? firstDocs.concat(secondDocs) : traverseBacklink ? secondDocs : firstDocs);
-        const linkDoc = linkDocList.length && linkDocList[0];
-        if (linkDoc) {
-            const target = (doc === linkDoc.anchor1 ? linkDoc.anchor2 : doc === linkDoc.anchor2 ? linkDoc.anchor1 :
-                (Doc.AreProtosEqual(doc, linkDoc.anchor1 as Doc) ? linkDoc.anchor2 : linkDoc.anchor1)) as Doc;
-            const targetTimecode = (doc === linkDoc.anchor1 ? Cast(linkDoc.anchor2_timecode, "number") :
-                doc === linkDoc.anchor2 ? Cast(linkDoc.anchor1_timecode, "number") :
-                    (Doc.AreProtosEqual(doc, linkDoc.anchor1 as Doc) ? Cast(linkDoc.anchor2_timecode, "number") : Cast(linkDoc.anchor1_timecode, "number")));
-            if (target) {
-                const containerDoc = (await Cast(target.annotationOn, Doc)) || target;
-                containerDoc.currentTimecode = targetTimecode;
-                const targetContext = await target?.context as Doc;
-                const targetNavContext = !Doc.AreProtosEqual(targetContext, currentContext) ? targetContext : undefined;
-                DocumentManager.Instance.jumpToDocument(target, zoom, (doc, finished) => createViewFunc(doc, StrCast(linkDoc.followLinkLocation, "onRight"), finished), targetNavContext, linkDoc, undefined, doc, finished);
+        const followLinks = linkDocList.length ? (doc.isPushpin ? linkDocList : [linkDocList[0]]) : [];
+        followLinks.forEach(async linkDoc => {
+            if (linkDoc) {
+                const target = (doc === linkDoc.anchor1 ? linkDoc.anchor2 : doc === linkDoc.anchor2 ? linkDoc.anchor1 :
+                    (Doc.AreProtosEqual(doc, linkDoc.anchor1 as Doc) ? linkDoc.anchor2 : linkDoc.anchor1)) as Doc;
+                const targetTimecode = (doc === linkDoc.anchor1 ? Cast(linkDoc.anchor2_timecode, "number") :
+                    doc === linkDoc.anchor2 ? Cast(linkDoc.anchor1_timecode, "number") :
+                        (Doc.AreProtosEqual(doc, linkDoc.anchor1 as Doc) ? Cast(linkDoc.anchor2_timecode, "number") : Cast(linkDoc.anchor1_timecode, "number")));
+                if (target) {
+                    const containerDoc = (await Cast(target.annotationOn, Doc)) || target;
+                    containerDoc.currentTimecode = targetTimecode;
+                    const targetContext = await target?.context as Doc;
+                    const targetNavContext = !Doc.AreProtosEqual(targetContext, currentContext) ? targetContext : undefined;
+                    DocumentManager.Instance.jumpToDocument(target, zoom, (doc, finished) => createViewFunc(doc, StrCast(linkDoc.followLinkLocation, "onRight"), finished), targetNavContext, linkDoc, undefined, doc, finished);
+                } else {
+                    finished?.();
+                }
             } else {
                 finished?.();
             }
-        } else {
-            finished?.();
-        }
+        });
     }
 }
 Scripting.addGlobal(function DocFocus(doc: any) { DocumentManager.Instance.getDocumentViews(Doc.GetProto(doc)).map(view => view.props.focus(doc, true)); });
\ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 38c1edde2..079add4b0 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -944,6 +944,9 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
     parentActive = (outsideReaction: boolean) => this.props.active(outsideReaction) || this.backgroundActive ? true : false;
     getChildDocumentViewProps(childLayout: Doc, childData?: Doc): DocumentViewProps {
         return {
+            addDocument: this.props.addDocument,
+            removeDocument: this.props.removeDocument,
+            moveDocument: this.props.moveDocument,
             pinToPres: this.props.pinToPres,
             whenActiveChanged: this.props.whenActiveChanged,
             NativeHeight: returnZero,
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 53234d819..966aaa895 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -1406,7 +1406,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
                         <div className={`formattedTextBox-inner${rounded}${selclass}`} ref={this.createDropTarget}
                             style={{
                                 padding: this.layoutDoc._textBoxPadding ? StrCast(this.layoutDoc._textBoxPadding) : `${Math.max(0, NumCast(this.layoutDoc._yMargin, this.props.yMargin || 0) + selPad)}px  ${NumCast(this.layoutDoc._xMargin, this.props.xMargin || 0) + selPad}px`,
-                                pointerEvents: !this.props.isSelected() ? ((this.layoutDoc.isLinkButton || this.props.onClick) ? "none" : "all") : undefined
+                                pointerEvents: !this.props.isSelected() ? ((this.layoutDoc.isLinkButton || this.props.onClick) ? "none" : undefined) : undefined
                             }}
                         />
                     </div>
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 9020dcc84..3ea53a9e0 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -916,7 +916,6 @@ export namespace Doc {
         return doc;
     }
     export function UnBrushDoc(doc: Doc) {
-
         if (!doc || GetEffectiveAcl(doc) === AclPrivate || GetEffectiveAcl(Doc.GetProto(doc)) === AclPrivate) return doc;
         brushManager.BrushedDoc.delete(doc);
         brushManager.BrushedDoc.delete(Doc.GetProto(doc));
-- 
cgit v1.2.3-70-g09d2

