From 19adcca1a89405a47da57fee64fe5fde4720c16a Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 5 May 2025 14:05:32 -0400
Subject: got rid of dash _xPadding/_yPadding in favor of just using
 _xMargin/_yMargin.  cleaned up linearView field names

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

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

diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index c51f6c38b..abf44b54d 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -1164,7 +1164,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
     };
 
     @computed get tagsHeight() {
-        return this.DocumentView?.().showTags ? Math.max(0, 20 - Math.max(this._props.yPadding ?? 0, NumCast(this.layoutDoc._yMargin))) * this.ScreenToLocalBoxXf().Scale : 0;
+        return this.DocumentView?.().showTags ? Math.max(0, 20 - Math.max(this._props.yMargin ?? 0, NumCast(this.layoutDoc._yMargin))) * this.ScreenToLocalBoxXf().Scale : 0;
     }
 
     @computed get contentScaling() {
@@ -1880,7 +1880,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         }
     };
     tryUpdateScrollHeight = () => {
-        const margins = 2 * NumCast(this.layoutDoc._yMargin, this._props.yPadding || 0);
+        const margins = 2 * NumCast(this.layoutDoc._yMargin, this._props.yMargin || 0);
         const children = this.ProseRef?.children.length ? Array.from(this.ProseRef.children[0].children) : undefined;
         if (this.EditorView && children && !SnappingManager.IsDragging) {
             const getChildrenHeights = (kids: Element[] | undefined) => kids?.reduce((p, child) => p + toHgt(child), margins) ?? 0;
@@ -2112,8 +2112,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         const rounded = StrCast(this.layoutDoc._layout_borderRounding) === '100%' ? '-rounded' : '';
         setTimeout(() => !this._props.isContentActive() && FormattedTextBoxComment.textBox === this && FormattedTextBoxComment.Hide);
 
-        const paddingX = Math.max(NumCast(this.layoutDoc._xMargin), 0, this._props.xPadding ?? 0, this._props.screenXPadding?.(this._props.DocumentView?.()) ?? 0);
-        const paddingY = Math.max(NumCast(this.layoutDoc._yMargin), 0, this._props.yPadding ?? 0); // prettier-ignore
+        const paddingX = Math.max(NumCast(this.layoutDoc._xMargin), 0, this._props.xMargin ?? 0, this._props.screenXPadding?.(this._props.DocumentView?.()) ?? 0);
+        const paddingY = Math.max(NumCast(this.layoutDoc._yMargin), 0, this._props.yMargin ?? 0); // prettier-ignore
         const styleFromLayout = styleFromLayoutString(this.Document, this._props, scale); // this converts any expressions in the format string to style props.  e.g., <FormattedTextBox height='{this._header_height}px' >
         return this.isLabel ? (
             <LabelBox {...this._props} />
-- 
cgit v1.2.3-70-g09d2


From 74836a59c4af0a3c240c5e3435359935282ce049 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 5 May 2025 14:53:49 -0400
Subject: fixed keeping focus on labelBox when text butons are clicked.

---
 src/client/views/nodes/LabelBox.tsx                | 35 ++++++++++++----------
 .../views/nodes/formattedText/FormattedTextBox.tsx | 11 ++++---
 src/client/views/nodes/trails/PresSlideBox.tsx     |  4 +--
 3 files changed, 28 insertions(+), 22 deletions(-)

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

diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx
index f901c32fc..4cbe01b82 100644
--- a/src/client/views/nodes/LabelBox.tsx
+++ b/src/client/views/nodes/LabelBox.tsx
@@ -27,7 +27,7 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
     private dropDisposer?: DragManager.DragDropDisposer;
     private _timeout: NodeJS.Timeout | undefined;
     private _divRef: HTMLDivElement | null = null;
-    private _reaction: IReactionDisposer | undefined;
+    private _disposers: { [key: string]: IReactionDisposer } = {};
 
     constructor(props: FieldViewProps) {
         super(props);
@@ -43,7 +43,7 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
 
     componentDidMount() {
         this._props.setContentViewBox?.(this);
-        this._reaction = reaction(
+        this._disposers.active = reaction(
             () => this.Title,
             () => document.activeElement !== this._divRef && this._forceRerender++
         );
@@ -51,7 +51,7 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
     componentWillUnMount() {
         this._timeout && clearTimeout(this._timeout);
         this.setText(this._divRef?.innerText ?? '');
-        this._reaction?.();
+        Object.values(this._disposers).forEach(disposer => disposer());
     }
 
     @observable _forceRerender = 0;
@@ -171,20 +171,21 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
     render() {
         TraceMobx();
         const boxParams = this.fitTextToBox(undefined); // this causes mobx to trigger re-render when data changes
+        const [xmargin, ymargin] = [NumCast(this.layoutDoc._xMargin), NumCast(this.layoutDoc._uMargin)];
         return (
             <div className="labelBox-outerDiv" ref={this.createDropTarget} style={{ boxShadow: this.boxShadow }}>
                 <div
                     className="labelBox-mainButton"
                     style={{
                         backgroundColor: this.backgroundColor,
-                        color: StrCast(this.layoutDoc._text_fontColor, StrCast(this.layoutDoc._color)),
-                        fontFamily: StrCast(this.layoutDoc._text_fontFamily, StrCast(Doc.UserDoc().fontFamily)) || 'inherit',
+                        color: StrCast(this.layoutDoc[`${this.fieldKey}_fontColor`], StrCast(this.layoutDoc._color)),
+                        fontFamily: StrCast(this.layoutDoc[`${this.fieldKey}_fontFamily`], StrCast(Doc.UserDoc().fontFamily)) || 'inherit',
                         letterSpacing: StrCast(this.layoutDoc.letterSpacing),
-                        textTransform: StrCast(this.layoutDoc[this.fieldKey + '_transform']) as Property.TextTransform,
-                        paddingLeft: NumCast(this.layoutDoc._xMargin),
-                        paddingRight: NumCast(this.layoutDoc._xMargin),
-                        paddingTop: NumCast(this.layoutDoc._yMargin),
-                        paddingBottom: NumCast(this.layoutDoc._yMargin),
+                        textTransform: StrCast(this.layoutDoc[`${this.fieldKey}_transform`]) as Property.TextTransform,
+                        paddingLeft: xmargin,
+                        paddingRight: xmargin,
+                        paddingTop: ymargin,
+                        paddingBottom: ymargin,
                         width: this._props.PanelWidth(),
                         height: this._props.PanelHeight(),
                         whiteSpace: boxParams.multiLine ? 'pre-wrap' : 'pre',
@@ -192,8 +193,8 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
                     <div
                         key={this._forceRerender}
                         style={{
-                            width: this._props.PanelWidth() - 2 * NumCast(this.layoutDoc._xMargin),
-                            height: this._props.PanelHeight() - 2 * NumCast(this.layoutDoc._yMargin),
+                            width: this._props.PanelWidth() - 2 * xmargin,
+                            height: this._props.PanelHeight() - 2 * ymargin,
                             outline: 'unset !important',
                         }}
                         onKeyDown={e => {
@@ -214,12 +215,14 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
                             this._divRef?.removeEventListener('focusout', this.keepFocus);
                             this._divRef?.addEventListener('focusout', this.keepFocus);
                         }}
-                        onBlur={() => {
+                        onBlur={e => {
                             this._divRef?.removeEventListener('focusout', this.keepFocus);
                             this.setText(this._divRef?.innerText ?? '');
-                            RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined, undefined);
-                            FormattedTextBox.LiveTextUndo?.end();
-                            FormattedTextBox.LiveTextUndo = undefined;
+                            if (!FormattedTextBox.tryKeepingFocus(e.relatedTarget, () => this._divRef?.focus())) {
+                                RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined, undefined);
+                                FormattedTextBox.LiveTextUndo?.end();
+                                FormattedTextBox.LiveTextUndo = undefined;
+                            }
                         }}
                         dangerouslySetInnerHTML={{
                             __html: `<span class="textFitted textFitAlignVert" style="display: inline-block; text-align: center; font-size: 100px; height: 0px;">${this.Title?.startsWith('#') ? '' : (this.Title ?? '')}</span>`,
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index abf44b54d..824ac97da 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -1750,19 +1750,22 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
      * When a text box loses focus, it might be because a text button was clicked (eg, bold, italics) or color picker.
      * In these cases, force focus back onto the text box.
      * @param target
+     * @returns true if focus was kept on the text box, false otherwise
      */
-    tryKeepingFocus = (target: Element | null) => {
+    public static tryKeepingFocus(target: Element | null, refocusFunc?: () => void) {
         for (let newFocusEle = target instanceof HTMLElement ? target : null; newFocusEle; newFocusEle = newFocusEle?.parentElement) {
             // bcz: HACK!! test if parent of new focused element is a UI button (should be more specific than testing className)
             if (['fonticonbox', 'antimodeMenu-cont', 'popup-container'].includes(newFocusEle?.className ?? '')) {
-                return this.EditorView?.focus(); // keep focus on text box
+                refocusFunc?.(); // keep focus on text box
+                return true;
             }
         }
-    };
+        return false;
+    }
 
     @action
     onBlur = (e: React.FocusEvent) => {
-        this.tryKeepingFocus(e.relatedTarget);
+        FormattedTextBox.tryKeepingFocus(e.relatedTarget, () => this.EditorView?.focus());
         if (this.ProseRef?.children[0] !== e.nativeEvent.target) return;
         if (!(this.EditorView?.state.selection instanceof NodeSelection) || this.EditorView.state.selection.node.type !== this.EditorView.state.schema.nodes.footnote) {
             const stordMarks = this.EditorView?.state.storedMarks?.slice();
diff --git a/src/client/views/nodes/trails/PresSlideBox.tsx b/src/client/views/nodes/trails/PresSlideBox.tsx
index 3440b29dd..55a655c7a 100644
--- a/src/client/views/nodes/trails/PresSlideBox.tsx
+++ b/src/client/views/nodes/trails/PresSlideBox.tsx
@@ -559,8 +559,8 @@ export class PresSlideBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 style={{
                     backgroundColor: presColorBool ? (isSelected ? 'rgba(250,250,250,0.3)' : 'transparent') : isSelected ? Colors.LIGHT_BLUE : 'transparent',
                     opacity: this._dragging ? 0.3 : 1,
-                    paddingLeft: NumCast(this.layoutDoc._xMargin, this._props._xMargin),
-                    paddingRight: NumCast(this.layoutDoc._xMargin, this._props._xMargin),
+                    paddingLeft: NumCast(this.layoutDoc._xMargin, this._props.xMargin),
+                    paddingRight: NumCast(this.layoutDoc._xMargin, this._props.xMargin),
                     paddingTop: NumCast(this.layoutDoc._yPadding, this._props.yMargin),
                     paddingBottom: NumCast(this.layoutDoc._yPadding, this._props.yMargin),
                 }}
-- 
cgit v1.2.3-70-g09d2


From 299398cb7e21259de3bf7597995840b84e7e0590 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Fri, 9 May 2025 23:01:36 -0400
Subject: added user templates to template_user global field. fixed schemas to
 not erase field values when setting a new column name. fixed templates to
 render properly when switching by fixing contentsRef to be observable. fixed
 templates with fields in stacks to autoHeight update correctly by .  fixed
 images in templates to set their nativeDim after rendering.  fixed text views
 of descriptions to auto-updated by setting the modification date of the
 description field.

---
 src/client/documents/DocUtils.ts                   | 20 +++++++++----------
 src/client/util/DropConverter.ts                   |  4 +++-
 .../CollectionStackingViewFieldColumn.tsx          |  6 ++++--
 src/client/views/collections/CollectionSubView.tsx | 13 ++++++++++--
 .../collectionSchema/CollectionSchemaView.tsx      |  2 +-
 src/client/views/nodes/DocumentView.tsx            |  6 +++---
 src/client/views/nodes/ImageBox.tsx                |  5 +++--
 .../views/nodes/formattedText/FormattedTextBox.tsx |  3 ++-
 src/fields/Doc.ts                                  | 23 +++++++++++++++++++---
 src/server/SharedMediaTypes.ts                     |  3 +++
 10 files changed, 60 insertions(+), 25 deletions(-)

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

diff --git a/src/client/documents/DocUtils.ts b/src/client/documents/DocUtils.ts
index 9135899d7..36e03daed 100644
--- a/src/client/documents/DocUtils.ts
+++ b/src/client/documents/DocUtils.ts
@@ -642,8 +642,15 @@ export namespace DocUtils {
         return dd;
     }
 
-    export function assignImageInfo(result: Upload.FileInformation, protoIn: Doc) {
+    export function assignUploadInfo(result: Upload.FileInformation, protoIn: Doc) {
         const proto = protoIn;
+
+        if (Upload.isTextInformation(result)) {
+            proto.text = result.rawText;
+        }
+        if (Upload.isVideoInformation(result)) {
+            proto.data_duration = result.duration;
+        }
         if (Upload.isImageInformation(result)) {
             const maxNativeDim = Math.max(result.nativeHeight, result.nativeWidth);
             const exifRotation = StrCast(result.exifData?.data?.Orientation).toLowerCase();
@@ -677,15 +684,8 @@ export namespace DocUtils {
         const pathname = result.accessPaths.agnostic.client;
         const doc = await DocUtils.DocumentFromType(type, pathname, full, overwriteDoc);
         if (doc) {
-            const proto = Doc.GetProto(doc);
-            proto.text = result.rawText;
-            !(result instanceof Error) && DocUtils.assignImageInfo(result, proto);
-            if (Upload.isVideoInformation(result)) {
-                proto.data_duration = result.duration;
-            }
-            if (overwriteDoc) {
-                Doc.removeCurrentlyLoading(overwriteDoc);
-            }
+            DocUtils.assignUploadInfo(result, Doc.GetProto(doc));
+            overwriteDoc && Doc.removeCurrentlyLoading(overwriteDoc);
             generatedDocuments.push(doc);
         }
         return doc;
diff --git a/src/client/util/DropConverter.ts b/src/client/util/DropConverter.ts
index b6b111930..1d4779d8a 100644
--- a/src/client/util/DropConverter.ts
+++ b/src/client/util/DropConverter.ts
@@ -41,7 +41,7 @@ function makeTemplate(doc: Doc, first: boolean = true): boolean {
     if (first && !docs.length) {
         // bcz: feels hacky : if the root level document has items, it's not a field template
         isTemplate = Doc.MakeMetadataFieldTemplate(doc, layoutDoc[DocData], true) || isTemplate;
-    } else if (docData instanceof RichTextField || docData instanceof ImageField) {
+    } else if (docData instanceof RichTextField || docData instanceof ImageField || (docData === undefined && doc.type === DocumentType.IMG)) {
         if (!StrCast(layoutDoc.title).startsWith('-')) {
             isTemplate = Doc.MakeMetadataFieldTemplate(layoutDoc, layoutDoc[DocData], true);
         }
@@ -86,6 +86,8 @@ export function makeUserTemplateButtonOrImage(doc: Doc, image?: string) {
     dbox.dragFactory = layoutDoc;
     dbox.dropPropertiesToRemove = doc.dropPropertiesToRemove instanceof ObjectField ? ObjectField.MakeCopy(doc.dropPropertiesToRemove) : undefined;
     dbox.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory)');
+    const userTemplatesDoc = DocCast(Doc.UserDoc().template_user);
+    userTemplatesDoc && Doc.AddDocToList(userTemplatesDoc, 'data', layoutDoc);
     return dbox;
 }
 
diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
index 994669734..345f60e75 100644
--- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
+++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
@@ -71,6 +71,7 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent<
     }
 
     _ele: HTMLElement | null = null;
+    _eleMasonrySingle = React.createRef<HTMLDivElement>();
 
     protected onInternalPreDrop = (e: Event, de: DragManager.DropEvent, targetDropAction: dropActionType) => {
         const dragData = de.complete.docDragData;
@@ -92,13 +93,13 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent<
     createColumnDropRef = (ele: HTMLDivElement | null) => {
         this.dropDisposer?.();
         if (ele) this.dropDisposer = DragManager.MakeDropTarget(ele, this.columnDrop.bind(this), this._props.Doc, this.onInternalPreDrop.bind(this));
-        else if (this._ele) this.props.refList.splice(this.props.refList.indexOf(this._ele), 1);
+        else if (this._eleMasonrySingle.current) this.props.refList.splice(this.props.refList.indexOf(this._eleMasonrySingle.current), 1);
         this._ele = ele;
     };
 
     @action
     componentDidMount() {
-        this._ele && this.props.refList.push(this._ele);
+        this._eleMasonrySingle.current && this.props.refList.push(this._eleMasonrySingle.current);
         this._disposers.collapser = reaction(
             () => this._props.headingObject?.collapsed,
             collapsed => { this.collapsed = collapsed !== undefined ? BoolCast(collapsed) : false; }, // prettier-ignore
@@ -363,6 +364,7 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent<
                         }}>
                         <div
                             key={`${heading}-stack`}
+                            ref={this._eleMasonrySingle}
                             className="collectionStackingView-masonrySingle"
                             style={{
                                 padding: `${columnYMargin}px ${0}px ${this._props.yMargin}px ${0}px`,
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index e79d0a76d..4ac755b9d 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -9,7 +9,7 @@ import { Id } from '../../../fields/FieldSymbols';
 import { List } from '../../../fields/List';
 import { listSpec } from '../../../fields/Schema';
 import { ScriptField } from '../../../fields/ScriptField';
-import { BoolCast, Cast, DateCast, NumCast, ScriptCast, StrCast, toList } from '../../../fields/Types';
+import { BoolCast, Cast, DateCast, DocCast, NumCast, ScriptCast, StrCast, toList } from '../../../fields/Types';
 import { WebField } from '../../../fields/URLField';
 import { GetEffectiveAcl, TraceMobx } from '../../../fields/util';
 import { GestureUtils } from '../../../pen-gestures/GestureUtils';
@@ -561,7 +561,16 @@ export function CollectionSubView<X>() {
                             }
                             const loading = Docs.Create.LoadingDocument(file, options);
                             Doc.addCurrentlyLoading(loading);
-                            DocUtils.uploadFileToDoc(file, {}, loading);
+                            DocUtils.uploadFileToDoc(file, {}, loading).then(d => {
+                                if (d && d?.type === DocumentType.IMG) {
+                                    const imgTemplate = DocListCast(DocCast(Doc.UserDoc().template_user)?.data).find(d => d.title === 'shower');
+                                    if (imgTemplate) {
+                                        d.layout_fieldKey = 'layout_shower';
+                                        d.layout_shower = imgTemplate;
+                                    }
+                                }
+                            });
+
                             return loading;
                         })
                     ))
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
index c06391f35..6442385c0 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
@@ -359,7 +359,7 @@ export class CollectionSchemaView extends CollectionSubView() {
     @action
     addNewKey = (key: string, defaultVal: FieldType | undefined) => {
         this.childDocs.forEach(doc => {
-            doc[DocData][key] = defaultVal;
+            if (doc[DocData][key] === undefined) doc[DocData][key] = defaultVal;
         });
     };
 
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index bdb97d7bb..f9c21451e 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -740,7 +740,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewProps & Field
     }
     widgetOverlayFunc = () => (this.widgetDecorations ? this.widgetOverlay : null);
     viewingAiEditor = () => (this._props.showAIEditor && this._componentView?.componentAIView?.() !== undefined ? this.aiEditor : null);
-    _contentsRef = React.createRef<DocumentContentsView>();
+    @observable _contentsRef: DocumentContentsView | undefined = undefined;
     @computed get viewBoxContents() {
         TraceMobx();
         const isInk = this.layoutDoc._layout_isSvg && !this._props.LayoutTemplateString;
@@ -756,7 +756,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewProps & Field
                     }}>
                     <DocumentContentsView
                         {...this._props}
-                        ref={this._contentsRef}
+                        ref={action((r: DocumentContentsView) => (this._contentsRef = r))}
                         layoutFieldKey={StrCast(this.Document.layout_fieldKey, 'layout')}
                         pointerEvents={this.contentPointerEvents}
                         setContentViewBox={this.setContentView}
@@ -1168,7 +1168,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
      * @returns boolean whether sub-component Doc is in synch with the layoutDoc that this view thinks its rendering
      */
     IsInvalid = (renderDoc?: Doc): boolean => {
-        const docContents = this._docViewInternal?._contentsRef.current;
+        const docContents = this._docViewInternal?._contentsRef;
         return !(
             (!renderDoc ||
                 (docContents?.layoutDoc === renderDoc && //
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index b648f2adb..f7ad5c7e2 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -152,7 +152,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         this._disposers.path = reaction(
             () => ({ nativeSize: this.nativeSize, width: NumCast(this.layoutDoc._width), height: this.layoutDoc._height }),
             ({ nativeSize, width, height }) => {
-                if (!this.layoutDoc._layout_nativeDimEditable || !height) {
+                if (!this.layoutDoc._layout_nativeDimEditable || !height || this.layoutDoc.layout_resetNativeDim) {
+                    this.layoutDoc.layout_resetNativeDim = undefined; // template images need to reset their dimensions when they are rendered with content.  afterwards, remove this flag.
                     this.layoutDoc._height = (width * nativeSize.nativeHeight) / nativeSize.nativeWidth;
                 }
             },
@@ -1048,7 +1049,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                     alert('Error uploading files - possibly due to unsupported file types');
                 } else {
                     this.dataDoc[this.fieldKey] = new ImageField(result.accessPaths.agnostic.client);
-                    !(result instanceof Error) && DocUtils.assignImageInfo(result, this.dataDoc);
+                    !(result instanceof Error) && DocUtils.assignUploadInfo(result, this.dataDoc);
                 }
                 disposer();
             } else {
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 824ac97da..57720baae 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -1224,13 +1224,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
             incomingValue => {
                 if (this.EditorView && this.ApplyingChange !== this.fieldKey) {
                     if (incomingValue?.data) {
-                        const updatedState = JSON.parse(incomingValue.data.Data);
+                        const updatedState = JSON.parse(incomingValue.data.Data.replace(/\n/g, ''));
                         if (JSON.stringify(this.EditorView.state.toJSON()) !== JSON.stringify(updatedState)) {
                             this.EditorView.updateState(EditorState.fromJSON(this.config, updatedState));
                             this.tryUpdateScrollHeight();
                         }
                     } else if (this.EditorView.state.doc.textContent !== (incomingValue?.str ?? '')) {
                         selectAll(this.EditorView.state, tx => this.EditorView?.dispatch(tx.insertText(incomingValue?.str ?? '')));
+                        this.tryUpdateScrollHeight();
                     }
                 }
             },
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index ba94f0504..990b6606f 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -25,6 +25,7 @@ import { ComputedField, ScriptField } from './ScriptField';
 import { BoolCast, Cast, DocCast, FieldValue, ImageCastWithSuffix, NumCast, RTFCast, StrCast, ToConstructor, toList } from './Types';
 import { containedFieldChangedHandler, deleteProperty, GetEffectiveAcl, getField, getter, makeEditable, makeReadOnly, setter, SharingPermissions } from './util';
 import { gptImageLabel } from '../client/apis/gpt/GPT';
+import { DateField } from './DateField';
 
 export let ObjGetRefField: (id: string, force?: boolean) => Promise<Doc | undefined>;
 export let ObjGetRefFields: (ids: string[]) => Promise<Map<string, Doc | undefined>>;
@@ -1109,6 +1110,10 @@ export namespace Doc {
             Cast(templateFieldValue, listSpec(Doc), [])?.map(d => d instanceof Doc && MakeMetadataFieldTemplate(d, templateDoc));
             Doc.GetProto(templateField)[metadataFieldKey] = ObjectField.MakeCopy(templateFieldValue);
         }
+        if (templateField.type === DocumentType.IMG) {
+            // bcz: should be a better way .. but, if the image is a template, then we can't expect to know the aspect ratio. When the image is replaced by data and rendered, we want to recomputed the native dimensions.
+            templateField[DocData].layout_resetNativeDim = true;
+        }
         // get the layout string that the template uses to specify its layout
         const templateFieldLayoutString = StrCast(Doc.LayoutField(templateField[DocLayout]));
 
@@ -1184,13 +1189,15 @@ export namespace Doc {
         return Doc.NativeWidth(doc, dataDoc, useDim) / (Doc.NativeHeight(doc, dataDoc, useDim) || 1);
     }
     export function NativeWidth(doc?: Doc, dataDoc?: Doc, useWidth?: boolean) {
-        return !doc ? 0 : NumCast(doc._nativeWidth, NumCast((dataDoc || doc)[Doc.LayoutDataKey(doc) + '_nativeWidth'], useWidth ? NumCast(doc._width) : 0));
+        // if this is a field template, then don't use the doc's nativeWidth/height
+        return !doc ? 0 : NumCast(doc.isTemplateForField ? undefined : doc._nativeWidth, NumCast((dataDoc || doc)[Doc.LayoutDataKey(doc) + '_nativeWidth'], !doc.isTemplateForField && useWidth ? NumCast(doc._width) : 0));
     }
     export function NativeHeight(doc?: Doc, dataDoc?: Doc, useHeight?: boolean) {
         if (!doc) return 0;
         const nheight = (Doc.NativeWidth(doc, dataDoc, useHeight) / NumCast(doc._width)) * NumCast(doc._height); // divide before multiply to avoid floating point errrorin case nativewidth = width
         const dheight = NumCast((dataDoc || doc)[Doc.LayoutDataKey(doc) + '_nativeHeight'], useHeight ? NumCast(doc._height) : 0);
-        return NumCast(doc._nativeHeight, nheight || dheight);
+        // if this is a field template, then don't use the doc's nativeWidth/height
+        return NumCast(doc.isTemplateForField ? undefined : doc._nativeHeight, nheight || dheight);
     }
 
     export function OutpaintingWidth(doc?: Doc, dataDoc?: Doc, useWidth?: boolean) {
@@ -1504,7 +1511,13 @@ export namespace Doc {
                 case DocumentType.RTF: return RTFCast(tdoc[Doc.LayoutDataKey(tdoc)])?.Text ?? StrCast(tdoc[Doc.LayoutDataKey(tdoc)]);
                 default:               return StrCast(tdoc.title).startsWith("Untitled") ? "" : StrCast(tdoc.title);
         }}); // prettier-ignore
-        return docText(doc).then(text => (doc['$' + Doc.LayoutDataKey(doc) + '_description'] = text));
+        return docText(doc).then(
+            action(text => {
+                // set the time when the date changes.  This also allows a live textbox view to react to the update, otherwise, it wouldn't take effect until the next time the view is rerendered.
+                doc['$' + Doc.LayoutDataKey(doc) + '_description_modificationDate'] = new DateField();
+                return (doc['$' + Doc.LayoutDataKey(doc) + '_description'] = text);
+            })
+        );
     }
 
     // prettier-ignore
@@ -1820,3 +1833,7 @@ ScriptingGlobals.add(function setDocRangeFilter(container: Doc, key: string, ran
 ScriptingGlobals.add(function toJavascriptString(str: string) {
     return Field.toJavascriptString(str as FieldType);
 });
+// eslint-disable-next-line prefer-arrow-callback
+ScriptingGlobals.add(function getDescription(doc: Doc) {
+    return Doc.getDescription(doc);
+});
diff --git a/src/server/SharedMediaTypes.ts b/src/server/SharedMediaTypes.ts
index 9aa4b120f..43a9ce963 100644
--- a/src/server/SharedMediaTypes.ts
+++ b/src/server/SharedMediaTypes.ts
@@ -19,6 +19,9 @@ export enum AudioAnnoState {
 }
 
 export namespace Upload {
+    export function isTextInformation(uploadResponse: Upload.FileInformation): uploadResponse is Upload.ImageInformation {
+        return 'rawText' in uploadResponse;
+    }
     export function isImageInformation(uploadResponse: Upload.FileInformation): uploadResponse is Upload.ImageInformation {
         return 'nativeWidth' in uploadResponse;
     }
-- 
cgit v1.2.3-70-g09d2

