From 51d7ce5f71465f2f578a08a998b2df353242ff4d Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 5 Apr 2022 13:54:26 -0400
Subject: fixes to allow dragging golden layout windows to be aborted with
 'esc'.  some code cleanup as well to avoid rebuilding golden layout when tabs
 are closed.  Fixes for undo and goldenlayout

---
 .../views/collections/CollectionDockingView.tsx    | 109 +++++++++++++--------
 1 file changed, 70 insertions(+), 39 deletions(-)

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

diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 9e8374605..6931d9896 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -11,6 +11,7 @@ import { List } from '../../../fields/List';
 import { listSpec } from '../../../fields/Schema';
 import { Cast, NumCast, StrCast } from "../../../fields/Types";
 import { inheritParentAcls } from '../../../fields/util';
+import { emptyFunction } from '../../../Utils';
 import { DocServer } from "../../DocServer";
 import { Docs } from '../../documents/Documents';
 import { DocumentType } from '../../documents/DocumentTypes';
@@ -46,7 +47,7 @@ export class CollectionDockingView extends CollectionSubView() {
     private _reactionDisposer?: IReactionDisposer;
     private _lightboxReactionDisposer?: IReactionDisposer;
     private _containerRef = React.createRef<HTMLDivElement>();
-    private _flush: UndoManager.Batch | undefined;
+    public _flush: UndoManager.Batch | undefined;
     private _ignoreStateChange = "";
     public tabMap: Set<any> = new Set();
     public get initialized() { return this._goldenLayout !== null; }
@@ -62,15 +63,37 @@ export class CollectionDockingView extends CollectionSubView() {
         DragManager.StartWindowDrag = this.StartOtherDrag;
     }
 
-    public StartOtherDrag = (e: any, dragDocs: Doc[]) => {
-        !this._flush && (this._flush = UndoManager.StartBatch("golden layout drag"));
+    /**
+     * Switches from dragging a document around a freeform canvas to dragging it as a tab to be docked.
+     * 
+     * @param e fake mouse down event position data containing pageX and pageY coordinates
+     * @param dragDocs the documents to be dragged
+     * @param batch optionally an undo batch that has been started to use instead of starting a new batch
+    */
+    public StartOtherDrag = (e: { pageX: number, pageY: number }, dragDocs: Doc[], finishDrag?: (aborted: boolean) => void) => {
+        this._flush = this._flush ?? UndoManager.StartBatch("golden layout drag");
         const config = dragDocs.length === 1 ? CollectionDockingView.makeDocumentConfig(dragDocs[0]) :
-            { type: 'row', content: dragDocs.map((doc, i) => CollectionDockingView.makeDocumentConfig(doc)) };
+            { type: 'row', content: dragDocs.map(doc => CollectionDockingView.makeDocumentConfig(doc)) };
         const dragSource = this._goldenLayout.createDragSource(document.createElement("div"), config);
-        //dragSource._dragListener.on("dragStop", dragSource.destroy);
-        dragSource._dragListener.onMouseDown(e);
+        this.tabDragStart(dragSource, finishDrag);
+        dragSource._dragListener.onMouseDown({ pageX: e.pageX, pageY: e.pageY, preventDefault: emptyFunction, button: 0 });
     }
 
+    tabItemDropped = () => DragManager.CompleteWindowDrag?.(false);
+    tabDragStart = (proxy: any, finishDrag?: (aborted: boolean) => void) => {
+        DragManager.CompleteWindowDrag = (aborted: boolean) => {
+            if (aborted) {
+                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);
+        }
+    }
     @undoBatch
     public CloseFullScreen = () => {
         this._goldenLayout._maximisedItem?.toggleMaximise();
@@ -106,7 +129,6 @@ export class CollectionDockingView extends CollectionSubView() {
         docconfig.callDownwards('_$init');
         instance._goldenLayout._$maximiseItem(docconfig);
         instance._goldenLayout.emit('stateChanged');
-        instance._ignoreStateChange = JSON.stringify(instance._goldenLayout.toConfig());
         instance.stateChanged();
         return true;
     }
@@ -255,7 +277,6 @@ export class CollectionDockingView extends CollectionSubView() {
     layoutChanged() {
         this._goldenLayout.root.callDownwards('setSize', [this._goldenLayout.width, this._goldenLayout.height]);
         this._goldenLayout.emit('stateChanged');
-        this._ignoreStateChange = JSON.stringify(this._goldenLayout.toConfig());
         this.stateChanged();
         return true;
     }
@@ -294,6 +315,9 @@ export class CollectionDockingView extends CollectionSubView() {
                 }
             }
             this._goldenLayout.init();
+            this._goldenLayout.root.layoutManager.on('itemDropped', this.tabItemDropped);
+            this._goldenLayout.root.layoutManager.on('dragStart', this.tabDragStart)
+            this._goldenLayout.root.layoutManager.on('activeContentItemChanged', this.stateChanged);
         }
     }
 
@@ -335,13 +359,12 @@ export class CollectionDockingView extends CollectionSubView() {
     @action
     onPointerUp = (e: MouseEvent): void => {
         window.removeEventListener("pointerup", this.onPointerUp);
-        if (this._flush) {
-            setTimeout(() => {
-                CollectionDockingView.Instance._ignoreStateChange = JSON.stringify(CollectionDockingView.Instance._goldenLayout.toConfig());
-                this.stateChanged();
-                this._flush?.end();
-                this._flush = undefined;
-            }, 10);
+        const flush = this._flush;
+        this._flush = undefined;
+        if (flush) {
+            DragManager.CompleteWindowDrag = undefined;
+            if (!this.stateChanged()) flush.cancel();
+            else flush.end();
         }
     }
 
@@ -352,9 +375,12 @@ export class CollectionDockingView extends CollectionSubView() {
             hitFlyout = (par.className === "dockingViewButtonSelector");
         }
         if (!hitFlyout) {
+            const htmlTarget = e.target as HTMLElement;
             window.addEventListener("mouseup", this.onPointerUp);
-            if (!(e.target as HTMLElement).closest("*.lm_content") && ((e.target as HTMLElement).closest("*.lm_tab") || (e.target as HTMLElement).closest("*.lm_stack"))) {
-                this._flush = UndoManager.StartBatch("golden layout edit");
+            if (!htmlTarget.closest("*.lm_content") && (htmlTarget.closest("*.lm_tab") || htmlTarget.closest("*.lm_stack"))) {
+                if (htmlTarget.className !== "lm_close_tab") {
+                    this._flush = UndoManager.StartBatch("golden layout edit");
+                }
             }
         }
         if (!e.nativeEvent.cancelBubble && !InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) && !InteractionUtils.IsType(e, InteractionUtils.PENTYPE) &&
@@ -388,38 +414,43 @@ export class CollectionDockingView extends CollectionSubView() {
 
     @action
     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);
-
-        this.props.Document.dockingConfig = json;
-        setTimeout(async () => {
-            const sublists = await DocListCastAsync(this.props.Document[this.props.fieldKey]);
-            const tabs = sublists && Cast(sublists[0], Doc, null);
-            // const other = sublists && Cast(sublists[1], Doc, null);
-            const tabdocs = await DocListCastAsync(tabs?.data);
-            // const otherdocs = await DocListCastAsync(other?.data);
-            if (tabs) {
-                tabs.data = new List<Doc>(docs);
-                // DocListCast(tabs.aliases).forEach(tab => tab !== tabs && (tab.data = new List<Doc>(docs)));
-            }
-            // const otherSet = new Set<Doc>();
-            // otherdocs?.filter(doc => !docs.includes(doc)).forEach(doc => otherSet.add(doc));
-            // tabdocs?.filter(doc => !docs.includes(doc) && doc.type !== DocumentType.KVP).forEach(doc => otherSet.add(doc));
-            // const vals = Array.from(otherSet.values()).filter(val => val instanceof Doc).map(d => d).filter(d => d.type !== DocumentType.KVP);
-            // this.props.Document[DataSym][this.props.fieldKey + "-all"] = new List<Doc>([...docs, ...vals]);
-            // if (other) {
-            //     other.data = new List<Doc>(vals);
-            //     // DocListCast(other.aliases).forEach(tab => tab !== other && (tab.data = new List<Doc>(vals)));
-            // }
-        }, 0);
+        const changesMade = this.props.Document.dockingConfig !== json;
+        if (changesMade && !this._flush) {
+            this.props.Document.dockingConfig = json;
+            setTimeout(async () => {
+                const sublists = await DocListCastAsync(this.props.Document[this.props.fieldKey]);
+                const tabs = sublists && Cast(sublists[0], Doc, null);
+                // const other = sublists && Cast(sublists[1], Doc, null);
+                const tabdocs = await DocListCastAsync(tabs?.data);
+                // const otherdocs = await DocListCastAsync(other?.data);
+                if (tabs) {
+                    tabs.data = new List<Doc>(docs);
+                    // DocListCast(tabs.aliases).forEach(tab => tab !== tabs && (tab.data = new List<Doc>(docs)));
+                }
+                // const otherSet = new Set<Doc>();
+                // otherdocs?.filter(doc => !docs.includes(doc)).forEach(doc => otherSet.add(doc));
+                // tabdocs?.filter(doc => !docs.includes(doc) && doc.type !== DocumentType.KVP).forEach(doc => otherSet.add(doc));
+                // const vals = Array.from(otherSet.values()).filter(val => val instanceof Doc).map(d => d).filter(d => d.type !== DocumentType.KVP);
+                // this.props.Document[DataSym][this.props.fieldKey + "-all"] = new List<Doc>([...docs, ...vals]);
+                // if (other) {
+                //     other.data = new List<Doc>(vals);
+                //     // DocListCast(other.aliases).forEach(tab => tab !== other && (tab.data = new List<Doc>(vals)));
+                // }
+            }, 0);
+        }
+        return changesMade;
     }
 
     tabDestroyed = (tab: any) => {
         this.tabMap.delete(tab);
         tab._disposers && Object.values(tab._disposers).forEach((disposer: any) => disposer?.());
         tab.reactComponents?.forEach((ele: any) => ReactDOM.unmountComponentAtNode(ele));
+        this.stateChanged();
     }
     tabCreated = (tab: any) => {
         tab.contentItem.element[0]?.firstChild?.firstChild?.InitTab?.(tab);  // have to explicitly initialize tabs that reuse contents from previous tabs (ie, when dragging a tab around a new tab is created for the old content)
-- 
cgit v1.2.3-70-g09d2


From fd58a5f8598815ab1ab3893e60973654a59a52c1 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 12 Apr 2022 10:54:28 -0400
Subject: cleaned up errors/warnings.   fixed dragging to turn off pointer
 events on whatever's being dragged.

---
 src/client/util/DocumentManager.ts                             | 10 ++++++----
 src/client/util/DragManager.ts                                 |  2 +-
 src/client/views/StyleProvider.tsx                             |  4 +++-
 src/client/views/collections/CollectionDockingView.tsx         |  4 ++--
 .../collections/collectionFreeForm/CollectionFreeFormView.tsx  |  2 +-
 src/client/views/nodes/CollectionFreeFormDocumentView.tsx      |  4 ++--
 src/client/views/nodes/LabelBox.tsx                            |  7 +------
 src/client/views/nodes/PDFBox.tsx                              |  6 +++---
 src/client/views/nodes/VideoBox.tsx                            |  2 +-
 src/client/views/nodes/formattedText/FormattedTextBox.tsx      |  2 +-
 10 files changed, 21 insertions(+), 22 deletions(-)

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

diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 6076de055..1d603e18f 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -169,10 +169,12 @@ export class DocumentManager {
         const getFirstDocView = LightboxView.LightboxDoc ? DocumentManager.Instance.getLightboxDocumentView : DocumentManager.Instance.getFirstDocumentView;
         const docView = getFirstDocView(targetDoc, originatingDoc);
         const wasHidden = targetDoc.hidden;  // 
-        if (wasHidden) runInAction(() => {
-            targetDoc.hidden = false;
-            docView?.props.bringToFront(targetDoc);
-        });  // if the target is hidden, un-hide it here.
+        if (wasHidden) {
+            runInAction(() => {
+                targetDoc.hidden = false;
+                docView?.props.bringToFront(targetDoc);
+            });  // if the target is hidden, un-hide it here.
+        }
         const focusAndFinish = (didFocus: boolean) => {
             if (originatingDoc?.isPushpin) {
                 if (!didFocus && !wasHidden) {  // don't toggle the hidden state if the doc was already un-hidden as part of this document traversal
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 94a09eac4..13e5940d6 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -405,7 +405,7 @@ export namespace DragManager {
                 Array.from(pdfBox).filter(pb => pb.width && pb.height).map((pb, i) => pb.getContext('2d')!.drawImage(pdfBoxSrc[i], 0, 0));
             }
             [dragElement, ...Array.from(dragElement.getElementsByTagName('*'))].forEach(ele =>
-                ele.hasAttribute("style") && ((ele as any).style.pointerEvents = "none"));
+                (ele as any).style && ((ele as any).style.pointerEvents = "none"));
 
             dragDiv.appendChild(dragElement);
             if (dragElement !== ele) {
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index 2ce78f64f..9803f9782 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -149,7 +149,9 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
                                     (doc?._isGroup ? undefined :
                                         StrCast((props?.renderDepth || 0) > 0 ?
                                             Doc.UserDoc().activeCollectionNestedBackground :
-                                            Doc.UserDoc().activeCollectionBackground ?? (darkScheme() ? Colors.BLACK : Colors.WHITE))
+                                            Doc.UserDoc().activeCollectionBackground ?? (darkScheme() ?
+                                                Colors.BLACK :
+                                                "linear-gradient(#065fff, #85c1f9)"))
                                     ));
                     break;
                 //if (doc._viewType !== CollectionViewType.Freeform && doc._viewType !== CollectionViewType.Time) return "rgb(62,62,62)";
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 6931d9896..e95371aaa 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -92,7 +92,7 @@ export class CollectionDockingView extends CollectionSubView() {
                 DragManager.CompleteWindowDrag = undefined;
             }
             finishDrag?.(aborted);
-        }
+        };
     }
     @undoBatch
     public CloseFullScreen = () => {
@@ -316,7 +316,7 @@ export class CollectionDockingView extends CollectionSubView() {
             }
             this._goldenLayout.init();
             this._goldenLayout.root.layoutManager.on('itemDropped', this.tabItemDropped);
-            this._goldenLayout.root.layoutManager.on('dragStart', this.tabDragStart)
+            this._goldenLayout.root.layoutManager.on('dragStart', this.tabDragStart);
             this._goldenLayout.root.layoutManager.on('activeContentItemChanged', this.stateChanged);
         }
     }
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 04beed541..9b0acffdf 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1417,7 +1417,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     updateIcon = () => {
         this.props.docViewPath().lastElement().ContentDiv!.style.width = (this.layoutDoc[WidthSym]()).toString();
         this.props.docViewPath().lastElement().ContentDiv!.style.height = (this.layoutDoc[HeightSym]()).toString();
-        var htmlString = this._mainCont && new XMLSerializer().serializeToString(this.props.docViewPath().lastElement().ContentDiv!);
+        const htmlString = this._mainCont && new XMLSerializer().serializeToString(this.props.docViewPath().lastElement().ContentDiv!);
         this.props.docViewPath().lastElement().ContentDiv!.style.width = "";
         this.props.docViewPath().lastElement().ContentDiv!.style.height = "";
         const nativeWidth = this.layoutDoc[WidthSym]();
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 7fb2c235a..3e35039e3 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -168,8 +168,8 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
             PanelWidth: this.panelWidth,
             PanelHeight: this.panelHeight,
         };
-        var background = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor);
-        const mixBlendMode = StrCast(this.layoutDoc.mixBlendMode) as any || (typeof background === "string" && background && DashColor(background).alpha() !== 1 ? "multiply" : undefined);
+        const background = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor);
+        const mixBlendMode = StrCast(this.layoutDoc.mixBlendMode) as any || (typeof background === "string" && background && (!background.startsWith("linear") && DashColor(background).alpha() !== 1) ? "multiply" : undefined);
         return <div className={"collectionFreeFormDocumentView-container"}
             style={{
                 outline: this.Highlight ? "orange solid 2px" : "",
diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx
index 3405a7635..d539ca9b8 100644
--- a/src/client/views/nodes/LabelBox.tsx
+++ b/src/client/views/nodes/LabelBox.tsx
@@ -30,11 +30,6 @@ export class LabelBox extends ViewBoxBaseComponent<(FieldViewProps & LabelBoxPro
 
     componentDidMount() {
         this.props.setContentView?.(this);
-        console.log("MOUNTING")
-    }
-
-    componentDidUpdate() {
-        console.log("UPDATING")
     }
 
     getTitle() {
@@ -89,7 +84,7 @@ export class LabelBox extends ViewBoxBaseComponent<(FieldViewProps & LabelBoxPro
             verticalAlign: "center",
             textAlign: "center",
             whiteSpace: "nowrap"
-        })
+        });
     }
     // (!missingParams || !missingParams.length ? "" : "(" + missingParams.map(m => m + ":").join(" ") + ")")
     render() {
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 3f1771e68..c73823a2b 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -63,7 +63,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
         }
         if (oldDiv instanceof HTMLCanvasElement) {
             const canvas = oldDiv as HTMLCanvasElement;
-            var img = document.createElement('img'); // create a Image Element
+            const img = document.createElement('img'); // create a Image Element
             img.src = canvas.toDataURL();   //image source
             img.style.width = canvas.style.width;
             img.style.height = canvas.style.height;
@@ -79,8 +79,8 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
         const newDiv = docViewContent.cloneNode(true) as HTMLDivElement;
         newDiv.style.width = (this.layoutDoc[WidthSym]()).toString();
         newDiv.style.height = (this.layoutDoc[HeightSym]()).toString();
-        this.replaceCanvases(docViewContent, newDiv)
-        var htmlString = this._pdfViewer?._mainCont.current && new XMLSerializer().serializeToString(newDiv);
+        this.replaceCanvases(docViewContent, newDiv);
+        const htmlString = this._pdfViewer?._mainCont.current && new XMLSerializer().serializeToString(newDiv);
         const nativeWidth = this.layoutDoc[WidthSym]();
         const nativeHeight = this.layoutDoc[HeightSym]();
 
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index b7b8ce064..c31d38b0d 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -265,7 +265,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
             this.dataDoc.icon = new ImageField(returnedfilename);
             this.dataDoc["icon-nativeWidth"] = this.layoutDoc[WidthSym]();
             this.dataDoc["icon-nativeHeight"] = this.layoutDoc[HeightSym]();
-        }
+        };
         this.Snapshot(undefined, undefined, makeIcon);
     }
 
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index ec88b8d1d..b6713d878 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -364,7 +364,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
     }
 
     updateTitle = () => {
-        const title = StrCast(this.dataDoc.title)
+        const title = StrCast(this.dataDoc.title);
         if (!this.props.dontRegisterView &&  // (this.props.Document.isTemplateForField === "text" || !this.props.Document.isTemplateForField) && // only update the title if the data document's data field is changing
             (title.startsWith("-") || title.startsWith("@")) && this._editorView && !this.dataDoc["title-custom"] &&
             (Doc.LayoutFieldKey(this.rootDoc) === this.fieldKey || this.fieldKey === "text")) {
-- 
cgit v1.2.3-70-g09d2


From 5f297a17b38bdcd4d3928137daacf14fe2fbec9f Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 25 Apr 2022 21:45:20 -0400
Subject: put snapshot dash in novice mode.  fixed undo for snapshot/new
 dashboard.  changed titling of snapshots to use {#}.  no current UI for
 cloning dashboards.

---
 src/Utils.ts                                       |  6 +++++
 .../views/collections/CollectionDockingView.tsx    |  7 +++---
 src/client/views/topbar/TopBar.tsx                 | 26 +++++++++++++---------
 src/fields/Doc.ts                                  |  7 ++++--
 4 files changed, 30 insertions(+), 16 deletions(-)

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

diff --git a/src/Utils.ts b/src/Utils.ts
index d1d400de7..38eb51529 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -399,6 +399,12 @@ export function timenow() {
     return now.toLocaleDateString() + ' ' + h + ':' + m + ' ' + ampm;
 }
 
+export function incrementTitleCopy(title: string) {
+    const numstr = title.match(/.*(\{([0-9]*)\})+/);
+    const copyNumStr = `{${1 + (numstr ? (+numstr[2]) : 0)}}`;
+    return (numstr ? title.replace(numstr[1], "") : title) + copyNumStr;
+}
+
 export function formatTime(time: number) {
     time = Math.round(time);
     const hours = Math.floor(time / 60 / 60);
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index e95371aaa..64a5f38d4 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -11,7 +11,7 @@ import { List } from '../../../fields/List';
 import { listSpec } from '../../../fields/Schema';
 import { Cast, NumCast, StrCast } from "../../../fields/Types";
 import { inheritParentAcls } from '../../../fields/util';
-import { emptyFunction } from '../../../Utils';
+import { emptyFunction, incrementTitleCopy } from '../../../Utils';
 import { DocServer } from "../../DocServer";
 import { Docs } from '../../documents/Documents';
 import { DocumentType } from '../../documents/DocumentTypes';
@@ -390,7 +390,6 @@ export class CollectionDockingView extends CollectionSubView() {
     }
 
     public static async Copy(doc: Doc, clone = false) {
-        clone = !Doc.UserDoc().noviceMode;
         let json = StrCast(doc.dockingConfig);
         if (clone) {
             const cloned = (await Doc.MakeClone(doc));
@@ -403,13 +402,13 @@ export class CollectionDockingView extends CollectionSubView() {
         const origtabs = origtabids.map(id => DocServer.GetCachedRefField(id)).filter(f => f).map(f => f as Doc);
         const newtabs = origtabs.map(origtab => {
             const origtabdocs = DocListCast(origtab.data);
-            const newtab = origtabdocs.length ? Doc.MakeCopy(origtab, true) : Doc.MakeAlias(origtab);
+            const newtab = origtabdocs.length ? Doc.MakeCopy(origtab, true, undefined, true) : Doc.MakeAlias(origtab);
             const newtabdocs = origtabdocs.map(origtabdoc => Doc.MakeAlias(origtabdoc));
             newtabdocs.length && (Doc.GetProto(newtab).data = new List<Doc>(newtabdocs));
             json = json.replace(origtab[Id], newtab[Id]);
             return newtab;
         });
-        return Docs.Create.DockDocument(newtabs, json, { title: "Snapshot: " + doc.title });
+        return Docs.Create.DockDocument(newtabs, json, { title: incrementTitleCopy(StrCast(doc.title)) });
     }
 
     @action
diff --git a/src/client/views/topbar/TopBar.tsx b/src/client/views/topbar/TopBar.tsx
index d5254e315..0af7de6af 100644
--- a/src/client/views/topbar/TopBar.tsx
+++ b/src/client/views/topbar/TopBar.tsx
@@ -7,7 +7,7 @@ import { StrCast } from '../../../fields/Types';
 import { Utils } from '../../../Utils';
 import { CurrentUserUtils } from "../../util/CurrentUserUtils";
 import { SettingsManager } from "../../util/SettingsManager";
-import { undoBatch } from "../../util/UndoManager";
+import { undoBatch, UndoManager } from "../../util/UndoManager";
 import { Borders, Colors } from "../global/globalEnums";
 import "./TopBar.scss";
 
@@ -33,30 +33,36 @@ export class TopBar extends React.Component {
                     </div>
                     <div className="topbar-center" >
                         <div className="topbar-lozenge-dashboard">
-                            <select className="topbar-dashSelect" onChange={e => CurrentUserUtils.openDashboard(Doc.UserDoc(), myDashboards[Number(e.target.value)])}
+                            <select className="topbar-dashSelect" onChange={undoBatch(e => CurrentUserUtils.openDashboard(Doc.UserDoc(), myDashboards[Number(e.target.value)]))}
                                 value={myDashboards.indexOf(CurrentUserUtils.ActiveDashboard)}
                                 style={{ color: Colors.WHITE }}>
                                 {myDashboards.map((dash, i) => <option key={dash[Id]} value={i}> {StrCast(dash.title)} </option>)}
                             </select>
                         </div>
                         <div className="topbar-dashboards">
-                            <div className="topbar-icon" onClick={undoBatch(() => CurrentUserUtils.createNewDashboard(Doc.UserDoc()))}
-                            >
-                                {"New"}<FontAwesomeIcon icon="plus"></FontAwesomeIcon>
+                            <div className="topbar-icon" onClick={async () => {
+                                const batch = UndoManager.StartBatch("new dash");
+                                await CurrentUserUtils.createNewDashboard(Doc.UserDoc());
+                                batch.end();
+                            }}>
+                                {"New"}<FontAwesomeIcon icon="plus" />
                             </div>
-                            {Doc.UserDoc().noviceMode ? (null) : <div className="topbar-icon" onClick={undoBatch(() => CurrentUserUtils.snapshotDashboard(Doc.UserDoc()))}
-                            >
-                                {"Snapshot"}<FontAwesomeIcon icon="camera"></FontAwesomeIcon>
+                            {<div className="topbar-icon" onClick={async () => {
+                                const batch = UndoManager.StartBatch("snapshot");
+                                await CurrentUserUtils.snapshotDashboard(Doc.UserDoc());
+                                batch.end();
+                            }}>
+                                {"Snapshot"}<FontAwesomeIcon icon="camera" />
                             </div>}
                         </div>
                     </div>
                     <div className="topbar-right" >
                         <div className="topbar-icon" onClick={() => window.open(
                             "https://brown-dash.github.io/Dash-Documentation/", "_blank")}>
-                            {"Help"}<FontAwesomeIcon icon="question-circle"></FontAwesomeIcon>
+                            {"Help"}<FontAwesomeIcon icon="question-circle" />
                         </div>
                         <div className="topbar-icon" onClick={() => SettingsManager.Instance.open()}>
-                            {"Settings"}<FontAwesomeIcon icon="cog"></FontAwesomeIcon>
+                            {"Settings"}<FontAwesomeIcon icon="cog" />
                         </div>
 
                     </div>
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 63af8401c..50e5fcbc4 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -11,7 +11,7 @@ import { scriptingGlobal, ScriptingGlobals } from "../client/util/ScriptingGloba
 import { SelectionManager } from "../client/util/SelectionManager";
 import { afterDocDeserialize, autoObject, Deserializable, SerializationHelper } from "../client/util/SerializationHelper";
 import { UndoManager } from "../client/util/UndoManager";
-import { DashColor, intersectRect, Utils } from "../Utils";
+import { DashColor, incrementTitleCopy, intersectRect, Utils } from "../Utils";
 import { DateField } from "./DateField";
 import { Copy, HandleUpdate, Id, OnUpdate, Parent, Self, SelfProxy, ToScriptString, ToString, Update } from "./FieldSymbols";
 import { List } from "./List";
@@ -770,7 +770,7 @@ export namespace Doc {
         return overwrite;
     }
 
-    export function MakeCopy(doc: Doc, copyProto: boolean = false, copyProtoId?: string): Doc {
+    export function MakeCopy(doc: Doc, copyProto: boolean = false, copyProtoId?: string, retitle = false): Doc {
         const copy = new Doc(copyProtoId, true);
         const exclude = Cast(doc.cloneFieldFilter, listSpec("string"), []);
         Object.keys(doc).forEach(key => {
@@ -806,6 +806,9 @@ export namespace Doc {
         }
         copy.context = undefined;
         Doc.UserDoc().defaultAclPrivate && (copy["acl-Public"] = "Not Shared");
+        if (retitle) {
+            copy.title = incrementTitleCopy(StrCast(copy.title));
+        }
         return copy;
     }
 
-- 
cgit v1.2.3-70-g09d2

