From 8ac814bbb81b690a6a10f5a07aa5ce0e8cafe283 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 30 Jan 2024 00:40:43 -0500
Subject: changed dropConverter to keep title of dropped Doc. added paintFunc
 node/ checkbox view to formatted text.  changed paintFunc to be computed
 based on layouytfieldkey being text in a freeformview.  changed some
 inputRules to apply to code blocks.  changed : contextmenu to allow regular
 note to be created.  changed experimental tools to be user tmeplate tools. 
 fixed focus on search bar when opening context menu

---
 src/client/views/nodes/formattedText/FormattedTextBox.tsx | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

(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 0b857794b..973f90501 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -68,6 +68,7 @@ import { RichTextRules } from './RichTextRules';
 import { schema } from './schema_rts';
 import { SummaryView } from './SummaryView';
 import { CollectionView } from '../../collections/CollectionView';
+import { PaintButtonView } from './PaintButtonView';
 // import * as applyDevTools from 'prosemirror-dev-tools';
 @observer
 export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implements ViewBoxInterface {
@@ -1410,6 +1411,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
                     dashField(node: any, view: any, getPos: any) {
                         return new DashFieldView(node, view, getPos, self);
                     },
+                    paintButton(node: any, view: any, getPos: any) {
+                        return new PaintButtonView(node, view, getPos, self);
+                    },
                     equation(node: any, view: any, getPos: any) {
                         return new EquationView(node, view, getPos, self);
                     },
@@ -1538,15 +1542,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
         this._downX = e.clientX;
         this._downY = e.clientY;
         this._downTime = Date.now();
-        this._hadDownFocus = this.ProseRef?.children[0].className.includes('focused') ?? false;
         FormattedTextBoxComment.textBox = this;
         if (e.button === 0 && this._props.rootSelected?.() && !e.altKey && !e.ctrlKey && !e.metaKey) {
             if (e.clientX < this.ProseRef!.getBoundingClientRect().right) {
                 // stop propagation if not in sidebar, otherwise nested boxes will lose focus to outer boxes.
                 e.stopPropagation(); // if the text box's content is active, then it consumes all down events
                 document.addEventListener('pointerup', this.onSelectEnd);
+                (this.ProseRef?.children?.[0] as any).focus();
             }
         }
+        this._hadDownFocus = this.ProseRef?.children[0].className.includes('focused') ?? false;
         if (e.button === 2 || (e.button === 0 && e.ctrlKey)) {
             e.preventDefault();
         }
-- 
cgit v1.2.3-70-g09d2


From 0d8c05db9cbb5a23a94554c65015c347ff8c38b9 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 31 Jan 2024 20:39:47 -0500
Subject: cleaned up accessing/setting proto_embeddings with api on Doc.  fixed
 some css related to IconButtons.  added a paintView toggle button to dec
 decorations for text with code blocks. enabled text with code to modify Docs
 and get this and documentView as params.

---
 package-lock.json                                  |   9 +-
 package.json                                       |   2 +-
 src/client/util/DocumentManager.ts                 |   4 +-
 src/client/util/DropConverter.ts                   |   1 +
 src/client/util/Scripting.ts                       |   3 +-
 src/client/views/DocComponent.tsx                  |   2 +-
 src/client/views/DocumentDecorations.tsx           |   2 +-
 src/client/views/PropertiesDocContextSelector.tsx  |   6 +-
 src/client/views/PropertiesView.tsx                |   5 +-
 src/client/views/StyleProvider.scss                |   4 +
 src/client/views/StyleProvider.tsx                 |  18 +++-
 .../views/collections/CollectionDockingView.tsx    |   2 +-
 src/client/views/collections/TreeView.scss         |   4 +
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  14 ++-
 .../collectionSchema/CollectionSchemaView.scss     |   7 +-
 .../collections/collectionSchema/SchemaRowBox.tsx  |   2 +-
 src/client/views/nodes/DocumentView.tsx            |   6 +-
 .../views/nodes/formattedText/FormattedTextBox.tsx |  55 +++++-----
 .../views/nodes/formattedText/PaintButtonView.tsx  | 113 ---------------------
 .../views/nodes/formattedText/RichTextRules.ts     |  49 +++------
 src/fields/Doc.ts                                  |  25 +++--
 21 files changed, 127 insertions(+), 206 deletions(-)
 delete mode 100644 src/client/views/nodes/formattedText/PaintButtonView.tsx

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

diff --git a/package-lock.json b/package-lock.json
index aa4108243..aafc14a86 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -154,7 +154,7 @@
                 "prosemirror-commands": "^1.5.2",
                 "prosemirror-find-replace": "^0.9.0",
                 "prosemirror-history": "^1.3.2",
-                "prosemirror-inputrules": "github:bobzel/prosemirror-inputrules#3b3b3e0b1a1dcf80490d81675206369b6be96276",
+                "prosemirror-inputrules": "^1.4.0",
                 "prosemirror-keymap": "^1.2.2",
                 "prosemirror-model": "^1.19.3",
                 "prosemirror-schema-list": "^1.3.0",
@@ -28699,10 +28699,9 @@
             }
         },
         "node_modules/prosemirror-inputrules": {
-            "version": "1.3.0",
-            "resolved": "git+ssh://git@github.com/bobzel/prosemirror-inputrules.git#3b3b3e0b1a1dcf80490d81675206369b6be96276",
-            "integrity": "sha512-l81tcwS7ugPJEvCy78RtEvR2/2mVaTkFHJD9ACRxRDXhrfPWV0FkFYBAO7GolN4XlzIbIsal2MVvq2baLQ2guw==",
-            "license": "MIT",
+            "version": "1.4.0",
+            "resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.4.0.tgz",
+            "integrity": "sha512-6ygpPRuTJ2lcOXs9JkefieMst63wVJBgHZGl5QOytN7oSZs3Co/BYbc3Yx9zm9H37Bxw8kVzCnDsihsVsL4yEg==",
             "dependencies": {
                 "prosemirror-state": "^1.0.0",
                 "prosemirror-transform": "^1.0.0"
diff --git a/package.json b/package.json
index f8144a0b3..a94f8c9be 100644
--- a/package.json
+++ b/package.json
@@ -237,7 +237,7 @@
         "prosemirror-commands": "^1.5.2",
         "prosemirror-find-replace": "^0.9.0",
         "prosemirror-history": "^1.3.2",
-        "prosemirror-inputrules": "github:bobzel/prosemirror-inputrules#3b3b3e0b1a1dcf80490d81675206369b6be96276",
+        "prosemirror-inputrules": "^1.4.0",
         "prosemirror-keymap": "^1.2.2",
         "prosemirror-model": "^1.19.3",
         "prosemirror-schema-list": "^1.3.0",
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index f730d17fe..53d472c66 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -355,8 +355,8 @@ export function DocFocusOrOpen(doc: Doc, options: FocusViewOptions = { willZoomC
             });
         }
     };
-    if (Doc.IsDataProto(doc) && DocListCast(doc.proto_embeddings).some(embed => embed.hidden && [AclAdmin, AclEdit].includes(GetEffectiveAcl(embed)))) {
-        doc = DocListCast(doc.proto_embeddings).find(embed => embed.hidden && [AclAdmin, AclEdit].includes(GetEffectiveAcl(embed)))!;
+    if (Doc.IsDataProto(doc) && Doc.GetEmbeddings(doc).some(embed => embed.hidden && [AclAdmin, AclEdit].includes(GetEffectiveAcl(embed)))) {
+        doc = Doc.GetEmbeddings(doc).find(embed => embed.hidden && [AclAdmin, AclEdit].includes(GetEffectiveAcl(embed)))!;
     }
     if (doc.hidden) {
         doc.hidden = false;
diff --git a/src/client/util/DropConverter.ts b/src/client/util/DropConverter.ts
index ba981145d..54066d267 100644
--- a/src/client/util/DropConverter.ts
+++ b/src/client/util/DropConverter.ts
@@ -73,6 +73,7 @@ export function convertDropDataToButtons(data: DragManager.DocumentDragData) {
                 _nativeHeight: 100,
                 _width: 100,
                 _height: 100,
+                _layout_hideContextMenu: true,
                 backgroundColor: StrCast(doc.backgroundColor),
                 title: StrCast(layoutDoc.title),
                 btnType: ButtonType.ClickButton,
diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts
index dfaacf318..f5e162d16 100644
--- a/src/client/util/Scripting.ts
+++ b/src/client/util/Scripting.ts
@@ -61,7 +61,8 @@ function Run(script: string | undefined, customParams: string[], diagnostics: an
     const compiledFunction = (() => {
         try {
             return new Function(...paramNames, `return ${script}`);
-        } catch {
+        } catch (e) {
+            console.log(e);
             return undefined;
         }
     })();
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index 73fa6709c..58be41f53 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -189,7 +189,7 @@ export function ViewBoxAnnotatableComponent<P extends FieldViewProps>() {
                 toRemove.forEach(doc => {
                     leavePushpin && DocUtils.LeavePushpin(doc, annotationKey ?? this.annotationKey);
                     Doc.RemoveDocFromList(targetDataDoc, annotationKey ?? this.annotationKey, doc, true);
-                    Doc.RemoveDocFromList(doc[DocData], 'proto_embeddings', doc, true);
+                    Doc.RemoveEmbedding(doc, doc);
                     doc.embedContainer = undefined;
                     if (recent && !dontAddToRemoved) {
                         doc.type !== DocumentType.LOADING && Doc.AddDocToList(recent, 'data', doc, undefined, true, true);
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 2193acf62..dec109d7b 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -283,7 +283,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
             } else {
                 var openDoc = selectedDocs[0].Document;
                 if (openDoc.layout_fieldKey === 'layout_icon') {
-                    openDoc = DocListCast(openDoc.proto_embeddings).find(embedding => !embedding.embedContainer) ?? Doc.MakeEmbedding(openDoc);
+                    openDoc = Doc.GetEmbeddings(openDoc).find(embedding => !embedding.embedContainer) ?? Doc.MakeEmbedding(openDoc);
                     Doc.deiconifyView(openDoc);
                 }
                 LightboxView.Instance.SetLightboxDoc(
diff --git a/src/client/views/PropertiesDocContextSelector.tsx b/src/client/views/PropertiesDocContextSelector.tsx
index 54f141a36..361451c4d 100644
--- a/src/client/views/PropertiesDocContextSelector.tsx
+++ b/src/client/views/PropertiesDocContextSelector.tsx
@@ -28,14 +28,14 @@ export class PropertiesDocContextSelector extends ObservableReactComponent<Prope
         if (!this._props.DocView) return [];
         const target = this._props.DocView._props.Document;
         const targetContext = this._props.DocView.containerViewPath?.().lastElement()?.Document;
-        const embeddings = DocListCast(target.proto_embeddings);
+        const embeddings = Doc.GetEmbeddings(target);
         const containerProtos = embeddings.filter(embedding => embedding.embedContainer && embedding.embedContainer instanceof Doc).reduce((set, embedding) => set.add(Cast(embedding.embedContainer, Doc, null)), new Set<Doc>());
-        const containerSets = Array.from(containerProtos.keys()).map(container => DocListCast(container.proto_embeddings));
+        const containerSets = Array.from(containerProtos.keys()).map(container => Doc.GetEmbeddings(container));
         const containers = containerSets.reduce((p, set) => {
             set.map(s => p.add(s));
             return p;
         }, new Set<Doc>());
-        const doclayoutSets = Array.from(containers.keys()).map(dp => DocListCast(dp.proto_embeddings));
+        const doclayoutSets = Array.from(containers.keys()).map(dp => Doc.GetEmbeddings(dp));
         const doclayouts = Array.from(
             doclayoutSets
                 .reduce((p, set) => {
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index 208ed56c9..a7a065a95 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -173,7 +173,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
         if (this.dataDoc && this.selectedDoc) {
             const ids = new Set<string>(reqdKeys);
             const docs: Doc[] = SelectionManager.Views.length < 2 ? [this.layoutFields ? Doc.Layout(this.selectedDoc) : this.dataDoc] : SelectionManager.Views.map(dv => (this.layoutFields ? dv.layoutDoc : dv.dataDoc));
-            docs.forEach(doc => Object.keys(doc).forEach(key => doc[key] !== ComputedField.undefined && ids.add(key)));
+            docs.forEach(doc => Object.keys(doc).forEach(key => doc[key] !== ComputedField.undefined && key && ids.add(key)));
 
             // prettier-ignore
             Array.from(ids).filter(filter).sort().map(key => {
@@ -263,8 +263,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
     @computed get contextCount() {
         if (this.selectedDocumentView) {
             const target = this.selectedDocumentView.Document;
-            const embeddings = DocListCast(target.proto_embeddings);
-            return embeddings.length - 1;
+            return Doc.GetEmbeddings(target).length - 1;
         } else {
             return 0;
         }
diff --git a/src/client/views/StyleProvider.scss b/src/client/views/StyleProvider.scss
index 4d3096f71..00bf503f5 100644
--- a/src/client/views/StyleProvider.scss
+++ b/src/client/views/StyleProvider.scss
@@ -1,5 +1,6 @@
 .styleProvider-filter,
 .styleProvider-audio,
+.styleProvider-paint,
 .styleProvider-lock {
     font-size: 10;
     width: 15;
@@ -28,6 +29,9 @@
 .styleProvider-audio {
     right: 30;
 }
+.styleProvider-paint {
+    top: 15;
+}
 .styleProvider-lock:hover,
 .styleProvider-audio:hover,
 .styleProvider-filter:hover {
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index 5a0167338..d3d13988f 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -9,7 +9,7 @@ import { BsArrowDown, BsArrowDownUp, BsArrowUp } from 'react-icons/bs';
 import { FaFilter } from 'react-icons/fa';
 import { Doc, Opt, StrListCast } from '../../fields/Doc';
 import { DocViews } from '../../fields/DocSymbols';
-import { BoolCast, Cast, DocCast, ImageCast, NumCast, StrCast } from '../../fields/Types';
+import { BoolCast, Cast, DocCast, ImageCast, NumCast, ScriptCast, StrCast } from '../../fields/Types';
 import { DashColor, lightOrDark, Utils } from '../../Utils';
 import { CollectionViewType, DocumentType } from '../documents/DocumentTypes';
 import { DocFocusOrOpen, DocumentManager } from '../util/DocumentManager';
@@ -53,6 +53,16 @@ export enum StyleProp {
 function toggleLockedPosition(doc: Doc) {
     UndoManager.RunInBatch(() => Doc.toggleLockedPosition(doc), 'toggleBackground');
 }
+function togglePaintView(e: React.MouseEvent, doc: Opt<Doc>, props: Opt<FieldViewProps & DocumentViewProps>) {
+    const scriptProps = {
+        this: doc,
+        _readOnly_: false,
+        documentView: props?.DocumentView?.(),
+        value: undefined,
+    };
+    e.stopPropagation();
+    UndoManager.RunInBatch(() => doc && ScriptCast(doc.onPaint).script.run(scriptProps), 'togglePaintView');
+}
 
 export function wavyBorderPath(pw: number, ph: number, inset: number = 0.05) {
     return `M ${pw * 0.5} ${ph * inset}   C ${pw * 0.6} ${ph * inset} ${pw * (1 - 2 * inset)} 0 ${pw * (1 - inset)} ${ph * inset}   C ${pw} ${ph * (2 * inset)} ${pw * (1 - inset)} ${ph * 0.25} ${pw * (1 - inset)} ${ph * 0.3}   C ${
@@ -267,6 +277,11 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
                             <FontAwesomeIcon icon='lock' size="lg" />
                         </div>
                     );
+            const paint = () => !doc?.onPaint ? null : (
+                        <div className="styleProvider-paint" onClick={e => togglePaintView(e, doc, props)}>
+                            <FontAwesomeIcon icon='pen' size="lg" />
+                        </div>
+                    );
             const filter = () => {
                 const dashView = untracked(() => DocumentManager.Instance.getDocumentView(Doc.ActiveDashboard));
                 const showFilterIcon =
@@ -329,6 +344,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
             };
             return (
                 <>
+                    {paint()}
                     {lock()}
                     {filter()}
                     {audio()}
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 87973fd81..31ca86f0f 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -490,7 +490,7 @@ export class CollectionDockingView extends CollectionSubView() {
             // if you close a tab that is not embedded somewhere else (an embedded Doc can be opened simultaneously in a tab), then add the tab to recently closed
             if (tab.DashDoc.embedContainer === this.Document) tab.DashDoc.embedContainer = undefined;
             if (!tab.DashDoc.embedContainer) Doc.AddDocToList(Doc.MyRecentlyClosed, 'data', tab.DashDoc, undefined, true, true);
-            Doc.RemoveDocFromList(tab.DashDoc[DocData], 'proto_embeddings', tab.DashDoc);
+            Doc.RemoveEmbedding(tab.DashDoc, tab.DashDoc);
         }
         if (CollectionDockingView.Instance) {
             const dview = CollectionDockingView.Instance.Document;
diff --git a/src/client/views/collections/TreeView.scss b/src/client/views/collections/TreeView.scss
index 0a1946f09..09701ddb5 100644
--- a/src/client/views/collections/TreeView.scss
+++ b/src/client/views/collections/TreeView.scss
@@ -128,6 +128,10 @@
     position: relative;
     z-index: 1;
 
+    .treeView-rightButtons > .iconButton-container {
+        min-height: unset;
+    }
+
     .treeView-background {
         width: 100%;
         height: 100%;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 54e8b08b6..9368560e9 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -54,6 +54,7 @@ import { CollectionFreeFormRemoteCursors } from './CollectionFreeFormRemoteCurso
 import './CollectionFreeFormView.scss';
 import { MarqueeView } from './MarqueeView';
 import { RichTextField } from '../../../../fields/RichTextField';
+import { CompileScript } from '../../../util/Scripting';
 
 export interface collectionFreeformViewProps {
     NativeWidth?: () => number;
@@ -80,8 +81,10 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         const paintFunc = StrCast(Field.toJavascriptString(Cast(field, RichTextField, null)?.Text as Field)).trim();
         return !paintFunc
             ? ''
-            : `const dashDiv = document.querySelector('#${this._paintedId}'); 
-        (async () => {  ${paintFunc} })()`;
+            : paintFunc.includes('dashDiv')
+              ? `const dashDiv = document.querySelector('#${this._paintedId}');
+                    (async () => {  ${paintFunc} })()`
+              : paintFunc;
     }
     constructor(props: any) {
         super(props);
@@ -1496,7 +1499,12 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
 
         this._disposers.paintFunc = reaction(
             () => ({ code: this.paintFunc, first: this._firstRender, width: this.Document._width, height: this.Document._height }),
-            ({ code, first }) => code && !first && eval(code),
+            ({ code, first }) => {
+                if (!code.includes('dashDiv')) {
+                    const script = CompileScript(code, { params: { docView: 'any' }, typecheck: false, editable: true });
+                    if (script.compiled) script.run({ this: this.Document, docView: this.DocumentView?.() });
+                } else code && !first && eval(code);
+            },
             { fireImmediately: true }
         );
 
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.scss b/src/client/views/collections/collectionSchema/CollectionSchemaView.scss
index 29d121974..29491569a 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaView.scss
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.scss
@@ -128,7 +128,7 @@
 
     .row-menu {
         display: flex;
-        justify-content: flex-end;
+        justify-content: center;
     }
 }
 
@@ -224,7 +224,10 @@
         display: flex;
         flex-direction: row;
         min-width: 50px;
-        justify-content: flex-end;
+        justify-content: center;
+        .iconButton-container {
+            min-width: unset !important;
+        }
     }
 
     .row-cells {
diff --git a/src/client/views/collections/collectionSchema/SchemaRowBox.tsx b/src/client/views/collections/collectionSchema/SchemaRowBox.tsx
index f2fe0dde7..39fea2d2e 100644
--- a/src/client/views/collections/collectionSchema/SchemaRowBox.tsx
+++ b/src/client/views/collections/collectionSchema/SchemaRowBox.tsx
@@ -121,7 +121,7 @@ export class SchemaRowBox extends ViewBoxBaseComponent<SchemaRowBoxProps>() {
                         pointerEvents: !this._props.isContentActive() ? 'none' : undefined,
                     }}>
                     <IconButton
-                        tooltip="whether document interations are enabled"
+                        tooltip="whether document interactions are enabled"
                         icon={this.Document._lockedPosition ? <CgLockUnlock size="12px" /> : <CgLock size="12px" />}
                         size={Size.XSMALL}
                         onPointerDown={e =>
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 51f4b1a68..b5355fb99 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -456,8 +456,8 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
 
     deleteClicked = undoable(() => this._props.removeDocument?.(this.Document), 'delete doc');
     setToggleDetail = undoable(
-        (defaultLayout: string) =>
-            (this.Document.onClick = ScriptField.MakeScript(
+        (defaultLayout: string, scriptFieldKey: 'onClick') =>
+            (this.Document[scriptFieldKey] = ScriptField.MakeScript(
                 `toggleDetail(documentView, "${StrCast(this.Document.layout_fieldKey)
                     .replace('layout_', '')
                     .replace(/^layout$/, 'detail')}", "${defaultLayout}")`,
@@ -1212,7 +1212,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
     };
     public noOnClick = () => this._docViewInternal?.noOnClick();
     public toggleFollowLink = (zoom?: boolean, setTargetToggle?: boolean): void => this._docViewInternal?.toggleFollowLink(zoom, setTargetToggle);
-    public setToggleDetail = (defaultLayout = '') => this._docViewInternal?.setToggleDetail(defaultLayout);
+    public setToggleDetail = (defaultLayout = '', scriptFieldKey = 'onClick') => this._docViewInternal?.setToggleDetail(defaultLayout, scriptFieldKey);
     public onContextMenu = (e?: React.MouseEvent, pageX?: number, pageY?: number) => this._docViewInternal?.onContextMenu?.(e, pageX, pageY);
     public cleanupPointerEvents = () => this._docViewInternal?.cleanupPointerEvents();
     public startDragging = (x: number, y: number, dropAction: dropActionType, hideSource = false) => this._docViewInternal?.startDragging(x, y, dropAction, hideSource);
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 973f90501..b82ab4219 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -67,8 +67,6 @@ import { RichTextMenu, RichTextMenuPlugin } from './RichTextMenu';
 import { RichTextRules } from './RichTextRules';
 import { schema } from './schema_rts';
 import { SummaryView } from './SummaryView';
-import { CollectionView } from '../../collections/CollectionView';
-import { PaintButtonView } from './PaintButtonView';
 // import * as applyDevTools from 'prosemirror-dev-tools';
 @observer
 export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implements ViewBoxInterface {
@@ -488,14 +486,29 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
     };
 
     // creates links between terms in a document and published documents (myPublishedDocs) that have titles starting with an '@'
+    /**
+     * Searches the text for occurences of any strings that match the names of 'published' documents.  These document
+     * names will begin with an '@' prefix.  However, valid matches within the text can have any of the following formats:
+     *    name, @<name>, or ^@<name>
+     * The last of these is interpreted as an include directive when converting the text into evaluated code in the paint
+     * function of a freeform view that is driven by the text box's text.  The include directive will copy the code of the published
+     * document into the code being evaluated.
+     */
     hyperlinkTerm = (tr: any, target: Doc, newAutoLinks: Set<Doc>) => {
         const editorView = this._editorView;
         if (editorView && (editorView as any).docView && !Doc.AreProtosEqual(target, this.Document)) {
             const autoLinkTerm = StrCast(target.title).replace(/^@/, '');
             var alink: Doc | undefined;
             this.findInNode(editorView, editorView.state.doc, autoLinkTerm).forEach(sel => {
-                const splitter = editorView.state.schema.marks.splitter.create({ id: Utils.GenerateGuid() });
-                if (!sel.$anchor.pos || [autoLinkTerm, StrCast(target.title)].includes(editorView.state.doc.textBetween(sel.$anchor.pos - 1, sel.$to.pos).trim())) {
+                if (
+                    !sel.$anchor.pos ||
+                    autoLinkTerm ===
+                        editorView.state.doc
+                            .textBetween(sel.$anchor.pos - 1, sel.$to.pos)
+                            .trim()
+                            .replace(/[\^@]+/, '')
+                ) {
+                    const splitter = editorView.state.schema.marks.splitter.create({ id: Utils.GenerateGuid() });
                     tr = tr.addMark(sel.from, sel.to, splitter);
                     tr.doc.nodesBetween(sel.from, sel.to, (node: any, pos: number, parent: any) => {
                         if (node.firstChild === null && !node.marks.find((m: Mark) => m.type.name === schema.marks.noAutoLinkAnchor.name) && node.marks.find((m: Mark) => m.type.name === schema.marks.splitter.name)) {
@@ -668,12 +681,22 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
             let index = 0,
                 foundAt;
             const ep = this.getNodeEndpoints(pm.state.doc, node);
-            const regexp = new RegExp(find.replace('*', ''), 'i');
+            const regexp = new RegExp(find, 'i');
             if (regexp) {
-                while (ep && (foundAt = node.textContent.slice(index).search(regexp)) > -1) {
-                    const sel = new TextSelection(pm.state.doc.resolve(ep.from + index + foundAt + 1), pm.state.doc.resolve(ep.from + index + foundAt + find.length + 1));
-                    ret.push(sel);
-                    index = index + foundAt + find.length;
+                var blockOffset = 0;
+                for (var i = 0; i < node.childCount; i++) {
+                    var textContent = '';
+                    while (i < node.childCount && node.child(i).type === pm.state.schema.nodes.text) {
+                        textContent += node.child(i).textContent;
+                        i++;
+                    }
+                    while (ep && (foundAt = textContent.slice(index).search(regexp)) > -1) {
+                        const sel = new TextSelection(pm.state.doc.resolve(ep.from + index + blockOffset + foundAt + 1), pm.state.doc.resolve(ep.from + index + blockOffset + foundAt + find.length + 1));
+                        ret.push(sel);
+                        index = index + foundAt + find.length;
+                    }
+                    blockOffset += textContent.length;
+                    if (i < node.childCount) blockOffset += node.child(i).nodeSize;
                 }
             }
         } else {
@@ -934,17 +957,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
                 event: () => (this.layoutDoc._createDocOnCR = !this.layoutDoc._createDocOnCR),
                 icon: !this.Document._createDocOnCR ? 'grip-lines' : 'bars',
             });
-        optionItems.push({
-            description: 'Make Paint Function',
-            event: () => {
-                this.dataDoc.layout_painted = CollectionView.LayoutString('painted');
-                this.layoutDoc.layout_fieldKey = 'layout_painted';
-                this.layoutDoc.type_collection = CollectionViewType.Freeform;
-                this.DocumentView?.().setToggleDetail();
-                this.dataDoc.paintFunc = ComputedField.MakeFunction(`toJavascriptString(this['${this.fieldKey}']?.Text)`);
-            },
-            icon: !this.Document._layout_enableAltContentUI ? 'eye-slash' : 'eye',
-        });
         !Doc.noviceMode &&
             optionItems.push({
                 description: `${this.Document._layout_autoHeight ? 'Lock' : 'Auto'} Height`,
@@ -1411,9 +1423,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
                     dashField(node: any, view: any, getPos: any) {
                         return new DashFieldView(node, view, getPos, self);
                     },
-                    paintButton(node: any, view: any, getPos: any) {
-                        return new PaintButtonView(node, view, getPos, self);
-                    },
                     equation(node: any, view: any, getPos: any) {
                         return new EquationView(node, view, getPos, self);
                     },
diff --git a/src/client/views/nodes/formattedText/PaintButtonView.tsx b/src/client/views/nodes/formattedText/PaintButtonView.tsx
deleted file mode 100644
index 74423c772..000000000
--- a/src/client/views/nodes/formattedText/PaintButtonView.tsx
+++ /dev/null
@@ -1,113 +0,0 @@
-import { action, computed, IReactionDisposer, makeObservable } from 'mobx';
-import { observer } from 'mobx-react';
-import * as React from 'react';
-import * as ReactDOM from 'react-dom/client';
-import { Doc } from '../../../../fields/Doc';
-import { ObservableReactComponent } from '../../ObservableReactComponent';
-import './DashFieldView.scss';
-import { FormattedTextBox } from './FormattedTextBox';
-import { CollectionViewType } from '../../../documents/DocumentTypes';
-import { CollectionView } from '../../collections/CollectionView';
-import { StrCast } from '../../../../fields/Types';
-
-export class PaintButtonView {
-    dom: HTMLDivElement; // container for label and value
-    root: any;
-    node: any;
-    tbox: FormattedTextBox;
-
-    constructor(node: any, view: any, getPos: any, tbox: FormattedTextBox) {
-        this.node = node;
-        this.tbox = tbox;
-        this.dom = document.createElement('div');
-        this.dom.style.width = node.attrs.width;
-        this.dom.style.height = node.attrs.height;
-        this.dom.style.position = 'relative';
-        this.dom.style.display = 'inline-block';
-        this.dom.onkeypress = function (e: any) {
-            e.stopPropagation();
-        };
-        this.dom.onkeydown = function (e: any) {
-            e.stopPropagation();
-        };
-        this.dom.onkeyup = function (e: any) {
-            e.stopPropagation();
-        };
-        this.dom.onmousedown = function (e: any) {
-            e.stopPropagation();
-        };
-
-        this.root = ReactDOM.createRoot(this.dom);
-        this.root.render(<PaintButtonViewInternal node={node} getPos={getPos} width={node.attrs.width} height={node.attrs.height} tbox={tbox} />);
-    }
-    destroy() {
-        setTimeout(() => {
-            try {
-                this.root.unmount();
-            } catch {}
-        });
-    }
-    deselectNode() {
-        this.dom.classList.remove('ProseMirror-selectednode');
-    }
-    selectNode() {
-        this.dom.classList.add('ProseMirror-selectednode');
-    }
-}
-
-interface IPaintButtonViewInternal {
-    tbox: FormattedTextBox;
-    width: number;
-    height: number;
-    node: any;
-    getPos: any;
-}
-
-@observer
-export class PaintButtonViewInternal extends ObservableReactComponent<IPaintButtonViewInternal> {
-    _reactionDisposer: IReactionDisposer | undefined;
-    _textBoxDoc: Doc;
-
-    constructor(props: IPaintButtonViewInternal) {
-        super(props);
-        makeObservable(this);
-        this._textBoxDoc = this._props.tbox.Document;
-    }
-
-    return100 = () => 100;
-    @computed get _checked() {
-        return this._props.tbox.Document.onClick ? true : false;
-    }
-
-    onCheckClick = () => {
-        const textView = this._props.tbox.DocumentView?.();
-        if (textView) {
-            const paintedField = 'layout_' + this._props.tbox.fieldKey + 'Painted';
-            const layoutFieldKey = StrCast(textView.layoutDoc.layout_fieldKey, 'layout');
-            if (textView.layoutDoc.onClick) {
-                textView.layoutDoc[paintedField] = undefined;
-                textView.layoutDoc.onClick = undefined;
-            } else {
-                textView.layoutDoc.type_collection = CollectionViewType.Freeform;
-                textView.dataDoc[paintedField] = CollectionView.LayoutString(this._props.tbox.fieldKey);
-                textView.layoutDoc.layout_fieldKey = paintedField;
-                textView.setToggleDetail(layoutFieldKey.replace('layout_', '').replace('layout', ''));
-                textView.layoutDoc.layout_fieldKey = layoutFieldKey;
-            }
-        }
-    };
-
-    render() {
-        return (
-            <div
-                className="dashFieldView"
-                style={{
-                    width: this._props.width,
-                    height: this._props.height,
-                    pointerEvents: this._props.tbox._props.isSelected() || this._props.tbox.isAnyChildContentActive?.() ? undefined : 'none',
-                }}>
-                <input type="checkbox" value="paint" checked={this._checked} onChange={e => this.onCheckClick()} />
-            </div>
-        );
-    }
-}
diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts
index 8f7bc5282..ce2c33fb4 100644
--- a/src/client/views/nodes/formattedText/RichTextRules.ts
+++ b/src/client/views/nodes/formattedText/RichTextRules.ts
@@ -68,40 +68,21 @@ export class RichTextRules {
             ),
 
             // ``` create code block
-            textblockTypeInputRule(/^```$/, schema.nodes.code_block),
-            new InputRule(
-                new RegExp(/(^|\n)\^@paint/), // for code blocks '^' means the beginning of the block, not the line, so need to test for \n
-                (state, match, start, end) => {
-                    const { dataDoc, layoutDoc, fieldKey } = this.TextBox;
-                    layoutDoc.type_collection = CollectionViewType.Freeform;
-                    const paintedField = 'layout_' + this.TextBox.fieldKey + 'Painted';
-                    dataDoc[paintedField] = CollectionView.LayoutString(this.TextBox.fieldKey);
-                    const layoutFieldKey = StrCast(layoutDoc.layout_fieldKey);
-                    layoutDoc.layout_fieldKey = paintedField;
-                    this.TextBox.DocumentView?.().setToggleDetail(layoutFieldKey.replace('layout_', '').replace('layout', ''));
-                    layoutDoc.layout_fieldKey = layoutFieldKey;
-                    const comment = '/* enable as paint function ';
-                    const endComment = ' */\n';
-                    const inCode = state.tr.selection.$anchor.node().type === schema.nodes.code_block;
-                    if (inCode) {
-                        const tr = state.tr
-                            .deleteRange(start, end)
-                            .insertText(comment)
-                            .insert(start + comment.length, schema.nodes.paintButton.create())
-                            .insertText(endComment);
-                        return tr.setSelection(new TextSelection(tr.doc.resolve(start + comment.length + endComment.length + 1)));
-                    } else {
-                        const tr = state.tr
-                            .deleteRange(start, end)
-                            .insertText(comment)
-                            .insert(start + comment.length, schema.nodes.paintButton.create())
-                            .insertText(endComment)
-                            .insert(start + comment.length + endComment.length + 1, schema.nodes.code_block.create());
-                        return tr.setSelection(new TextSelection(tr.doc.resolve(start + comment.length + endComment.length + 3)));
-                    }
-                },
-                { inCode: true }
-            ),
+            new InputRule(/^```$/, (state, match, start, end) => {
+                let $start = state.doc.resolve(start);
+                if (!$start.node(-1).canReplaceWith($start.index(-1), $start.indexAfter(-1), schema.nodes.code_block)) return null;
+
+                // this enables text with code blocks to be used as a 'paint' function via a styleprovider button that is added to Docs that have an onPaint script
+                this.TextBox.layoutDoc.type_collection = CollectionViewType.Freeform; // make it a freeform when rendered as a collection since those are the only views that know about the paint function
+                const paintedField = 'layout_' + this.TextBox.fieldKey + 'Painted'; // make a layout field key for storing the CollectionView jsx string pointing to the textbox's text
+                this.TextBox.dataDoc[paintedField] = CollectionView.LayoutString(this.TextBox.fieldKey);
+                const layoutFieldKey = StrCast(this.TextBox.layoutDoc.layout_fieldKey); // save the current layout fieldkey
+                this.TextBox.layoutDoc.layout_fieldKey = paintedField; // setup the paint layout field key
+                this.TextBox.DocumentView?.().setToggleDetail(layoutFieldKey.replace('layout_', '').replace('layout', ''), 'onPaint'); // create the script to toggle between the painted and regular view
+                this.TextBox.layoutDoc.layout_fieldKey = layoutFieldKey; // restore the layout field key to text
+
+                return state.tr.delete(start, end).setBlockType(start, start, schema.nodes.code_block);
+            }),
 
             // %<font-size> set the font size
             new InputRule(new RegExp(/%([0-9]+)\s$/), (state, match, start, end) => {
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 30e3aa5f0..f3fc51671 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -572,6 +572,16 @@ export namespace Doc {
         return false;
     }
 
+    export function RemoveEmbedding(doc: Doc, embedding: Doc) {
+        Doc.RemoveDocFromList(doc[DocData], 'proto_embeddings', embedding);
+    }
+    export function AddEmbedding(doc: Doc, embedding: Doc) {
+        Doc.AddDocToList(doc[DocData], 'proto_embeddings', embedding, undefined, undefined, undefined, undefined, undefined, true);
+    }
+    export function GetEmbeddings(doc: Doc) {
+        return DocListCast(Doc.Get(doc[DocData], 'proto_embeddings', true));
+    }
+
     export function MakeEmbedding(doc: Doc, id?: string) {
         const embedding = (!GetT(doc, 'isDataDoc', 'boolean', true) && doc.proto) || doc.type === DocumentType.CONFIG ? Doc.MakeCopy(doc, undefined, id) : Doc.MakeDelegate(doc, id);
         const layout = Doc.LayoutField(embedding);
@@ -579,20 +589,18 @@ export namespace Doc {
             Doc.SetLayout(embedding, Doc.MakeEmbedding(layout));
         }
         embedding.createdFrom = doc;
-        embedding.proto_embeddingId = doc[DocData].proto_embeddingId = DocListCast(doc[DocData].proto_embeddings).length - 1;
+        embedding.proto_embeddingId = doc[DocData].proto_embeddingId = Doc.GetEmbeddings(doc).length - 1;
         !Doc.GetT(embedding, 'title', 'string', true) && (embedding.title = ComputedField.MakeFunction(`renameEmbedding(this)`));
         embedding.author = Doc.CurrentUserEmail;
 
-        Doc.AddDocToList(doc[DocData], 'proto_embeddings', embedding);
-
         return embedding;
     }
 
     export function BestEmbedding(doc: Doc) {
         const dataDoc = doc[DocData];
-        const availableEmbeddings = DocListCast(dataDoc.proto_embeddings);
+        const availableEmbeddings = Doc.GetEmbeddings(dataDoc);
         const bestEmbedding = [...(dataDoc !== doc ? [doc] : []), ...availableEmbeddings].find(doc => !doc.embedContainer && doc.author === Doc.CurrentUserEmail);
-        bestEmbedding && Doc.AddDocToList(dataDoc, 'protoEmbeddings', doc);
+        bestEmbedding && Doc.AddDocToList(dataDoc, 'protoEmbeddings', doc, undefined, undefined, undefined, undefined, undefined, true);
         return bestEmbedding ?? Doc.MakeEmbedding(doc);
     }
 
@@ -951,7 +959,7 @@ export namespace Doc {
             Doc.GetProto(copy).embedContainer = undefined;
             Doc.GetProto(copy).proto_embeddings = new List<Doc>([copy]);
         } else {
-            Doc.AddDocToList(copy[DocData], 'proto_embeddings', copy);
+            Doc.AddEmbedding(copy, copy);
         }
         copy.embedContainer = undefined;
         if (retitle) {
@@ -972,9 +980,10 @@ export namespace Doc {
             Object.keys(doc)
                 .filter(key => key.startsWith('acl'))
                 .forEach(key => (delegate[key] = doc[key]));
-            if (!Doc.IsSystem(doc)) Doc.AddDocToList(doc[DocData], 'proto_embeddings', delegate);
+            if (!Doc.IsSystem(doc)) Doc.AddEmbedding(doc, delegate);
             title && (delegate.title = title);
             delegate[Initializing] = false;
+            Doc.AddEmbedding(doc, delegate);
             return delegate;
         }
         return undefined;
@@ -995,7 +1004,7 @@ export namespace Doc {
         delegate[Initializing] = true;
         delegate.proto = delegateProto;
         delegate.author = Doc.CurrentUserEmail;
-        Doc.AddDocToList(delegateProto[DocData], 'proto_embeddings', delegate);
+        Doc.AddEmbedding(delegateProto, delegate);
         delegate[Initializing] = false;
         delegateProto[Initializing] = false;
         return delegate;
-- 
cgit v1.2.3-70-g09d2


From a888150f2e220eff4629551aa03813f92aa0b12f Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 5 Feb 2024 22:56:04 -0500
Subject: changed backgroundColor to set on dataDocs. fixed pivoting on tags. 
 fixed link description editing popup.  fixed showing link editor in property
 view - still some weirdness in what is selected.  fixed dragging tree view
 items to set dragData.treeview and be able to drop at bottom of tree. fixed
 addFolder menu option for TreeViews to add locally..  added a function to
 collect all docs of a given tag into a collection.  fixed setting default
 font size to update autolayouts.  changed dropping link onto same collection
 to not leave pushpin. fixed minimap thumb updating.  added fieldvalue
 dropdown for dashFieldViews in text.

---
 src/client/documents/Documents.ts                  |  4 +-
 src/client/util/CurrentUserUtils.ts                | 22 ++-----
 src/client/util/DragManager.ts                     |  2 +-
 src/client/util/LinkManager.ts                     |  4 +-
 src/client/util/SearchUtil.ts                      |  7 +-
 src/client/util/SelectionManager.ts                |  4 +-
 src/client/util/SettingsManager.tsx                | 12 +++-
 src/client/views/DocComponent.tsx                  |  4 +-
 src/client/views/DocumentDecorations.tsx           |  3 +
 src/client/views/FilterPanel.tsx                   |  6 +-
 .../views/PropertiesDocBacklinksSelector.tsx       |  2 +-
 src/client/views/PropertiesSection.scss            | 26 ++++----
 src/client/views/PropertiesView.tsx                | 74 ++++++++++++----------
 src/client/views/StyleProvider.tsx                 |  2 +-
 .../views/collections/CollectionTimeView.tsx       |  7 +-
 .../views/collections/CollectionTreeView.tsx       | 73 ++++++++++++++-------
 src/client/views/collections/TabDocView.tsx        |  1 +
 src/client/views/collections/TreeView.scss         |  6 +-
 src/client/views/collections/TreeView.tsx          | 34 +++++-----
 .../CollectionFreeFormLinkView.tsx                 |  6 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 30 +++------
 src/client/views/global/globalScripts.ts           |  7 +-
 src/client/views/linking/LinkMenuItem.tsx          |  6 +-
 src/client/views/nodes/DocumentLinksButton.tsx     |  2 +-
 src/client/views/nodes/DocumentView.tsx            | 34 ++++++++--
 src/client/views/nodes/LabelBox.tsx                |  2 +-
 src/client/views/nodes/LinkBox.scss                | 25 ++++++++
 src/client/views/nodes/LinkBox.tsx                 | 69 +++++++++++++-------
 src/client/views/nodes/LinkDescriptionPopup.tsx    | 18 ++++--
 src/client/views/nodes/LinkDocPreview.tsx          |  4 +-
 .../views/nodes/formattedText/DashFieldView.scss   | 10 +++
 .../views/nodes/formattedText/DashFieldView.tsx    | 27 ++++++--
 .../views/nodes/formattedText/FormattedTextBox.tsx | 11 ++--
 .../views/nodes/formattedText/RichTextRules.ts     |  2 +-
 src/fields/Doc.ts                                  |  2 +-
 src/fields/List.ts                                 |  2 +-
 36 files changed, 343 insertions(+), 207 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index cc983ffa7..95058da22 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -437,7 +437,7 @@ export class DocumentOptions {
     clickFactory?: DOCt = new DocInfo('document to create when clicking on a button with a suitable onClick script', false);
     onDragStart?: ScriptField; //script to execute at start of drag operation --  e.g., when a "creator" button is dragged this script generates a different document to drop
     target?: Doc; // available for use in scripts.  used to provide a document parameter to the script (Note, this is a convenience entry since any field could be used for parameterizing a script)
-
+    tags?: LISTt = new ListInfo('hashtags added to document, typically using a text view', true);
     treeView_HideTitle?: BOOLt = new BoolInfo('whether to hide the top document title of a tree view');
     treeView_HideUnrendered?: BOOLt = new BoolInfo("tells tree view not to display documents that have an 'layout_unrendered' tag unless they also have a treeView_FieldKey tag (presBox)");
     treeView_HideHeaderIfTemplate?: BOOLt = new BoolInfo('whether to hide the header for a document in a tree view only if a childLayoutTemplate is provided (presBox)');
@@ -1459,7 +1459,7 @@ export namespace DocUtils {
 
         const makeLink = action((linkDoc: Doc, showPopup?: number[]) => {
             if (showPopup) {
-                LinkManager.currentLink = linkDoc;
+                LinkManager.Instance.currentLink = linkDoc;
 
                 TaskCompletionBox.textDisplayed = 'Link Created';
                 TaskCompletionBox.popupX = showPopup[0];
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 31f0308b7..714e33d25 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -17,7 +17,7 @@ import { CollectionViewType, DocumentType } from "../documents/DocumentTypes";
 import { DocUtils, Docs, DocumentOptions, FInfo } from "../documents/Documents";
 import { DashboardView } from "../views/DashboardView";
 import { OverlayView } from "../views/OverlayView";
-import { TreeViewType } from "../views/collections/CollectionTreeView";
+import { CollectionTreeView, TreeViewType } from "../views/collections/CollectionTreeView";
 import { Colors } from "../views/global/globalEnums";
 import { media_state } from "../views/nodes/AudioBox";
 import { OpenWhere } from "../views/nodes/DocumentView";
@@ -78,7 +78,7 @@ export class CurrentUserUtils {
         const tempClicks = DocCast(doc[field]);
         const reqdClickOpts:DocumentOptions = {_width: 300, _height:200, isSystem: true};
         const reqdTempOpts:{opts:DocumentOptions, script: string}[] = [
-            { opts: { title: "Open In Target",        targetScriptKey: "onChildClick"},       script: "docCastAsync(documentView?.containerViewPath().lastElement()?.Document.target).then((target) => target && (target.proto.data = new List([self])))"},
+            { opts: { title: "Open In Target",        targetScriptKey: "onChildClick"},       script: "docCastAsync(documentView?.containerViewPath().lastElement()?.Document.target).then((target) => target && (target.proto.data = new List([this])))"},
             { opts: { title: "Open Detail On Right",  targetScriptKey: "onChildDoubleClick"}, script: `openDoc(this.doubleClickView.${OpenWhere.addRight})`}];
         const reqdClickList = reqdTempOpts.map(opts => {
             const allOpts = {...reqdClickOpts, ...opts.opts};
@@ -502,29 +502,21 @@ export class CurrentUserUtils {
     static setupFilesystem(doc: Doc, field:string) {
         var myFilesystem = DocCast(doc[field]);
         
-        const newFolder = `TreeView_addNewFolder()`;
         const newFolderOpts: DocumentOptions = {
-            _forceActive: true, _dragOnlyWithinContainer: true, _layout_hideContextMenu: true, _width: 30, _height: 30, undoIgnoreFields:new List<string>(['treeView_SortCriterion']),
+            _forceActive: true, _dragOnlyWithinContainer: true, _embedContainer: Doc.MyFilesystem, _layout_hideContextMenu: true, _width: 30, _height: 30, undoIgnoreFields:new List<string>(['treeView_SortCriterion']),
             title: "New folder", color: Colors.BLACK, btnType: ButtonType.ClickButton, toolTip: "Create new folder", buttonText: "New folder", icon: "folder-plus", isSystem: true
         };
-        const newFolderScript = { onClick: newFolder};
+        const newFolderScript = { onClick: CollectionTreeView.AddTreeFunc};
         const newFolderButton = DocUtils.AssignScripts(DocUtils.AssignOpts(DocCast(myFilesystem?.layout_headerButton), newFolderOpts) ?? Docs.Create.FontIconDocument(newFolderOpts), newFolderScript);
         
         const reqdOpts:DocumentOptions = { _layout_showTitle: "title", _height: 100, _forceActive: true, 
             title: "My Documents", layout_headerButton: newFolderButton, treeView_HideTitle: true, dropAction: 'add', isSystem: true,
             isFolder: true, treeView_Type: TreeViewType.fileSystem, childHideLinkButton: true, layout_boxShadow: "0 0", childDontRegisterViews: true, 
             treeView_TruncateTitleWidth: 350, ignoreClick: true, childDragAction: "embed",
-            childContextMenuLabels: new List<string>(["Create new folder"]),
-            childContextMenuIcons: new List<string>(["plus"]),
             layout_explainer: "This is your file manager where you can create folders to keep track of documents independently of your dashboard."
         };
         const fileFolders = new Set(DocListCast(DocCast(doc[field])?.data));
-        myFilesystem = DocUtils.AssignDocField(doc, field, (opts, items) => Docs.Create.TreeDocument(items??[], opts), reqdOpts, Array.from(fileFolders));
-        const childContextMenuScripts = [newFolder];
-        if (Cast(myFilesystem.childContextMenuScripts, listSpec(ScriptField), null)?.length !== childContextMenuScripts.length) {
-            myFilesystem.childContextMenuScripts = new List<ScriptField>(childContextMenuScripts.map(script => ScriptField.MakeFunction(script)!));
-        }
-        return myFilesystem;
+        return  DocUtils.AssignDocField(doc, field, (opts, items) => Docs.Create.TreeDocument(items??[], opts), reqdOpts, Array.from(fileFolders));
     }
 
     /// initializes the panel displaying docs that have been recently closed
@@ -544,8 +536,8 @@ export class CurrentUserUtils {
             toolTip: "Empty recently closed",};
         DocUtils.AssignDocField(recentlyClosed, "layout_headerButton", (opts) => Docs.Create.FontIconDocument(opts), clearBtnsOpts, undefined, {onClick: clearAll("this.target")});
 
-        if (!Cast(recentlyClosed.contextMenuScripts, listSpec(ScriptField),null)?.find((script) => script?.script.originalScript === clearAll("self"))) {
-            recentlyClosed.contextMenuScripts = new List<ScriptField>([ScriptField.MakeScript(clearAll("self"))!])
+        if (!Cast(recentlyClosed.contextMenuScripts, listSpec(ScriptField),null)?.find((script) => script?.script.originalScript === clearAll("this"))) {
+            recentlyClosed.contextMenuScripts = new List<ScriptField>([ScriptField.MakeScript(clearAll("this"))!])
         }
         return recentlyClosed;
     }
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index a6ad0f1b3..071b25a0e 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -243,7 +243,7 @@ export namespace DragManager {
         };
         dragData.draggedDocuments.map(d => d.dragFactory); // does this help?  trying to make sure the dragFactory Doc is loaded
         StartDrag(eles, dragData, downX, downY, options, finishDrag);
-        dragData.draggedViews.forEach(view => view.props.dragStarting?.());
+        dragData.draggedViews.forEach(view => view.props.dragStarting?.(dragData));
         return true;
     }
 
diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index 353f28a92..dd3b9bd07 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -23,8 +23,8 @@ import { ScriptingGlobals } from './ScriptingGlobals';
 export class LinkManager {
     @observable static _instance: LinkManager;
     @observable.shallow userLinkDBs: Doc[] = [];
-    @observable public static currentLink: Opt<Doc> = undefined;
-    @observable public static currentLinkAnchor: Opt<Doc> = undefined;
+    @observable public currentLink: Opt<Doc> = undefined;
+    @observable public currentLinkAnchor: Opt<Doc> = undefined;
     public static get Instance() {
         return LinkManager._instance;
     }
diff --git a/src/client/util/SearchUtil.ts b/src/client/util/SearchUtil.ts
index 218667d3e..fff2737b6 100644
--- a/src/client/util/SearchUtil.ts
+++ b/src/client/util/SearchUtil.ts
@@ -8,7 +8,7 @@ import { DocOptions, FInfo } from '../documents/Documents';
 export namespace SearchUtil {
     export type HighlightingResult = { [id: string]: { [key: string]: string[] } };
 
-    export function SearchCollection(collectionDoc: Opt<Doc>, query: string, matchKeyNames: boolean) {
+    export function SearchCollection(collectionDoc: Opt<Doc>, query: string, matchKeyNames: boolean, onlyKeys?: string[]) {
         const blockedTypes = [DocumentType.PRESELEMENT, DocumentType.CONFIG, DocumentType.KVP, DocumentType.FONTICON, DocumentType.BUTTON, DocumentType.SCRIPTING];
         const blockedKeys = matchKeyNames
             ? []
@@ -27,8 +27,9 @@ export namespace SearchUtil {
                 const dtype = StrCast(doc.type) as DocumentType;
                 if (dtype && !blockedTypes.includes(dtype) && !docIDs.includes(doc[Id]) && depth >= 0) {
                     const hlights = new Set<string>();
-                    SearchUtil.documentKeys(doc).forEach(
-                        key => (val => (exact ? val === query.toLowerCase() : val.includes(query.toLowerCase())))(
+                    (onlyKeys ?? SearchUtil.documentKeys(doc)).forEach(
+                        key =>
+                            (val => (exact ? val === query.toLowerCase() : val.includes(query.toLowerCase())))(
                             matchKeyNames ? key : Field.toString(doc[key] as Field)) 
                         && hlights.add(key)
                     ); // prettier-ignore
diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts
index f2a327445..b79281802 100644
--- a/src/client/util/SelectionManager.ts
+++ b/src/client/util/SelectionManager.ts
@@ -51,8 +51,8 @@ export class SelectionManager {
 
     public static DeselectAll = (except?: Doc): void => {
         const found = this.Instance.SelectedViews.find(dv => dv.Document === except);
-        LinkManager.currentLink = undefined;
-        LinkManager.currentLinkAnchor = undefined;
+        LinkManager.Instance.currentLink = undefined;
+        LinkManager.Instance.currentLinkAnchor = undefined;
         runInAction(() => (this.Instance.SelectedSchemaDocument = undefined));
         this.Instance.SelectedViews.forEach(dv => {
             dv.IsSelected = false;
diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx
index 5bf9e5b00..682704770 100644
--- a/src/client/util/SettingsManager.tsx
+++ b/src/client/util/SettingsManager.tsx
@@ -317,7 +317,17 @@ export class SettingsManager extends React.Component<{}> {
                     <div className="tab-column-content">
                         {/* <NumberInput/> */}
                         <Group formLabel={'Default Font'}>
-                            <NumberDropdown color={SettingsManager.userColor} numberDropdownType={'input'} min={0} max={50} step={2} type={Type.TERT} number={0} unit={'px'} setNumber={() => {}} />
+                            <NumberDropdown
+                                color={SettingsManager.userColor}
+                                numberDropdownType="slider"
+                                min={0}
+                                max={50}
+                                step={2}
+                                type={Type.PRIM}
+                                number={NumCast(Doc.UserDoc().fontSize, Number(StrCast(Doc.UserDoc().fontSize).replace('px', '')))}
+                                unit={'px'}
+                                setNumber={val => (Doc.UserDoc().fontSize = val + 'px')}
+                            />
                             <Dropdown
                                 items={fontFamilies.map(val => {
                                     return {
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index 58be41f53..3c772bd42 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -47,7 +47,7 @@ export interface ViewBoxInterface {
     setData?: (data: Field | Promise<RefField | undefined>) => boolean;
     componentUI?: (boundsLeft: number, boundsTop: number) => JSX.Element | null;
     dragStarting?: (snapToDraggedDoc: boolean, showGroupDragTarget: boolean, visited: Set<Doc>) => void;
-    dragConfig?: (dragData: DragManager.DocumentDragData) => void;
+    dragConfig?: (dragData: DragManager.DocumentDragData) => void; // function to setup dragData in custom way (see TreeViews which add a tree view flag)
     incrementalRendering?: () => void;
     infoUI?: () => JSX.Element | null;
     screenBounds?: () => Opt<{ left: number; top: number; right: number; bottom: number; center?: { X: number; Y: number } }>;
@@ -57,7 +57,7 @@ export interface ViewBoxInterface {
     search?: (str: string, bwd?: boolean, clear?: boolean) => boolean;
 }
 /**
- * DocComponent returns a React base class used by Doc views with accessors for unpacking he Document,layoutDoc, and dataDoc's
+ * DocComponent returns a React base class used by Doc views with accessors for unpacking the Document,layoutDoc, and dataDoc's
  *  (note: this should not be used for the 'Box' views that render the contents of Doc views)
  *  Example derived views: CollectionFreeFormDocumentView, DocumentView, DocumentViewInternal)
  * */
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index dec109d7b..e9c4d9cc5 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -114,6 +114,9 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
         if (this._accumulatedTitle.startsWith('#') || this._accumulatedTitle.startsWith('=')) {
             this._titleControlString = this._accumulatedTitle;
         } else if (this._titleControlString.startsWith('#')) {
+            if (this._accumulatedTitle.startsWith('-->#')) {
+                SelectionManager.Docs.forEach(doc => (doc[DocData].onViewMounted = ScriptField.MakeScript(`updateTagsCollection(this)`)));
+            }
             const titleFieldKey = this._titleControlString.substring(1);
             UndoManager.RunInBatch(
                 () =>
diff --git a/src/client/views/FilterPanel.tsx b/src/client/views/FilterPanel.tsx
index c4f65a5ca..818c81c9a 100644
--- a/src/client/views/FilterPanel.tsx
+++ b/src/client/views/FilterPanel.tsx
@@ -117,8 +117,8 @@ export class FilterPanel extends ObservableReactComponent<filterProps> {
     //     return this.activeFilters.map(filter => filter.split(Doc.FilterSep)[0]);
     // }
 
-    gatherFieldValues(childDocs: Doc[], facetKey: string) {
-        const valueSet = new Set<string>(StrListCast(this.targetDoc.childFilters).map(filter => filter.split(Doc.FilterSep)[1]));
+    static gatherFieldValues(childDocs: Doc[], facetKey: string, childFilters: string[]) {
+        const valueSet = new Set<string>(childFilters.map(filter => filter.split(Doc.FilterSep)[1]));
         let rtFields = 0;
         let subDocs = childDocs;
         if (subDocs.length > 0) {
@@ -165,7 +165,7 @@ export class FilterPanel extends ObservableReactComponent<filterProps> {
     @computed get activeRenderedFacetInfos() {
         return new Set(
             Array.from(new Set(Array.from(this._selectedFacetHeaders).concat(this.activeFacetHeaders))).map(facetHeader => {
-                const facetValues = this.gatherFieldValues(this.targetDocChildren, facetHeader);
+                const facetValues = FilterPanel.gatherFieldValues(this.targetDocChildren, facetHeader, StrListCast(this.targetDoc.childFilters));
 
                 let nonNumbers = 0;
                 let minVal = Number.MAX_VALUE,
diff --git a/src/client/views/PropertiesDocBacklinksSelector.tsx b/src/client/views/PropertiesDocBacklinksSelector.tsx
index 244cd4aa0..cf5105efc 100644
--- a/src/client/views/PropertiesDocBacklinksSelector.tsx
+++ b/src/client/views/PropertiesDocBacklinksSelector.tsx
@@ -25,7 +25,7 @@ export class PropertiesDocBacklinksSelector extends React.Component<PropertiesDo
         const linkAnchor = this.props.Document;
         const other = LinkManager.getOppositeAnchor(link, linkAnchor);
         const otherdoc = !other ? undefined : other.annotationOn && other.type !== DocumentType.RTF ? Cast(other.annotationOn, Doc, null) : other;
-        LinkManager.currentLink = link;
+        LinkManager.Instance.currentLink = link;
         if (otherdoc) {
             otherdoc.hidden = false;
             this.props.addDocTab(Doc.IsDataProto(otherdoc) ? Doc.MakeDelegate(otherdoc) : otherdoc, OpenWhere.toggleRight);
diff --git a/src/client/views/PropertiesSection.scss b/src/client/views/PropertiesSection.scss
index 3f92a70f8..d32da1bf1 100644
--- a/src/client/views/PropertiesSection.scss
+++ b/src/client/views/PropertiesSection.scss
@@ -4,20 +4,20 @@
     .propertiesView-content {
         padding: 10px;
     }
+}
 
-    .propertiesView-sectionTitle {
-        text-align: center;
-        display: flex;
-        padding: 3px 10px;
-        font-size: 14px;
-        font-weight: bold;
-        justify-content: space-between;
-        align-items: center;
+.propertiesView-sectionTitle {
+    text-align: center;
+    display: flex;
+    padding: 3px 10px;
+    font-size: 14px;
+    font-weight: bold;
+    justify-content: space-between;
+    align-items: center;
 
-        .propertiesView-sectionTitle-icon {
-            width: 20px;
-            height: 20px;
-            align-items: flex-end;
-        }
+    .propertiesView-sectionTitle-icon {
+        width: 20px;
+        height: 20px;
+        align-items: flex-end;
     }
 }
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index a7a065a95..07f285eaf 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -110,7 +110,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
 
     componentDidMount() {
         this._disposers.link = reaction(
-            () => LinkManager.currentLink,
+            () => LinkManager.Instance.currentLink,
             link => {
                 link && this.CloseAll();
                 link && (this.openLinks = true);
@@ -173,10 +173,14 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
         if (this.dataDoc && this.selectedDoc) {
             const ids = new Set<string>(reqdKeys);
             const docs: Doc[] = SelectionManager.Views.length < 2 ? [this.layoutFields ? Doc.Layout(this.selectedDoc) : this.dataDoc] : SelectionManager.Views.map(dv => (this.layoutFields ? dv.layoutDoc : dv.dataDoc));
-            docs.forEach(doc => Object.keys(doc).forEach(key => doc[key] !== ComputedField.undefined && key && ids.add(key)));
+            docs.forEach(doc =>
+                Object.keys(doc)
+                    .filter(filter)
+                    .forEach(key => doc[key] !== ComputedField.undefined && key && ids.add(key))
+            );
 
             // prettier-ignore
-            Array.from(ids).filter(filter).sort().map(key => {
+            Array.from(ids).sort().map(key => {
                 const multiple = Array.from(docs.reduce((set,doc) => set.add(doc[key]), new Set<FieldResult>()).keys()).length > 1;
                 const editableContents = multiple ? '-multiple-' : Field.toKeyValueString(docs[0], key);
                 const displayContents = multiple ? '-multiple-' : Field.toString(docs[0][key] as Field);
@@ -270,12 +274,12 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
     }
 
     @computed get links() {
-        const selAnchor = this.selectedDocumentView?.anchorViewDoc ?? LinkManager.currentLinkAnchor ?? this.selectedDoc;
+        const selAnchor = this.selectedDocumentView?.anchorViewDoc ?? LinkManager.Instance.currentLinkAnchor ?? this.selectedDoc;
         return !selAnchor ? null : <PropertiesDocBacklinksSelector Document={selAnchor} hideTitle={true} addDocTab={this._props.addDocTab} />;
     }
 
     @computed get linkCount() {
-        const selAnchor = this.selectedDocumentView?.anchorViewDoc ?? LinkManager.currentLinkAnchor ?? this.selectedDoc;
+        const selAnchor = this.selectedDocumentView?.anchorViewDoc ?? LinkManager.Instance.currentLinkAnchor ?? this.selectedDoc;
         var counter = 0;
 
         LinkManager.Links(selAnchor).forEach((l, i) => counter++);
@@ -571,19 +575,19 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
         return (
             <div>
                 <EditableText val={title} setVal={this.setTitle} color={this.color} type={Type.SEC} formLabel={'Title'} fillWidth />
-                {LinkManager.currentLinkAnchor ? (
+                {LinkManager.Instance.currentLinkAnchor ? (
                     <p className="propertiesView-titleExtender">
                         <>
                             <b>Anchor:</b>
-                            {LinkManager.currentLinkAnchor.title}
+                            {LinkManager.Instance.currentLinkAnchor.title}
                         </>
                     </p>
                 ) : null}
-                {LinkManager.currentLink?.title ? (
+                {LinkManager.Instance.currentLink?.title ? (
                     <p className="propertiesView-titleExtender">
                         <>
                             <b>Link:</b>
-                            {LinkManager.currentLink.title}
+                            {LinkManager.Instance.currentLink.title}
                         </>
                     </p>
                 ) : null}
@@ -1222,10 +1226,10 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
     }
 
     @computed get description() {
-        return Field.toString(LinkManager.currentLink?.link_description as any as Field);
+        return Field.toString(LinkManager.Instance.currentLink?.link_description as any as Field);
     }
     @computed get relationship() {
-        return StrCast(LinkManager.currentLink?.link_relationship);
+        return StrCast(LinkManager.Instance.currentLink?.link_relationship);
     }
     @observable private relationshipButtonColor: string = '';
 
@@ -1235,7 +1239,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
 
     handleDescriptionChange = undoable(
         action((value: string) => {
-            if (LinkManager.currentLink && this.selectedDoc) {
+            if (LinkManager.Instance.currentLink && this.selectedDoc) {
                 this.setDescripValue(value);
             }
         }),
@@ -1244,7 +1248,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
 
     handlelinkRelationshipChange = undoable(
         action((value: string) => {
-            if (LinkManager.currentLink && this.selectedDoc) {
+            if (LinkManager.Instance.currentLink && this.selectedDoc) {
                 this.setlinkRelationshipValue(value);
             }
         }),
@@ -1253,17 +1257,17 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
 
     @undoBatch
     setDescripValue = action((value: string) => {
-        if (LinkManager.currentLink) {
-            Doc.GetProto(LinkManager.currentLink).link_description = value;
+        if (LinkManager.Instance.currentLink) {
+            Doc.GetProto(LinkManager.Instance.currentLink).link_description = value;
         }
     });
 
     @undoBatch
     setlinkRelationshipValue = action((value: string) => {
-        if (LinkManager.currentLink) {
-            const prevRelationship = StrCast(LinkManager.currentLink.link_relationship);
-            LinkManager.currentLink.link_relationship = value;
-            Doc.GetProto(LinkManager.currentLink).link_relationship = value;
+        if (LinkManager.Instance.currentLink) {
+            const prevRelationship = StrCast(LinkManager.Instance.currentLink.link_relationship);
+            LinkManager.Instance.currentLink.link_relationship = value;
+            Doc.GetProto(LinkManager.Instance.currentLink).link_relationship = value;
             const linkRelationshipList = StrListCast(Doc.UserDoc().link_relationshipList);
             const linkRelationshipSizes = NumListCast(Doc.UserDoc().link_relationshipSizes);
             const linkColorList = StrListCast(Doc.UserDoc().link_ColorList);
@@ -1347,20 +1351,20 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
     };
 
     toggleLinkProp = (e: React.PointerEvent, prop: string) => {
-        setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => LinkManager.currentLink && (LinkManager.currentLink[prop] = !LinkManager.currentLink[prop]))));
+        setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => LinkManager.Instance.currentLink && (LinkManager.Instance.currentLink[prop] = !LinkManager.Instance.currentLink[prop]))));
     };
 
     @computed get destinationAnchor() {
-        const ldoc = LinkManager.currentLink;
-        const lanch = this.selectedDocumentView?.anchorViewDoc ?? LinkManager.currentLinkAnchor;
+        const ldoc = LinkManager.Instance.currentLink;
+        const lanch = this.selectedDocumentView?.anchorViewDoc ?? LinkManager.Instance.currentLinkAnchor;
         if (ldoc && lanch) return LinkManager.getOppositeAnchor(ldoc, lanch) ?? lanch;
         return ldoc ? DocCast(ldoc.link_anchor_2) : ldoc;
     }
 
     @computed get sourceAnchor() {
-        const selAnchor = this.selectedDocumentView?.anchorViewDoc ?? LinkManager.currentLinkAnchor;
+        const selAnchor = this.selectedDocumentView?.anchorViewDoc ?? LinkManager.Instance.currentLinkAnchor;
 
-        return selAnchor ?? (LinkManager.currentLink && this.destinationAnchor ? LinkManager.getOppositeAnchor(LinkManager.currentLink, this.destinationAnchor) : LinkManager.currentLink);
+        return selAnchor ?? (LinkManager.Instance.currentLink && this.destinationAnchor ? LinkManager.getOppositeAnchor(LinkManager.Instance.currentLink, this.destinationAnchor) : LinkManager.Instance.currentLink);
     }
 
     toggleAnchorProp = (e: React.PointerEvent, prop: string, anchor?: Doc, value: any = true, ovalue: any = false, cb: (val: any) => any = val => val) => {
@@ -1386,7 +1390,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
                 style={{ color: SettingsManager.userColor, backgroundColor: SettingsManager.userBackgroundColor }}
                 autoComplete={'off'}
                 id="link_relationship_input"
-                value={StrCast(LinkManager.currentLink?.link_relationship)}
+                value={StrCast(LinkManager.Instance.currentLink?.link_relationship)}
                 onKeyDown={this.onRelationshipKey}
                 onBlur={this.onSelectOutRelationship}
                 onChange={e => this.handlelinkRelationshipChange(e.currentTarget.value)}
@@ -1403,7 +1407,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
                 autoComplete="off"
                 style={{ textAlign: 'left', color: SettingsManager.userColor, backgroundColor: SettingsManager.userBackgroundColor }}
                 id="link_description_input"
-                value={StrCast(LinkManager.currentLink?.link_description)}
+                value={StrCast(LinkManager.Instance.currentLink?.link_description)}
                 onKeyDown={this.onDescriptionKey}
                 onBlur={this.onSelectOutDesc}
                 onChange={e => this.handleDescriptionChange(e.currentTarget.value)}
@@ -1425,7 +1429,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
         const zoom = Number((NumCast(this.sourceAnchor?.followLinkZoomScale, 1) * 100).toPrecision(3));
         const targZoom = this.sourceAnchor?.followLinkZoom;
         const indent = 30;
-        const hasSelectedAnchor = LinkManager.Links(this.sourceAnchor).includes(LinkManager.currentLink!);
+        const hasSelectedAnchor = LinkManager.Links(this.sourceAnchor).includes(LinkManager.Instance.currentLink!);
 
         return (
             <>
@@ -1441,7 +1445,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
                     <div className="propertiesView-input inline">
                         <p>Show link</p>
                         <button
-                            style={{ background: !LinkManager.currentLink?.link_displayLine ? '' : '#4476f7', borderRadius: 3 }}
+                            style={{ background: !LinkManager.Instance.currentLink?.link_displayLine ? '' : '#4476f7', borderRadius: 3 }}
                             onPointerDown={e => this.toggleLinkProp(e, 'link_displayLine')}
                             onClick={e => e.stopPropagation()}
                             className="propertiesButton">
@@ -1451,7 +1455,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
                     <div className="propertiesView-input inline" style={{ marginLeft: 10 }}>
                         <p>Auto-move anchors</p>
                         <button
-                            style={{ background: !LinkManager.currentLink?.link_autoMoveAnchors ? '' : '#4476f7', borderRadius: 3 }}
+                            style={{ background: !LinkManager.Instance.currentLink?.link_autoMoveAnchors ? '' : '#4476f7', borderRadius: 3 }}
                             onPointerDown={e => this.toggleLinkProp(e, 'link_autoMoveAnchors')}
                             onClick={e => e.stopPropagation()}
                             className="propertiesButton">
@@ -1461,7 +1465,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
                     <div className="propertiesView-input inline" style={{ marginLeft: 10 }}>
                         <p>Display arrow</p>
                         <button
-                            style={{ background: !LinkManager.currentLink?.link_displayArrow ? '' : '#4476f7', borderRadius: 3 }}
+                            style={{ background: !LinkManager.Instance.currentLink?.link_displayArrow ? '' : '#4476f7', borderRadius: 3 }}
                             onPointerDown={e => this.toggleLinkProp(e, 'link_displayArrow')}
                             onClick={e => e.stopPropagation()}
                             className="propertiesButton">
@@ -1486,7 +1490,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
                                 <option value={OpenWhere.add}>Opening in new tab</option>
                                 <option value={OpenWhere.replace}>Replacing current tab</option>
                                 <option value={OpenWhere.inParent}>Opening in same collection</option>
-                                {LinkManager.currentLink?.linksToAnnotation ? <option value="openExternal">Open in external page</option> : null}
+                                {LinkManager.Instance.currentLink?.linksToAnnotation ? <option value="openExternal">Open in external page</option> : null}
                             </select>
                         </div>
                         <div className="propertiesView-input inline first" style={{ display: 'grid', gridTemplateColumns: '84px calc(100% - 134px) 50px' }}>
@@ -1671,7 +1675,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
 
     render() {
         const isNovice = Doc.noviceMode;
-        const hasSelectedAnchor = LinkManager.Links(this.sourceAnchor).includes(LinkManager.currentLink!);
+        const hasSelectedAnchor = LinkManager.Links(this.sourceAnchor).includes(LinkManager.Instance.currentLink!);
         if (!this.selectedDoc && !this.isPres) {
             return (
                 <div className="propertiesView" style={{ width: this._props.width }}>
@@ -1692,7 +1696,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
                             minWidth: this._props.width,
                         }}>
                         <div className="propertiesView-propAndInfoGrouping">
-                            <div className="propertiesView-title" style={{ width: this._props.width }}>
+                            <div className="propertiesView-sectionTitle" style={{ width: this._props.width }}>
                                 Properties
                                 <div className="propertiesView-info" onClick={() => window.open('https://brown-dash.github.io/Dash-Documentation/properties')}>
                                     <IconButton icon={<FontAwesomeIcon icon="info-circle" />} color={SettingsManager.userColor} />
@@ -1702,12 +1706,12 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
 
                         <div className="propertiesView-name">{this.editableTitle}</div>
                         <div className="propertiesView-type"> {this.currentType} </div>
+                        {this.fieldsSubMenu}
                         {this.optionsSubMenu}
                         {this.linksSubMenu}
-                        {!LinkManager.currentLink || !this.openLinks ? null : this.linkProperties}
+                        {!LinkManager.Instance.currentLink || !this.openLinks ? null : this.linkProperties}
                         {this.inkSubMenu}
                         {this.contextsSubMenu}
-                        {this.fieldsSubMenu}
                         {isNovice ? null : this.sharingSubMenu}
                         {this.filtersSubMenu}
                         {isNovice ? null : this.layoutSubMenu}
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index fa0be225e..0e38790b6 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -191,7 +191,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
                 : 0;
         case StyleProp.BackgroundColor: {
             if (SettingsManager.Instance.LastPressedBtn === doc) return SettingsManager.userColor; // hack to indicate active menu panel item
-            let docColor: Opt<string> = StrCast(doc?.[fieldKey + 'backgroundColor'], StrCast(doc?._backgroundColor, isCaption ? 'rgba(0,0,0,0.4)' : ''));
+            let docColor: Opt<string> = StrCast(doc?.[fieldKey + 'backgroundColor'], StrCast(doc?.backgroundColor, isCaption ? 'rgba(0,0,0,0.4)' : ''));
             // prettier-ignore
             switch (doc?.type) {
                 case DocumentType.PRESELEMENT: docColor = docColor || ""; break;
diff --git a/src/client/views/collections/CollectionTimeView.tsx b/src/client/views/collections/CollectionTimeView.tsx
index ee5147428..38f6aa3e7 100644
--- a/src/client/views/collections/CollectionTimeView.tsx
+++ b/src/client/views/collections/CollectionTimeView.tsx
@@ -200,8 +200,7 @@ export class CollectionTimeView extends CollectionSubView() {
     }
     menuCallback = (x: number, y: number) => {
         ContextMenu.Instance.clearItems();
-        const docItems: ContextMenuProps[] = [];
-        const keySet: Set<string> = new Set();
+        const keySet: Set<string> = new Set(['tags']);
 
         this.childLayoutPairs.map(pair =>
             this._allFacets
@@ -209,7 +208,9 @@ export class CollectionTimeView extends CollectionSubView() {
                 .filter(fieldKey => fieldKey[0] !== '_' && (fieldKey === 'tags' || fieldKey[0] === toUpper(fieldKey)[0]))
                 .map(fieldKey => keySet.add(fieldKey))
         );
-        Array.from(keySet).map(fieldKey => docItems.push({ description: ':' + fieldKey, event: () => (this.layoutDoc._pivotField = fieldKey), icon: 'compress-arrows-alt' }));
+
+        const docItems: ContextMenuProps[] = Array.from(keySet).map(fieldKey => 
+                     ({ description: ':' + fieldKey, event: () => (this.layoutDoc._pivotField = fieldKey), icon: 'compress-arrows-alt' })); // prettier-ignore
         docItems.push({ description: ':default', event: () => (this.layoutDoc._pivotField = undefined), icon: 'compress-arrows-alt' });
         ContextMenu.Instance.addItem({ description: 'Pivot Fields ...', subitems: docItems, icon: 'eye' });
         ContextMenu.Instance.displayMenu(x, y, ':');
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index 741013148..786301136 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -8,8 +8,8 @@ import { listSpec } from '../../../fields/Schema';
 import { ScriptField } from '../../../fields/ScriptField';
 import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
 import { TraceMobx } from '../../../fields/util';
-import { emptyFunction, returnAll, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnNone, returnOne, returnTrue, returnZero } from '../../../Utils';
-import { DocUtils } from '../../documents/Documents';
+import { emptyFunction, returnAll, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnNone, returnOne, returnTrue, returnZero, Utils } from '../../../Utils';
+import { Docs, DocUtils } from '../../documents/Documents';
 import { DocumentManager } from '../../util/DocumentManager';
 import { DragManager, dropActionType } from '../../util/DragManager';
 import { SelectionManager } from '../../util/SelectionManager';
@@ -27,6 +27,7 @@ import { CollectionFreeFormView } from './collectionFreeForm';
 import { CollectionSubView } from './CollectionSubView';
 import './CollectionTreeView.scss';
 import { TreeView } from './TreeView';
+import { ScriptingGlobals } from '../../util/ScriptingGlobals';
 const _global = (window /* browser */ || global) /* node */ as any;
 
 export type collectionTreeViewProps = {
@@ -51,6 +52,7 @@ export enum TreeViewType {
 
 @observer
 export class CollectionTreeView extends CollectionSubView<Partial<collectionTreeViewProps>>() {
+    public static AddTreeFunc = 'addTreeFolder(this.embedContainer)';
     private _treedropDisposer?: DragManager.DragDropDisposer;
     private _mainEle?: HTMLDivElement;
     private _titleRef?: HTMLDivElement | HTMLInputElement | null;
@@ -140,6 +142,17 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
         if ((this._mainEle = ele)) this._treedropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.Document, this.onInternalPreDrop.bind(this));
     };
 
+    protected onInternalDrop(e: Event, de: DragManager.DropEvent) {
+        const res = super.onInternalDrop(e, de);
+        if (res && de.complete.docDragData) {
+            if (this.Document !== Doc.MyRecentlyClosed)
+                de.complete.docDragData.droppedDocuments.forEach(doc => {
+                    if (this.Document !== Doc.MyRecentlyClosed) Doc.RemoveDocFromList(Doc.MyRecentlyClosed, undefined, doc);
+                });
+        }
+        return res;
+    }
+
     protected onInternalPreDrop = (e: Event, de: DragManager.DropEvent, dropAction: dropActionType) => {
         const dragData = de.complete.docDragData;
         if (dragData) {
@@ -150,9 +163,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
         }
     };
 
-    configDrag = (dragData: DragManager.DocumentDragData) => {
-        dragData.treeViewDoc = this.Document;
-    };
+    dragConfig = (dragData: DragManager.DocumentDragData) => (dragData.treeViewDoc = this.Document);
 
     screenToLocalTransform = () => this.ScreenToLocalBoxXf().translate(0, -this._headerHeight);
 
@@ -163,34 +174,41 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
         const value = DocListCast(targetDataDoc[this._props.fieldKey]);
         const result = value.filter(v => !docs.includes(v));
         if ((doc instanceof Doc ? [doc] : doc).some(doc => SelectionManager.Views.some(dv => Doc.AreProtosEqual(dv.Document, doc)))) SelectionManager.DeselectAll();
-        if (result.length !== value.length && doc instanceof Doc) {
-            const ind = DocListCast(targetDataDoc[this._props.fieldKey]).indexOf(doc);
-            const prev = ind && DocListCast(targetDataDoc[this._props.fieldKey])[ind - 1];
-            this._props.removeDocument?.(doc);
-            if (ind > 0 && prev) {
-                FormattedTextBox.SetSelectOnLoad(prev);
-                DocumentManager.Instance.getDocumentView(prev, this.DocumentView?.())?.select(false);
+        if (result.length !== value.length) {
+            if (doc instanceof Doc) {
+                const ind = DocListCast(targetDataDoc[this._props.fieldKey]).indexOf(doc);
+                const prev = ind && DocListCast(targetDataDoc[this._props.fieldKey])[ind - 1];
+                this._props.removeDocument?.(doc);
+                if (ind > 0 && prev) {
+                    FormattedTextBox.SetSelectOnLoad(prev);
+                    DocumentManager.Instance.getDocumentView(prev, this.DocumentView?.())?.select(false);
+                }
+                return true;
             }
-            return true;
+            return this._props.removeDocument?.(doc) ?? false;
         }
         return false;
     };
 
     @action
     addDoc = (docs: Doc | Doc[], relativeTo: Opt<Doc>, before?: boolean): boolean => {
-        const doAddDoc = (doc: Doc | Doc[]) =>
-            (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => {
-                const res = flg && Doc.AddDocToList(this.Document[DocData], this._props.fieldKey, doc, relativeTo, before);
-                res && Doc.SetContainer(doc, this.Document);
-                return res;
-            }, true);
+        const doclist = docs instanceof Doc ? [docs] : docs;
+        const addDocRelativeTo = (doc: Doc | Doc[]) => doclist.reduce((flg, doc) => flg && Doc.AddDocToList(this.Document[DocData], this._props.fieldKey, doc, relativeTo, before), true);
         if (this.Document.resolvedDataDoc instanceof Promise) return false;
-        return relativeTo === undefined ? this._props.addDocument?.(docs) || false : doAddDoc(docs);
+        const res = relativeTo === undefined ? this._props.addDocument?.(docs) || false : addDocRelativeTo(docs);
+        res &&
+            doclist.forEach(doc => {
+                Doc.SetContainer(doc, this.Document);
+                if (this.Document !== Doc.MyRecentlyClosed) Doc.RemoveDocFromList(Doc.MyRecentlyClosed, undefined, doc);
+            });
+        return res;
     };
     onContextMenu = (e: React.MouseEvent): void => {
         // need to test if propagation has stopped because GoldenLayout forces a parallel react hierarchy to be created for its top-level layout
+        const layoutItems: ContextMenuProps[] = [];
+        const menuDoc = ScriptCast(Cast(this.layoutDoc.layout_headerButton, Doc, null)?.onClick).script.originalScript === CollectionTreeView.AddTreeFunc;
+        menuDoc && layoutItems.push({ description: 'Create new folder', event: () => CollectionTreeView.addTreeFolder(this.Document), icon: 'paint-brush' });
         if (!Doc.noviceMode) {
-            const layoutItems: ContextMenuProps[] = [];
             layoutItems.push({
                 description: 'Make tree state ' + (this.Document.treeView_OpenIsTransient ? 'persistent' : 'transient'),
                 event: () => (this.Document.treeView_OpenIsTransient = !this.Document.treeView_OpenIsTransient),
@@ -198,7 +216,9 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
             });
             layoutItems.push({ description: (this.Document.treeView_HideHeaderFields ? 'Show' : 'Hide') + ' Header Fields', event: () => (this.Document.treeView_HideHeaderFields = !this.Document.treeView_HideHeaderFields), icon: 'paint-brush' });
             layoutItems.push({ description: (this.Document.treeView_HideTitle ? 'Show' : 'Hide') + ' Title', event: () => (this.Document.treeView_HideTitle = !this.Document.treeView_HideTitle), icon: 'paint-brush' });
-            ContextMenu.Instance.addItem({ description: 'Options...', subitems: layoutItems, icon: 'eye' });
+        }
+        ContextMenu.Instance.addItem({ description: 'Options...', subitems: layoutItems, icon: 'eye' });
+        if (!Doc.noviceMode) {
             const existingOnClick = ContextMenu.Instance.findByDescription('OnClick...');
             const onClicks: ContextMenuProps[] = existingOnClick && 'subitems' in existingOnClick ? existingOnClick.subitems : [];
             onClicks.push({ description: 'Edit onChecked Script', event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this.Document, undefined, 'onCheckedClick'), 'edit onCheckedClick'), icon: 'edit' });
@@ -467,4 +487,13 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
             </div>
         );
     }
+    static addTreeFolder(container: Doc) {
+        TreeView._editTitleOnLoad = { id: Utils.GenerateGuid(), parent: undefined };
+        const opts = { title: 'Untitled folder', _dragOnlyWithinContainer: true, isFolder: true };
+        return Doc.AddDocToList(container, 'data', Docs.Create.TreeDocument([], opts, TreeView._editTitleOnLoad.id));
+    }
 }
+
+ScriptingGlobals.add(function addTreeFolder(doc: Doc) {
+    CollectionTreeView.addTreeFolder(doc);
+});
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index 9bc3ef822..02aa76d82 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -520,6 +520,7 @@ interface TabMiniThumbProps {
     miniLeft: () => number;
 }
 
+@observer
 class TabMiniThumb extends React.Component<TabMiniThumbProps> {
     render() {
         return <div className="miniThumb" style={{ width: `${this.props.miniWidth()}% `, height: `${this.props.miniHeight()}% `, left: `${this.props.miniLeft()}% `, top: `${this.props.miniTop()}% ` }} />;
diff --git a/src/client/views/collections/TreeView.scss b/src/client/views/collections/TreeView.scss
index 09701ddb5..2ab1a5ac1 100644
--- a/src/client/views/collections/TreeView.scss
+++ b/src/client/views/collections/TreeView.scss
@@ -224,13 +224,13 @@
 }
 
 .treeView-header-above {
-    border-top: black 1px solid;
+    border-top: red 1px solid;
 }
 
 .treeView-header-below {
-    border-bottom: black 1px solid;
+    border-bottom: red 1px solid;
 }
 
 .treeView-header-inside {
-    border: black 1px solid;
+    border: red 1px solid;
 }
diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx
index be5737a25..85f7cf7fe 100644
--- a/src/client/views/collections/TreeView.tsx
+++ b/src/client/views/collections/TreeView.tsx
@@ -383,7 +383,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
     makeFolder = () => {
         const folder = Docs.Create.TreeDocument([], { title: 'Untitled folder', _dragOnlyWithinContainer: true, isFolder: true });
         TreeView._editTitleOnLoad = { id: folder[Id], parent: this._props.parentTreeView };
-        return this._props.addDocument(folder);
+        return this.localAdd(folder);
     };
 
     preTreeDrop = (e: Event, de: DragManager.DropEvent, docDropAction: dropActionType) => {
@@ -424,6 +424,16 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
         return false;
     };
 
+    localAdd = (doc: Doc | Doc[]) => {
+        const innerAdd = (doc: Doc) => {
+            const dataIsComputed = ComputedField.WithoutComputed(() => FieldValue(this.dataDoc[this.fieldKey])) instanceof ComputedField;
+            const added = (!dataIsComputed || (this.dropping && this.moving)) && Doc.AddDocToList(this.dataDoc, this.fieldKey, doc);
+            dataIsComputed && Doc.SetContainer(doc, DocCast(this.Document.embedContainer));
+            return added;
+        };
+        return (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && innerAdd(doc), true as boolean);
+    };
+
     dropping: boolean = false;
     dropDocuments(
         droppedDocuments: Doc[],
@@ -436,16 +446,8 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
         canEmbed?: boolean
     ) {
         const parentAddDoc = (doc: Doc | Doc[]) => this._props.addDocument(doc, undefined, undefined, before);
-        const localAdd = (doc: Doc | Doc[]) => {
-            const innerAdd = (doc: Doc) => {
-                const dataIsComputed = ComputedField.WithoutComputed(() => FieldValue(this.dataDoc[this.fieldKey])) instanceof ComputedField;
-                const added = (!dataIsComputed || (this.dropping && this.moving)) && Doc.AddDocToList(this.dataDoc, this.fieldKey, doc);
-                dataIsComputed && Doc.SetContainer(doc, DocCast(this.Document.embedContainer));
-                return added;
-            };
-            return (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && innerAdd(doc), true as boolean);
-        };
-        const addDoc = inside ? localAdd : parentAddDoc;
+
+        const addDoc = inside ? this.localAdd : parentAddDoc;
         const canAdd = !StrCast((inside ? this.Document : this._props.treeViewParent)?.treeView_FreezeChildren).includes('add') || forceAdd;
         if (canAdd && (dropAction !== 'inSame' || droppedDocuments.every(d => d.embedContainer === this._props.parentTreeView?.Document))) {
             const move = (!dropAction || canEmbed || dropAction === 'proto' || dropAction === 'move' || dropAction === 'same' || dropAction === 'inSame') && moveDocument;
@@ -839,14 +841,13 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
     };
     contextMenuItems = () => {
         const makeFolder = { script: ScriptField.MakeFunction(`scriptContext.makeFolder()`, { scriptContext: 'any' })!, icon: 'folder-plus', label: 'New Folder' };
-        const folderOp = this.childDocs?.length ? [makeFolder] : [];
         const openEmbedding = { script: ScriptField.MakeFunction(`openDoc(getEmbedding(this), "${OpenWhere.addRight}")`)!, icon: 'copy', label: 'Open New Embedding' };
         const focusDoc = { script: ScriptField.MakeFunction(`DocFocusOrOpen(this)`)!, icon: 'eye', label: 'Focus or Open' };
         const reopenDoc = { script: ScriptField.MakeFunction(`DocFocusOrOpen(this)`)!, icon: 'eye', label: 'Reopen' };
         return [
             ...(this._props.contextMenuItems ?? []).filter(mi => (!mi.filter ? true : mi.filter.script.run({ doc: this.Document })?.result)),
             ...(this.Document.isFolder
-                ? folderOp
+                ? [makeFolder]
                 : Doc.IsSystem(this.Document)
                   ? []
                   : this.treeView.fileSysMode && this.Document === this.Document[DocData]
@@ -993,6 +994,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
                 onClickScript={this.onChildClick}
                 onDoubleClickScript={this.onChildDoubleClick}
                 dragAction={this._props.dragAction}
+                dragConfig={this.treeView.dragConfig}
                 moveDocument={this.move}
                 removeDocument={this._props.removeDoc}
                 ScreenToLocalTransform={this.getTransform}
@@ -1328,9 +1330,3 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
             });
     }
 }
-
-ScriptingGlobals.add(function TreeView_addNewFolder() {
-    TreeView._editTitleOnLoad = { id: Utils.GenerateGuid(), parent: undefined };
-    const opts = { title: 'Untitled folder', _dragOnlyWithinContainer: true, isFolder: true };
-    return Doc.AddDocToList(Doc.MyFilesystem, 'data', Docs.Create.TreeDocument([], opts, TreeView._editTitleOnLoad.id));
-});
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
index f0a31a8c6..a45a1fb0f 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -127,7 +127,7 @@ export class CollectionFreeFormLinkView extends ObservableReactComponent<Collect
             action(() => {
                 SelectionManager.DeselectAll();
                 SelectionManager.SelectSchemaViewDoc(this._props.LinkDocs[0], true);
-                LinkManager.currentLink = this._props.LinkDocs[0];
+                LinkManager.Instance.currentLink = this._props.LinkDocs[0];
                 this.toggleProperties();
                 // OverlayView.Instance.addElement(
                 //     <LinkEditor sourceDoc={this._props.A.Document} linkDoc={this._props.LinkDocs[0]}
@@ -184,7 +184,7 @@ export class CollectionFreeFormLinkView extends ObservableReactComponent<Collect
     onClickLine = () => {
         SelectionManager.DeselectAll();
         SelectionManager.SelectSchemaViewDoc(this._props.LinkDocs[0], true);
-        LinkManager.currentLink = this._props.LinkDocs[0];
+        LinkManager.Instance.currentLink = this._props.LinkDocs[0];
         this.toggleProperties();
     };
 
@@ -295,7 +295,7 @@ export class CollectionFreeFormLinkView extends ObservableReactComponent<Collect
                     </filter>
                 </defs>
                 <path
-                    filter={LinkManager.currentLink === link ? 'url(#outline)' : ''}
+                    filter={LinkManager.Instance.currentLink === link ? 'url(#outline)' : ''}
                     fill="pink"
                     stroke="antiquewhite"
                     strokeWidth="4"
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index bd046d6ff..b08221f99 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -424,27 +424,13 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             const [x, y] = this.screenToFreeformContentsXf.transformPoint(de.x, de.y);
             let added = false;
             //  do nothing if link is dropped into any freeform view parent of dragged document
-            const source =
-                !linkDragData.dragDocument.embedContainer || linkDragData.dragDocument.embedContainer !== this.Document
-                    ? Docs.Create.TextDocument('', { _width: 200, _height: 75, x, y, title: 'dropped annotation' })
-                    : Docs.Create.FontIconDocument({
-                          title: 'anchor',
-                          icon_label: '',
-                          followLinkToggle: true,
-                          icon: 'map-pin',
-                          x,
-                          y,
-                          backgroundColor: '#ACCEF7',
-                          layout_hideAllLinks: true,
-                          layout_hideLinkButton: true,
-                          _width: 15,
-                          _height: 15,
-                          _xPadding: 0,
-                          onClick: FollowLinkScript(),
-                      });
+            const source = Docs.Create.TextDocument('', { _width: 200, _height: 75, x, y, title: 'dropped annotation' });
             added = this._props.addDocument?.(source) ? true : false;
             de.complete.linkDocument = DocUtils.MakeLink(linkDragData.linkSourceGetAnchor(), source, { link_relationship: 'annotated by:annotation of' }); // TODODO this is where in text links get passed
-
+            if (de.complete.linkDocument) {
+                de.complete.linkDocument.layout_isSvg = true;
+                this.addDocument(de.complete.linkDocument);
+            }
             e.stopPropagation();
             !added && e.preventDefault();
             return added;
@@ -1651,9 +1637,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     toggleResetView = () => {
         this.dataDoc[this.autoResetFieldKey] = !this.dataDoc[this.autoResetFieldKey];
         if (this.dataDoc[this.autoResetFieldKey]) {
-            this.dataDoc[this.panXFieldKey + '_reset'] = this.dataDoc[this.panXFieldKey];
-            this.dataDoc[this.panYFieldKey + '_reset'] = this.dataDoc[this.panYFieldKey];
-            this.dataDoc[this.scaleFieldKey + '_reset'] = this.dataDoc[this.scaleFieldKey];
+            this.dataDoc[this.panXFieldKey + '_reset'] = this.layoutDoc[this.panXFieldKey];
+            this.dataDoc[this.panYFieldKey + '_reset'] = this.layoutDoc[this.panYFieldKey];
+            this.dataDoc[this.scaleFieldKey + '_reset'] = this.layoutDoc[this.scaleFieldKey];
         }
     };
 
diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts
index 3084a7972..813cb9338 100644
--- a/src/client/views/global/globalScripts.ts
+++ b/src/client/views/global/globalScripts.ts
@@ -20,6 +20,7 @@ import { DocumentView } from '../nodes/DocumentView';
 import { RichTextMenu } from '../nodes/formattedText/RichTextMenu';
 import { WebBox } from '../nodes/WebBox';
 import { VideoBox } from '../nodes/VideoBox';
+import { DocData } from '../../../fields/DocSymbols';
 
 ScriptingGlobals.add(function IsNoneSelected() {
     return SelectionManager.Views.length <= 0;
@@ -57,16 +58,16 @@ ScriptingGlobals.add(function setBackgroundColor(color?: string, checkResult?: b
                 obj[fieldKey] = color;
                 CollectionFreeFormDocumentView.setStringValues(contentFrameNumber, dv.Document, obj);
             } else {
-                dv.Document['_' + fieldKey] = color;
+                dv.Document[DocData][fieldKey] = color;
             }
         });
     } else {
-        const selected = SelectionManager.Docs.length ? SelectionManager.Docs : LinkManager.currentLink ? [LinkManager.currentLink] : [];
+        const selected = SelectionManager.Docs.length ? SelectionManager.Docs : LinkManager.Instance.currentLink ? [LinkManager.Instance.currentLink] : [];
         if (checkResult) {
             return selected.lastElement()?._backgroundColor ?? 'transparent';
         }
         SetActiveFillColor(color ?? 'transparent');
-        selected.forEach(doc => (doc._backgroundColor = color));
+        selected.forEach(doc => (doc[DocData].backgroundColor = color));
     }
 });
 
diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx
index ad6deeefb..7427f4310 100644
--- a/src/client/views/linking/LinkMenuItem.tsx
+++ b/src/client/views/linking/LinkMenuItem.tsx
@@ -92,8 +92,8 @@ export class LinkMenuItem extends ObservableReactComponent<LinkMenuItemProps> {
                     DocumentViewInternal.addDocTabFunc(trail, OpenWhere.replaceRight);
                 } else {
                     SelectionManager.SelectView(this._props.docView, false);
-                    LinkManager.currentLink = this._props.linkDoc === LinkManager.currentLink ? undefined : this._props.linkDoc;
-                    LinkManager.currentLinkAnchor = LinkManager.currentLink ? this.sourceAnchor : undefined;
+                    LinkManager.Instance.currentLink = this._props.linkDoc === LinkManager.Instance.currentLink ? undefined : this._props.linkDoc;
+                    LinkManager.Instance.currentLinkAnchor = LinkManager.Instance.currentLink ? this.sourceAnchor : undefined;
 
                     if ((SettingsManager.Instance.propertiesWidth ?? 0) < 100) {
                         setTimeout(action(() => (SettingsManager.Instance.propertiesWidth = 250)));
@@ -159,7 +159,7 @@ export class LinkMenuItem extends ObservableReactComponent<LinkMenuItemProps> {
                 style={{
                     fontSize: this._hover ? 'larger' : undefined,
                     fontWeight: this._hover ? 'bold' : undefined,
-                    background: LinkManager.currentLink === this._props.linkDoc ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor,
+                    background: LinkManager.Instance.currentLink === this._props.linkDoc ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor,
                 }}>
                 <div className="linkMenu-item-content expand-two">
                     <div
diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx
index d1805308d..2a68d2bf6 100644
--- a/src/client/views/nodes/DocumentLinksButton.tsx
+++ b/src/client/views/nodes/DocumentLinksButton.tsx
@@ -157,7 +157,7 @@ export class DocumentLinksButton extends ObservableReactComponent<DocumentLinksB
                 startLink = DocumentLinksButton.StartLinkView?.ComponentView?.getAnchor?.(true) || startLink;
                 const linkDoc = DocUtils.MakeLink(startLink, endLink, { link_relationship: DocumentLinksButton.AnnotationId ? 'hypothes.is annotation' : undefined });
 
-                LinkManager.currentLink = linkDoc;
+                LinkManager.Instance.currentLink = linkDoc;
 
                 if (linkDoc) {
                     if (DocumentLinksButton.AnnotationId && DocumentLinksButton.AnnotationUri) {
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index b5355fb99..862dae83a 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1,7 +1,7 @@
 import { IconProp } from '@fortawesome/fontawesome-svg-core';
 import { Dropdown, DropdownType, Type } from 'browndash-components';
 import { Howl } from 'howler';
-import { IReactionDisposer, action, computed, makeObservable, observable, reaction, runInAction } from 'mobx';
+import { IReactionDisposer, action, computed, makeObservable, observable, reaction, runInAction, trace } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { Bounce, Fade, Flip, JackInTheBox, Roll, Rotate, Zoom } from 'react-awesome-reveal';
@@ -20,13 +20,14 @@ import { DocServer } from '../../DocServer';
 import { Networking } from '../../Network';
 import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils';
 import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes';
-import { DocOptions, DocUtils, Docs, FInfo } from '../../documents/Documents';
+import { DocOptions, DocUtils, Docs } from '../../documents/Documents';
 import { DictationManager } from '../../util/DictationManager';
 import { DocumentManager } from '../../util/DocumentManager';
 import { DragManager, dropActionType } from '../../util/DragManager';
 import { FollowLinkScript } from '../../util/LinkFollower';
 import { LinkManager } from '../../util/LinkManager';
 import { ScriptingGlobals } from '../../util/ScriptingGlobals';
+import { SearchUtil } from '../../util/SearchUtil';
 import { SelectionManager } from '../../util/SelectionManager';
 import { SettingsManager } from '../../util/SettingsManager';
 import { SharingManager } from '../../util/SharingManager';
@@ -106,6 +107,7 @@ export interface DocumentViewProps extends FieldViewSharedProps {
     NativeWidth?: () => number;
     NativeHeight?: () => number;
     contextMenuItems?: () => { script: ScriptField; filter?: ScriptField; label: string; icon: string }[];
+    dragConfig?: (data: DragManager.DocumentDragData) => void;
     dragStarting?: () => void;
     dragEnding?: () => void;
 }
@@ -288,7 +290,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
             dragData.moveDocument = this._props.moveDocument;
             dragData.draggedViews = [docView];
             dragData.canEmbed = this.Document.dragAction ?? this._props.dragAction ? true : false;
-            this._componentView?.dragConfig?.(dragData);
+            (this._props.dragConfig ?? this._componentView?.dragConfig)?.(dragData);
             DragManager.StartDocumentDrag(
                 selected.map(dv => dv.ContentDiv!),
                 dragData,
@@ -1466,7 +1468,7 @@ ScriptingGlobals.add(function toggleDetail(dv: DocumentView, detailLayoutKeySuff
 });
 
 ScriptingGlobals.add(function updateLinkCollection(linkCollection: Doc, linkSource: Doc) {
-    const collectedLinks = DocListCast(Doc.GetProto(linkCollection).data);
+    const collectedLinks = DocListCast(linkCollection[DocData].data);
     let wid = NumCast(linkSource._width);
     let embedding: Doc | undefined;
     const links = LinkManager.Links(linkSource);
@@ -1485,3 +1487,27 @@ ScriptingGlobals.add(function updateLinkCollection(linkCollection: Doc, linkSour
     embedding && DocServer.UPDATE_SERVER_CACHE(); // if a new embedding was made, update the client's server cache so that it will not come back as a promise
     return links;
 });
+ScriptingGlobals.add(function updateTagsCollection(collection: Doc) {
+    const tag = StrCast(collection.title).split('-->')[1];
+    const matchedTags = Array.from(SearchUtil.SearchCollection(Doc.MyFilesystem, tag, false, ['tags']).keys());
+    const collectionDocs = DocListCast(collection[DocData].data).concat(collection);
+    let wid = 100;
+    let created = false;
+    const matchedDocs = matchedTags
+        .filter(tagDoc => !Doc.AreProtosEqual(collection, tagDoc))
+        .map(tagDoc => {
+            let embedding = collectionDocs.find(doc => Doc.AreProtosEqual(tagDoc, doc));
+            if (!embedding) {
+                embedding = Doc.MakeEmbedding(tagDoc);
+                embedding.x = wid;
+                embedding.y = 0;
+                embedding._lockedPosition = false;
+                wid += NumCast(tagDoc._width);
+                created = true;
+            }
+            return embedding;
+        });
+
+    created && (collection[DocData].data = new List<Doc>(matchedDocs));
+    return true;
+});
diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx
index 10eeff08d..fd3074a88 100644
--- a/src/client/views/nodes/LabelBox.tsx
+++ b/src/client/views/nodes/LabelBox.tsx
@@ -88,7 +88,7 @@ export class LabelBox extends ViewBoxBaseComponent<LabelBoxProps>() {
 
     @observable _mouseOver = false;
     @computed get hoverColor() {
-        return this._mouseOver ? StrCast(this.layoutDoc._hoverBackgroundColor) : this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor);
+        return this._mouseOver ? StrCast(this.layoutDoc._hoverBackgroundColor) : this._props.styleProvider?.(this.Document, this._props, StyleProp.BackgroundColor);
     }
 
     getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
diff --git a/src/client/views/nodes/LinkBox.scss b/src/client/views/nodes/LinkBox.scss
index 767f0291b..484fb301e 100644
--- a/src/client/views/nodes/LinkBox.scss
+++ b/src/client/views/nodes/LinkBox.scss
@@ -5,3 +5,28 @@
 .linkBox-container {
     width: 100%;
 }
+
+.linkBox {
+    transition: inherit;
+    pointer-events: none;
+    position: absolute;
+    width: 100%;
+    height: 100%;
+    path {
+        transition: inherit;
+        fill: transparent;
+    }
+    svg {
+        transition: inherit;
+        overflow: visible;
+    }
+    text {
+        cursor: default;
+        text-anchor: middle;
+        font-size: 12;
+        stroke: black;
+    }
+    circle {
+        cursor: default;
+    }
+}
diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx
index 8b6293806..7ade846e7 100644
--- a/src/client/views/nodes/LinkBox.tsx
+++ b/src/client/views/nodes/LinkBox.tsx
@@ -4,7 +4,7 @@ import { observer } from 'mobx-react';
 import * as React from 'react';
 import { Id } from '../../../fields/FieldSymbols';
 import { DocCast, NumCast, StrCast } from '../../../fields/Types';
-import { aggregateBounds, emptyFunction, returnAlways, returnFalse, Utils } from '../../../Utils';
+import { aggregateBounds, emptyFunction, returnAlways, returnFalse, setupMoveUpEvents, Utils } from '../../../Utils';
 import { DocumentManager } from '../../util/DocumentManager';
 import { Transform } from '../../util/Transform';
 import { CollectionFreeFormView } from '../collections/collectionFreeForm';
@@ -13,6 +13,8 @@ import { StyleProp } from '../StyleProvider';
 import { ComparisonBox } from './ComparisonBox';
 import { FieldView, FieldViewProps } from './FieldView';
 import './LinkBox.scss';
+import { LinkDescriptionPopup } from './LinkDescriptionPopup';
+import { LinkManager } from '../../util/LinkManager';
 
 @observer
 export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() {
@@ -97,8 +99,8 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() {
                     ) {
                         this.layoutDoc.x = params?.lx;
                         this.layoutDoc.y = params?.ty;
-                        this.layoutDoc._width = params.rx - params?.lx;
-                        this.layoutDoc._height = params?.by - params?.ty;
+                        this.layoutDoc._width = Math.max(1, params.rx - params?.lx);
+                        this.layoutDoc._height = Math.max(1, params?.by - params?.ty);
                     }
                 } else {
                     this.layoutDoc._width = Math.max(50, NumCast(this.layoutDoc._width));
@@ -108,6 +110,22 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() {
             { fireImmediately: true }
         );
     }
+    descriptionDown = (e: React.PointerEvent) => {
+        setupMoveUpEvents(this, e, returnFalse, returnFalse, () => {
+            LinkManager.Instance.currentLink = this.Document;
+            LinkDescriptionPopup.Instance.popupX = e.clientX;
+            LinkDescriptionPopup.Instance.popupY = e.clientY;
+            LinkDescriptionPopup.Instance.display = true;
+
+            const rect = document.body.getBoundingClientRect();
+            if (LinkDescriptionPopup.Instance.popupX + 200 > rect.width) {
+                LinkDescriptionPopup.Instance.popupX -= 190;
+            }
+            if (LinkDescriptionPopup.Instance.popupY + 100 > rect.height) {
+                LinkDescriptionPopup.Instance.popupY -= 40;
+            }
+        });
+    };
     componentWillUnmount(): void {
         this.disposer?.();
     }
@@ -117,19 +135,21 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() {
             const highlight = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Highlighting);
             const highlightColor = highlight?.highlightIndex ? highlight?.highlightColor : undefined;
 
-            const bez = new Bezier(this.renderProps.pts.map(p => ({ x: p[0], y: p[1] })));
-            const text = bez.get(0.5);
-            const linkDesc = StrCast(this.dataDoc.link_description) || 'description';
+            const { pts, lx, ty, rx, by } = this.renderProps;
+            const bez = new Bezier(pts.map(p => ({ x: p[0] - lx, y: p[1] - ty })));
+            const { x, y } = bez.get(0.5);
+            const linkDesc = StrCast(this.dataDoc.link_description) || ' ';
             const strokeWidth = NumCast(this.dataDoc.stroke_width, 4);
             const dash = StrCast(this.Document.stroke_dash);
             const strokeDasharray = dash && Number(dash) ? String(strokeWidth * Number(dash)) : undefined;
-            const { pts, lx, ty, rx, by } = this.renderProps;
+            const pointerEvents = this._props.pointerEvents?.() === 'none' ? 'none' : 'all';
+            const stroke = highlightColor ?? 'lightblue';
             return (
-                <div style={{ transition: 'inherit', pointerEvents: 'none', position: 'absolute', width: '100%', height: '100%' }}>
-                    <svg width={Math.max(100, rx - lx)} height={Math.max(100, by - ty)} style={{ transition: 'inherit', overflow: 'visible' }}>
+                <div className="linkBox">
+                    <svg width={Math.max(100, rx - lx)} height={Math.max(100, by - ty)}>
                         <defs>
                             <filter x="0" y="0" width="1" height="1" id={`${this.Document[Id] + 'background'}`}>
-                                <feFlood floodColor={`${StrCast(this.layoutDoc._backgroundColor, 'lightblue')}`} result="bg" />
+                                <feFlood floodColor={`${highlightColor ?? StrCast(this.layoutDoc._backgroundColor, 'lightblue')}`} result="bg" />
                                 <feMerge>
                                     <feMergeNode in="bg" />
                                     <feMergeNode in="SourceGraphic" />
@@ -137,26 +157,27 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() {
                             </filter>
                         </defs>
                         <path
-                            className="collectionfreeformlinkview-linkLine"
                             style={{
                                 pointerEvents: this._props.pointerEvents?.() === 'none' ? 'none' : 'visibleStroke', //
-                                stroke: highlightColor ?? 'lightblue',
+                                stroke,
                                 strokeDasharray,
                                 strokeWidth,
-                                transition: 'inherit',
                             }}
-                            d={`M ${pts[1][0] - lx} ${pts[1][1] - ty} C ${pts[1][0] + pts[1][0] - pts[0][0] - lx} ${pts[1][1] + pts[1][1] - pts[0][1] - ty}, 
-                            ${pts[2][0] + pts[2][0] - pts[3][0] - lx} ${pts[2][1] + pts[2][1] - pts[3][1] - ty}, ${pts[2][0] - lx} ${pts[2][1] - ty}`}
+                            d={`M ${bez.points[0].x} ${bez.points[0].y} C ${bez.points[1].x} ${bez.points[1].y}, 
+                            ${bez.points[2].x} ${bez.points[2].y}, ${bez.points[3].x} ${bez.points[3].y}`}
                         />
-                        <text
-                            filter={`url(#${this.Document[Id] + 'background'})`}
-                            style={{ pointerEvents: this._props.pointerEvents?.() === 'none' ? 'none' : 'all', textAnchor: 'middle', fontSize: '12', stroke: 'black' }}
-                            x={text.x - lx}
-                            y={text.y - ty}>
-                            <tspan>&nbsp;</tspan>
-                            <tspan dy="2">{linkDesc.substring(0, 50) + (linkDesc.length > 50 ? '...' : '')}</tspan>
-                            <tspan dy="2">&nbsp;</tspan>
-                        </text>
+                        {!linkDesc.trim().length ? (
+                            <circle r={5} onPointerDown={this.descriptionDown} style={{ fill: stroke, pointerEvents }} cx={x} cy={y} />
+                        ) : (
+                            <text
+                                onPointerDown={this.descriptionDown} //
+                                filter={`url(#${this.Document[Id] + 'background'})`}
+                                style={{ pointerEvents, background: stroke }}
+                                x={x}
+                                y={y}>
+                                <tspan style={{ background: stroke }}>&nbsp;{linkDesc.substring(0, 50) + (linkDesc.length > 50 ? '...' : '')}&nbsp;</tspan>
+                            </text>
+                        )}
                     </svg>
                 </div>
             );
diff --git a/src/client/views/nodes/LinkDescriptionPopup.tsx b/src/client/views/nodes/LinkDescriptionPopup.tsx
index 13f0ac4fc..1645d0813 100644
--- a/src/client/views/nodes/LinkDescriptionPopup.tsx
+++ b/src/client/views/nodes/LinkDescriptionPopup.tsx
@@ -1,10 +1,11 @@
-import { action, makeObservable, observable } from 'mobx';
+import { action, makeObservable, observable, reaction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { DocData } from '../../../fields/DocSymbols';
 import { LinkManager } from '../../util/LinkManager';
 import './LinkDescriptionPopup.scss';
 import { TaskCompletionBox } from './TaskCompletedBox';
+import { StrCast } from '../../../fields/Types';
 
 @observer
 export class LinkDescriptionPopup extends React.Component<{}> {
@@ -31,21 +32,26 @@ export class LinkDescriptionPopup extends React.Component<{}> {
     onDismiss = (add: boolean) => {
         this.display = false;
         if (add) {
-            LinkManager.currentLink && (LinkManager.currentLink[DocData].link_description = this.description);
+            LinkManager.Instance.currentLink && (LinkManager.Instance.currentLink[DocData].link_description = this.description);
         }
+        this.description = '';
     };
 
     @action
     onClick = (e: PointerEvent) => {
         if (this.popupRef && !!!this.popupRef.current?.contains(e.target as any)) {
             this.display = false;
+            this.description = '';
             TaskCompletionBox.taskCompleted = false;
         }
     };
 
-    @action
     componentDidMount() {
         document.addEventListener('pointerdown', this.onClick, true);
+        reaction(
+            () => this.display,
+            display => display && (this.description = StrCast(LinkManager.Instance.currentLink?.link_description))
+        );
     }
 
     componentWillUnmount() {
@@ -65,8 +71,10 @@ export class LinkDescriptionPopup extends React.Component<{}> {
                     className="linkDescriptionPopup-input"
                     onKeyDown={e => e.stopPropagation()}
                     onKeyPress={e => e.key === 'Enter' && this.onDismiss(true)}
-                    placeholder={'(Optional) Enter link description...'}
-                    onChange={e => this.descriptionChanged(e)}></input>
+                    value={this.description}
+                    placeholder={this.description || '(Optional) Enter link description...'}
+                    onChange={e => this.descriptionChanged(e)}
+                />
                 <div className="linkDescriptionPopup-btn">
                     <div className="linkDescriptionPopup-btn-dismiss" onPointerDown={e => this.onDismiss(false)}>
                         {' '}
diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx
index 3f793b85e..ae25ff179 100644
--- a/src/client/views/nodes/LinkDocPreview.tsx
+++ b/src/client/views/nodes/LinkDocPreview.tsx
@@ -152,8 +152,8 @@ export class LinkDocPreview extends ObservableReactComponent<LinkDocPreviewProps
             returnFalse,
             emptyFunction,
             action(() => {
-                LinkManager.currentLink = this._linkDoc;
-                LinkManager.currentLinkAnchor = this._linkSrc;
+                LinkManager.Instance.currentLink = this._linkDoc;
+                LinkManager.Instance.currentLinkAnchor = this._linkSrc;
                 this._props.DocumentView?.().select(false);
                 if ((SettingsManager.Instance.propertiesWidth ?? 0) < 100) {
                     SettingsManager.Instance.propertiesWidth = 250;
diff --git a/src/client/views/nodes/formattedText/DashFieldView.scss b/src/client/views/nodes/formattedText/DashFieldView.scss
index 3426ba1a7..7a0ff8776 100644
--- a/src/client/views/nodes/formattedText/DashFieldView.scss
+++ b/src/client/views/nodes/formattedText/DashFieldView.scss
@@ -5,6 +5,16 @@
     display: inline-flex;
     align-items: center;
 
+    select {
+        display: none;
+    }
+
+    &:hover {
+        select {
+            display: unset;
+        }
+    }
+
     .dashFieldView-enumerables {
         width: 10px;
         height: 10px;
diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx
index 18286267a..ec0b76aa8 100644
--- a/src/client/views/nodes/formattedText/DashFieldView.tsx
+++ b/src/client/views/nodes/formattedText/DashFieldView.tsx
@@ -4,21 +4,24 @@ import { action, computed, IReactionDisposer, makeObservable, observable, reacti
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import * as ReactDOM from 'react-dom/client';
-import { Doc, Field } from '../../../../fields/Doc';
+import Select from 'react-select';
+import { Doc, DocListCast, Field } from '../../../../fields/Doc';
 import { List } from '../../../../fields/List';
 import { listSpec } from '../../../../fields/Schema';
 import { SchemaHeaderField } from '../../../../fields/SchemaHeaderField';
-import { Cast } from '../../../../fields/Types';
+import { Cast, StrCast } from '../../../../fields/Types';
 import { emptyFunction, returnFalse, returnZero, setupMoveUpEvents } from '../../../../Utils';
 import { DocServer } from '../../../DocServer';
 import { CollectionViewType } from '../../../documents/DocumentTypes';
 import { Transform } from '../../../util/Transform';
+import { undoable, undoBatch } from '../../../util/UndoManager';
 import { AntimodeMenu, AntimodeMenuProps } from '../../AntimodeMenu';
 import { SchemaTableCell } from '../../collections/collectionSchema/SchemaTableCell';
 import { ObservableReactComponent } from '../../ObservableReactComponent';
 import { OpenWhere } from '../DocumentView';
 import './DashFieldView.scss';
 import { FormattedTextBox } from './FormattedTextBox';
+import { FilterPanel } from '../../FilterPanel';
 
 export class DashFieldView {
     dom: HTMLDivElement; // container for label and value
@@ -97,7 +100,7 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi
     _reactionDisposer: IReactionDisposer | undefined;
     _textBoxDoc: Doc;
     _fieldKey: string;
-    _fieldStringRef = React.createRef<HTMLSpanElement>();
+    _fieldRef = React.createRef<HTMLDivElement>();
     @observable _dashDoc: Doc | undefined = undefined;
     @observable _expanded = false;
 
@@ -180,10 +183,22 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi
         });
     };
 
+    @undoBatch
+    selectVal = (event: React.ChangeEvent<HTMLSelectElement> | undefined) => {
+        event && this._dashDoc && (this._dashDoc[this._fieldKey] = event.target.value);
+    };
+
+    @computed get values() {
+        const vals = FilterPanel.gatherFieldValues(DocListCast(Doc.ActiveDashboard?.data), this._fieldKey, []);
+
+        return vals.strings.map(facet => ({ value: facet, label: facet }));
+    }
+
     render() {
         return (
             <div
                 className="dashFieldView"
+                ref={this._fieldRef}
                 style={{
                     width: this._props.width,
                     height: this._props.height,
@@ -194,8 +209,12 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi
                         {(this._textBoxDoc === this._dashDoc ? '' : this._dashDoc?.title + ':') + this._fieldKey}
                     </span>
                 )}
-
                 {this._props.fieldKey.startsWith('#') ? null : this.fieldValueContent}
+                <select onChange={this.selectVal} style={{ height: '10px', width: '15px', fontSize: '12px', background: 'transparent' }}>
+                    {this.values.map(val => (
+                        <option value={val.value}>{val.label}</option>
+                    ))}
+                </select>
             </div>
         );
     }
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index b82ab4219..f2c4c6c8f 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -13,7 +13,7 @@ import { EditorView } from 'prosemirror-view';
 import * as React from 'react';
 import { BsMarkdownFill } from 'react-icons/bs';
 import { DateField } from '../../../../fields/DateField';
-import { Doc, DocListCast, Field, Opt } from '../../../../fields/Doc';
+import { Doc, DocListCast, Field, Opt, StrListCast } from '../../../../fields/Doc';
 import { AclAdmin, AclAugment, AclEdit, AclSelfEdit, DocCss, DocData, ForceServerWrite, UpdatingFromServer } from '../../../../fields/DocSymbols';
 import { Id } from '../../../../fields/FieldSymbols';
 import { InkTool } from '../../../../fields/InkField';
@@ -67,6 +67,7 @@ import { RichTextMenu, RichTextMenuPlugin } from './RichTextMenu';
 import { RichTextRules } from './RichTextRules';
 import { schema } from './schema_rts';
 import { SummaryView } from './SummaryView';
+import Select from 'react-select';
 // import * as applyDevTools from 'prosemirror-dev-tools';
 @observer
 export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implements ViewBoxInterface {
@@ -360,7 +361,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
                         accumTags.push(node.attrs.fieldKey);
                     }
                 });
-                dataDoc.tags = accumTags.length ? new List<string>(Array.from(new Set<string>(accumTags))) : undefined;
+                if (accumTags.some(atag => !StrListCast(dataDoc.tags).includes(atag))) {
+                    dataDoc.tags = new List<string>(Array.from(new Set<string>(accumTags)));
+                }
 
                 let unchanged = true;
                 if (this._applyingChange !== this.fieldKey && (force || removeSelection(newJson) !== removeSelection(prevData?.Data))) {
@@ -1189,8 +1192,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
         this._cachedLinks = LinkManager.Links(this.Document);
         this._disposers.breakupDictation = reaction(() => Doc.RecordingEvent, this.breakupDictation);
         this._disposers.layout_autoHeight = reaction(
-            () => this.layout_autoHeight,
-            layout_autoHeight => layout_autoHeight && this.tryUpdateScrollHeight()
+            () => ({ autoHeight: this.layout_autoHeight, fontSize: this.fontSize }),
+            (autoHeight, fontSize) => setTimeout(() => autoHeight && this.tryUpdateScrollHeight())
         );
         this._disposers.highlights = reaction(
             () => Array.from(FormattedTextBox._globalHighlights).slice(),
diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts
index 2fdd6374a..9bd41f42c 100644
--- a/src/client/views/nodes/formattedText/RichTextRules.ts
+++ b/src/client/views/nodes/formattedText/RichTextRules.ts
@@ -264,7 +264,7 @@ export class RichTextRules {
                 return null;
             }),
 
-            // stop using active style
+            // toggle alternate text UI  %/
             new InputRule(new RegExp(/%\//), (state, match, start, end) => {
                 setTimeout(this.TextBox.cycleAlternateText);
                 return state.tr.deleteRange(start, end);
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index ab6d0390b..3cd4efcf7 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -390,7 +390,7 @@ export class Doc extends RefField {
 
 export namespace Doc {
     export function SetContainer(doc: Doc, container: Doc) {
-        doc.embedContainer = container;
+        container !== Doc.MyRecentlyClosed && (doc.embedContainer = container);
     }
     export function RunCachedUpdate(doc: Doc, field: string) {
         const update = doc[CachedUpdates][field];
diff --git a/src/fields/List.ts b/src/fields/List.ts
index 9458a9611..ec31f7dae 100644
--- a/src/fields/List.ts
+++ b/src/fields/List.ts
@@ -317,7 +317,7 @@ class ListImpl<T extends Field> extends ObjectField {
         return `new List([${(this as any).map((field: any) => Field.toScriptString(field))}])`;
     }
     [ToString]() {
-        return `List(${(this as any).length})`;
+        return `[${(this as any).map((field: any) => Field.toString(field))}]`;
     }
 }
 export type List<T extends Field> = ListImpl<T> & (T | (T extends RefField ? Promise<T> : never))[];
-- 
cgit v1.2.3-70-g09d2

