From f6b542b9ab015483393c605db60683c3883c3a39 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Mon, 22 Jun 2020 11:26:01 -0400
Subject: prevent users from deleting the activeWorspace with 'delete' menu
 item

---
 src/client/views/nodes/DocumentView.tsx | 9 ++++++++-
 1 file changed, 8 insertions(+), 1 deletion(-)

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

diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 345ca479f..9c8c05176 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -576,7 +576,14 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     }
 
     @undoBatch
-    deleteClicked = (): void => { SelectionManager.DeselectAll(); this.props.removeDocument?.(this.props.Document); }
+    deleteClicked = (): void => {
+        if (Doc.UserDoc().activeWorkspace === this.props.Document) {
+            alert("Can't delete the active workspace");
+        } else {
+            SelectionManager.DeselectAll();
+            this.props.removeDocument?.(this.props.Document);
+        }
+    }
 
 
     @undoBatch
-- 
cgit v1.2.3-70-g09d2


From b4b1a14f442bdfd41f79c67a6545335e43f85f2f Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Mon, 22 Jun 2020 11:37:47 -0400
Subject: fixed onClick events for text.

---
 src/client/views/nodes/DocumentView.tsx                   | 8 --------
 src/client/views/nodes/formattedText/FormattedTextBox.tsx | 5 +----
 2 files changed, 1 insertion(+), 12 deletions(-)

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

diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 9c8c05176..71cb18e08 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -323,20 +323,12 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                 const alias = Doc.MakeAlias(this.props.Document);
                 DocUtils.makeCustomViewClicked(alias, undefined, "onClick");
                 this.props.addDocTab(alias, "onRight");
-                // UndoManager.RunInBatch(() => Doc.makeCustomViewClicked(this.props.Document, undefined, "onClick"), "edit onClick");
-                //ScriptBox.EditButtonScript("On Button Clicked ...", this.props.Document, "onClick", e.clientX, e.clientY), "on button click");
             } else if (this.props.Document.links && this.Document.isLinkButton && !e.shiftKey && !e.ctrlKey) {
                 DocListCast(this.props.Document.links).length && this.followLinkClick(e.altKey, e.ctrlKey, e.shiftKey);
             } else {
                 if ((this.props.Document.onDragStart || (this.props.Document.rootDocument)) && !(e.ctrlKey || e.button > 0)) {  // onDragStart implies a button doc that we don't want to select when clicking.   RootDocument & isTEmplaetForField implies we're clicking on part of a template instance and we want to select the whole template, not the part
                     stopPropagate = false; // don't stop propagation for field templates -- want the selection to propagate up to the root document of the template
                 } else {
-                    // if (this.props.Document.type === DocumentType.RTF) {
-                    //     DocumentView._focusHack = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY) || [0, 0];
-                    //     DocumentView._focusHack = [DocumentView._focusHack[0] + NumCast(this.props.Document.x), DocumentView._focusHack[1] + NumCast(this.props.Document.y)];
-
-                    //     this.props.focus(this.props.Document, false);
-                    // }
                     SelectionManager.SelectDoc(this, e.ctrlKey || e.shiftKey);
                 }
                 preventDefault = false;
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index f45a86b56..86757296e 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -1066,12 +1066,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
         if ((e.nativeEvent as any).formattedHandled) { e.stopPropagation(); return; }
         (e.nativeEvent as any).formattedHandled = true;
 
-        if (Math.abs(e.clientX - this._downX) < 4 && Math.abs(e.clientX - this._downX) < 4) {
-            this.props.select(e.ctrlKey);
-            this.hitBulletTargets(e.clientX, e.clientY, e.shiftKey, false);
-        }
         if (this.props.isSelected(true)) { // if text box is selected, then it consumes all click events
             e.stopPropagation();
+            this.hitBulletTargets(e.clientX, e.clientY, e.shiftKey, false);
         }
     }
 
-- 
cgit v1.2.3-70-g09d2


From d7dbc15ecc211ab9533c0cffa40227523b6a5831 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Mon, 22 Jun 2020 21:07:45 -0400
Subject: made chrome commands fire by clicking bullseye.  added commands for
 freeformview.  made backgroundHalo() work diffrently for ink strokes

---
 src/client/views/InkingStroke.tsx                  |  4 +-
 .../views/collections/CollectionViewChromes.tsx    | 32 ++++++---
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 76 +++++++++++-----------
 .../collections/collectionFreeForm/MarqueeView.tsx |  1 -
 .../views/nodes/CollectionFreeFormDocumentView.tsx |  2 +-
 src/client/views/nodes/DocumentView.tsx            |  1 +
 src/client/views/nodes/FieldView.tsx               |  1 +
 .../views/nodes/formattedText/FormattedTextBox.tsx |  2 +-
 8 files changed, 68 insertions(+), 51 deletions(-)

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

diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index 7e3bd1c17..cdabd213b 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -4,7 +4,7 @@ import { observer } from "mobx-react";
 import { documentSchema } from "../../fields/documentSchemas";
 import { InkData, InkField, InkTool } from "../../fields/InkField";
 import { makeInterface } from "../../fields/Schema";
-import { Cast, StrCast } from "../../fields/Types";
+import { Cast, StrCast, BoolCast } from "../../fields/Types";
 import { TraceMobx } from "../../fields/util";
 import { CognitiveServices } from "../cognitive_services/CognitiveServices";
 import { InteractionUtils } from "../util/InteractionUtils";
@@ -54,7 +54,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
         const scaleY = (this.props.PanelHeight() - strokeWidth) / (height - strokeWidth);
         const strokeColor = StrCast(this.layoutDoc.color, ActiveInkColor());
         const points = InteractionUtils.CreatePolyline(data, left, top, strokeColor, strokeWidth, strokeWidth,
-            StrCast(this.layoutDoc.strokeBezier, ActiveInkBezierApprox()), scaleX, scaleY, "", "none", this.props.isSelected() && strokeWidth <= 5);
+            StrCast(this.layoutDoc.strokeBezier, ActiveInkBezierApprox()), scaleX, scaleY, "", "none", (this.props.isSelected() || BoolCast(this.props.backgroundHalo?.())) && strokeWidth <= 5);
         const hpoints = InteractionUtils.CreatePolyline(data, left, top,
             this.props.isSelected() && strokeWidth > 5 ? strokeColor : "transparent", strokeWidth, (strokeWidth + 15),
             StrCast(this.layoutDoc.strokeBezier, ActiveInkBezierApprox()), scaleX, scaleY, "", this.props.active() ? "visiblestroke" : "none", false);
diff --git a/src/client/views/collections/CollectionViewChromes.tsx b/src/client/views/collections/CollectionViewChromes.tsx
index 52fb63386..ac7136d4d 100644
--- a/src/client/views/collections/CollectionViewChromes.tsx
+++ b/src/client/views/collections/CollectionViewChromes.tsx
@@ -40,28 +40,41 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
     _templateCommand = {
         params: ["target", "source"], title: "=> item view",
         script: "this.target.childLayout = getDocTemplate(this.source?.[0])",
-        immediate: (source: Doc[]) => this.target.childLayout = Doc.getDocTemplate(source?.[0]),
+        immediate: undoBatch((source: Doc[]) => source.length && (this.target.childLayout = Doc.getDocTemplate(source?.[0]))),
         initialize: emptyFunction,
     };
     _narrativeCommand = {
         params: ["target", "source"], title: "=> child click view",
         script: "this.target.childClickedOpenTemplateView = getDocTemplate(this.source?.[0])",
-        immediate: (source: Doc[]) => this.target.childClickedOpenTemplateView = Doc.getDocTemplate(source?.[0]),
+        immediate: undoBatch((source: Doc[]) => source.length && (this.target.childClickedOpenTemplateView = Doc.getDocTemplate(source?.[0]))),
         initialize: emptyFunction,
     };
     _contentCommand = {
-        params: ["target", "source"], title: "=> content",
+        params: ["target", "source"], title: "=> clear content",
         script: "getProto(this.target).data = copyField(this.source);",
-        immediate: (source: Doc[]) => Doc.GetProto(this.target).data = new List<Doc>(source), // Doc.aliasDocs(source),
+        immediate: undoBatch((source: Doc[]) => Doc.GetProto(this.target).data = new List<Doc>(source)), // Doc.aliasDocs(source),
         initialize: emptyFunction,
     };
     _viewCommand = {
-        params: ["target"], title: "=> saved view",
+        params: ["target"], title: "=> reset view",
         script: "this.target._panX = this.restoredPanX; this.target._panY = this.restoredPanY; this.target.scale = this.restoredScale;",
-        immediate: (source: Doc[]) => { this.target._panX = 0; this.target._panY = 0; this.target.scale = 1; },
+        immediate: undoBatch((source: Doc[]) => { this.target._panX = 0; this.target._panY = 0; this.target.scale = 1; }),
         initialize: (button: Doc) => { button.restoredPanX = this.target._panX; button.restoredPanY = this.target._panY; button.restoredScale = this.target.scale; },
     };
-    _freeform_commands = [this._contentCommand, this._templateCommand, this._narrativeCommand, this._viewCommand];
+    _clusterCommand = {
+        params: ["target"], title: "=> fit content",
+        script: "this.target._fitToBox = !this.target._fitToBox;",
+        immediate: undoBatch((source: Doc[]) => this.target._fitToBox = !this.target._fitToBox),
+        initialize: emptyFunction
+    };
+    _fitContentCommand = {
+        params: ["target"], title: "=> toggle clusters",
+        script: "this.target.useClusters = !this.target.useClusters;",
+        immediate: undoBatch((source: Doc[]) => this.target.useClusters = !this.target.useClusters),
+        initialize: emptyFunction
+    };
+
+    _freeform_commands = [this._viewCommand, this._fitContentCommand, this._clusterCommand, this._contentCommand, this._templateCommand, this._narrativeCommand];
     _stacking_commands = [this._contentCommand, this._templateCommand];
     _masonry_commands = [this._contentCommand, this._templateCommand];
     _schema_commands = [this._templateCommand, this._narrativeCommand];
@@ -84,6 +97,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
     @observable private _currentKey: string = "";
 
     componentDidMount = action(() => {
+        this._currentKey = this._currentKey || (this._buttonizableCommands.length ? this._buttonizableCommands[0]?.title : "");
         // chrome status is one of disabled, collapsed, or visible. this determines initial state from document
         switch (this.props.CollectionView.props.Document._chromeStatus) {
             case "disabled":
@@ -248,7 +262,9 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
                 DragManager.StartButtonDrag([this._commandRef.current!], c.script, c.title,
                     { target: this.props.CollectionView.props.Document }, c.params, c.initialize, e.clientX, e.clientY));
             return true;
-        }, emptyFunction, emptyFunction);
+        }, emptyFunction, () => {
+            this._buttonizableCommands.filter(c => c.title === this._currentKey).map(c => c.immediate([]));
+        });
     }
 
     @computed get templateChrome() {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 960b6efef..ac1456970 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1128,6 +1128,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
             this.props.Document[this.scaleFieldKey] = Math.max(1, NumCast(this.props.Document[this.scaleFieldKey]));
         }
 
+        this.Document.useClusters && !this._clusterSets.length && this.childDocs.length && this.updateClusters(true);
         return elements;
     }
 
@@ -1197,59 +1198,58 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
     onContextMenu = (e: React.MouseEvent) => {
         if (this.props.annotationsKey) return;
 
-        !this.props.isAnnotationOverlay && ContextMenu.Instance.addItem({
-            description: (this._timelineVisible ? "Close" : "Open") + " Animation Timeline", event: action(() => {
-                this._timelineVisible = !this._timelineVisible;
-            }), icon: this._timelineVisible ? faEyeSlash : faEye
-        });
-
         const appearance = ContextMenu.Instance.findByDescription("Appearance...");
         const appearanceItems = appearance && "subitems" in appearance ? appearance.subitems : [];
         appearanceItems.push({ description: "reset view", event: () => { this.props.Document._panX = this.props.Document._panY = 0; this.props.Document[this.scaleFieldKey] = 1; }, icon: "compress-arrows-alt" });
         appearanceItems.push({ description: `${this.fitToContent ? "Unset" : "Set"} Fit To Container`, event: () => this.Document._fitToBox = !this.fitToContent, icon: !this.fitToContent ? "expand-arrows-alt" : "compress-arrows-alt" });
         appearanceItems.push({ description: `${this.Document.useClusters ? "Uncluster" : "Use Clusters"}`, event: () => this.updateClusters(!this.Document.useClusters), icon: "braille" });
-        appearanceItems.push({ description: "Use Background Color as Default", event: () => Cast(Doc.UserDoc().emptyCollection, Doc, null)!._backgroundColor = StrCast(this.layoutDoc._backgroundColor), icon: "palette" });
+        appearanceItems.push({ description: "Use Background Color as Default", event: () => Cast(Doc.UserDoc().emptyCollection, Doc, null)._backgroundColor = StrCast(this.layoutDoc._backgroundColor), icon: "palette" });
         !appearance && ContextMenu.Instance.addItem({ description: "Appearance...", subitems: appearanceItems, icon: "eye" });
 
         const options = ContextMenu.Instance.findByDescription("Options...");
         const optionItems = options && "subitems" in options ? options.subitems : [];
-        optionItems.push({ description: "toggle snap line display", event: () => Doc.UserDoc().showSnapLines = !Doc.UserDoc().showSnapLines, icon: "compress-arrows-alt" });
-        !Doc.UserDoc().noviceMode && optionItems.push({ description: (!this.layoutDoc._nativeWidth || !this.layoutDoc._nativeHeight ? "Freeze" : "Unfreeze") + " Aspect", event: this.toggleNativeDimensions, icon: "snowflake" });
-        this.props.ContainingCollectionView && optionItems.push({ description: "Promote Collection", event: this.promoteCollection, icon: "table" });
+        !this.props.isAnnotationOverlay &&
+            optionItems.push({ description: (this.showTimeline ? "Close" : "Open") + " Animation Timeline", event: action(() => this.showTimeline = !this.showTimeline), icon: faEye });
+        this.props.ContainingCollectionView &&
+            optionItems.push({ description: "Promote Collection", event: this.promoteCollection, icon: "table" });
+        optionItems.push({ description: (Doc.UserDoc().showSnapLines ? "Hide" : "Show") + " snap lines", event: () => Doc.UserDoc().showSnapLines = !Doc.UserDoc().showSnapLines, icon: "compress-arrows-alt" });
         optionItems.push({ description: this.layoutDoc._lockedTransform ? "Unlock Transform" : "Lock Transform", event: this.toggleLockTransform, icon: this.layoutDoc._lockedTransform ? "unlock" : "lock" });
         optionItems.push({ description: "Arrange contents in grid", event: this.layoutDocsInGrid, icon: "table" });
-        !Doc.UserDoc().noviceMode && optionItems.push({
-            description: "Import document", icon: "upload", event: ({ x, y }) => {
-                const input = document.createElement("input");
-                input.type = "file";
-                input.accept = ".zip";
-                input.onchange = async _e => {
-                    const upload = Utils.prepend("/uploadDoc");
-                    const formData = new FormData();
-                    const file = input.files && input.files[0];
-                    if (file) {
-                        formData.append('file', file);
-                        formData.append('remap', "true");
-                        const response = await fetch(upload, { method: "POST", body: formData });
-                        const json = await response.json();
-                        if (json !== "error") {
-                            const doc = await DocServer.GetRefField(json);
-                            if (doc instanceof Doc) {
-                                const [xx, yy] = this.props.ScreenToLocalTransform().transformPoint(x, y);
-                                doc.x = xx, doc.y = yy;
-                                this.props.addDocument?.(doc);
+        if (!Doc.UserDoc().noviceMode) {
+            optionItems.push({ description: (!this.layoutDoc._nativeWidth || !this.layoutDoc._nativeHeight ? "Freeze" : "Unfreeze") + " Aspect", event: this.toggleNativeDimensions, icon: "snowflake" });
+            optionItems.push({ description: `${this.Document._LODdisable ? "Enable LOD" : "Disable LOD"}`, event: () => this.Document._LODdisable = !this.Document._LODdisable, icon: "table" });
+            optionItems.push({
+                description: "Import document", icon: "upload", event: ({ x, y }) => {
+                    const input = document.createElement("input");
+                    input.type = "file";
+                    input.accept = ".zip";
+                    input.onchange = async _e => {
+                        const upload = Utils.prepend("/uploadDoc");
+                        const formData = new FormData();
+                        const file = input.files && input.files[0];
+                        if (file) {
+                            formData.append('file', file);
+                            formData.append('remap', "true");
+                            const response = await fetch(upload, { method: "POST", body: formData });
+                            const json = await response.json();
+                            if (json !== "error") {
+                                const doc = await DocServer.GetRefField(json);
+                                if (doc instanceof Doc) {
+                                    const [xx, yy] = this.props.ScreenToLocalTransform().transformPoint(x, y);
+                                    doc.x = xx, doc.y = yy;
+                                    this.props.addDocument?.(doc);
+                                }
                             }
                         }
-                    }
-                };
-                input.click();
-            }
-        });
-        !Doc.UserDoc().noviceMode && optionItems.push({ description: `${this.Document._LODdisable ? "Enable LOD" : "Disable LOD"}`, event: () => this.Document._LODdisable = !this.Document._LODdisable, icon: "table" });
+                    };
+                    input.click();
+                }
+            });
+        }
         !options && ContextMenu.Instance.addItem({ description: "Options...", subitems: optionItems, icon: "eye" });
 
     }
-    @observable _timelineVisible = false;
+    @observable showTimeline = false;
 
     intersectRect(r1: { left: number, top: number, width: number, height: number },
         r2: { left: number, top: number, width: number, height: number }) {
@@ -1350,7 +1350,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
                 zoomScaling={this.zoomScaling} panX={this.panX} panY={this.panY}>
                 {this.children}
             </CollectionFreeFormViewPannableContents>
-            {this._timelineVisible ? <Timeline ref={this._timelineRef} {...this.props} /> : (null)}
+            {this.showTimeline ? <Timeline ref={this._timelineRef} {...this.props} /> : (null)}
         </MarqueeView>;
     }
 
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 23efeec8d..1bc7c6fb5 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -741,7 +741,6 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
         return <div className="marqueeView"
             style={{ overflow: StrCast(this.props.Document._overflow), cursor: MarqueeView.DragMarquee && this ? "crosshair" : "hand" }}
             onDragOver={e => e.preventDefault()}
-            onPaste={this.paste}
             onScroll={(e) => e.currentTarget.scrollTop = e.currentTarget.scrollLeft = 0} onClick={this.onClick} onPointerDown={this.onPointerDown}>
             {this._visible ? this.marqueeDiv : null}
             {this.props.children}
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index f934945a6..404d69730 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -144,7 +144,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
                 boxShadow:
                     this.Opacity === 0 ? undefined :  // if it's not visible, then no shadow
                         this.layoutDoc.z ? `#9c9396  ${StrCast(this.layoutDoc.boxShadow, "10px 10px 0.9vw")}` :  // if it's a floating doc, give it a big shadow
-                            this.props.backgroundHalo?.() ? (`${this.props.backgroundColor?.(this.props.Document)} ${StrCast(this.layoutDoc.boxShadow, `0vw 0vw ${(this.layoutDoc.isBackground ? 100 : 50) / this.props.ContentScaling()}px`)}`) :  // if it's just in a cluster, make the shadown roughly match the cluster border extent
+                            this.props.backgroundHalo?.() && this.props.Document.type !== DocumentType.INK ? (`${this.props.backgroundColor?.(this.props.Document)} ${StrCast(this.layoutDoc.boxShadow, `0vw 0vw ${(this.layoutDoc.isBackground ? 100 : 50) / this.props.ContentScaling()}px`)}`) :  // if it's just in a cluster, make the shadown roughly match the cluster border extent
                                 this.layoutDoc.isBackground ? undefined :  // if it's a background & has a cluster color, make the shadow spread really big
                                     StrCast(this.layoutDoc.boxShadow, ""),
                 borderRadius: StrCast(Doc.Layout(this.layoutDoc).borderRounding),
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 71cb18e08..4e4c859ef 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1007,6 +1007,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                 LayoutTemplate={this.props.LayoutTemplate}
                 makeLink={this.makeLink}
                 rootSelected={this.rootSelected}
+                backgroundHalo={this.props.backgroundHalo}
                 dontRegisterView={this.props.dontRegisterView}
                 fitToBox={this.props.fitToBox}
                 LibraryPath={this.props.LibraryPath}
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index 305c04a90..c57738361 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -27,6 +27,7 @@ export interface FieldViewProps {
     LibraryPath: Doc[];
     onClick?: ScriptField;
     dropAction: dropActionType;
+    backgroundHalo?: () => boolean;
     docFilters: () => string[];
     isSelected: (outsideReaction?: boolean) => boolean;
     select: (isCtrlPressed: boolean) => void;
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 5e0585169..2a019ada7 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -452,7 +452,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
         appearanceItems.push({
             description: "Convert to be a template style", event: () => {
                 if (!this.layoutDoc.isTemplateDoc) {
-                    const title = StrCast(this.rootDoc.title)
+                    const title = StrCast(this.rootDoc.title);
                     this.rootDoc.title = "text";
                     this.rootDoc.isTemplateDoc = makeTemplate(this.rootDoc, true, title);
                 } else {
-- 
cgit v1.2.3-70-g09d2

