From 42afc0250de658fc3e924864bfae5afb4edec335 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Sun, 14 May 2023 12:03:40 -0400
Subject: major overhaul of field naming conventions.

---
 src/client/views/PropertiesDocContextSelector.tsx | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

(limited to 'src/client/views/PropertiesDocContextSelector.tsx')

diff --git a/src/client/views/PropertiesDocContextSelector.tsx b/src/client/views/PropertiesDocContextSelector.tsx
index c14d7ef73..12a09196e 100644
--- a/src/client/views/PropertiesDocContextSelector.tsx
+++ b/src/client/views/PropertiesDocContextSelector.tsx
@@ -23,14 +23,14 @@ export class PropertiesDocContextSelector extends React.Component<PropertiesDocC
         if (!this.props.DocView) return [];
         const target = this.props.DocView.props.Document;
         const targetContext = this.props.DocView.props.docViewPath().lastElement()?.rootDoc;
-        const aliases = DocListCast(target.aliases);
-        const containerProtos = aliases.filter(alias => alias.context && alias.context instanceof Doc).reduce((set, alias) => set.add(Cast(alias.context, Doc, null)), new Set<Doc>());
-        const containerSets = Array.from(containerProtos.keys()).map(container => DocListCast(container.aliases));
+        const embeddings = DocListCast(target.proto_embeddings);
+        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 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.aliases));
+        const doclayoutSets = Array.from(containers.keys()).map(dp => DocListCast(dp.proto_embeddings));
         const doclayouts = Array.from(
             doclayoutSets
                 .reduce((p, set) => {
@@ -48,7 +48,7 @@ export class PropertiesDocContextSelector extends React.Component<PropertiesDocC
 
     getOnClick = (col: Doc, target: Doc) => {
         if (!this.props.DocView) return;
-        col = Doc.IsPrototype(col) ? Doc.MakeDelegate(col) : col;
+        col = Doc.IsDocDataProto(col) ? Doc.MakeDelegate(col) : col;
         DocFocusOrOpen(Doc.GetProto(this.props.DocView.props.Document), undefined, col);
     };
 
-- 
cgit v1.2.3-70-g09d2


From 46cf6c823ca8ab628cd8c5bd7fdfe8945344a014 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 16 May 2023 14:50:29 -0400
Subject: fixed bugs with goldenlayout dragging and undoing.  fixed searching
 for filter values in sidebars.  Stopped creating empty list for collections
 when datafield() is accessed because it messes up undo of a collection. fixed
 tab title editing.  from marquee.  Added UndoStack UI and additional naming
 support in code.

---
 src/client/documents/Documents.ts                  | 12 ++--
 src/client/goldenLayout.js                         |  2 +-
 src/client/util/CurrentUserUtils.ts                | 44 +++++++-------
 src/client/util/DragManager.ts                     |  6 +-
 src/client/util/LinkFollower.ts                    |  2 +-
 src/client/util/SelectionManager.ts                |  2 +-
 src/client/util/UndoManager.ts                     | 53 ++++++++++++-----
 src/client/views/ContextMenuItem.tsx               |  5 +-
 src/client/views/DocumentDecorations.tsx           | 46 +++++----------
 src/client/views/EditableView.tsx                  |  2 +
 src/client/views/FilterPanel.tsx                   |  9 ++-
 src/client/views/GlobalKeyHandler.ts               | 10 ++--
 src/client/views/MainView.tsx                      |  8 +--
 src/client/views/PropertiesButtons.tsx             |  6 +-
 .../views/PropertiesDocBacklinksSelector.tsx       |  2 +-
 src/client/views/PropertiesDocContextSelector.tsx  |  2 +-
 src/client/views/PropertiesView.tsx                | 32 +++++-----
 src/client/views/SidebarAnnos.scss                 | 14 +++--
 src/client/views/SidebarAnnos.tsx                  | 15 ++++-
 src/client/views/UndoStack.tsx                     | 30 ++++++++++
 .../views/collections/CollectionDockingView.tsx    | 63 ++++++++++----------
 .../collections/CollectionNoteTakingViewColumn.tsx |  5 +-
 .../collections/CollectionStackedTimeline.tsx      |  3 +-
 .../CollectionStackingViewFieldColumn.tsx          |  4 +-
 src/client/views/collections/CollectionSubView.tsx | 13 ++--
 src/client/views/collections/TabDocView.tsx        | 21 ++++---
 src/client/views/collections/TreeView.tsx          |  4 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  2 -
 .../collections/collectionFreeForm/MarqueeView.tsx | 26 +++-----
 .../collectionSchema/CollectionSchemaView.tsx      | 32 +++++-----
 .../collectionSchema/SchemaColumnHeader.tsx        |  2 -
 .../collections/collectionSchema/SchemaRowBox.tsx  | 10 ++--
 .../collectionSchema/SchemaTableCell.tsx           |  2 +-
 src/client/views/linking/LinkMenu.tsx              |  2 +-
 src/client/views/nodes/AudioBox.tsx                |  2 +-
 src/client/views/nodes/DocumentLinksButton.tsx     | 20 +++----
 src/client/views/nodes/DocumentView.tsx            | 57 ++++++++++--------
 src/client/views/nodes/LinkDocPreview.tsx          |  3 +-
 src/client/views/nodes/button/FontIconBox.tsx      | 60 ++++++++-----------
 .../views/nodes/formattedText/FormattedTextBox.tsx | 69 +++++++++++-----------
 src/client/views/nodes/trails/PresBox.tsx          |  9 ++-
 src/client/views/nodes/trails/PresElementBox.tsx   | 13 ++--
 src/client/views/pdf/AnchorMenu.tsx                | 11 ++--
 src/client/views/search/SearchBox.tsx              |  2 +
 src/fields/Doc.ts                                  | 18 ++++--
 src/fields/util.ts                                 | 38 +++++++-----
 46 files changed, 421 insertions(+), 372 deletions(-)
 create mode 100644 src/client/views/UndoStack.tsx

(limited to 'src/client/views/PropertiesDocContextSelector.tsx')

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 2151d0ec7..795d60b48 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -24,7 +24,7 @@ import { DirectoryImportBox } from '../util/Import & Export/DirectoryImportBox';
 import { FollowLinkScript } from '../util/LinkFollower';
 import { LinkManager } from '../util/LinkManager';
 import { ScriptingGlobals } from '../util/ScriptingGlobals';
-import { undoBatch, UndoManager } from '../util/UndoManager';
+import { undoable, undoBatch, UndoManager } from '../util/UndoManager';
 import { CollectionDockingView } from '../views/collections/CollectionDockingView';
 import { DimUnit } from '../views/collections/collectionMulticolumn/CollectionMulticolumnView';
 import { CollectionView } from '../views/collections/CollectionView';
@@ -249,7 +249,7 @@ export class DocumentOptions {
     contextMenuIcons?: List<string>;
     defaultDoubleClick?: 'ignore' | 'default'; // ignore double clicks, or deafult (undefined) means open document full screen
     waitForDoubleClickToClick?: 'always' | 'never' | 'default'; // whether a click function wait for double click to expire.  'default' undefined = wait only if there's a click handler, "never" = never wait, "always" = alway wait
-    dontUndo?: boolean; // whether button clicks should be undoable (this is set to true for Undo/Redo/and sidebar buttons that open the siebar panel)
+    dontUndo?: boolean; // whether button clicks should be undoable ( true for Undo/Redo/and sidebar) AND whether modifications to document are undoable (true for linearview menu buttons to prevent open/close from entering undo stack)
     layout?: string | Doc; // default layout string for a document
     contentPointerEvents?: string; // pointer events allowed for content of a document view.  eg. set to "none" in menuSidebar for sharedDocs so that you can select a document, but not interact with its contents
     childLimitHeight?: number; // whether to limit the height of collection children.  0 - means  height can be no bigger than width
@@ -1532,7 +1532,7 @@ export namespace DocUtils {
                 description: 'Quick Notes',
                 subitems: DocListCast((Doc.UserDoc()['template-notes'] as Doc).data).map((note, i) => ({
                     description: ':' + StrCast(note.title),
-                    event: undoBatch((args: { x: number; y: number }) => {
+                    event: undoable((args: { x: number; y: number }) => {
                         const textDoc = Docs.Create.TextDocument('', {
                             _width: 200,
                             x,
@@ -1546,7 +1546,7 @@ export namespace DocUtils {
                             textDoc[pivotField] = pivotValue;
                         }
                         docTextAdder(textDoc);
-                    }),
+                    }, 'create quick note'),
                     icon: StrCast(note.icon) as IconProp,
                 })) as ContextMenuProps[],
                 icon: 'sticky-note',
@@ -1557,7 +1557,7 @@ export namespace DocUtils {
             .filter(doc => doc && doc !== Doc.UserDoc().emptyTrail && doc !== Doc.UserDoc().emptyDataViz)
             .map((dragDoc, i) => ({
                 description: ':' + StrCast(dragDoc.title).replace('Untitled ', ''),
-                event: undoBatch((args: { x: number; y: number }) => {
+                event: undoable((args: { x: number; y: number }) => {
                     const newDoc = DocUtils.copyDragFactory(dragDoc);
                     if (newDoc) {
                         newDoc.author = Doc.CurrentUserEmail;
@@ -1570,7 +1570,7 @@ export namespace DocUtils {
                         }
                         docAdder?.(newDoc);
                     }
-                }),
+                }, StrCast(dragDoc.title)),
                 icon: Doc.toIcon(dragDoc),
             })) as ContextMenuProps[];
         ContextMenu.Instance.addItem({
diff --git a/src/client/goldenLayout.js b/src/client/goldenLayout.js
index 9cb20d834..843b8bb5f 100644
--- a/src/client/goldenLayout.js
+++ b/src/client/goldenLayout.js
@@ -4740,7 +4740,7 @@
                         newstack._$init();
                         newstack.addChild(this.contentItems[0]);
                     }
-                    correctRowOrCol.addChild(newstack,  insertBefore ? 0 : undefined, true);
+                    correctRowOrCol.addChild(newstack,  !insertBefore ? 0 : undefined, true);
                     newstack.config[dimension] = 50;
                     contentItem.config[dimension] = 50;
                     correctRowOrCol.callDownwards('setSize');
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 26fe8f440..b0a2c7d60 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -363,7 +363,7 @@ export class CurrentUserUtils {
         const menuBtns = CurrentUserUtils.leftSidebarMenuBtnDescriptions(doc).map(({ title, target, icon, scripts, funcs }) => {
             const btnDoc = myLeftSidebarMenu ? DocListCast(myLeftSidebarMenu.data).find(doc => doc.title === title) : undefined;
             const reqdBtnOpts:DocumentOptions = {
-                title, icon, target, btnType: ButtonType.MenuButton, isSystem: true, dontUndo: true,  dontRegisterView: true,
+                title, icon, target, btnType: ButtonType.MenuButton, isSystem: true, dontUndo: true, dontRegisterView: true,
                 _width: 60, _height: 60,  _stayInCollection: true, _hideContextMenu: true,
                 _removeDropProperties: new List<string>(["_stayInCollection"]),
             };
@@ -598,12 +598,12 @@ export class CurrentUserUtils {
                             CurrentUserUtils.createToolButton(opts), scripts, funcs);
         
         const btnDescs = [// setup reactions to change the highlights on the undo/redo buttons -- would be better to encode this in the undo/redo buttons, but the undo/redo stacks are not wired up that way yet
-            { scripts: { onClick: "undo()"}, opts: { title: "undo", icon: "undo-alt", toolTip: "Click to undo" }},
-            { scripts: { onClick: "redo()"}, opts: { title: "redo", icon: "redo-alt", toolTip: "Click to redo" }},
-            { scripts: { },                  opts: { title: "linker", icon: "linkui", toolTip: "link started"}},
-            { scripts: { },                  opts: { title: "currently playing", icon: "currentlyplayingui", toolTip: "currently playing media"}},
+            { scripts: { onClick: "undo()"},      opts: { title: "undo", icon: "undo-alt", toolTip: "Click to undo" }},
+            { scripts: { onClick: "redo()"},      opts: { title: "redo", icon: "redo-alt", toolTip: "Click to redo" }},
+            { scripts: { },                       opts: { title: "linker", icon: "linkui", toolTip: "link started"}},
+            { scripts: { },                       opts: { title: "currently playing", icon: "currentlyplayingui", toolTip: "currently playing media"}},
         ];
-        const btns = btnDescs.map(desc => dockBtn({_width: 30, _height: 30, dontUndo: true, _stayInCollection: true, ...desc.opts}, desc.scripts));
+        const btns = btnDescs.map(desc => dockBtn({_width: 30, _height: 30, defaultDoubleClick: 'ignore', dontUndo: true, _stayInCollection: true, ...desc.opts}, desc.scripts));
         const dockBtnsReqdOpts:DocumentOptions = {
             title: "docked buttons", _height: 40, flexGap: 0, boxShadow: "standard", childDropAction: 'embed',
             childDontRegisterViews: true, linearViewIsExpanded: true, linearViewExpandable: true, ignoreClick: true
@@ -632,21 +632,21 @@ export class CurrentUserUtils {
     }
     static textTools():Button[] {
         return [
-            { title: "Font",    toolTip: "Font", width: 100,     btnType: ButtonType.DropdownList,                      toolType:"font",     ignoreClick: true,   scripts: {script:  '{ return setFontAttr(self.toolType, value, _readOnly_);}'},
+            { title: "Font",     toolTip: "Font", width: 100,     btnType: ButtonType.DropdownList,                      toolType:"font",     ignoreClick: true,   scripts: {script:  '{ return setFontAttr(self.toolType, value, _readOnly_);}'},
               btnList: new List<string>(["Roboto", "Roboto Mono", "Nunito", "Times New Roman", "Arial", "Georgia", "Comic Sans MS", "Tahoma", "Impact", "Crimson Text"]) },
-            { title: "Size",    toolTip: "Font size (%size)",    btnType: ButtonType.NumberDropdownButton, width: 75,           toolType:"fontSize", ignoreClick: true,   scripts: {script:  '{ return setFontAttr(self.toolType, value, _readOnly_);}'}, numBtnMax: 200, numBtnMin: 0 },
-            { title: "Color",   toolTip: "Font color (%color)",  btnType: ButtonType.ColorButton,  icon: "font",        toolType:"fontColor",ignoreClick: true,   scripts: {script:  '{ return setFontAttr(self.toolType, value, _readOnly_);}'}},
+            { title: "Font Size",toolTip: "Font size (%size)",    btnType: ButtonType.NumberDropdownButton, width: 75,           toolType:"fontSize", ignoreClick: true,   scripts: {script:  '{ return setFontAttr(self.toolType, value, _readOnly_);}'}, numBtnMax: 200, numBtnMin: 0 },
+            { title: "Color",    toolTip: "Font color (%color)",  btnType: ButtonType.ColorButton,  icon: "font",        toolType:"fontColor",ignoreClick: true,   scripts: {script:  '{ return setFontAttr(self.toolType, value, _readOnly_);}'}},
             { title: "Highlight",toolTip:"Font highlight",       btnType: ButtonType.ColorButton,  icon: "highlighter", toolType:"highlight",ignoreClick: true,   scripts: {script:  '{ return setFontAttr(self.toolType, value, _readOnly_);}'},funcs: {hidden: "IsNoviceMode()"} },
-            { title: "Bold",    toolTip: "Bold (Ctrl+B)",        btnType: ButtonType.ToggleButton, icon: "bold",        toolType:"bold",                          scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} },
-            { title: "Italic",  toolTip: "Italic (Ctrl+I)",      btnType: ButtonType.ToggleButton, icon: "italic",      toolType:"italics",                       scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} },
-            { title: "Under",   toolTip: "Underline (Ctrl+U)",   btnType: ButtonType.ToggleButton, icon: "underline",   toolType:"underline",                     scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} },
-            { title: "Bullets", toolTip: "Bullet List",          btnType: ButtonType.ToggleButton, icon: "list",        toolType:"bullet",                        scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} },
-            { title: "#",       toolTip: "Number List",          btnType: ButtonType.ToggleButton, icon: "list-ol",     toolType:"decimal",                       scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} },
-            { title: "Left",    toolTip: "Left align (Cmd-[)",   btnType: ButtonType.ToggleButton, icon: "align-left",  toolType:"left",                          scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}' }},
-            { title: "Center",  toolTip: "Center align (Cmd-\\)",btnType: ButtonType.ToggleButton, icon: "align-center",toolType:"center",                        scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} },
-            { title: "Right",   toolTip: "Right align (Cmd-])",  btnType: ButtonType.ToggleButton, icon: "align-right", toolType:"right",                         scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} },
-            { title: "Dictate", toolTip: "Dictate",              btnType: ButtonType.ToggleButton, icon: "microphone",  toolType:"dictation",                     scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'}},
-            { title: "NoLink",  toolTip: "Auto Link",            btnType: ButtonType.ToggleButton, icon: "link",        toolType:"noAutoLink", expertMode:true,   scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'}, funcs: {hidden: 'IsNoviceMode()'}},
+            { title: "Bold",     toolTip: "Bold (Ctrl+B)",        btnType: ButtonType.ToggleButton, icon: "bold",        toolType:"bold",                          scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} },
+            { title: "Italic",   toolTip: "Italic (Ctrl+I)",      btnType: ButtonType.ToggleButton, icon: "italic",      toolType:"italics",                       scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} },
+            { title: "Under",    toolTip: "Underline (Ctrl+U)",   btnType: ButtonType.ToggleButton, icon: "underline",   toolType:"underline",                     scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} },
+            { title: "Bullets",  toolTip: "Bullet List",          btnType: ButtonType.ToggleButton, icon: "list",        toolType:"bullet",                        scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} },
+            { title: "#",        toolTip: "Number List",          btnType: ButtonType.ToggleButton, icon: "list-ol",     toolType:"decimal",                       scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} },
+            { title: "Left",     toolTip: "Left align (Cmd-[)",   btnType: ButtonType.ToggleButton, icon: "align-left",  toolType:"left",                          scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}' }},
+            { title: "Center",   toolTip: "Center align (Cmd-\\)",btnType: ButtonType.ToggleButton, icon: "align-center",toolType:"center",                        scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} },
+            { title: "Right",    toolTip: "Right align (Cmd-])",  btnType: ButtonType.ToggleButton, icon: "align-right", toolType:"right",                         scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} },
+            { title: "Dictate",  toolTip: "Dictate",              btnType: ButtonType.ToggleButton, icon: "microphone",  toolType:"dictation",                     scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'}},
+            { title: "NoLink",   toolTip: "Auto Link",            btnType: ButtonType.ToggleButton, icon: "link",        toolType:"noAutoLink", expertMode:true,   scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'}, funcs: {hidden: 'IsNoviceMode()'}},
             // { title: "Strikethrough", tooltip: "Strikethrough", btnType: ButtonType.ToggleButton, icon: "strikethrough", scripts: {onClick:: 'toggleStrikethrough()'}},
             // { title: "Superscript", tooltip: "Superscript", btnType: ButtonType.ToggleButton, icon: "superscript", scripts: {onClick:: 'toggleSuperscript()'}},
             // { title: "Subscript", tooltip: "Subscript", btnType: ButtonType.ToggleButton, icon: "subscript", scripts: {onClick:: 'toggleSubscript()'}},
@@ -711,7 +711,7 @@ export class CurrentUserUtils {
         const reqdOpts:DocumentOptions = {
             ...OmitKeys(params, ["scripts", "funcs", "subMenu"]).omit,
             backgroundColor: params.backgroundColor ??"transparent", ///  a bit hacky. if an onClick is specified, then assume a toggle uses onClick to get the backgroundColor (see below).  Otherwise, assume a transparent background
-            color: Colors.WHITE, isSystem: true, dontUndo: true, 
+            color: Colors.WHITE, isSystem: true, //dontUndo: true, 
             _nativeWidth: params.width ?? 30, _width: params.width ?? 30,
             _height: 30, _nativeHeight: 30, linearBtnWidth: params.linearBtnWidth,
             toolType: params.toolType, expertMode: params.expertMode,
@@ -727,14 +727,14 @@ export class CurrentUserUtils {
 
     /// Initializes all the default buttons for the top bar context menu
     static setupContextMenuButtons(doc: Doc, field="myContextMenuBtns") {
-        const reqdCtxtOpts:DocumentOptions = { title: "context menu buttons", flexGap: 0, childDropAction: 'embed', childDontRegisterViews: true,  linearViewIsExpanded: true, ignoreClick: true, linearViewExpandable: false, _height: 35 };
+        const reqdCtxtOpts:DocumentOptions = { title: "context menu buttons", dontUndo:true, flexGap: 0, childDropAction: 'embed', childDontRegisterViews: true,  linearViewIsExpanded: true, ignoreClick: true, linearViewExpandable: false, _height: 35 };
         const ctxtMenuBtnsDoc = DocUtils.AssignDocField(doc, field, (opts, items) => this.linearButtonList(opts, items??[]), reqdCtxtOpts, undefined);
         const ctxtMenuBtns = CurrentUserUtils.contextMenuTools().map(params => {
             const menuBtnDoc = DocListCast(ctxtMenuBtnsDoc?.data).find(doc => doc.title === params.title);
             if (!params.subMenu) {
                 return this.setupContextMenuButton(params, menuBtnDoc);
             } else {
-                const reqdSubMenuOpts = { ...OmitKeys(params, ["scripts", "funcs", "subMenu"]).omit,
+                const reqdSubMenuOpts = { ...OmitKeys(params, ["scripts", "funcs", "subMenu"]).omit, dontUndo: true,
                     childDontRegisterViews: true, flexGap: 0, _height: 30, ignoreClick: params.scripts?.onClick ? false : true,
                     linearViewSubMenu: true, linearViewExpandable: true, };
                 const items = params.subMenu?.map(sub =>
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 070e0f918..e3798233e 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -263,7 +263,7 @@ export namespace DragManager {
 
     // drags a column from a schema view
     export function StartColumnDrag(ele: HTMLElement[], dragData: ColumnDragData, downX: number, downY: number, options?: DragOptions) {
-        StartDrag(ele, dragData, downX, downY, options);
+        StartDrag(ele, dragData, downX, downY, options, undefined, 'Drag Column');
     }
 
     export function SetSnapLines(horizLines: number[], vertLines: number[]) {
@@ -325,10 +325,10 @@ export namespace DragManager {
     export let docsBeingDragged: Doc[] = observable([] as Doc[]);
     export let CanEmbed = false;
     export let DocDragData: DocumentDragData | undefined;
-    export function StartDrag(eles: HTMLElement[], dragData: { [id: string]: any }, downX: number, downY: number, options?: DragOptions, finishDrag?: (dropData: DragCompleteEvent) => void) {
+    export function StartDrag(eles: HTMLElement[], dragData: { [id: string]: any }, downX: number, downY: number, options?: DragOptions, finishDrag?: (dropData: DragCompleteEvent) => void, dragUndoName?:string) {
         if (dragData.dropAction === 'none') return;
         DocDragData = dragData as DocumentDragData;
-        const batch = UndoManager.StartBatch('dragging');
+        const batch = UndoManager.StartBatch(dragUndoName ?? 'document drag');
         eles = eles.filter(e => e);
         CanEmbed = dragData.canEmbed || false;
         if (!dragDiv) {
diff --git a/src/client/util/LinkFollower.ts b/src/client/util/LinkFollower.ts
index 2812d6c88..f74409e42 100644
--- a/src/client/util/LinkFollower.ts
+++ b/src/client/util/LinkFollower.ts
@@ -28,7 +28,7 @@ export class LinkFollower {
     // if the target isn't onscreen, then it will open up the target in the lightbox, or in place
     // depending on the followLinkLocation property of the source (or the link itself as a fallback);
     public static FollowLink = (linkDoc: Opt<Doc>, sourceDoc: Doc, altKey: boolean) => {
-        const batch = UndoManager.StartBatch('follow link click');
+        const batch = UndoManager.StartBatch('Follow Link');
         runInAction(() => (LinkFollower.IsFollowing = true)); // turn off decoration bounds while following links since animations may occur, and DocDecorations is based on screenToLocal which is not always an observable value
         LinkFollower.traverseLink(
             linkDoc,
diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts
index fba0a4f76..0125331ec 100644
--- a/src/client/util/SelectionManager.ts
+++ b/src/client/util/SelectionManager.ts
@@ -118,6 +118,6 @@ ScriptingGlobals.add(function SelectionManager_selectedDocType(type: string, exp
     if (type === 'tab') {
         return SelectionManager.Views().lastElement()?.props.renderDepth === 0;
     }
-    let selected = (sel => (checkContext ? DocCast(sel?.context) : sel))(SelectionManager.SelectedSchemaDoc() ?? SelectionManager.Docs().lastElement());
+    let selected = (sel => (checkContext ? DocCast(sel?.embedContainer) : sel))(SelectionManager.SelectedSchemaDoc() ?? SelectionManager.Docs().lastElement());
     return selected?.type === type || selected?.type_collection === type || !type;
 });
diff --git a/src/client/util/UndoManager.ts b/src/client/util/UndoManager.ts
index d0aec45a6..6fef9d660 100644
--- a/src/client/util/UndoManager.ts
+++ b/src/client/util/UndoManager.ts
@@ -3,7 +3,7 @@ import { Without } from '../../Utils';
 
 function getBatchName(target: any, key: string | symbol): string {
     const keyName = key.toString();
-    if (target && target.constructor && target.constructor.name) {
+    if (target?.constructor?.name) {
         return `${target.constructor.name}.${keyName}`;
     }
     return keyName;
@@ -34,6 +34,17 @@ function propertyDecorator(target: any, key: string | symbol) {
     });
 }
 
+export function undoable(fn: (...args: any[]) => any, batchName: string): (...args: any[]) => any {
+    return function () {
+        const batch = UndoManager.StartBatch(batchName);
+        try {
+            return fn.apply(undefined, arguments as any);
+        } finally {
+            batch.end();
+        }
+    };
+}
+
 export function undoBatch(target: any, key: string | symbol, descriptor?: TypedPropertyDescriptor<any>): any;
 export function undoBatch(fn: (...args: any[]) => any): (...args: any[]) => any;
 export function undoBatch(target: any, key?: string | symbol, descriptor?: TypedPropertyDescriptor<any>): any {
@@ -73,15 +84,18 @@ export namespace UndoManager {
     }
     type UndoBatch = UndoEvent[];
 
+    export let undoStackNames: string[] = observable([]);
+    export let redoStackNames: string[] = observable([]);
     export let undoStack: UndoBatch[] = observable([]);
     export let redoStack: UndoBatch[] = observable([]);
     let currentBatch: UndoBatch | undefined;
-    export let batchCounter = 0;
+    export let batchCounter = observable.box(0);
     let undoing = false;
     let tempEvents: UndoEvent[] | undefined = undefined;
 
-    export function AddEvent(event: UndoEvent): void {
-        if (currentBatch && batchCounter && !undoing) {
+    export function AddEvent(event: UndoEvent, value?: any): void {
+        if (currentBatch && batchCounter.get() && !undoing) {
+            console.log('                         '.slice(0, batchCounter.get()) + 'UndoEvent : ' + event.prop + ' = ' + value);
             currentBatch.push(event);
             tempEvents?.push(event);
         }
@@ -135,11 +149,13 @@ export namespace UndoManager {
 
         private dispose = (cancel: boolean) => {
             if (this.disposed) {
-                throw new Error('Cannot dispose an already disposed batch');
+                console.log('WARNING: undo batch already disposed');
+                return false;
+            } else {
+                this.disposed = true;
+                openBatches.splice(openBatches.indexOf(this));
+                return EndBatch(this.batchName, cancel);
             }
-            this.disposed = true;
-            openBatches.splice(openBatches.indexOf(this));
-            return EndBatch(cancel);
         };
 
         end = () => this.dispose(false);
@@ -147,22 +163,23 @@ export namespace UndoManager {
     }
 
     export function StartBatch(batchName: string): Batch {
-        // console.log("Start " + batchCounter + " " + batchName);
-        batchCounter++;
-        if (batchCounter > 0 && currentBatch === undefined) {
+        console.log('                          '.slice(0, batchCounter.get()) + 'Start ' + batchCounter + ' ' + batchName);
+        runInAction(() => batchCounter.set(batchCounter.get() + 1));
+        if (currentBatch === undefined) {
             currentBatch = [];
         }
         return new Batch(batchName);
     }
 
-    const EndBatch = action((cancel: boolean = false) => {
-        batchCounter--;
-        // console.log("End " + batchCounter);
-        if (batchCounter === 0 && currentBatch?.length) {
-            // console.log("------ended----")
+    const EndBatch = action((batchName: string, cancel: boolean = false) => {
+        runInAction(() => batchCounter.set(batchCounter.get() - 1));
+        console.log('                          '.slice(0, batchCounter.get()) + 'End ' + batchName + ' (' + currentBatch?.length + ')');
+        if (batchCounter.get() === 0 && currentBatch?.length) {
             if (!cancel) {
                 undoStack.push(currentBatch);
+                undoStackNames.push(batchName ?? '???');
             }
+            redoStackNames.length = 0;
             redoStack.length = 0;
             currentBatch = undefined;
             return true;
@@ -204,6 +221,7 @@ export namespace UndoManager {
             return;
         }
 
+        const names = undoStackNames.pop();
         const commands = undoStack.pop();
         if (!commands) {
             return;
@@ -215,6 +233,7 @@ export namespace UndoManager {
         }
         undoing = false;
 
+        redoStackNames.push(names ?? '???');
         redoStack.push(commands);
     });
 
@@ -223,6 +242,7 @@ export namespace UndoManager {
             return;
         }
 
+        const names = redoStackNames.pop();
         const commands = redoStack.pop();
         if (!commands) {
             return;
@@ -234,6 +254,7 @@ export namespace UndoManager {
         }
         undoing = false;
 
+        undoStackNames.push(names ?? '???');
         undoStack.push(commands);
     });
 }
diff --git a/src/client/views/ContextMenuItem.tsx b/src/client/views/ContextMenuItem.tsx
index e87d2046b..33f250986 100644
--- a/src/client/views/ContextMenuItem.tsx
+++ b/src/client/views/ContextMenuItem.tsx
@@ -40,10 +40,7 @@ export class ContextMenuItem extends React.Component<ContextMenuProps & { select
     handleEvent = async (e: React.MouseEvent<HTMLDivElement>) => {
         if ('event' in this.props) {
             this.props.closeMenu?.();
-            let batch: UndoManager.Batch | undefined;
-            if (this.props.undoable !== false) {
-                batch = UndoManager.StartBatch(`Context menu event: ${this.props.description}`);
-            }
+            const batch = this.props.undoable !== false ? UndoManager.StartBatch(`Click Menu item: ${this.props.description}`) : undefined;
             await this.props.event({ x: e.clientX, y: e.clientY });
             batch?.end();
         }
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 39073d763..8077b9af1 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -2,23 +2,25 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Tooltip } from '@material-ui/core';
 import { IconButton } from 'browndash-components';
-import { action, computed, observable, reaction, runInAction } from 'mobx';
+import { action, computed, observable, reaction } from 'mobx';
 import { observer } from 'mobx-react';
 import { FaUndo } from 'react-icons/fa';
 import { DateField } from '../../fields/DateField';
 import { AclAdmin, AclEdit, DataSym, Doc, DocListCast, Field, HeightSym, WidthSym } from '../../fields/Doc';
-import { Document } from '../../fields/documentSchemas';
 import { InkField } from '../../fields/InkField';
+import { RichTextField } from '../../fields/RichTextField';
 import { ScriptField } from '../../fields/ScriptField';
 import { Cast, NumCast, StrCast } from '../../fields/Types';
 import { GetEffectiveAcl } from '../../fields/util';
 import { aggregateBounds, emptyFunction, numberValue, returnFalse, setupMoveUpEvents, Utils } from '../../Utils';
 import { Docs } from '../documents/Documents';
 import { DocumentType } from '../documents/DocumentTypes';
+import { DocumentManager } from '../util/DocumentManager';
 import { DragManager } from '../util/DragManager';
+import { LinkFollower } from '../util/LinkFollower';
 import { SelectionManager } from '../util/SelectionManager';
 import { SnappingManager } from '../util/SnappingManager';
-import { undoBatch, UndoManager } from '../util/UndoManager';
+import { UndoManager } from '../util/UndoManager';
 import { CollectionDockingView } from './collections/CollectionDockingView';
 import { CollectionFreeFormView } from './collections/collectionFreeForm';
 import { DocumentButtonBar } from './DocumentButtonBar';
@@ -27,15 +29,11 @@ import { Colors } from './global/globalEnums';
 import { InkingStroke } from './InkingStroke';
 import { InkStrokeProperties } from './InkStrokeProperties';
 import { LightboxView } from './LightboxView';
-import { DocumentView, OpenWhere, OpenWhereMod } from './nodes/DocumentView';
+import { DocumentView, OpenWhereMod } from './nodes/DocumentView';
 import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
 import { ImageBox } from './nodes/ImageBox';
 import React = require('react');
-import { RichTextField } from '../../fields/RichTextField';
-import { LinkFollower } from '../util/LinkFollower';
 import _ = require('lodash');
-import { DocumentManager } from '../util/DocumentManager';
-import { isUndefined } from 'lodash';
 
 @observer
 export class DocumentDecorations extends React.Component<{ PanelWidth: number; PanelHeight: number; boundsLeft: number; boundsTop: number }, { value: string }> {
@@ -150,7 +148,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
                             Doc.SetInPlace(d.rootDoc, titleFieldKey, titleField, true);
                         }
                     }),
-                'title blur'
+                'edit title'
             );
         }
     };
@@ -227,11 +225,6 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
             .filter(v => v && v.props.renderDepth > 0);
         if (forceDeleteOrIconify === false && this._iconifyBatch) return;
         this._deleteAfterIconify = forceDeleteOrIconify || this._iconifyBatch ? true : false;
-        if (!this._iconifyBatch) {
-            this._iconifyBatch = UndoManager.StartBatch('iconifying');
-        } else {
-            forceDeleteOrIconify = false; // can't force immediate close in the middle of iconifying -- have to wait until iconifying completes
-        }
         var iconifyingCount = views.length;
         const finished = action((force?: boolean) => {
             if ((force || --iconifyingCount === 0) && this._iconifyBatch) {
@@ -250,6 +243,12 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
                 this._iconifyBatch = undefined;
             }
         });
+        if (!this._iconifyBatch) {
+            this._iconifyBatch = UndoManager.StartBatch(forceDeleteOrIconify ? 'delete selected docs' : 'iconifying');
+        } else {
+            forceDeleteOrIconify = false; // can't force immediate close in the middle of iconifying -- have to wait until iconifying completes
+        }
+
         if (forceDeleteOrIconify) finished(forceDeleteOrIconify);
         else if (!this._deleteAfterIconify) views.forEach(dv => dv.iconify(finished));
     };
@@ -397,7 +396,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
     onRotateDown = (e: React.PointerEvent): void => {
         this._isRotating = true;
         const rcScreen = { X: this.rotCenter[0], Y: this.rotCenter[1] };
-        const rotateUndo = UndoManager.StartBatch('rotatedown');
+        const rotateUndo = UndoManager.StartBatch('drag rotation');
         const selectedInk = SelectionManager.Views().filter(i => i.ComponentView instanceof InkingStroke);
         const centerPoint = this.rotCenter.slice();
         const infos = new Map<Doc, { unrotatedDocPos: { x: number; y: number }; startRotCtr: { x: number; y: number }; accumRot: number }>();
@@ -465,7 +464,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
         const bounds = e.currentTarget.getBoundingClientRect();
         this._offX = this._resizeHdlId.toLowerCase().includes('left') ? bounds.right - e.clientX : bounds.left - e.clientX;
         this._offY = this._resizeHdlId.toLowerCase().includes('top') ? bounds.bottom - e.clientY : bounds.top - e.clientY;
-        this._resizeUndo = UndoManager.StartBatch('DocDecs resize');
+        this._resizeUndo = UndoManager.StartBatch('drag resizing');
         this._snapX = e.pageX;
         this._snapY = e.pageY;
         const ffviewSet = new Set<CollectionFreeFormView>();
@@ -770,20 +769,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
 
         const topBtn = (key: string, icon: string, pointerDown: undefined | ((e: React.PointerEvent) => void), click: undefined | ((e: any) => void), title: string) => (
             <Tooltip key={key} title={<div className="dash-tooltip">{title}</div>} placement="top">
-                <div
-                    className={`documentDecorations-${key}Button`}
-                    onContextMenu={e => e.preventDefault()}
-                    onPointerDown={
-                        pointerDown ??
-                        (e =>
-                            setupMoveUpEvents(
-                                this,
-                                e,
-                                returnFalse,
-                                emptyFunction,
-                                undoBatch(e => click!(e))
-                            ))
-                    }>
+                <div className={`documentDecorations-${key}Button`} onContextMenu={e => e.preventDefault()} onPointerDown={pointerDown ?? (e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, e => click!(e)))}>
                     <FontAwesomeIcon icon={icon as any} />
                 </div>
             </Tooltip>
diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx
index 6b4132814..7043edcee 100644
--- a/src/client/views/EditableView.tsx
+++ b/src/client/views/EditableView.tsx
@@ -156,12 +156,14 @@ export class EditableView extends React.Component<EditableProps> {
                 break;
             case ':':
                 if (this.props.menuCallback) {
+                    e.stopPropagation();
                     this.props.menuCallback(e.currentTarget.getBoundingClientRect().x, e.currentTarget.getBoundingClientRect().y);
                     break;
                 }
 
             default:
                 if (this.props.textCallback?.(e.key)) {
+                    e.stopPropagation();
                     this._editing = false;
                     this.props.isEditingCallback?.(false);
                 }
diff --git a/src/client/views/FilterPanel.tsx b/src/client/views/FilterPanel.tsx
index 75e0e7c4c..53c1f1018 100644
--- a/src/client/views/FilterPanel.tsx
+++ b/src/client/views/FilterPanel.tsx
@@ -10,6 +10,7 @@ import { UserOptions } from '../util/GroupManager';
 import './FilterPanel.scss';
 import { FieldView } from './nodes/FieldView';
 import { SearchBox } from './search/SearchBox';
+import { undoable } from '../util/UndoManager';
 
 interface filterProps {
     rootDoc: Doc;
@@ -31,7 +32,7 @@ export class FilterPanel extends React.Component<filterProps> {
         return targetView?.ComponentView?.annotationKey ?? targetView?.ComponentView?.fieldKey ?? 'data';
     }
     @computed get targetDocChildren() {
-        return DocListCast(this.targetDoc?.[this.targetDocChildKey] || Doc.ActiveDashboard?.data);
+        return [...DocListCast(this.targetDoc?.[this.targetDocChildKey] || Doc.ActiveDashboard?.data), ...DocListCast(this.targetDoc[Doc.LayoutFieldKey(this.targetDoc) + '_sidebar'])];
     }
 
     @computed get allDocs() {
@@ -89,6 +90,7 @@ export class FilterPanel extends React.Component<filterProps> {
                     const fieldKey = Doc.LayoutFieldKey(t);
                     const annos = !Field.toString(Doc.LayoutField(t) as Field).includes('CollectionView');
                     DocListCast(t[annos ? fieldKey + '_annotations' : fieldKey]).forEach(newdoc => newarray.push(newdoc));
+                    annos && DocListCast(t[fieldKey + '_sidebar']).forEach(newdoc => newarray.push(newdoc));
                 });
                 subDocs = newarray;
             }
@@ -204,7 +206,8 @@ export class FilterPanel extends React.Component<filterProps> {
                                 .find(filter => filter.split(':')[0] === facetHeader)
                                 ?.split(':')[1] ?? '-empty-'
                         }
-                        onKeyDown={e => e.key === 'Enter' && Doc.setDocFilter(this.targetDoc, facetHeader, e.currentTarget.value, !e.currentTarget.value ? 'remove' : 'match')}
+                        onBlur={undoable(e => Doc.setDocFilter(this.targetDoc, facetHeader, e.currentTarget.value, !e.currentTarget.value ? 'remove' : 'match'), 'set text filter')}
+                        onKeyDown={e => e.key === 'Enter' && undoable(e => Doc.setDocFilter(this.targetDoc, facetHeader, e.currentTarget.value, !e.currentTarget.value ? 'remove' : 'match'), 'set text filter')(e)}
                     />
                 );
             case 'checkbox':
@@ -220,7 +223,7 @@ export class FilterPanel extends React.Component<filterProps> {
                                         ?.split(':')[2] === 'check'
                                 }
                                 type={type}
-                                onChange={e => Doc.setDocFilter(this.targetDoc, facetHeader, fval, e.target.checked ? 'check' : 'remove')}
+                                onChange={undoable(e => Doc.setDocFilter(this.targetDoc, facetHeader, fval, e.target.checked ? 'check' : 'remove'), 'set filter')}
                             />
                             {facetValue}
                         </div>
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index b9b92dd2b..625bc760d 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -161,12 +161,10 @@ export class KeyManager {
             case 'delete':
             case 'backspace':
                 if (document.activeElement?.tagName !== 'INPUT' && document.activeElement?.tagName !== 'TEXTAREA') {
-                    UndoManager.RunInBatch(() => {
-                        if (LightboxView.LightboxDoc) {
-                            LightboxView.SetLightboxDoc(undefined);
-                            SelectionManager.DeselectAll();
-                        } else DocumentDecorations.Instance.onCloseClick(true);
-                    }, 'backspace');
+                    if (LightboxView.LightboxDoc) {
+                        LightboxView.SetLightboxDoc(undefined);
+                        SelectionManager.DeselectAll();
+                    } else DocumentDecorations.Instance.onCloseClick(true);
                     return { stopPropagation: true, preventDefault: true };
                 }
                 break;
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 853f9cace..50f451c0a 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -915,11 +915,11 @@ export class MainView extends React.Component {
         return !SelectionManager.Views().some(dv => dv.rootDoc.freeform_snapLines) ? null : (
             <div className="mainView-snapLines">
                 <svg style={{ width: '100%', height: '100%' }}>
-                    {SnappingManager.horizSnapLines().map(l => (
-                        <line x1="0" y1={l} x2="2000" y2={l} stroke="black" opacity={0.3} strokeWidth={0.5} strokeDasharray={'1 1'} />
+                    {SnappingManager.horizSnapLines().map((l, i) => (
+                        <line key={i} x1="0" y1={l} x2="2000" y2={l} stroke="black" opacity={0.3} strokeWidth={0.5} strokeDasharray={'1 1'} />
                     ))}
-                    {SnappingManager.vertSnapLines().map(l => (
-                        <line y1="0" x1={l} y2="2000" x2={l} stroke="black" opacity={0.3} strokeWidth={0.5} strokeDasharray={'1 1'} />
+                    {SnappingManager.vertSnapLines().map((l, i) => (
+                        <line key={i} y1="0" x1={l} y2="2000" x2={l} stroke="black" opacity={0.3} strokeWidth={0.5} strokeDasharray={'1 1'} />
                     ))}
                 </svg>
             </div>
diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx
index a37447505..a5c58c9d2 100644
--- a/src/client/views/PropertiesButtons.tsx
+++ b/src/client/views/PropertiesButtons.tsx
@@ -14,7 +14,7 @@ import { DocUtils } from '../documents/Documents';
 import { CollectionViewType, DocumentType } from '../documents/DocumentTypes';
 import { LinkManager } from '../util/LinkManager';
 import { SelectionManager } from '../util/SelectionManager';
-import { undoBatch } from '../util/UndoManager';
+import { undoable, undoBatch } from '../util/UndoManager';
 import { Colors } from './global/globalEnums';
 import { InkingStroke } from './InkingStroke';
 import { DocumentView, OpenWhere } from './nodes/DocumentView';
@@ -51,11 +51,11 @@ export class PropertiesButtons extends React.Component<{}, {}> {
                     <div
                         className={`propertiesButtons-linkButton-empty toggle-${StrCast(targetDoc[property]).includes(':hover') ? 'hover' : targetDoc[property] ? 'on' : 'off'}`}
                         onPointerDown={e => e.stopPropagation()}
-                        onClick={undoBatch(() => {
+                        onClick={undoable(() => {
                             if (SelectionManager.Views().length > 1) {
                                 SelectionManager.Views().forEach(dv => (onClick ?? onPropToggle)(dv, dv.rootDoc, property));
                             } else if (targetDoc) (onClick ?? onPropToggle)(undefined, targetDoc, property);
-                        })}>
+                        }, property)}>
                         <FontAwesomeIcon className="documentdecorations-icon" size="lg" icon={icon(BoolCast(targetDoc?.[property])) as any} />
                     </div>
                     <div className="propertiesButtons-title">{label}</div>
diff --git a/src/client/views/PropertiesDocBacklinksSelector.tsx b/src/client/views/PropertiesDocBacklinksSelector.tsx
index 46e6fd188..7b21629da 100644
--- a/src/client/views/PropertiesDocBacklinksSelector.tsx
+++ b/src/client/views/PropertiesDocBacklinksSelector.tsx
@@ -25,7 +25,7 @@ export class PropertiesDocBacklinksSelector extends React.Component<PropertiesDo
 
         if (otherdoc) {
             otherdoc.hidden = false;
-            this.props.addDocTab(Doc.IsDocDataProto(otherdoc) ? Doc.MakeDelegate(otherdoc) : otherdoc, OpenWhere.toggleRight);
+            this.props.addDocTab(Doc.IsDataProto(otherdoc) ? Doc.MakeDelegate(otherdoc) : otherdoc, OpenWhere.toggleRight);
         }
     };
 
diff --git a/src/client/views/PropertiesDocContextSelector.tsx b/src/client/views/PropertiesDocContextSelector.tsx
index 12a09196e..e1279c9a7 100644
--- a/src/client/views/PropertiesDocContextSelector.tsx
+++ b/src/client/views/PropertiesDocContextSelector.tsx
@@ -48,7 +48,7 @@ export class PropertiesDocContextSelector extends React.Component<PropertiesDocC
 
     getOnClick = (col: Doc, target: Doc) => {
         if (!this.props.DocView) return;
-        col = Doc.IsDocDataProto(col) ? Doc.MakeDelegate(col) : col;
+        col = Doc.IsDataProto(col) ? Doc.MakeDelegate(col) : col;
         DocFocusOrOpen(Doc.GetProto(this.props.DocView.props.Document), undefined, col);
     };
 
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index 19c138a21..297820e37 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -21,7 +21,7 @@ import { LinkManager } from '../util/LinkManager';
 import { SelectionManager } from '../util/SelectionManager';
 import { SharingManager } from '../util/SharingManager';
 import { Transform } from '../util/Transform';
-import { undoBatch, UndoManager } from '../util/UndoManager';
+import { undoable, undoBatch, UndoManager } from '../util/UndoManager';
 import { EditableView } from './EditableView';
 import { FilterPanel } from './FilterPanel';
 import { Colors } from './global/globalEnums';
@@ -538,6 +538,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
                     className="inputBox-input"
                     type="text"
                     value={value}
+                    readOnly={true}
                     onChange={e => {
                         setter(e.target.value);
                     }}
@@ -656,21 +657,21 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
         return this.inputBoxDuo(
             'hgt',
             this.shapeHgt,
-            (val: string) => {
+            undoable((val: string) => {
                 if (!isNaN(Number(val))) {
                     this.shapeHgt = val;
                 }
                 return true;
-            },
+            }, 'set height'),
             'H:',
             'wid',
             this.shapeWid,
-            (val: string) => {
+            undoable((val: string) => {
                 if (!isNaN(Number(val))) {
                     this.shapeWid = val;
                 }
                 return true;
-            },
+            }, 'set width'),
             'W:'
         );
     }
@@ -678,21 +679,21 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
         return this.inputBoxDuo(
             'Xps',
             this.shapeXps,
-            (val: string) => {
+            undoable((val: string) => {
                 if (val !== '0' && !isNaN(Number(val))) {
                     this.shapeXps = val;
                 }
                 return true;
-            },
+            }, 'set x coord'),
             'X:',
             'Yps',
             this.shapeYps,
-            (val: string) => {
+            undoable((val: string) => {
                 if (val !== '0' && !isNaN(Number(val))) {
                     this.shapeYps = val;
                 }
                 return true;
-            },
+            }, 'set y coord'),
             'Y:'
         );
     }
@@ -867,7 +868,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
     regInput = (key: string, value: any, setter: (val: string) => {}) => {
         return (
             <div className="inputBox">
-                <input className="inputBox-input" type="text" value={value} onChange={e => setter(e.target.value)} />
+                <input className="inputBox-input" type="text" readOnly={true} value={value} onChange={e => setter(e.target.value)} />
                 <div className="inputBox-button">
                     <div className="inputBox-button-up" key="up2" onPointerDown={undoBatch(action(() => this.upDownButtons('up', key)))}>
                         <FontAwesomeIcon icon="caret-up" color="white" size="sm" />
@@ -1228,8 +1229,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
         }
     });
 
-    @undoBatch
-    changeFollowBehavior = action((follow: Opt<string>) => this.sourceAnchor && (this.sourceAnchor.followLinkLocation = follow));
+    changeFollowBehavior = undoable((loc: Opt<string>) => this.sourceAnchor && (this.sourceAnchor.followLinkLocation = loc), 'change follow behavior');
 
     @undoBatch
     changeAnimationBehavior = action((behavior: string) => this.sourceAnchor && (this.sourceAnchor.followLinkAnimEffect = behavior));
@@ -1315,6 +1315,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
                 autoComplete={'off'}
                 id="link_relationship_input"
                 value={StrCast(LinkManager.currentLink?.link_relationship)}
+                readOnly={true}
                 onKeyDown={this.onRelationshipKey}
                 onBlur={this.onSelectOutRelationship}
                 onChange={e => this.handlelinkRelationshipChange(e.currentTarget.value)}
@@ -1332,6 +1333,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
                 style={{ textAlign: 'left' }}
                 id="link_description_input"
                 value={Field.toString(LinkManager.currentLink?.link_description as any as Field)}
+                readOnly={true}
                 onKeyDown={this.onDescriptionKey}
                 onBlur={this.onSelectOutDesc}
                 onChange={e => this.handleDescriptionChange(e.currentTarget.value)}
@@ -1457,7 +1459,9 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
                                             <select style={{ width: '100%', gridColumn: 2 }} onChange={e => this.changeAnimationBehavior(e.currentTarget.value)} value={StrCast(this.sourceAnchor?.followLinkAnimEffect, 'default')}>
                                                 <option value="default">Default</option>
                                                 {[PresEffect.None, PresEffect.Zoom, PresEffect.Lightspeed, PresEffect.Fade, PresEffect.Flip, PresEffect.Rotate, PresEffect.Bounce, PresEffect.Roll].map(effect => (
-                                                    <option value={effect.toString()}>{effect.toString()}</option>
+                                                    <option key={effect.toString()} value={effect.toString()}>
+                                                        {effect.toString()}
+                                                    </option>
                                                 ))}
                                             </select>
                                             <div className="effectDirection" style={{ marginLeft: '10px', display: 'grid', width: 40, height: 36, gridColumn: 3, gridTemplateRows: '12px 12px 12px' }}>
@@ -1567,7 +1571,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
                                         <div className="propertiesView-input inline" style={{ display: 'grid', gridTemplateColumns: '78px calc(100% - 108px) 50px' }}>
                                             <p>Zoom %</p>
                                             <div className="ribbon-property" style={{ display: !targZoom ? 'none' : 'inline-flex' }}>
-                                                <input className="presBox-input" style={{ width: '100%' }} type="number" value={zoom} />
+                                                <input className="presBox-input" style={{ width: '100%' }} readOnly={true} type="number" value={zoom} />
                                                 <div className="ribbon-propertyUpDown" style={{ display: 'flex', flexDirection: 'column' }}>
                                                     <div className="ribbon-propertyUpDownItem" onClick={undoBatch(() => this.setZoom(String(zoom), 0.1))}>
                                                         <FontAwesomeIcon icon={'caret-up'} />
diff --git a/src/client/views/SidebarAnnos.scss b/src/client/views/SidebarAnnos.scss
index a0506cb3a..d7de2b641 100644
--- a/src/client/views/SidebarAnnos.scss
+++ b/src/client/views/SidebarAnnos.scss
@@ -4,22 +4,26 @@
     overflow: auto;
     flex-flow: row;
     flex-wrap: wrap;
-    .sidebarAnnos-filterTag, .sidebarAnnos-filterTag-active,
-    .sidebarAnnos-filterUser, .sidebarAnnos-filterUser-active {
+    .sidebarAnnos-filterTag,
+    .sidebarAnnos-filterTag-active,
+    .sidebarAnnos-filterUser,
+    .sidebarAnnos-filterUser-active {
         font-weight: bold;
         font-size: 10px;
         padding-left: 5;
         padding-right: 5;
         box-shadow: black 1px 1px 3px;
         border-radius: 5;
-        margin: 2; 
+        margin: 2;
         height: 15;
         background-color: lightgrey;
     }
-    .sidebarAnnos-filterUser, .sidebarAnnos-filterUser-active {
+    .sidebarAnnos-filterUser,
+    .sidebarAnnos-filterUser-active {
         border-radius: 15px;
     }
+    .sidebarAnnos-filterUser-active,
     .sidebarAnnos-filterTag-active {
         background-color: white;
     }
-}
\ No newline at end of file
+}
diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx
index e12621f35..c9e52a1db 100644
--- a/src/client/views/SidebarAnnos.tsx
+++ b/src/client/views/SidebarAnnos.tsx
@@ -49,13 +49,21 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
         );
         return keys;
     }
+    @computed get allHashtags() {
+        const keys = new Set<string>();
+        DocListCast(this.props.rootDoc[this.sidebarKey]).forEach(doc => StrListCast(doc.tags).forEach(tag => keys.add(tag)));
+        return Array.from(keys.keys())
+            .filter(key => key[0])
+            .filter(key => !key.startsWith('_') && (key[0] === '#' || key[0] === key[0].toUpperCase()))
+            .sort();
+    }
     @computed get allUsers() {
         const keys = new Set<string>();
         DocListCast(this.props.rootDoc[this.sidebarKey]).forEach(doc => keys.add(StrCast(doc.author)));
         return Array.from(keys.keys()).sort();
     }
     get filtersKey() {
-        return '_' + this.sidebarKey + '-docFilters';
+        return '_' + this.sidebarKey + '_docFilters';
     }
 
     anchorMenuClick = (anchor: Doc) => {
@@ -179,9 +187,9 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
     };
     render() {
         const renderTag = (tag: string) => {
-            const active = StrListCast(this.props.rootDoc[this.filtersKey]).includes(`${tag}:${tag}:check`);
+            const active = StrListCast(this.props.rootDoc[this.filtersKey]).includes(`tags:${tag}:check`);
             return (
-                <div key={tag} className={`sidebarAnnos-filterTag${active ? '-active' : ''}`} onClick={e => Doc.setDocFilter(this.props.rootDoc, tag, tag, 'check', true, this.sidebarKey, e.shiftKey)}>
+                <div key={tag} className={`sidebarAnnos-filterTag${active ? '-active' : ''}`} onClick={e => Doc.setDocFilter(this.props.rootDoc, 'tags', tag, 'check', true, this.sidebarKey, e.shiftKey)}>
                     {tag}
                 </div>
             );
@@ -216,6 +224,7 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
                 }}>
                 <div className="sidebarAnnos-tagList" style={{ height: this.filtersHeight() }} onWheel={e => e.stopPropagation()}>
                     {this.allUsers.map(renderUsers)}
+                    {this.allHashtags.map(renderTag)}
                     {Array.from(this.allMetadata.keys())
                         .sort()
                         .map(key => renderMeta(key, this.allMetadata.get(key)))}
diff --git a/src/client/views/UndoStack.tsx b/src/client/views/UndoStack.tsx
new file mode 100644
index 000000000..01e184d6b
--- /dev/null
+++ b/src/client/views/UndoStack.tsx
@@ -0,0 +1,30 @@
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import { UndoManager } from '../util/UndoManager';
+import './ScriptingRepl.scss';
+
+@observer
+export class UndoStack extends React.Component {
+    render() {
+        return (
+            <div className="scriptingRepl-outerContainer">
+                <div className="scriptingRepl-commandsContainer" ref={r => r?.scroll({ behavior: 'auto', top: r?.scrollHeight + 20 })} style={{ background: UndoManager.batchCounter.get() ? 'yellow' : undefined }}>
+                    {UndoManager.undoStackNames.map((name, i) => (
+                        <div className="scriptingRepl-resultContainer" key={i}>
+                            <div className="scriptingRepl-commandString">{name.replace(/[^\.]*\./, '')}</div>
+                        </div>
+                    ))}
+                    {Array.from(UndoManager.redoStackNames)
+                        .reverse()
+                        .map((name, i) => (
+                            <div className="scriptingRepl-resultContainer" key={i}>
+                                <div className="scriptingRepl-commandString" style={{ fontWeight: 'bold', color: 'red' }}>
+                                    {name.replace(/[^\.]*\./, '')}
+                                </div>
+                            </div>
+                        ))}
+                </div>
+            </div>
+        );
+    }
+}
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index cce21a3aa..4ae24af60 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -6,7 +6,7 @@ import { Doc, DocListCast, Opt } from '../../../fields/Doc';
 import { Id } from '../../../fields/FieldSymbols';
 import { InkTool } from '../../../fields/InkField';
 import { List } from '../../../fields/List';
-import { Cast, ImageCast, NumCast, StrCast } from '../../../fields/Types';
+import { ImageCast, NumCast, StrCast } from '../../../fields/Types';
 import { ImageField } from '../../../fields/URLField';
 import { inheritParentAcls } from '../../../fields/util';
 import { emptyFunction, incrementTitleCopy } from '../../../Utils';
@@ -20,14 +20,14 @@ import { SelectionManager } from '../../util/SelectionManager';
 import { undoBatch, UndoManager } from '../../util/UndoManager';
 import { DashboardView } from '../DashboardView';
 import { LightboxView } from '../LightboxView';
+import { OpenWhere, OpenWhereMod } from '../nodes/DocumentView';
+import { OverlayView } from '../OverlayView';
+import { ScriptingRepl } from '../ScriptingRepl';
 import './CollectionDockingView.scss';
 import { CollectionFreeFormView } from './collectionFreeForm';
 import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView';
 import { TabDocView } from './TabDocView';
 import React = require('react');
-import { OpenWhere, OpenWhereMod } from '../nodes/DocumentView';
-import { OverlayView } from '../OverlayView';
-import { ScriptingRepl } from '../ScriptingRepl';
 const _global = (window /* browser */ || global) /* node */ as any;
 
 @observer
@@ -85,6 +85,7 @@ export class CollectionDockingView extends CollectionSubView() {
 
     tabItemDropped = () => DragManager.CompleteWindowDrag?.(false);
     tabDragStart = (proxy: any, finishDrag?: (aborted: boolean) => void) => {
+        this._flush = this._flush ?? UndoManager.StartBatch('tab move');
         const dashDoc = proxy?._contentItem?.tab?.DashDoc as Doc;
         dashDoc && (DragManager.DocDragData = new DragManager.DocumentDragData([proxy._contentItem.tab.DashDoc]));
         DragManager.CompleteWindowDrag = (aborted: boolean) => {
@@ -92,12 +93,12 @@ export class CollectionDockingView extends CollectionSubView() {
                 proxy._dragListener.AbortDrag();
                 if (this._flush) {
                     this._flush.cancel(); // cancel the undo change being logged
-                    this._flush = undefined;
                     this.setupGoldenLayout(); // restore golden layout to where it was before the drag (this is a no-op when using StartOtherDrag because the proxy dragged item was never in the golden layout)
                 }
                 DragManager.CompleteWindowDrag = undefined;
             }
             finishDrag?.(aborted);
+            setTimeout(this.endUndoBatch, 100);
         };
     };
     @undoBatch
@@ -180,7 +181,6 @@ export class CollectionDockingView extends CollectionSubView() {
     //
     //  Creates a split on any side of the docking view based on the passed input pullSide and then adds the Document to the requested side
     //
-    @undoBatch
     @action
     public static AddSplit(document: Doc, pullSide: OpenWhereMod, stack?: any, panelName?: string, keyValue?: boolean) {
         if (document?._type_collection === CollectionViewType.Docking) return DashboardView.openDashboard(document);
@@ -195,6 +195,8 @@ export class CollectionDockingView extends CollectionSubView() {
         if (!instance) return false;
         const docContentConfig = CollectionDockingView.makeDocumentConfig(document, panelName, undefined, keyValue);
 
+        CollectionDockingView.Instance._flush = CollectionDockingView.Instance._flush ?? UndoManager.StartBatch('Add Split');
+        setTimeout(CollectionDockingView.Instance.endUndoBatch, 100);
         if (!pullSide && stack) {
             stack.addChild(docContentConfig, undefined);
             setTimeout(() => stack.setActiveContentItem(stack.contentItems[stack.contentItems.length - 1]));
@@ -370,16 +372,30 @@ export class CollectionDockingView extends CollectionSubView() {
         !LightboxView.LightboxDoc && cur && this._goldenLayout?.updateSize(cur.getBoundingClientRect().width, cur.getBoundingClientRect().height);
     };
 
+    endUndoBatch = () => {
+        const json = JSON.stringify(this._goldenLayout.toConfig());
+        const matches = json.match(/\"documentId\":\"[a-z0-9-]+\"/g);
+        const docids = matches?.map(m => m.replace('"documentId":"', '').replace('"', ''));
+        const docs = !docids
+            ? []
+            : docids
+                  .map(id => DocServer.GetCachedRefField(id))
+                  .filter(f => f)
+                  .map(f => f as Doc);
+        const changesMade = this.props.Document.dockingConfig !== json;
+        if (changesMade) {
+            this.props.Document.dockingConfig = json;
+            this.props.Document.data = new List<Doc>(docs);
+        }
+        this._flush?.end();
+        this._flush = undefined;
+    };
+
     @action
     onPointerUp = (e: MouseEvent): void => {
         window.removeEventListener('pointerup', this.onPointerUp);
-        const flush = this._flush;
-        this._flush = undefined;
-        if (flush) {
-            DragManager.CompleteWindowDrag = undefined;
-            if (!this.stateChanged()) flush.cancel();
-            else flush.end();
-        }
+        DragManager.CompleteWindowDrag = undefined;
+        setTimeout(this.endUndoBatch, 100);
     };
 
     @action
@@ -393,10 +409,8 @@ export class CollectionDockingView extends CollectionSubView() {
             window.addEventListener('mouseup', this.onPointerUp);
             if (!htmlTarget.closest('*.lm_content') && (htmlTarget.closest('*.lm_tab') || htmlTarget.closest('*.lm_stack'))) {
                 const className = typeof htmlTarget.className === 'string' ? htmlTarget.className : '';
-                if (!className.includes('lm_close') && !className.includes('lm_maximise')) {
-                    this._flush = UndoManager.StartBatch('golden layout edit');
-                    DocServer.UPDATE_SERVER_CACHE();
-                }
+                if (className.includes('lm_maximise')) this._flush = UndoManager.StartBatch('tab maximize');
+                else if (!className.includes('lm_close')) DocServer.UPDATE_SERVER_CACHE();
             }
         }
         if (!InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) && !InteractionUtils.IsType(e, InteractionUtils.PENTYPE) && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) {
@@ -452,25 +466,12 @@ export class CollectionDockingView extends CollectionSubView() {
     stateChanged = () => {
         this._ignoreStateChange = JSON.stringify(this._goldenLayout.toConfig());
         const json = JSON.stringify(this._goldenLayout.toConfig());
-        const matches = json.match(/\"documentId\":\"[a-z0-9-]+\"/g);
-        const docids = matches?.map(m => m.replace('"documentId":"', '').replace('"', ''));
-        const docs = !docids
-            ? []
-            : docids
-                  .map(id => DocServer.GetCachedRefField(id))
-                  .filter(f => f)
-                  .map(f => f as Doc);
         const changesMade = this.props.Document.dockingConfig !== json;
-        if (changesMade && !this._flush) {
-            UndoManager.RunInBatch(() => {
-                this.props.Document.dockingConfig = json;
-                this.props.Document.data = new List<Doc>(docs);
-            }, 'state changed');
-        }
         return changesMade;
     };
 
     tabDestroyed = (tab: any) => {
+        this._flush = this._flush ?? UndoManager.StartBatch('tab movement');
         if (tab.DashDoc && ![DocumentType.KVP, DocumentType.PRES].includes(tab.DashDoc?.type)) {
             Doc.AddDocToList(Doc.MyHeaderBar, 'data', tab.DashDoc);
             Doc.AddDocToList(Doc.MyRecentlyClosed, 'data', tab.DashDoc, undefined, true, true);
diff --git a/src/client/views/collections/CollectionNoteTakingViewColumn.tsx b/src/client/views/collections/CollectionNoteTakingViewColumn.tsx
index 63becac1e..2f28ecd00 100644
--- a/src/client/views/collections/CollectionNoteTakingViewColumn.tsx
+++ b/src/client/views/collections/CollectionNoteTakingViewColumn.tsx
@@ -121,7 +121,8 @@ export class CollectionNoteTakingViewColumn extends React.Component<CSVFieldColu
 
     @action pointerEntered = () => SnappingManager.GetIsDragging() && (this._background = '#b4b4b4');
     @action pointerLeave = () => (this._background = 'inherit');
-    textCallback = (char: string) => this.addNewTextDoc('-typed text-', false, true);
+    @undoBatch
+    addTextNote = (char: string) => this.addNewTextDoc('-typed text-', false, true);
 
     // addNewTextDoc is called when a user starts typing in a column to create a new node
     @action
@@ -272,7 +273,7 @@ export class CollectionNoteTakingViewColumn extends React.Component<CSVFieldColu
                         {!this.props.chromeHidden ? (
                             <div className="collectionNoteTakingView-DocumentButtons" style={{ marginBottom: 10 }}>
                                 <div key={`${heading}-add-document`} className="collectionNoteTakingView-addDocumentButton">
-                                    <EditableView GetValue={returnEmptyString} SetValue={this.addNewTextDoc} textCallback={this.textCallback} placeholder={"Type ':' for commands"} contents={'+ New Node'} menuCallback={this.menuCallback} />
+                                    <EditableView GetValue={returnEmptyString} SetValue={this.addNewTextDoc} textCallback={this.addTextNote} placeholder={"Type ':' for commands"} contents={'+ New Node'} menuCallback={this.menuCallback} />
                                 </div>
                                 <div key={`${this.props.Document[Id]}-addGroup`} className="collectionNoteTakingView-addDocumentButton">
                                     <EditableView {...this.props.editableViewProps()} />
diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx
index 6b4c8a3e9..b131d38d8 100644
--- a/src/client/views/collections/CollectionStackedTimeline.tsx
+++ b/src/client/views/collections/CollectionStackedTimeline.tsx
@@ -119,7 +119,8 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
         // onClick play scripts
         CollectionStackedTimeline.RangeScript =
             CollectionStackedTimeline.RangeScript ||
-            ScriptField.MakeFunction(`scriptContext.clickAnchor(this, clientX)`, {
+            ScriptField.MakeFunction(`setTimeout(() => scriptContext.clickAnchor(this, clientX))`, {
+                // setTimeout is a hack to run script in its own properly named undo group (instead of being part of the generic onClick)
                 self: Doc.name,
                 scriptContext: 'any',
                 clientX: 'number',
diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
index 243550c0b..6be9cb72d 100644
--- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
+++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
@@ -128,7 +128,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
 
     @action pointerEntered = () => SnappingManager.GetIsDragging() && (this._background = '#b4b4b4');
     @action pointerLeave = () => (this._background = 'inherit');
-    textCallback = (char: string) => this.addNewTextDoc('-typed text-', false, true);
+    @undoBatch typedNote = (char: string) => this.addNewTextDoc('-typed text-', false, true);
 
     @action
     addNewTextDoc = (value: string, shiftDown?: boolean, forceEmptyNote?: boolean) => {
@@ -363,7 +363,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
                                 <EditableView
                                     GetValue={returnEmptyString}
                                     SetValue={this.addNewTextDoc}
-                                    textCallback={this.textCallback}
+                                    textCallback={this.typedNote}
                                     placeholder={"Type ':' for commands"}
                                     contents={<FontAwesomeIcon icon={'plus'} />}
                                     menuCallback={this.menuCallback}
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index bcda13c8b..cfe78afa1 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -5,7 +5,6 @@ import { AclPrivate, Doc, DocListCast, Field, Opt, StrListCast } from '../../../
 import { Id } from '../../../fields/FieldSymbols';
 import { List } from '../../../fields/List';
 import { listSpec } from '../../../fields/Schema';
-import { ScriptField } from '../../../fields/ScriptField';
 import { Cast, ScriptCast, StrCast } from '../../../fields/Types';
 import { WebField } from '../../../fields/URLField';
 import { GestureUtils } from '../../../pen-gestures/GestureUtils';
@@ -65,11 +64,7 @@ export function CollectionSubView<X>(moreProps?: X) {
         // to its children which may be templates.
         // If 'annotationField' is specified, then all children exist on that field of the extension document, otherwise, they exist directly on the data document under 'fieldKey'
         @computed get dataField() {
-            if (this.layoutDoc[this.props.fieldKey]) return this.layoutDoc[this.props.fieldKey];
-            // sets the dataDoc's data field to an empty list if the data field is undefined - prevents issues with addonly
-            // setTimeout changes it outside of the @computed section
-            !this.dataDoc[this.props.fieldKey] && setTimeout(() => !this.dataDoc[this.props.fieldKey] && (this.dataDoc[this.props.fieldKey] = new List<Doc>()));
-            return this.dataDoc[this.props.fieldKey];
+            return this.layoutDoc[this.props.fieldKey];
         }
 
         get childLayoutPairs(): { layout: Doc; data: Doc }[] {
@@ -130,8 +125,9 @@ export function CollectionSubView<X>(moreProps?: X) {
                     const fieldKey = Doc.LayoutFieldKey(d);
                     const annos = !Field.toString(Doc.LayoutField(d) as Field).includes(CollectionView.name);
                     const data = d[annos ? fieldKey + '_annotations' : fieldKey];
-                    if (data !== undefined) {
-                        let subDocs = DocListCast(data);
+                    const side = annos && d[fieldKey + '_sidebar'];
+                    if (data !== undefined || side !== undefined) {
+                        let subDocs = [...DocListCast(data), ...DocListCast(side)];
                         if (subDocs.length > 0) {
                             let newarray: Doc[] = [];
                             notFiltered = notFiltered || (!searchDocs.length && DocUtils.FilterDocs(subDocs, childDocFilters, docRangeFilters, d).length);
@@ -142,6 +138,7 @@ export function CollectionSubView<X>(moreProps?: X) {
                                     const annos = !Field.toString(Doc.LayoutField(t) as Field).includes(CollectionView.name);
                                     notFiltered = notFiltered || ((!searchDocs.length || searchDocs.includes(t)) && ((!childDocFilters.length && !docRangeFilters.length) || DocUtils.FilterDocs([t], childDocFilters, docRangeFilters, d).length));
                                     DocListCast(t[annos ? fieldKey + '_annotations' : fieldKey]).forEach(newdoc => newarray.push(newdoc));
+                                    annos && DocListCast(t[fieldKey + '_sidebar']).forEach(newdoc => newarray.push(newdoc));
                                 });
                                 subDocs = newarray;
                             }
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index 7e88959a4..33fa434e1 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -13,14 +13,13 @@ import { listSpec } from '../../../fields/Schema';
 import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../fields/Types';
 import { emptyFunction, lightOrDark, returnEmptyDoclist, returnFalse, returnTrue, setupMoveUpEvents, simulateMouseClick, Utils } from '../../../Utils';
 import { DocServer } from '../../DocServer';
-import { DocUtils } from '../../documents/Documents';
 import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes';
 import { DocumentManager } from '../../util/DocumentManager';
 import { DragManager, dropActionType } from '../../util/DragManager';
 import { SelectionManager } from '../../util/SelectionManager';
 import { SnappingManager } from '../../util/SnappingManager';
 import { Transform } from '../../util/Transform';
-import { undoBatch, UndoManager } from '../../util/UndoManager';
+import { undoable, UndoManager } from '../../util/UndoManager';
 import { DashboardView } from '../DashboardView';
 import { Colors, Shadows } from '../global/globalEnums';
 import { LightboxView } from '../LightboxView';
@@ -116,15 +115,13 @@ export class TabDocView extends React.Component<TabDocViewProps> {
 
             titleEle.size = StrCast(doc.title).length + 3;
             titleEle.value = doc.title;
-            titleEle.onkeydown = (e: KeyboardEvent) => {
-                e.stopPropagation();
-            };
-            titleEle.onchange = undoBatch(
-                action((e: any) => {
+            titleEle.onkeydown = (e: KeyboardEvent) => e.stopPropagation();
+            titleEle.onchange = (e: any) => {
+                undoable(() => {
                     titleEle.size = e.currentTarget.value.length + 3;
                     Doc.GetProto(doc).title = e.currentTarget.value;
-                })
-            );
+                }, 'edit tab title')();
+            };
 
             if (tab.element[0].children[1].children.length === 1) {
                 iconWrap.className = 'lm_iconWrap lm_moreInfo';
@@ -198,7 +195,9 @@ export class TabDocView extends React.Component<TabDocViewProps> {
                 action(selected => {
                     if (selected) this._activated = true;
                     const toggle = tab.element[0].children[2].children[0] as HTMLInputElement;
-                    selected && tab.contentItem !== tab.header.parent.getActiveContentItem() && UndoManager.RunInBatch(() => tab.header.parent.setActiveContentItem(tab.contentItem), 'tab switch');
+                    if (selected && tab.contentItem !== tab.header.parent.getActiveContentItem()) {
+                        undoable(() => tab.header.parent.setActiveContentItem(tab.contentItem), 'tab switch')();
+                    }
                     toggle.style.fontWeight = selected ? 'bold' : '';
                     // toggle.style.textTransform = selected ? "uppercase" : "";
                 }),
@@ -234,7 +233,7 @@ export class TabDocView extends React.Component<TabDocViewProps> {
     public static PinDoc(docs: Doc | Doc[], pinProps: PinProps) {
         const docList = docs instanceof Doc ? [docs] : docs;
 
-        const batch = UndoManager.StartBatch('pinning doc');
+        const batch = UndoManager.StartBatch('Pin doc to pres trail');
         const curPres = Doc.ActivePresentation ?? Doc.MakeCopy(Doc.UserDoc().emptyTrail as Doc, true);
 
         if (!Doc.ActivePresentation) {
diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx
index 037148bb9..f56eaee07 100644
--- a/src/client/views/collections/TreeView.tsx
+++ b/src/client/views/collections/TreeView.tsx
@@ -102,7 +102,7 @@ export class TreeView extends React.Component<TreeViewProps> {
     private _treedropDisposer?: DragManager.DragDropDisposer;
 
     get treeViewOpenIsTransient() {
-        return this.props.treeView.doc.treeViewOpenIsTransient || Doc.IsDocDataProto(this.doc);
+        return this.props.treeView.doc.treeViewOpenIsTransient || Doc.IsDataProto(this.doc);
     }
     set treeViewOpen(c: boolean) {
         if (this.treeViewOpenIsTransient) this._transientOpenState = c;
@@ -221,7 +221,7 @@ export class TreeView extends React.Component<TreeViewProps> {
         } else {
             // choose an appropriate embedding or make one. --- choose the first embedding that (1) user owns,  (2) has no context field ... otherwise make a new embedding
             const bestEmbedding =
-                docView.props.Document.author === Doc.CurrentUserEmail && !Doc.IsDocDataProto(docView.props.Document)
+                docView.props.Document.author === Doc.CurrentUserEmail && !Doc.IsDataProto(docView.props.Document)
                     ? docView.props.Document
                     : DocListCast(this.props.document.proto_embeddings).find(doc => !doc.embedContainer && doc.author === Doc.CurrentUserEmail);
             const nextBestEmbedding = DocListCast(this.props.document.proto_embeddings).find(doc => doc.author === Doc.CurrentUserEmail);
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 5ac444147..95046661e 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -483,7 +483,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         return false;
     }
 
-    @undoBatch
     @action
     updateClusters(_freeform_useClusters: boolean) {
         this.props.Document._freeform_useClusters = _freeform_useClusters;
@@ -534,7 +533,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         }
     }
 
-    @undoBatch
     @action
     updateCluster(doc: Doc) {
         const childLayouts = this.childLayoutPairs.map(pair => pair.layout);
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 47d7801e6..641088675 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -102,7 +102,6 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
         this._lassoPts = [];
     };
 
-    @undoBatch
     @action
     onKeyPress = (e: KeyboardEvent) => {
         //make textbox and add it to this collection
@@ -111,7 +110,6 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
         const [x, y] = this.Transform.transformPoint(this._downX, this._downY);
         if (e.key === '?') {
             cm.setDefaultItem('?', (str: string) => this.props.addDocTab(Docs.Create.WebDocument(`https://bing.com/search?q=${str}`, { _width: 400, x, y, _height: 512, _nativeWidth: 850, title: 'bing', data_useCors: true }), OpenWhere.addRight));
-
             cm.displayMenu(this._downX, this._downY, undefined, true);
             e.stopPropagation();
         } else if (e.key === 'u' && this.props.ungroup) {
@@ -173,7 +171,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
             e.stopPropagation();
         } else if (!e.ctrlKey && !e.metaKey && SelectionManager.Views().length < 2) {
             FormattedTextBox.SelectOnLoadChar = Doc.UserDoc().defaultTextLayout && !this.props.childLayoutString ? e.key : '';
-            FormattedTextBox.LiveTextUndo = UndoManager.StartBatch('live text batch');
+            FormattedTextBox.LiveTextUndo = UndoManager.StartBatch('type new note');
             this.props.addLiveTextDocument(DocUtils.GetNewTextDoc('-typed text-', x, y, 200, 100, this.props.xPadding === 0));
             e.stopPropagation();
         }
@@ -396,6 +394,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
         return newCollection;
     });
 
+    @undoBatch
     @action
     pileup = (e: KeyboardEvent | React.PointerEvent | undefined) => {
         const selected = this.marqueeSelect(false);
@@ -508,8 +507,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
     };
 
     @undoBatch
-    @action
-    summary = (e: KeyboardEvent | React.PointerEvent | undefined) => {
+    summary = action((e: KeyboardEvent | React.PointerEvent | undefined) => {
         const selected = this.marqueeSelect(false).map(d => {
             this.props.removeDocument?.(d);
             d.x = NumCast(d.x) - this.Bounds.left;
@@ -534,19 +532,10 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
         this.props.addDocument?.(portal);
         this.props.addLiveTextDocument(summary);
         MarqueeOptionsMenu.Instance.fadeOut(true);
-    };
+    });
 
     @action
-    background = (e: KeyboardEvent | React.PointerEvent | undefined) => {
-        const newCollection = this.getCollection([], undefined, undefined);
-        this.props.addDocument?.(newCollection);
-        MarqueeOptionsMenu.Instance.fadeOut(true);
-        this.hideMarquee();
-        setTimeout(() => this.props.selectDocuments([newCollection]));
-    };
-
-    @undoBatch
-    marqueeCommand = action((e: KeyboardEvent) => {
+    marqueeCommand = (e: KeyboardEvent) => {
         if (this._commandExecuted || (e as any).propagationIsStopped) {
             return;
         }
@@ -557,7 +546,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
             this.delete(e, e.key === 'h');
             e.stopPropagation();
         }
-        if ('cbtsSpg'.indexOf(e.key) !== -1) {
+        if ('ctsSpg'.indexOf(e.key) !== -1) {
             this._commandExecuted = true;
             e.stopPropagation();
             e.preventDefault();
@@ -565,7 +554,6 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
             if (e.key === 'g') this.collection(e, true);
             if (e.key === 'c' || e.key === 't') this.collection(e);
             if (e.key === 's' || e.key === 'S') this.summary(e);
-            if (e.key === 'b') this.background(e);
             if (e.key === 'p') this.pileup(e);
             this.cleanupInteractions(false);
         }
@@ -575,7 +563,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
             e.preventDefault();
             this._lassoFreehand = !this._lassoFreehand;
         }
-    });
+    };
 
     touchesLine(r1: { left: number; top: number; width: number; height: number }) {
         for (const lassoPt of this._lassoPts) {
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
index bcc2ca129..2a68f9b58 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
@@ -13,7 +13,7 @@ import { Docs, DocumentOptions, DocUtils, FInfo } from '../../../documents/Docum
 import { DocumentManager } from '../../../util/DocumentManager';
 import { DragManager } from '../../../util/DragManager';
 import { SelectionManager } from '../../../util/SelectionManager';
-import { undoBatch } from '../../../util/UndoManager';
+import { undoable, undoBatch } from '../../../util/UndoManager';
 import { ContextMenu } from '../../ContextMenu';
 import { EditableView } from '../../EditableView';
 import { Colors } from '../../global/globalEnums';
@@ -25,6 +25,8 @@ import { CollectionSubView } from '../CollectionSubView';
 import './CollectionSchemaView.scss';
 import { SchemaColumnHeader } from './SchemaColumnHeader';
 import { SchemaRowBox } from './SchemaRowBox';
+import { faHeartPulse } from '@fortawesome/free-solid-svg-icons';
+import { faGalacticSenate } from '@fortawesome/free-brands-svg-icons';
 
 export enum ColumnType {
     Number,
@@ -224,7 +226,7 @@ export class CollectionSchemaView extends CollectionSubView() {
 
     @undoBatch
     @action
-    setSort = (field: string | undefined, desc: boolean = false) => {
+    setColumnSort = (field: string | undefined, desc: boolean = false) => {
         this.layoutDoc.sortField = field;
         this.layoutDoc.sortDesc = desc;
     };
@@ -484,24 +486,11 @@ export class CollectionSchemaView extends CollectionSubView() {
         return false;
     };
 
-    @action
-    addNewTextDoc = (value: string, shiftDown?: boolean, forceEmptyNote?: boolean) => {
-        if (!value && !forceEmptyNote) return false;
-        const newDoc = Docs.Create.TextDocument(value, { title: value, _layout_autoHeight: true });
-        FormattedTextBox.SelectOnLoad = newDoc[Id];
-        FormattedTextBox.SelectOnLoadChar = forceEmptyNote ? '' : ' ';
-        return this.addRow(newDoc) || false;
-    };
-
     menuCallback = (x: number, y: number) => {
         ContextMenu.Instance.clearItems();
 
-        DocUtils.addDocumentCreatorMenuItems(doc => this.addRow(doc), this.addRow, x, y, true);
+        DocUtils.addDocumentCreatorMenuItems(this.addRow, this.addRow, x, y, true);
 
-        ContextMenu.Instance.setDefaultItem('::', (name: string): void => {
-            Doc.GetProto(this.props.Document)[name] = '';
-            this.addRow(Docs.Create.TextDocument('', { title: name, _layout_autoHeight: true }));
-        });
         ContextMenu.Instance.displayMenu(x, y, undefined, true);
     };
 
@@ -866,7 +855,7 @@ export class CollectionSchemaView extends CollectionSubView() {
                                 columnWidths={this.displayColumnWidths}
                                 sortField={this.sortField}
                                 sortDesc={this.sortDesc}
-                                setSort={this.setSort}
+                                setSort={this.setColumnSort}
                                 rowHeight={this.rowHeightFunc}
                                 removeColumn={this.removeColumn}
                                 resizeColumn={this.startResize}
@@ -879,7 +868,14 @@ export class CollectionSchemaView extends CollectionSubView() {
                     {this._columnMenuIndex !== undefined && this.renderColumnMenu}
                     {this._filterColumnIndex !== undefined && this.renderFilterMenu}
                     <CollectionSchemaViewDocs schema={this} childDocs={this.sortedDocsFunc} rowHeight={this.rowHeightFunc} setRef={(ref: HTMLDivElement | null) => (this._tableContentRef = ref)} />
-                    <EditableView GetValue={returnEmptyString} SetValue={this.addNewTextDoc} placeholder={"Type ':' for commands"} contents={'+ New Node'} menuCallback={this.menuCallback} height={CollectionSchemaView._newNodeInputHeight} />
+                    <EditableView
+                        GetValue={returnEmptyString}
+                        SetValue={undoable(value => (value ? this.addRow(Docs.Create.TextDocument(value, { title: value, _layout_autoHeight: true })) : false), 'add text doc')}
+                        placeholder={"Type ':' for commands"}
+                        contents={'+ New Node'}
+                        menuCallback={this.menuCallback}
+                        height={CollectionSchemaView._newNodeInputHeight}
+                    />
                 </div>
                 {this.previewWidth > 0 && <div className="schema-preview-divider" style={{ width: CollectionSchemaView._previewDividerWidth }} onPointerDown={this.onDividerDown}></div>}
                 {this.previewWidth > 0 && (
diff --git a/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx b/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx
index 7da3c042c..46c2f622b 100644
--- a/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx
+++ b/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx
@@ -5,8 +5,6 @@ import { observer } from 'mobx-react';
 import { emptyFunction, setupMoveUpEvents } from '../../../../Utils';
 import { Colors } from '../../global/globalEnums';
 import './CollectionSchemaView.scss';
-import { SnappingManager } from '../../../util/SnappingManager';
-import { DragManager } from '../../../util/DragManager';
 
 export interface SchemaColumnHeaderProps {
     columnKeys: string[];
diff --git a/src/client/views/collections/collectionSchema/SchemaRowBox.tsx b/src/client/views/collections/collectionSchema/SchemaRowBox.tsx
index 45bfe4f77..978b65053 100644
--- a/src/client/views/collections/collectionSchema/SchemaRowBox.tsx
+++ b/src/client/views/collections/collectionSchema/SchemaRowBox.tsx
@@ -7,7 +7,7 @@ import { Doc } from '../../../../fields/Doc';
 import { BoolCast } from '../../../../fields/Types';
 import { DragManager } from '../../../util/DragManager';
 import { SnappingManager } from '../../../util/SnappingManager';
-import { undoBatch } from '../../../util/UndoManager';
+import { undoable, undoBatch } from '../../../util/UndoManager';
 import { ViewBoxBaseComponent } from '../../DocComponent';
 import { Colors } from '../../global/globalEnums';
 import { OpenWhere } from '../../nodes/DocumentView';
@@ -111,18 +111,18 @@ export class SchemaRowBox extends ViewBoxBaseComponent<FieldViewProps>() {
                     }}>
                     <div
                         className="schema-row-button"
-                        onPointerDown={undoBatch(e => {
+                        onPointerDown={undoable(e => {
                             e.stopPropagation();
                             this.props.removeDocument?.(this.rootDoc);
-                        })}>
+                        }, 'Delete Row')}>
                         <FontAwesomeIcon icon="times" />
                     </div>
                     <div
                         className="schema-row-button"
-                        onPointerDown={e => {
+                        onPointerDown={undoable(e => {
                             e.stopPropagation();
                             this.props.addDocTab(this.rootDoc, OpenWhere.addRight);
-                        }}>
+                        }, 'Open Doc on Right')}>
                         <FontAwesomeIcon icon="external-link-alt" />
                     </div>
                 </div>
diff --git a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
index 42bf32475..6d5ef9df6 100644
--- a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
+++ b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
@@ -122,7 +122,7 @@ export class SchemaTableCell extends React.Component<SchemaTableCellProps> {
                         const ret = KeyValueBox.SetField(this.props.Document, this.props.fieldKey.replace(/^_/, ''), value);
                         this.props.finishEdit?.();
                         return ret;
-                    })}
+                    }, 'edit schema cell')}
                 />
             </div>
         );
diff --git a/src/client/views/linking/LinkMenu.tsx b/src/client/views/linking/LinkMenu.tsx
index 3f6369898..65d13a6c3 100644
--- a/src/client/views/linking/LinkMenu.tsx
+++ b/src/client/views/linking/LinkMenu.tsx
@@ -49,7 +49,7 @@ export class LinkMenu extends React.Component<Props> {
             <LinkMenuGroup key={group[0]} itemHandler={this.props.itemHandler} docView={this.props.docView} sourceDoc={this.props.docView.props.Document} group={group[1]} groupType={group[0]} clearLinkEditor={this.clear} />
         ));
 
-        return linkItems.length ? linkItems : this.props.style ? [<></>] : [<p key="">No links have been created yet. Drag the linking button onto another document to create a link.</p>];
+        return linkItems.length ? linkItems : this.props.style ? [] : [<p key="none">No links have been created yet. Drag the linking button onto another document to create a link.</p>];
     };
 
     render() {
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 8e83cf121..410e0bbdc 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -447,7 +447,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
 
     timelineScreenToLocal = () => this.props.ScreenToLocalTransform().translate(0, -AudioBox.topControlsHeight);
 
-    setPlayheadTime = (time: number) => (this._ele!.currentTime = this.layoutDoc._layout_currentTimecode = time);
+    setPlayheadTime = (time: number) => (this._ele!.currentTime /*= this.layoutDoc._layout_currentTimecode*/ = time);
 
     playing = () => this.mediaState === media_state.Playing;
 
diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx
index d5ca30957..bd1952ecb 100644
--- a/src/client/views/nodes/DocumentLinksButton.tsx
+++ b/src/client/views/nodes/DocumentLinksButton.tsx
@@ -9,7 +9,7 @@ import { DocUtils } from '../../documents/Documents';
 import { DragManager } from '../../util/DragManager';
 import { Hypothesis } from '../../util/HypothesisUtils';
 import { LinkManager } from '../../util/LinkManager';
-import { undoBatch, UndoManager } from '../../util/UndoManager';
+import { undoable, undoBatch, UndoManager } from '../../util/UndoManager';
 import './DocumentLinksButton.scss';
 import { DocumentView } from './DocumentView';
 import { LinkDescriptionPopup } from './LinkDescriptionPopup';
@@ -78,7 +78,6 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
         );
     };
 
-    @undoBatch
     onLinkButtonDown = (e: React.PointerEvent): void => {
         setupMoveUpEvents(
             this,
@@ -123,17 +122,14 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
             e,
             returnFalse,
             emptyFunction,
-            undoBatch(
-                action(e => {
-                    DocumentLinksButton.finishLinkClick(e.clientX, e.clientY, DocumentLinksButton.StartLink, this.props.View.props.Document, true, this.props.View);
-                })
-            )
+            action(e => DocumentLinksButton.finishLinkClick(e.clientX, e.clientY, DocumentLinksButton.StartLink, this.props.View.props.Document, true, this.props.View))
         );
     };
 
-    public static finishLinkClick = undoBatch(
-        action((screenX: number, screenY: number, startLink: Doc, endLink: Doc, startIsAnnotation: boolean, endLinkView?: DocumentView, pinProps?: PinProps) => {
-            if (startLink === endLink) {
+    @undoBatch
+    public static finishLinkClick(screenX: number, screenY: number, startLink: Doc | undefined, endLink: Doc, startIsAnnotation: boolean, endLinkView?: DocumentView, pinProps?: PinProps) {
+        runInAction(() => {
+            if (startLink === endLink || !startLink) {
                 DocumentLinksButton.StartLink = undefined;
                 DocumentLinksButton.StartLinkView = undefined;
                 DocumentLinksButton.AnnotationId = undefined;
@@ -185,8 +181,8 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
                     );
                 }
             }
-        })
-    );
+        });
+    }
 
     @action clearLinks() {
         DocumentLinksButton.StartLink = undefined;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 359b72352..52eee84ac 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -7,6 +7,7 @@ import { AclPrivate, AnimationSym, DataSym, Doc, DocListCast, Field, Opt, StrLis
 import { Id } from '../../../fields/FieldSymbols';
 import { InkTool } from '../../../fields/InkField';
 import { List } from '../../../fields/List';
+import { RefField } from '../../../fields/RefField';
 import { listSpec } from '../../../fields/Schema';
 import { ScriptField } from '../../../fields/ScriptField';
 import { BoolCast, Cast, DocCast, ImageCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
@@ -35,8 +36,11 @@ import { ContextMenuProps } from '../ContextMenuItem';
 import { DocComponent } from '../DocComponent';
 import { EditableView } from '../EditableView';
 import { GestureOverlay } from '../GestureOverlay';
+import { InkingStroke } from '../InkingStroke';
 import { LightboxView } from '../LightboxView';
+import { OverlayView } from '../OverlayView';
 import { StyleProp } from '../StyleProvider';
+import { UndoStack } from '../UndoStack';
 import { CollectionFreeFormDocumentView } from './CollectionFreeFormDocumentView';
 import { DocumentContentsView, ObserverJsxParser } from './DocumentContentsView';
 import { DocumentLinksButton } from './DocumentLinksButton';
@@ -47,8 +51,6 @@ import { LinkAnchorBox } from './LinkAnchorBox';
 import { PresEffect, PresEffectDirection } from './trails';
 import { PinProps, PresBox } from './trails/PresBox';
 import React = require('react');
-import { InkingStroke } from '../InkingStroke';
-import { RefField } from '../../../fields/RefField';
 const { Howl } = require('howler');
 
 interface Window {
@@ -456,27 +458,26 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                         // instead of in the global lightbox
                         const oldFunc = DocumentViewInternal.addDocTabFunc;
                         DocumentViewInternal.addDocTabFunc = this.props.addDocTab;
-                        const res =
-                            this.onClickHandler?.script.run(
-                                {
-                                    this: this.layoutDoc,
-                                    self: this.rootDoc,
-                                    _readOnly_: false,
-                                    scriptContext: this.props.scriptContext,
-                                    documentView: this.props.DocumentView(),
-                                    clientX,
-                                    clientY,
-                                    shiftKey,
-                                    altKey,
-                                    metaKey,
-                                },
-                                console.log
-                            ).result?.select === true
-                                ? this.props.select(false)
-                                : '';
+                        this.onClickHandler?.script.run(
+                            {
+                                this: this.layoutDoc,
+                                self: this.rootDoc,
+                                _readOnly_: false,
+                                scriptContext: this.props.scriptContext,
+                                documentView: this.props.DocumentView(),
+                                clientX,
+                                clientY,
+                                shiftKey,
+                                altKey,
+                                metaKey,
+                            },
+                            console.log
+                        ).result?.select === true
+                            ? this.props.select(false)
+                            : '';
                         DocumentViewInternal.addDocTabFunc = oldFunc;
                     };
-                    clickFunc = () => (this.props.Document.dontUndo ? func() : UndoManager.RunInBatch(func, 'on click'));
+                    clickFunc = () => (this.props.Document.dontUndo ? func() : UndoManager.RunInBatch(func, 'click ' + this.rootDoc.title));
                 } else {
                     // onDragStart implies a button doc that we don't want to select when clicking. RootDocument & isTemplateForField implies we're clicking on part of a template instance and we want to select the whole template, not the part
                     if ((this.layoutDoc.onDragStart || this.props.Document.rootDocument) && !(e.ctrlKey || e.button > 0)) {
@@ -490,7 +491,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                 if ((clickFunc && waitFordblclick !== 'never') || waitFordblclick === 'always') {
                     this._doubleClickTimeout && clearTimeout(this._doubleClickTimeout);
                     this._doubleClickTimeout = setTimeout(this._singleClickFunc, 300);
-                } else {
+                } else if (!DocumentView.LongPress) {
                     this._singleClickFunc();
                     this._singleClickFunc = undefined;
                 }
@@ -502,7 +503,15 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
 
     @action
     onPointerDown = (e: React.PointerEvent): void => {
-        this._longPressSelector = setTimeout(() => DocumentView.LongPress && this.props.select(false), 1000);
+        this._longPressSelector = setTimeout(() => {
+            if (DocumentView.LongPress) {
+                if (this.rootDoc.dontUndo) {
+                    OverlayView.Instance.addWindow(<UndoStack />, { x: 300, y: 100, width: 200, height: 200, title: 'Undo Stack' });
+                } else {
+                    this.props.select(false);
+                }
+            }
+        }, 1000);
         if (!GestureOverlay.DownDocView) GestureOverlay.DownDocView = this.props.DocumentView();
 
         this._downX = e.clientX;
@@ -557,7 +566,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
         if (this.onPointerUpHandler?.script) {
             this.onPointerUpHandler.script.run({ self: this.rootDoc, this: this.layoutDoc }, console.log);
         } else if (e.button === 0 && Utils.isClick(e.clientX, e.clientY, this._downX, this._downY, this._downTime)) {
-            this._doubleTap = Date.now() - this._lastTap < Utils.CLICK_TIME;
+            this._doubleTap = this.rootDoc.defaultDoubleClick !== 'ignore' && Date.now() - this._lastTap < Utils.CLICK_TIME;
             if (!this.props.isSelected(true)) this._lastTap = Date.now(); // don't want to process the start of a double tap if the doucment is selected
         }
         if (DocumentView.LongPress) e.preventDefault();
diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx
index c6172ee01..450176a49 100644
--- a/src/client/views/nodes/LinkDocPreview.tsx
+++ b/src/client/views/nodes/LinkDocPreview.tsx
@@ -123,7 +123,6 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
                                 this._markerTargetDoc = linkTarget;
                                 this._targetDoc = /*linkTarget?.type === DocumentType.MARKER &&*/ linkTarget?.annotationOn ? Cast(linkTarget.annotationOn, Doc, null) ?? linkTarget : linkTarget;
                             }
-                            this._toolTipText = 'link to ' + this._targetDoc?.title;
                             if (LinkDocPreview.LinkInfo?.noPreview || this._linkSrc?.followLinkToggle || this._markerTargetDoc?.type === DocumentType.PRES) this.followLink();
                         }
                     })
@@ -296,7 +295,7 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
                 className="linkDocPreview"
                 ref={this._linkDocRef}
                 onPointerDown={this.followLinkPointerDown}
-                style={{ display: !this._toolTipText ? 'none' : undefined, left: this.props.location[0], top: this.props.location[1], width: this.width() + borders, height: this.height() + borders + (this.props.showHeader ? 37 : 0) }}>
+                style={{ left: this.props.location[0], top: this.props.location[1], width: this.width() + borders, height: this.height() + borders + (this.props.showHeader ? 37 : 0) }}>
                 {this.docPreview}
             </div>
         );
diff --git a/src/client/views/nodes/button/FontIconBox.tsx b/src/client/views/nodes/button/FontIconBox.tsx
index 57aa852ac..b07cf7e00 100644
--- a/src/client/views/nodes/button/FontIconBox.tsx
+++ b/src/client/views/nodes/button/FontIconBox.tsx
@@ -16,7 +16,7 @@ import { CollectionViewType, DocumentType } from '../../../documents/DocumentTyp
 import { LinkManager } from '../../../util/LinkManager';
 import { ScriptingGlobals } from '../../../util/ScriptingGlobals';
 import { SelectionManager } from '../../../util/SelectionManager';
-import { undoBatch, UndoManager } from '../../../util/UndoManager';
+import { undoable, undoBatch, UndoManager } from '../../../util/UndoManager';
 import { CollectionFreeFormView } from '../../collections/collectionFreeForm';
 import { ContextMenu } from '../../ContextMenu';
 import { DocComponent } from '../../DocComponent';
@@ -129,29 +129,27 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
      * Number button
      */
     @computed get numberSliderButton() {
-        const numScript = ScriptCast(this.rootDoc.script);
-        const setValue = (value: number) => UndoManager.RunInBatch(() => numScript?.script.run({ self: this.rootDoc, value, _readOnly_: false }), 'set num value');
-
+        const numScript = (value?: number) => ScriptCast(this.rootDoc.script).script.run({ self: this.rootDoc, value, _readOnly_: value === undefined });
         // Script for checking the outcome of the toggle
-        const checkResult = Number(numScript?.script.run({ self: this.rootDoc, value: 0, _readOnly_: true }).result ?? 0).toPrecision(NumCast(this.dataDoc.numPrecision, 3));
-
+        const checkResult = Number(numScript().result ?? 0).toPrecision(NumCast(this.dataDoc.numPrecision, 3));
         const label = !FontIconBox.GetShowLabels() ? null : <div className="fontIconBox-label">{this.label}</div>;
 
         const dropdown = (
             <div className="menuButton-dropdownBox" onPointerDown={e => e.stopPropagation()}>
                 <input
+                    className="menu-slider"
                     type="range"
                     step="1"
                     min={NumCast(this.rootDoc.numBtnMin, 0)}
                     max={NumCast(this.rootDoc.numBtnMax, 100)}
+                    //readOnly={true}
                     value={checkResult}
-                    className="menu-slider"
-                    onPointerDown={() => (this._batch = UndoManager.StartBatch('presDuration'))}
+                    onPointerDown={() => (this._batch = UndoManager.StartBatch('num slider changing'))}
                     onPointerUp={() => this._batch?.end()}
-                    onChange={e => {
+                    onChange={undoable(e => {
                         e.stopPropagation();
-                        setValue(Number(e.target.value));
-                    }}
+                        numScript(Number(e.target.value));
+                    }, 'set num value')}
                 />
             </div>
         );
@@ -174,20 +172,13 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
      * Number button
      */
     @computed get numberDropdownButton() {
-        const numScript = ScriptCast(this.rootDoc.script);
-        const setValue = (value: number) => UndoManager.RunInBatch(() => numScript?.script.run({ self: this.rootDoc, value, _readOnly_: false }), 'set num value');
-
-        // Script for checking the outcome of the toggle
-        const checkResult = Number(numScript?.script.run({ self: this.rootDoc, value: 0, _readOnly_: true }).result ?? 0).toPrecision(NumCast(this.dataDoc.numPrecision, 3));
+        const numScript = (value?: number) => ScriptCast(this.rootDoc.script)?.script.run({ self: this.rootDoc, value, _readOnly_: value === undefined });
 
-        const label = !FontIconBox.GetShowLabels() ? null : <div className="fontIconBox-label">{this.label}</div>;
+        const checkResult = Number(numScript().result ?? 0).toPrecision(NumCast(this.dataDoc.numPrecision, 3));
 
         const items: number[] = [];
-        for (let i = 0; i < 100; i++) {
-            if (i % 2 === 0) {
-                items.push(i);
-            }
-        }
+        for (let i = 0; i < 100; i += 2) items.push(i);
+
         const list = items.map(value => {
             return (
                 <div
@@ -196,15 +187,15 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
                     style={{
                         backgroundColor: value.toString() === checkResult ? Colors.LIGHT_BLUE : undefined,
                     }}
-                    onClick={() => setValue(value)}>
+                    onClick={undoable(value => numScript(value), `${this.rootDoc.title} button set from list`)}>
                     {value}
                 </div>
             );
         });
         return (
             <div className="menuButton numBtn list">
-                <div className="button" onClick={action(e => setValue(Number(checkResult) - 1))}>
-                    <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={'minus'} />
+                <div className="button" onClick={undoable(e => numScript(Number(checkResult) - 1), `${this.rootDoc.title} decrement value`)}>
+                    <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon="minus" />
                 </div>
                 <div
                     className={`button ${'number'}`}
@@ -217,9 +208,9 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
                         this.noTooltip = this.rootDoc.dropDownOpen;
                         Doc.UnBrushAllDocs();
                     })}>
-                    <input style={{ width: 30 }} className="button-input" type="number" value={checkResult} onChange={undoBatch(action(e => setValue(Number(e.target.value))))} />
+                    <input style={{ width: 30 }} className="button-input" type="number" value={checkResult} readOnly={true} onChange={undoable(e => numScript(Number(e.target.value)), `${this.rootDoc.title} button set value`)} />
                 </div>
-                <div className={`button`} onClick={action(e => setValue(Number(checkResult) + 1))}>
+                <div className={`button`} onClick={undoable(e => numScript(Number(checkResult) + 1), `${this.rootDoc.title} increment value`)}>
                     <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={'plus'} />
                 </div>
 
@@ -322,12 +313,12 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
             .map(value => (
                 <div
                     className="list-item"
-                    key={`${value}`}
+                    key={value}
                     style={{
                         fontFamily: script.script.originalScript.startsWith('{ return setFont') ? value : undefined,
                         backgroundColor: value === text ? Colors.LIGHT_BLUE : undefined,
                     }}
-                    onClick={undoBatch(() => script.script.run({ self: this.rootDoc, value }))}>
+                    onClick={undoable(() => script.script.run({ self: this.rootDoc, value }), value)}>
                     {value[0].toUpperCase() + value.slice(1)}
                 </div>
             ));
@@ -640,29 +631,26 @@ ScriptingGlobals.add(function toggleOverlay(checkResult?: boolean) {
 ScriptingGlobals.add(function showFreeform(attr: 'grid' | 'snaplines' | 'clusters' | 'arrange' | 'viewAll', checkResult?: boolean) {
     const selected = SelectionManager.Docs().lastElement();
     // prettier-ignore
-    const map: Map<'grid' | 'snaplines' | 'clusters' | 'arrange'| 'viewAll', { undo: boolean, checkResult: (doc:Doc) => any; setDoc: (doc:Doc) => void;}> = new Map([
+    const map: Map<'grid' | 'snaplines' | 'clusters' | 'arrange'| 'viewAll', { waitForRender?: boolean, checkResult: (doc:Doc) => any; setDoc: (doc:Doc) => void;}> = new Map([
         ['grid', {
-            undo: false,
             checkResult: (doc:Doc) => doc._freeform_backgroundGrid,
             setDoc: (doc:Doc) => doc._freeform_backgroundGrid = !doc._freeform_backgroundGrid,
         }],
         ['snaplines', {
-            undo: false,
             checkResult: (doc:Doc) => doc._freeform_snapLines,
             setDoc: (doc:Doc) => doc._freeform_snapLines = !doc._freeform_snapLines,
         }],
         ['viewAll', {
-            undo: false,
             checkResult: (doc:Doc) => doc._freeform_fitContentsToBox,
             setDoc: (doc:Doc) => doc._freeform_fitContentsToBox = !doc._freeform_fitContentsToBox,
         }],
         ['clusters', {
-            undo: false,
+            waitForRender: true, // flags that undo batch should terminate after a re-render giving the script the chance to fire
             checkResult: (doc:Doc) => doc._freeform_useClusters,
             setDoc: (doc:Doc) => doc._freeform_useClusters = !doc._freeform_useClusters,
         }],
         ['arrange', {
-            undo: true,
+            waitForRender: true, // flags that undo batch should terminate after a re-render giving the script the chance to fire
             checkResult: (doc:Doc) => doc._autoArrange,
             setDoc: (doc:Doc) => doc._autoArrange = !doc._autoArrange,
         }],
@@ -671,7 +659,7 @@ ScriptingGlobals.add(function showFreeform(attr: 'grid' | 'snaplines' | 'cluster
     if (checkResult) {
         return map.get(attr)?.checkResult(selected) ? Colors.MEDIUM_BLUE : 'transparent';
     }
-    const batch = map.get(attr)?.undo ? UndoManager.StartBatch('set feature') : { end: () => {} };
+    const batch = map.get(attr)?.waitForRender ? UndoManager.StartBatch('set freeform attribute') : { end: () => {} };
     SelectionManager.Docs().map(dv => map.get(attr)?.setDoc(dv));
     setTimeout(() => batch.end(), 100);
 });
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 9c06aa7d8..e85835002 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -301,11 +301,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
             this._editorView.updateState(state);
 
             const dataDoc = Doc.IsDelegateField(DocCast(this.layoutDoc.proto), this.fieldKey) ? DocCast(this.layoutDoc.proto) : this.dataDoc;
-            const curText = state.doc.textBetween(0, state.doc.content.size, ' \n');
-            const curTemp = this.layoutDoc.resolvedDataDoc ? Cast(this.layoutDoc[this.fieldKey], RichTextField) : undefined; // the actual text in the text box
-            const curProto = Cast(Cast(dataDoc.proto, Doc, null)?.[this.fieldKey], RichTextField, null); // the default text inherited from a prototype
-            const curLayout = this.rootDoc !== this.layoutDoc ? Cast(this.layoutDoc[this.fieldKey], RichTextField, null) : undefined; // the default text stored in a layout template
-            const json = JSON.stringify(state.toJSON());
+            const newText = state.doc.textBetween(0, state.doc.content.size, ' \n');
+            const newJson = JSON.stringify(state.toJSON());
+            const prevData = Cast(this.layoutDoc[this.fieldKey], RichTextField, null); // the actual text in the text box
+            const prevLayoutData = this.rootDoc !== this.layoutDoc ? Cast(this.layoutDoc[this.fieldKey], RichTextField, null) : undefined; // the default text stored in a layout template
+            const protoData = Cast(Cast(dataDoc.proto, Doc, null)?.[this.fieldKey], RichTextField, null); // the default text inherited from a prototype
             const effectiveAcl = GetEffectiveAcl(dataDoc);
 
             const removeSelection = (json: string | undefined) => (json?.indexOf('"storedMarks"') === -1 ? json?.replace(/"selection":.*/, '') : json?.replace(/"selection":"\"storedMarks\""/, '"storedMarks"'));
@@ -320,29 +320,25 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
                 dataDoc.tags = accumTags.length ? new List<string>(Array.from(new Set<string>(accumTags))) : undefined;
 
                 let unchanged = true;
-                if (this._applyingChange !== this.fieldKey && removeSelection(json) !== removeSelection(curProto?.Data)) {
+                if (this._applyingChange !== this.fieldKey && removeSelection(newJson) !== removeSelection(prevData?.Data)) {
                     this._applyingChange = this.fieldKey;
-                    const textChange = curText !== Cast(dataDoc[this.fieldKey], RichTextField)?.Text;
+                    const textChange = newText !== prevData?.Text;
                     textChange && (dataDoc[this.fieldKey + '_modificationDate'] = new DateField(new Date(Date.now())));
-                    if ((!curTemp && !curProto) || curText || json.includes('dash')) {
+                    if ((!prevData && !protoData) || newText || (!newText && !protoData)) {
                         // if no template, or there's text that didn't come from the layout template, write it to the document. (if this is driven by a template, then this overwrites the template text which is intended)
-                        if (removeSelection(json) !== removeSelection(curLayout?.Data)) {
+                        if (removeSelection(newJson) !== removeSelection(prevLayoutData?.Data)) {
                             const numstring = NumCast(dataDoc[this.fieldKey], null);
-                            if (numstring !== undefined) {
-                                dataDoc[this.fieldKey] = Number(curText);
-                            } else {
-                                dataDoc[this.fieldKey] = new RichTextField(json, curText);
-                            }
+                            dataDoc[this.fieldKey] = numstring !== undefined ? Number(newText) : new RichTextField(newJson, newText);
                             dataDoc[this.fieldKey + '_noTemplate'] = true; //(curTemp?.Text || "") !== curText; // mark the data field as being split from the template if it has been edited
-                            textChange && ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.layoutDoc, self: this.rootDoc, text: curText });
+                            textChange && ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.layoutDoc, self: this.rootDoc, text: newText });
                             unchanged = false;
                         }
                     } else {
                         // if we've deleted all the text in a note driven by a template, then restore the template data
                         dataDoc[this.fieldKey] = undefined;
-                        this._editorView.updateState(EditorState.fromJSON(this.config, JSON.parse((curProto || curTemp).Data)));
+                        this._editorView.updateState(EditorState.fromJSON(this.config, JSON.parse((protoData || prevData).Data)));
                         dataDoc[this.fieldKey + '_noTemplate'] = undefined; // mark the data field as not being split from any template it might have
-                        ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.layoutDoc, self: this.rootDoc, text: curText });
+                        ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.layoutDoc, self: this.rootDoc, text: newText });
                         unchanged = false;
                     }
                     this._applyingChange = '';
@@ -667,7 +663,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
         this.layoutDoc._width = !preview && this.SidebarShown ? NumCast(this.layoutDoc._width) * 2 : Math.max(20, NumCast(this.layoutDoc._width) * prevWidth);
     };
     sidebarDown = (e: React.PointerEvent) => {
-        const batch = UndoManager.StartBatch('sidebar');
+        const batch = UndoManager.StartBatch('toggle sidebar');
         setupMoveUpEvents(
             this,
             e,
@@ -694,13 +690,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
         return false;
     };
 
-    @undoBatch
     deleteAnnotation = (anchor: Doc) => {
+        const batch = UndoManager.StartBatch('delete link');
         LinkManager.Instance.deleteLink(LinkManager.Links(anchor)[0]);
         // const docAnnotations = DocListCast(this.props.dataDoc[this.fieldKey]);
         // this.props.dataDoc[this.fieldKey] = new List<Doc>(docAnnotations.filter(a => a !== this.annoTextRegion));
         // AnchorMenu.Instance.fadeOut(true);
         this.props.select(false);
+        setTimeout(batch.end); // wait for reaction to remove link from document
     };
 
     @undoBatch
@@ -733,17 +730,22 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
                 .split(' ')
                 .filter(h => h);
             const anchorDoc = Array.from(hrefs).lastElement().replace(Doc.localServerPath(), '').split('?')[0];
+            const deleteMarkups = undoBatch(() => {
+                const sel = editor.state.selection;
+                editor.dispatch(editor.state.tr.removeMark(sel.from, sel.to, editor.state.schema.marks.linkAnchor));
+            });
             e.persist();
             anchorDoc &&
                 DocServer.GetRefField(anchorDoc).then(
                     action(anchor => {
+                        anchor && SelectionManager.SelectSchemaViewDoc(anchor as Doc);
                         AnchorMenu.Instance.Status = 'annotation';
-                        AnchorMenu.Instance.Delete = () => this.deleteAnnotation(anchor as Doc);
+                        AnchorMenu.Instance.Delete = !anchor && editor.state.selection.empty ? returnFalse : !anchor ? deleteMarkups : () => this.deleteAnnotation(anchor as Doc);
                         AnchorMenu.Instance.Pinned = false;
-                        AnchorMenu.Instance.PinToPres = () => this.pinToPres(anchor as Doc);
-                        AnchorMenu.Instance.MakeTargetToggle = () => this.makeTargetToggle(anchor as Doc);
-                        AnchorMenu.Instance.ShowTargetTrail = () => this.showTargetTrail(anchor as Doc);
-                        AnchorMenu.Instance.IsTargetToggler = () => this.isTargetToggler(anchor as Doc);
+                        AnchorMenu.Instance.PinToPres = !anchor ? returnFalse : () => this.pinToPres(anchor as Doc);
+                        AnchorMenu.Instance.MakeTargetToggle = !anchor ? returnFalse : () => this.makeTargetToggle(anchor as Doc);
+                        AnchorMenu.Instance.ShowTargetTrail = !anchor ? returnFalse : () => this.showTargetTrail(anchor as Doc);
+                        AnchorMenu.Instance.IsTargetToggler = !anchor ? returnFalse : () => this.isTargetToggler(anchor as Doc);
                         AnchorMenu.Instance.jumpTo(e.clientX, e.clientY, true);
                     })
                 );
@@ -1002,7 +1004,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
                     anchorDoc ??
                     Docs.Create.TextConfigDocument({
                         //
-                        title: '#' + this._editorView?.state.doc.textBetween(sel.from, sel.to),
+                        title: 'text(' + this._editorView?.state.doc.textBetween(sel.from, sel.to) + ')',
                         annotationOn: this.dataDoc,
                     });
                 const href = targetHref ?? Doc.localServerPath(anchor);
@@ -1480,11 +1482,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
         }
 
         const selectOnLoad = this.rootDoc[Id] === FormattedTextBox.SelectOnLoad && (!LightboxView.LightboxDoc || LightboxView.IsLightboxDocView(this.props.docViewPath()));
-        if (selectOnLoad && !this.props.dontRegisterView && !this.props.dontSelectOnLoad && this.isActiveTab(this.ProseRef)) {
+        if (this._editorView && selectOnLoad && !this.props.dontRegisterView && !this.props.dontSelectOnLoad && this.isActiveTab(this.ProseRef)) {
             const selLoadChar = FormattedTextBox.SelectOnLoadChar;
             FormattedTextBox.SelectOnLoad = '';
             this.props.select(false);
-            if (selLoadChar && this._editorView) {
+            if (selLoadChar) {
                 const $from = this._editorView.state.selection.anchor ? this._editorView.state.doc.resolve(this._editorView.state.selection.anchor - 1) : undefined;
                 const mark = schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) });
                 const curMarks = this._editorView.state.storedMarks ?? $from?.marksAcross(this._editorView.state.selection.$head) ?? [];
@@ -1494,10 +1496,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
                     .insertText(FormattedTextBox.SelectOnLoadChar, this._editorView.state.doc.content.size - 1, this._editorView.state.doc.content.size)
                     .setStoredMarks(storedMarks);
                 this._editorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(tr.doc.content.size))));
-            } else if (this._editorView && curText && !FormattedTextBox.DontSelectInitialText) {
+            } else if (curText && !FormattedTextBox.DontSelectInitialText) {
                 selectAll(this._editorView.state, this._editorView?.dispatch);
-                this.startUndoTypingBatch();
-            } else if (this._editorView) {
+            } else {
                 this._editorView.dispatch(this._editorView.state.tr.addStoredMark(schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) })));
             }
         }
@@ -1506,7 +1507,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
         if (this._editorView) {
             const tr = this._editorView.state.tr;
             const { from, to } = tr.selection;
-            // for some reason, the selection is sometimes lost in the sidebar view when prosemirror syncs the seledtion with the dom, so reset the selectoin after the document has ben fully instantiated.
+            // for some reason, the selection is sometimes lost in the sidebar view when prosemirror syncs the seledtion with the dom, so reset the selection after the document has ben fully instantiated.
             if (FormattedTextBox.DontSelectInitialText) setTimeout(() => this._editorView?.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(from), tr.doc.resolve(to)))), 250);
             this._editorView.state.storedMarks = [
                 ...(this._editorView.state.storedMarks ?? []),
@@ -1630,7 +1631,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
     onFocused = (e: React.FocusEvent): void => {
         //applyDevTools.applyDevTools(this._editorView);
         this.ProseRef?.children[0] === e.nativeEvent.target && this._editorView && RichTextMenu.Instance?.updateMenu(this._editorView, undefined, this.props);
-        this.startUndoTypingBatch();
+        e.stopPropagation();
     };
 
     onClick = (e: React.MouseEvent): void => {
@@ -1705,13 +1706,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
         }
     }
     startUndoTypingBatch() {
-        !this._undoTyping && (this._undoTyping = UndoManager.StartBatch('undoTyping'));
+        !this._undoTyping && (this._undoTyping = UndoManager.StartBatch('text edits on ' + this.rootDoc.title));
     }
     public endUndoTypingBatch() {
-        const wasUndoing = this._undoTyping;
         this._undoTyping?.end();
         this._undoTyping = undefined;
-        return wasUndoing;
     }
 
     @action
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index 3d1d11141..3eadc3047 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -1428,6 +1428,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 min={min}
                 max={max}
                 value={value}
+                readOnly={true}
                 style={{ marginLeft: hmargin, marginRight: hmargin, width: `calc(100% - ${2 * (hmargin ?? 0)}px)` }}
                 className={`toolbar-slider ${active ? '' : 'none'}`}
                 onPointerDown={e => {
@@ -1501,7 +1502,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                             <div className="ribbon-doubleButton">
                                 <div className="presBox-subheading">Slide Duration</div>
                                 <div className="ribbon-property">
-                                    <input className="presBox-input" type="number" value={duration} onKeyDown={e => e.stopPropagation()} onChange={e => this.updateDurationTime(e.target.value)} /> s
+                                    <input className="presBox-input" type="number" readOnly={true} value={duration} onKeyDown={e => e.stopPropagation()} onChange={e => this.updateDurationTime(e.target.value)} /> s
                                 </div>
                                 <div className="ribbon-propertyUpDown">
                                     <div className="ribbon-propertyUpDownItem" onClick={() => this.updateDurationTime(String(duration), 1000)}>
@@ -1654,7 +1655,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                         <div className="ribbon-doubleButton" style={{ display: activeItem.presMovement === PresMovement.Zoom ? 'inline-flex' : 'none' }}>
                             <div className="presBox-subheading">Zoom (% screen filled)</div>
                             <div className="ribbon-property">
-                                <input className="presBox-input" type="number" value={zoom} onChange={e => this.updateZoom(e.target.value)} />%
+                                <input className="presBox-input" type="number" readOnly={true} value={zoom} onChange={e => this.updateZoom(e.target.value)} />%
                             </div>
                             <div className="ribbon-propertyUpDown">
                                 <div className="ribbon-propertyUpDownItem" onClick={() => this.updateZoom(String(zoom), 0.1)}>
@@ -1669,7 +1670,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                         <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}>
                             <div className="presBox-subheading">Transition Time</div>
                             <div className="ribbon-property">
-                                <input className="presBox-input" type="number" value={transitionSpeed} onKeyDown={e => e.stopPropagation()} onChange={action(e => this.updateTransitionTime(e.target.value))} /> s
+                                <input className="presBox-input" type="number" readOnly={true} value={transitionSpeed} onKeyDown={e => e.stopPropagation()} onChange={action(e => this.updateTransitionTime(e.target.value))} /> s
                             </div>
                             <div className="ribbon-propertyUpDown">
                                 <div className="ribbon-propertyUpDownItem" onClick={() => this.updateTransitionTime(String(transitionSpeed), 1000)}>
@@ -1756,6 +1757,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                             className="presBox-input"
                                             style={{ textAlign: 'center', width: '100%', height: 15, fontSize: 10 }}
                                             type="number"
+                                            readOnly={true}
                                             value={NumCast(activeItem.presStartTime).toFixed(2)}
                                             onKeyDown={e => e.stopPropagation()}
                                             onChange={action((e: React.ChangeEvent<HTMLInputElement>) => {
@@ -1782,6 +1784,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                             onKeyDown={e => e.stopPropagation()}
                                             style={{ textAlign: 'center', width: '100%', height: 15, fontSize: 10 }}
                                             type="number"
+                                            readOnly={true}
                                             value={NumCast(activeItem.presEndTime).toFixed(2)}
                                             onChange={action((e: React.ChangeEvent<HTMLInputElement>) => {
                                                 activeItem.presEndTime = Number(e.target.value);
diff --git a/src/client/views/nodes/trails/PresElementBox.tsx b/src/client/views/nodes/trails/PresElementBox.tsx
index 4eb6aee25..2279ffe84 100644
--- a/src/client/views/nodes/trails/PresElementBox.tsx
+++ b/src/client/views/nodes/trails/PresElementBox.tsx
@@ -1,6 +1,6 @@
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Tooltip } from '@material-ui/core';
-import { action, computed, IReactionDisposer, observable, reaction } from 'mobx';
+import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import { Doc, DocListCast, HeightSym, Opt, WidthSym } from '../../../../fields/Doc';
 import { Id } from '../../../../fields/FieldSymbols';
@@ -13,7 +13,7 @@ import { DocumentManager } from '../../../util/DocumentManager';
 import { DragManager } from '../../../util/DragManager';
 import { SettingsManager } from '../../../util/SettingsManager';
 import { Transform } from '../../../util/Transform';
-import { undoBatch } from '../../../util/UndoManager';
+import { undoable, undoBatch } from '../../../util/UndoManager';
 import { ViewBoxBaseComponent } from '../../DocComponent';
 import { EditableView } from '../../EditableView';
 import { Colors } from '../../global/globalEnums';
@@ -263,16 +263,15 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
         }
     };
 
-    @undoBatch
-    removeItem = action((e: React.MouseEvent) => {
+    removePresentationItem = undoable((e: React.MouseEvent) => {
         e.stopPropagation();
         if (this.presBox && this.indexInPres < (this.presBoxView?.itemIndex || 0)) {
-            this.presBox.itemIndex = (this.presBoxView?.itemIndex || 0) - 1;
+            runInAction(() => (this.presBox!.itemIndex = (this.presBoxView?.itemIndex || 0) - 1));
         }
         this.props.removeDocument?.(this.rootDoc);
         this.presBoxView?.removeFromSelectedArray(this.rootDoc);
         this.removeAllRecordingInOverlay();
-    });
+    }, 'Remove doc from pres trail');
 
     // set the value/title of the individual pres element
     @undoBatch
@@ -476,7 +475,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
         );
         items.push(
             <Tooltip key="trash" title={<div className="dash-tooltip">Remove from presentation</div>}>
-                <div className={'slideButton'} onClick={this.removeItem}>
+                <div className={'slideButton'} onClick={this.removePresentationItem}>
                     <FontAwesomeIcon icon={'trash'} onPointerDown={e => e.stopPropagation()} />
                 </div>
             </Tooltip>
diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx
index d6dddf71a..5480600b0 100644
--- a/src/client/views/pdf/AnchorMenu.tsx
+++ b/src/client/views/pdf/AnchorMenu.tsx
@@ -395,22 +395,25 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
             ) : (
                 <>
                     <Tooltip key="trash" title={<div className="dash-tooltip">Remove Link Anchor</div>}>
-                        <button className="antimodeMenu-button" onPointerDown={this.Delete}>
+                        <button className="antimodeMenu-button" style={{ display: this.Delete === returnFalse ? 'none' : undefined }} onPointerDown={this.Delete}>
                             <FontAwesomeIcon icon="trash-alt" size="lg" />
                         </button>
                     </Tooltip>
                     <Tooltip key="Pin" title={<div className="dash-tooltip">Pin to Presentation</div>}>
-                        <button className="antimodeMenu-button" onPointerDown={this.PinToPres}>
+                        <button className="antimodeMenu-button" style={{ display: this.PinToPres === returnFalse ? 'none' : undefined }} onPointerDown={this.PinToPres}>
                             <FontAwesomeIcon icon="map-pin" size="lg" />
                         </button>
                     </Tooltip>
                     <Tooltip key="trail" title={<div className="dash-tooltip">Show Linked Trail</div>}>
-                        <button className="antimodeMenu-button" onPointerDown={this.ShowTargetTrail}>
+                        <button className="antimodeMenu-button" style={{ display: this.ShowTargetTrail === returnFalse ? 'none' : undefined }} onPointerDown={this.ShowTargetTrail}>
                             <FontAwesomeIcon icon="taxi" size="lg" />
                         </button>
                     </Tooltip>
                     <Tooltip key="toggle" title={<div className="dash-tooltip">make target visibility toggle on click</div>}>
-                        <button className="antimodeMenu-button" style={{ color: this.IsTargetToggler() ? 'black' : 'white', backgroundColor: this.IsTargetToggler() ? 'white' : 'black' }} onPointerDown={this.MakeTargetToggle}>
+                        <button
+                            className="antimodeMenu-button"
+                            style={{ display: this.IsTargetToggler === returnFalse ? 'none' : undefined, color: this.IsTargetToggler() ? 'black' : 'white', backgroundColor: this.IsTargetToggler() ? 'white' : 'black' }}
+                            onPointerDown={this.MakeTargetToggle}>
                             <FontAwesomeIcon icon="thumbtack" size="lg" />
                         </button>
                     </Tooltip>
diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx
index 1c1b41f73..3479cd20f 100644
--- a/src/client/views/search/SearchBox.tsx
+++ b/src/client/views/search/SearchBox.tsx
@@ -141,6 +141,8 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() {
                 const annos = !Field.toString(Doc.LayoutField(d) as Field).includes('CollectionView');
                 const data = d[annos ? fieldKey + '_annotations' : fieldKey];
                 data && newarray.push(...DocListCast(data));
+                const sidebar = d[fieldKey + '_sidebar'];
+                sidebar && newarray.push(...DocListCast(sidebar));
                 func(depth, d);
             });
             docs = newarray;
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 5312da009..d8447deb6 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -236,7 +236,7 @@ export class Doc extends RefField {
         if (
             doc &&
             Doc.MyFileOrphans instanceof Doc &&
-            Doc.IsDocDataProto(doc) &&
+            Doc.IsDataProto(doc) &&
             !Doc.IsSystem(doc) &&
             ![DocumentType.CONFIG, DocumentType.KVP, DocumentType.LINK, DocumentType.LINKANCHOR].includes(doc.type as any) &&
             !doc.isFolder &&
@@ -510,7 +510,7 @@ export namespace Doc {
     export function GetT<T extends Field>(doc: Doc, key: string, ctor: ToConstructor<T>, ignoreProto: boolean = false): FieldResult<T> {
         return Cast(Get(doc, key, ignoreProto), ctor) as FieldResult<T>;
     }
-    export function IsDocDataProto(doc: Doc) {
+    export function IsDataProto(doc: Doc) {
         return GetT(doc, 'isDataDoc', 'boolean', true);
     }
     export function IsBaseProto(doc: Doc) {
@@ -981,7 +981,7 @@ export namespace Doc {
     export function MakeCopy(doc: Doc, copyProto: boolean = false, copyProtoId?: string, retitle = false): Doc {
         const copy = new Doc(copyProtoId, true);
         updateCachedAcls(copy);
-        const exclude = [...Cast(doc.cloneFieldFilter, listSpec('string'), []), 'dragFactory_count', 'cloneFieldFilter'];
+        const exclude = [...StrListCast(doc.cloneFieldFilter), 'dragFactory_count', 'cloneFieldFilter'];
         Object.keys(doc).forEach(key => {
             if (exclude.includes(key)) return;
             const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key]));
@@ -1433,7 +1433,7 @@ export namespace Doc {
     export function setDocFilter(container: Opt<Doc>, key: string, value: any, modifiers: 'remove' | 'match' | 'check' | 'x' | 'exists' | 'unset', toggle?: boolean, fieldPrefix?: string, append: boolean = true) {
         if (!container) return;
         const filterField = '_' + (fieldPrefix ? fieldPrefix + '_' : '') + 'docFilters';
-        const docFilters = Cast(container[filterField], listSpec('string'), []);
+        const docFilters = StrListCast(container[filterField]);
         runInAction(() => {
             for (let i = 0; i < docFilters.length; i++) {
                 const fields = docFilters[i].split(':'); // split key:value:modifier
@@ -1745,6 +1745,16 @@ ScriptingGlobals.add(function undo() {
     SelectionManager.DeselectAll();
     return UndoManager.Undo();
 });
+
+export function ShowUndoStack() {
+    SelectionManager.DeselectAll();
+    var buffer = '';
+    UndoManager.undoStack.forEach((batch, i) => {
+        buffer += 'Batch => ' + UndoManager.undoStackNames[i] + '\n';
+        ///batch.forEach(undo => (buffer += '   ' + undo.prop + '\n'));
+    });
+    alert(buffer);
+}
 ScriptingGlobals.add(function redo() {
     SelectionManager.DeselectAll();
     return UndoManager.Redo();
diff --git a/src/fields/util.ts b/src/fields/util.ts
index f4fd3200c..a2b445d6c 100644
--- a/src/fields/util.ts
+++ b/src/fields/util.ts
@@ -65,6 +65,7 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number
     if (value instanceof RefField) {
         value = new ProxyField(value);
     }
+
     if (value instanceof ObjectField) {
         if (value[Parent] && value[Parent] !== receiver && !(value instanceof PrefetchProxy)) {
             throw new Error("Can't put the same object in multiple documents at the same time");
@@ -102,20 +103,24 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number
             DocServer.registerDocWithCachedUpdate(receiver, prop as string, curValue);
         }
         !receiver[Initializing] &&
+            !receiver.dontUndo &&
             (!receiver[UpdatingFromServer] || receiver[ForceServerWrite]) &&
-            UndoManager.AddEvent({
-                redo: () => (receiver[prop] = value),
-                undo: () => {
-                    const wasUpdate = receiver[UpdatingFromServer];
-                    const wasForce = receiver[ForceServerWrite];
-                    receiver[ForceServerWrite] = true; // needed since writes aren't propagated to server if UpdatingFromServerIsSet
-                    receiver[UpdatingFromServer] = true; // needed if the event caused ACL's to change such that the doc is otherwise no longer editable.
-                    receiver[prop] = curValue;
-                    receiver[ForceServerWrite] = wasForce;
-                    receiver[UpdatingFromServer] = wasUpdate;
+            UndoManager.AddEvent(
+                {
+                    redo: () => (receiver[prop] = value),
+                    undo: () => {
+                        const wasUpdate = receiver[UpdatingFromServer];
+                        const wasForce = receiver[ForceServerWrite];
+                        receiver[ForceServerWrite] = true; // needed since writes aren't propagated to server if UpdatingFromServerIsSet
+                        receiver[UpdatingFromServer] = true; // needed if the event caused ACL's to change such that the doc is otherwise no longer editable.
+                        receiver[prop] = curValue;
+                        receiver[ForceServerWrite] = wasForce;
+                        receiver[UpdatingFromServer] = wasUpdate;
+                    },
+                    prop: prop?.toString(),
                 },
-                prop: prop?.toString(),
-            });
+                value
+            );
         return true;
     }
     return false;
@@ -390,6 +395,7 @@ export function updateFunction(target: any, prop: any, value: any, receiver: any
                     diff?.op === '$addToSet'
                         ? {
                               redo: () => {
+                                  console.log('redo $add: ' + prop, diff.items); // bcz: uncomment to log undo
                                   receiver[prop].push(...diff.items.map((item: any) => item.value ?? item));
                                   lastValue = ObjectField.MakeCopy(receiver[prop]);
                               },
@@ -406,11 +412,12 @@ export function updateFunction(target: any, prop: any, value: any, receiver: any
                                   });
                                   lastValue = ObjectField.MakeCopy(receiver[prop]);
                               }),
-                              prop: '',
+                              prop: 'add ' + diff.items.length + ' items to list',
                           }
                         : diff?.op === '$remFromSet'
                         ? {
                               redo: action(() => {
+                                  console.log('redo $rem: ' + prop, diff.items); // bcz: uncomment to log undo
                                   diff.items.forEach((item: any) => {
                                       const ind = item instanceof SchemaHeaderField ? receiver[prop].findIndex((ele: any) => ele instanceof SchemaHeaderField && ele.heading === item.heading) : receiver[prop].indexOf(item.value ?? item);
                                       ind !== -1 && receiver[prop].splice(ind, 1);
@@ -430,10 +437,11 @@ export function updateFunction(target: any, prop: any, value: any, receiver: any
                                   });
                                   lastValue = ObjectField.MakeCopy(receiver[prop]);
                               },
-                              prop: '',
+                              prop: 'remove ' + diff.items.length + ' items from list',
                           }
                         : {
                               redo: () => {
+                                  console.log('redo list: ' + prop, receiver[prop]); // bcz: uncomment to log undo
                                   receiver[prop] = ObjectField.MakeCopy(newValue as List<any>);
                                   lastValue = ObjectField.MakeCopy(receiver[prop]);
                               },
@@ -442,7 +450,7 @@ export function updateFunction(target: any, prop: any, value: any, receiver: any
                                   receiver[prop] = ObjectField.MakeCopy(prevValue as List<any>);
                                   lastValue = ObjectField.MakeCopy(receiver[prop]);
                               },
-                              prop: '',
+                              prop: 'assign list',
                           }
                 );
         }
-- 
cgit v1.2.3-70-g09d2

