From e0b846bf66b5d830b452ce0faa73eeb5f8a6cdcd Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 25 Mar 2024 14:25:49 -0400
Subject: fixed exception in comparison box

---
 src/client/views/nodes/ComparisonBox.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

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

diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index 62f630c6c..59d99b371 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -182,7 +182,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         //     where (this) is replaced by the text in the fieldKey slot abd this.excludeWords is repalced by the conetnts of the excludeWords field
         // The GPT call will put the "answer"  in the second slot of the comparison (eg., text_2)
         if (whichSlot.endsWith('2') && !layoutTemplateString?.includes(whichSlot)) {
-            var queryText = altText.replace('(this)', subjectText); // TODO: this should be done in KeyValueBox.setField but it doesn't know about the fieldKey ...
+            var queryText = altText?.replace('(this)', subjectText); // TODO: this should be done in KeyValueBox.setField but it doesn't know about the fieldKey ...
             if (queryText && queryText.match(/\(\(.*\)\)/)) {
                 KeyValueBox.SetField(this.Document, whichSlot, ':=' + queryText, false); // make the second slot be a computed field on the data doc that calls ChatGpt
             }
-- 
cgit v1.2.3-70-g09d2


From 1b592f74a7df8f6dd7b2881032725f26aedff403 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 28 Mar 2024 11:38:56 -0400
Subject: fixed keyvaluebox to show props document, never the doc in the
 fieldKey slot.  changed computedFIelds to do mobx caching.  changed text
 boxes to do updating from templates based on a fieldKey_autoUpdate flag
 combined with modification timestamps.  enabled comparison box to work with
 text fields in addition to docs.

---
 src/client/util/Scripting.ts                       |  9 +--
 src/client/views/InkControlPtHandles.tsx           | 85 ++++++++++++----------
 src/client/views/PropertiesButtons.tsx             |  4 +-
 src/client/views/nodes/ComparisonBox.tsx           |  6 +-
 src/client/views/nodes/KeyValueBox.tsx             |  2 +-
 .../views/nodes/formattedText/FormattedTextBox.tsx | 31 +++++---
 src/fields/Doc.ts                                  |  2 +-
 src/fields/ScriptField.ts                          | 22 ++++--
 8 files changed, 90 insertions(+), 71 deletions(-)

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

diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts
index f5e162d16..31222aa50 100644
--- a/src/client/util/Scripting.ts
+++ b/src/client/util/Scripting.ts
@@ -88,15 +88,10 @@ function Run(script: string | undefined, customParams: string[], diagnostics: an
             }
 
             const result = compiledFunction.apply(thisParam, params).apply(thisParam, argsArray);
-            if (batch) {
-                batch.end();
-            }
-
+            batch?.end();
             return { success: true, result };
         } catch (error) {
-            if (batch) {
-                batch.end();
-            }
+            batch?.end();
             onError?.(script + ' ' + error);
             return { success: false, error, result: errorVal };
         }
diff --git a/src/client/views/InkControlPtHandles.tsx b/src/client/views/InkControlPtHandles.tsx
index 31b13d2c8..01d52135a 100644
--- a/src/client/views/InkControlPtHandles.tsx
+++ b/src/client/views/InkControlPtHandles.tsx
@@ -1,5 +1,5 @@
 import * as React from 'react';
-import { action, observable } from 'mobx';
+import { action, makeObservable, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import { Doc } from '../../fields/Doc';
 import { ControlPoint, InkData, PointData } from '../../fields/InkField';
@@ -13,6 +13,7 @@ import { Colors } from './global/globalEnums';
 import { InkingStroke } from './InkingStroke';
 import { InkStrokeProperties } from './InkStrokeProperties';
 import { SnappingManager } from '../util/SnappingManager';
+import { ObservableReactComponent } from './ObservableReactComponent';
 
 export interface InkControlProps {
     inkDoc: Doc;
@@ -24,10 +25,15 @@ export interface InkControlProps {
 }
 
 @observer
-export class InkControlPtHandles extends React.Component<InkControlProps> {
+export class InkControlPtHandles extends ObservableReactComponent<InkControlProps> {
     @observable private _overControl = -1;
     get docView() {
-        return this.props.inkView.DocumentView?.();
+        return this._props.inkView.DocumentView?.();
+    }
+
+    constructor(props: InkControlProps) {
+        super(props);
+        makeObservable(this);
     }
 
     componentDidMount() {
@@ -42,51 +48,51 @@ export class InkControlPtHandles extends React.Component<InkControlProps> {
      */
     @action
     onControlDown = (e: React.PointerEvent, controlIndex: number): void => {
-        const ptFromScreen = this.props.inkView.ptFromScreen;
+        const ptFromScreen = this._props.inkView.ptFromScreen;
         if (ptFromScreen) {
             const order = controlIndex % 4;
-            const handleIndexA = ((order === 3 ? controlIndex - 1 : controlIndex - 2) + this.props.inkCtrlPoints.length) % this.props.inkCtrlPoints.length;
-            const handleIndexB = (order === 3 ? controlIndex + 2 : controlIndex + 1) % this.props.inkCtrlPoints.length;
-            const brokenIndices = Cast(this.props.inkDoc.brokenInkIndices, listSpec('number'));
+            const handleIndexA = ((order === 3 ? controlIndex - 1 : controlIndex - 2) + this._props.inkCtrlPoints.length) % this._props.inkCtrlPoints.length;
+            const handleIndexB = (order === 3 ? controlIndex + 2 : controlIndex + 1) % this._props.inkCtrlPoints.length;
+            const brokenIndices = Cast(this._props.inkDoc.brokenInkIndices, listSpec('number'));
             const wasSelected = InkStrokeProperties.Instance._currentPoint === controlIndex;
             if (!wasSelected) InkStrokeProperties.Instance._currentPoint = -1;
-            const origInk = this.props.inkCtrlPoints.slice();
+            const origInk = this._props.inkCtrlPoints.slice();
             setupMoveUpEvents(
                 this,
                 e,
                 action((e: PointerEvent, down: number[], delta: number[]) => {
-                    if (!this.props.inkView.controlUndo) this.props.inkView.controlUndo = UndoManager.StartBatch('drag ink ctrl pt');
+                    if (!this._props.inkView.controlUndo) this._props.inkView.controlUndo = UndoManager.StartBatch('drag ink ctrl pt');
                     const inkMoveEnd = ptFromScreen({ X: delta[0], Y: delta[1] });
                     const inkMoveStart = ptFromScreen({ X: 0, Y: 0 });
                     this.docView && InkStrokeProperties.Instance.moveControlPtHandle(this.docView, inkMoveEnd.X - inkMoveStart.X, inkMoveEnd.Y - inkMoveStart.Y, controlIndex, origInk);
                     return false;
                 }),
                 action(() => {
-                    if (this.props.inkView.controlUndo && this.docView) {
+                    if (this._props.inkView.controlUndo && this.docView) {
                         InkStrokeProperties.Instance.snapControl(this.docView, controlIndex);
                     }
-                    this.props.inkView.controlUndo?.end();
-                    this.props.inkView.controlUndo = undefined;
+                    this._props.inkView.controlUndo?.end();
+                    this._props.inkView.controlUndo = undefined;
                     UndoManager.FilterBatches(['data', 'x', 'y', 'width', 'height']);
                 }),
                 action((e: PointerEvent, doubleTap: boolean | undefined) => {
-                    const equivIndex = controlIndex === 0 ? this.props.inkCtrlPoints.length - 1 : controlIndex === this.props.inkCtrlPoints.length - 1 ? 0 : controlIndex;
+                    const equivIndex = controlIndex === 0 ? this._props.inkCtrlPoints.length - 1 : controlIndex === this._props.inkCtrlPoints.length - 1 ? 0 : controlIndex;
                     if (doubleTap || e.button === 2) {
                         if (!brokenIndices?.includes(equivIndex) && !brokenIndices?.includes(controlIndex)) {
                             if (brokenIndices) brokenIndices.push(controlIndex);
-                            else this.props.inkDoc.brokenInkIndices = new List<number>([controlIndex]);
+                            else this._props.inkDoc.brokenInkIndices = new List<number>([controlIndex]);
                         } else {
                             if (brokenIndices?.includes(equivIndex)) {
-                                if (!this.props.inkView.controlUndo) this.props.inkView.controlUndo = UndoManager.StartBatch('make smooth');
+                                if (!this._props.inkView.controlUndo) this._props.inkView.controlUndo = UndoManager.StartBatch('make smooth');
                                 this.docView && InkStrokeProperties.Instance.snapHandleTangent(this.docView, equivIndex, handleIndexA, handleIndexB);
                             }
                             if (equivIndex !== controlIndex && brokenIndices?.includes(controlIndex)) {
-                                if (!this.props.inkView.controlUndo) this.props.inkView.controlUndo = UndoManager.StartBatch('make smooth');
+                                if (!this._props.inkView.controlUndo) this._props.inkView.controlUndo = UndoManager.StartBatch('make smooth');
                                 this.docView && InkStrokeProperties.Instance.snapHandleTangent(this.docView, controlIndex, handleIndexA, handleIndexB);
                             }
                         }
-                        this.props.inkView.controlUndo?.end();
-                        this.props.inkView.controlUndo = undefined;
+                        this._props.inkView.controlUndo?.end();
+                        this._props.inkView.controlUndo = undefined;
                     }
                     this.changeCurrPoint(controlIndex);
                 }),
@@ -126,14 +132,14 @@ export class InkControlPtHandles extends React.Component<InkControlProps> {
 
     render() {
         // Accessing the current ink's data and extracting all control points.
-        const scrData = this.props.screenCtrlPoints;
+        const scrData = this._props.screenCtrlPoints;
         const sreenCtrlPoints: ControlPoint[] = [];
         for (let i = 0; i <= scrData.length - 4; i += 4) {
             sreenCtrlPoints.push({ ...scrData[i], I: i });
             sreenCtrlPoints.push({ ...scrData[i + 3], I: i + 3 });
         }
 
-        const inkData = this.props.inkCtrlPoints;
+        const inkData = this._props.inkCtrlPoints;
         const inkCtrlPts: ControlPoint[] = [];
         for (let i = 0; i <= inkData.length - 4; i += 4) {
             inkCtrlPts.push({ ...inkData[i], I: i });
@@ -141,23 +147,23 @@ export class InkControlPtHandles extends React.Component<InkControlProps> {
         }
 
         const closed = InkingStroke.IsClosed(inkData);
-        const nearestScreenPt = this.props.nearestScreenPt();
+        const nearestScreenPt = this._props.nearestScreenPt();
         const TagType = (broken?: boolean) => (broken ? 'rect' : 'circle');
         const hdl = (control: { X: number; Y: number; I: number }, scale: number, color: string) => {
-            const broken = Cast(this.props.inkDoc.brokenInkIndices, listSpec('number'))?.includes(control.I);
+            const broken = Cast(this._props.inkDoc.brokenInkIndices, listSpec('number'))?.includes(control.I);
             const Tag = TagType((control.I === 0 || control.I === inkData.length - 1) && !closed) as keyof JSX.IntrinsicElements;
             return (
                 <Tag
                     key={control.I.toString() + scale}
-                    x={control.X - this.props.screenSpaceLineWidth * 2 * scale}
-                    y={control.Y - this.props.screenSpaceLineWidth * 2 * scale}
+                    x={control.X - this._props.screenSpaceLineWidth * 2 * scale}
+                    y={control.Y - this._props.screenSpaceLineWidth * 2 * scale}
                     cx={control.X}
                     cy={control.Y}
-                    r={this.props.screenSpaceLineWidth * 2 * scale}
-                    opacity={this.props.inkView.controlUndo ? 0.35 : 1}
-                    height={this.props.screenSpaceLineWidth * 4 * scale}
-                    width={this.props.screenSpaceLineWidth * 4 * scale}
-                    strokeWidth={this.props.screenSpaceLineWidth / 2}
+                    r={this._props.screenSpaceLineWidth * 2 * scale}
+                    opacity={this._props.inkView.controlUndo ? 0.35 : 1}
+                    height={this._props.screenSpaceLineWidth * 4 * scale}
+                    width={this._props.screenSpaceLineWidth * 4 * scale}
+                    strokeWidth={this._props.screenSpaceLineWidth / 2}
                     stroke={Colors.MEDIUM_BLUE}
                     fill={broken ? Colors.MEDIUM_BLUE : color}
                     onPointerDown={(e: React.PointerEvent) => this.onControlDown(e, control.I)}
@@ -170,7 +176,7 @@ export class InkControlPtHandles extends React.Component<InkControlProps> {
         };
         return (
             <svg>
-                {!nearestScreenPt ? null : <circle key={'npt'} cx={nearestScreenPt.X} cy={nearestScreenPt.Y} r={this.props.screenSpaceLineWidth * 2} fill={'#00007777'} stroke={'#00007777'} strokeWidth={0} pointerEvents="none" />}
+                {!nearestScreenPt ? null : <circle key={'npt'} cx={nearestScreenPt.X} cy={nearestScreenPt.Y} r={this._props.screenSpaceLineWidth * 2} fill={'#00007777'} stroke={'#00007777'} strokeWidth={0} pointerEvents="none" />}
                 {sreenCtrlPoints.map(control => hdl(control, this._overControl !== control.I ? 1 : 3 / 2, Colors.WHITE))}
             </svg>
         );
@@ -185,10 +191,15 @@ export interface InkEndProps {
     endPt: () => PointData;
 }
 @observer
-export class InkEndPtHandles extends React.Component<InkEndProps> {
+export class InkEndPtHandles extends ObservableReactComponent<InkEndProps> {
     @observable _overStart: boolean = false;
     @observable _overEnd: boolean = false;
 
+    constructor(props: InkEndProps) {
+        super(props);
+        makeObservable(this);
+    }
+
     _throttle = 0; // need to throttle dragging since the position may change when the control points change.  this allows the stroke to settle so that we don't get increasingly bad jitter
     @action
     dragRotate = (e: React.PointerEvent, pt1: () => { X: number; Y: number }, pt2: () => { X: number; Y: number }) => {
@@ -198,7 +209,7 @@ export class InkEndPtHandles extends React.Component<InkEndProps> {
             e,
             action(e => {
                 if (this._throttle++ % 2 !== 0) return false;
-                if (!this.props.inkView.controlUndo) this.props.inkView.controlUndo = UndoManager.StartBatch('stretch ink');
+                if (!this._props.inkView.controlUndo) this._props.inkView.controlUndo = UndoManager.StartBatch('stretch ink');
                 // compute stretch factor by finding scaling along axis between start and end points
                 const p1 = pt1();
                 const p2 = pt2();
@@ -216,8 +227,8 @@ export class InkEndPtHandles extends React.Component<InkEndProps> {
             }),
             action(() => {
                 SnappingManager.SetIsDragging(false);
-                this.props.inkView.controlUndo?.end();
-                this.props.inkView.controlUndo = undefined;
+                this._props.inkView.controlUndo?.end();
+                this._props.inkView.controlUndo = undefined;
                 UndoManager.FilterBatches(['stroke', 'x', 'y', 'width', 'height']);
             }),
             returnFalse
@@ -230,7 +241,7 @@ export class InkEndPtHandles extends React.Component<InkEndProps> {
                 key={key}
                 cx={pt.X}
                 cy={pt.Y}
-                r={this.props.screenSpaceLineWidth * 2}
+                r={this._props.screenSpaceLineWidth * 2}
                 fill={this._overStart ? '#aaaaaa' : '#99999977'}
                 stroke={'#00007777'}
                 strokeWidth={0}
@@ -242,8 +253,8 @@ export class InkEndPtHandles extends React.Component<InkEndProps> {
         );
         return (
             <svg>
-                {hdl('start', this.props.startPt(), e => this.dragRotate(e, this.props.startPt, this.props.endPt))}
-                {hdl('end', this.props.endPt(), e => this.dragRotate(e, this.props.endPt, this.props.startPt))}
+                {hdl('start', this._props.startPt(), e => this.dragRotate(e, this._props.startPt, this._props.endPt))}
+                {hdl('end', this._props.endPt(), e => this.dragRotate(e, this._props.endPt, this._props.startPt))}
             </svg>
         );
     }
diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx
index 02f288a68..517a80d63 100644
--- a/src/client/views/PropertiesButtons.tsx
+++ b/src/client/views/PropertiesButtons.tsx
@@ -87,7 +87,7 @@ export class PropertiesButtons extends React.Component<{}, {}> {
                     containerDoc._isLightbox = !containerDoc._isLightbox;
                     //containerDoc._xPadding = containerDoc._yPadding = containerDoc._isLightbox ? 10 : undefined;
                     const containerContents = DocListCast(dv.dataDoc[Doc.LayoutFieldKey(containerDoc)]);
-                    //dv.Docuemnt.onClick = ScriptField.MakeScript('{self.data = undefined; documentView.select(false)}', { documentView: 'any' });
+                    //dv.Docuemnt.onClick = ScriptField.MakeScript('{this.data = undefined; documentView.select(false)}', { documentView: 'any' });
                     containerContents.forEach(doc => LinkManager.Links(doc).forEach(link => (link.link_displayLine = false)));
                 });
             }
@@ -219,7 +219,7 @@ export class PropertiesButtons extends React.Component<{}, {}> {
     //                 containerDoc._isLightbox = !containerDoc._isLightbox;
     //                 //containerDoc._xPadding = containerDoc._yPadding = containerDoc._isLightbox ? 10 : undefined;
     //                 const containerContents = DocListCast(dv.dataDoc[dv.props.fieldKey ?? Doc.LayoutFieldKey(containerDoc)]);
-    //                 //dv.Document.onClick = ScriptField.MakeScript('{self.data = undefined; documentView.select(false)}', { documentView: 'any' });
+    //                 //dv.Document.onClick = ScriptField.MakeScript('{this.data = undefined; documentView.select(false)}', { documentView: 'any' });
     //                 containerContents.forEach(doc => LinkManager.Links(doc).forEach(link => (link.layout_linkDisplay = false)));
     //             });
     //         }
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index 59d99b371..9ffdc350d 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -69,8 +69,8 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                 action((e, doubleTap) => {
                     if (doubleTap) {
                         this._isAnyChildContentActive = true;
-                        if (!this.dataDoc[this.fieldKey + '_1']) this.dataDoc[this.fieldKey + '_1'] = DocUtils.copyDragFactory(Doc.UserDoc().emptyNote as Doc);
-                        if (!this.dataDoc[this.fieldKey + '_2']) this.dataDoc[this.fieldKey + '_2'] = DocUtils.copyDragFactory(Doc.UserDoc().emptyNote as Doc);
+                        if (!this.dataDoc[this.fieldKey + '_1'] && !this.dataDoc[this.fieldKey]) this.dataDoc[this.fieldKey + '_1'] = DocUtils.copyDragFactory(Doc.UserDoc().emptyNote as Doc);
+                        if (!this.dataDoc[this.fieldKey + '_2'] && !this.dataDoc[this.fieldKey + '_alternate']) this.dataDoc[this.fieldKey + '_2'] = DocUtils.copyDragFactory(Doc.UserDoc().emptyNote as Doc);
                     }
                 }),
                 false,
@@ -131,8 +131,6 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         return false;
     };
 
-    whenChildContentsActiveChanged = action((isActive: boolean) => (this._isAnyChildContentActive = isActive));
-
     closeDown = (e: React.PointerEvent, which: string) => {
         setupMoveUpEvents(
             this,
diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx
index 78e4435ce..31a2367fc 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -55,7 +55,7 @@ export class KeyValueBox extends ObservableReactComponent<FieldViewProps> {
     @observable _splitPercentage = 50;
 
     get fieldDocToLayout() {
-        return this._props.fieldKey ? DocCast(this._props.Document[this._props.fieldKey], DocCast(this._props.Document)) : this._props.Document;
+        return DocCast(this._props.Document);
     }
 
     @action
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index d25101844..f06e5fad0 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -21,7 +21,7 @@ import { List } from '../../../../fields/List';
 import { PrefetchProxy } from '../../../../fields/Proxy';
 import { RichTextField } from '../../../../fields/RichTextField';
 import { ComputedField } from '../../../../fields/ScriptField';
-import { BoolCast, Cast, DocCast, FieldValue, NumCast, RTFCast, ScriptCast, StrCast } from '../../../../fields/Types';
+import { BoolCast, Cast, DateCast, DocCast, FieldValue, NumCast, RTFCast, ScriptCast, StrCast } from '../../../../fields/Types';
 import { GetEffectiveAcl, TraceMobx } from '../../../../fields/util';
 import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, DivWidth, emptyFunction, numberRange, returnFalse, returnZero, setupMoveUpEvents, smoothScroll, unimplementedFunction, Utils } from '../../../../Utils';
 import { gptAPICall, GPTCallType } from '../../../apis/gpt/GPT';
@@ -349,6 +349,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
             const prevData = Cast(this.layoutDoc[this.fieldKey], RichTextField, null); // the actual text in the text box
             const templateData = this.Document !== this.layoutDoc ? prevData : 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 layoutData = this.layoutDoc.isTemplateDoc ? Cast(this.layoutDoc[this.fieldKey], RichTextField, null) : undefined; // the default text inherited from a prototype
             const effectiveAcl = GetEffectiveAcl(dataDoc);
 
             const removeSelection = (json: string | undefined) => json?.replace(/"selection":.*/, '');
@@ -369,22 +370,20 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                 if (this._applyingChange !== this.fieldKey && (force || textChange || removeSelection(newJson) !== removeSelection(prevData?.Data))) {
                     this._applyingChange = this.fieldKey;
                     textChange && (dataDoc[this.fieldKey + '_modificationDate'] = new DateField(new Date(Date.now())));
-                    if ((!prevData && !protoData) || newText || (!newText && !protoData)) {
+                    if ((!prevData && !protoData && !layoutData) || newText || (!newText && !protoData && !layoutData)) {
                         // 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 (force || ((this._finishingLink || this._props.isContentActive() || this._inDrop) && (textChange || removeSelection(newJson) !== removeSelection(prevData?.Data)))) {
                             const numstring = NumCast(dataDoc[this.fieldKey], null);
-                            dataDoc[this.fieldKey] = numstring !== undefined ? Number(newText) : newText || DocCast(dataDoc.proto)?.[this.fieldKey] === undefined ? new RichTextField(newJson, newText) : undefined;
-
+                            dataDoc[this.fieldKey] =
+                                numstring !== undefined ? Number(newText) : newText || (DocCast(dataDoc.proto)?.[this.fieldKey] === undefined && this.layoutDoc[this.fieldKey] === undefined) ? new RichTextField(newJson, newText) : undefined;
                             textChange && ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.Document, text: newText });
                             this._applyingChange = ''; // turning this off here allows a Doc to retrieve data from template if noTemplate below is changed to false
-                            dataDoc[this.fieldKey + '_noTemplate'] = newText ? true : false; // mark the data field as being split from the template if it has been edited
                             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((protoData || prevData).Data)));
-                        dataDoc[this.fieldKey + '_noTemplate'] = undefined; // mark the data field as not being split from any template it might have
+                        this._editorView.updateState(EditorState.fromJSON(this.config, JSON.parse((layoutData ?? protoData ?? prevData).Data)));
                         ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.layoutDoc, text: newText });
                         unchanged = false;
                     }
@@ -958,6 +957,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
 
         const options = cm.findByDescription('Options...');
         const optionItems = options && 'subitems' in options ? options.subitems : [];
+        optionItems.push({ description: `Toggle auto update from template`, event: () => (this.dataDoc[this.fieldKey + '_autoUpdate'] = !this.dataDoc[this.fieldKey + '_autoUpdate']), icon: 'star' });
         optionItems.push({ description: `Generate Dall-E Image`, event: () => this.generateImage(), icon: 'star' });
         optionItems.push({ description: `Ask GPT-3`, event: () => this.askGPT(), icon: 'lightbulb' });
         this._props.renderDepth &&
@@ -1244,9 +1244,15 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         );
         this._disposers.editorState = reaction(
             () => {
-                const dataDoc = Doc.IsDelegateField(DocCast(this.layoutDoc?.proto), this.fieldKey) ? DocCast(this.layoutDoc?.proto) : this?.dataDoc;
-                const whichDoc = !this.dataDoc || !this.layoutDoc ? undefined : dataDoc?.[this.fieldKey + '_noTemplate'] || !this.layoutDoc[this.fieldKey] ? dataDoc : this.layoutDoc;
-                return !whichDoc ? undefined : { data: Cast(whichDoc[this.fieldKey], RichTextField, null), str: Field.toString(DocCast(whichDoc[this.fieldKey]) ?? StrCast(whichDoc[this.fieldKey])) };
+                const protoData = DocCast(this.dataDoc.proto)?.[this.fieldKey];
+                const dataData = this.dataDoc[this.fieldKey];
+                const layoutData = Doc.AreProtosEqual(this.layoutDoc, this.dataDoc) ? undefined : this.layoutDoc[this.fieldKey];
+                const dataTime = dataData ? DateCast(this.dataDoc[this.fieldKey + '_modificationDate'])?.date.getTime() ?? 0 : 0;
+                const layoutTime = layoutData && this.dataDoc[this.fieldKey + '_autoUpdate'] ? DateCast(DocCast(this.layoutDoc)[this.fieldKey + '_modificationDate'])?.date.getTime() ?? 0 : 0;
+                const protoTime = protoData && this.dataDoc[this.fieldKey + '_autoUpdate'] ? DateCast(DocCast(this.dataDoc.proto)[this.fieldKey + '_modificationDate'])?.date.getTime() ?? 0 : 0;
+                const recentData = dataTime >= layoutTime ? (protoTime >= dataTime ? protoData : dataData) : layoutTime >= protoTime ? layoutData : protoData;
+                const whichData = recentData ?? (this.layoutDoc.isTemplateDoc ? layoutData : protoData) ?? protoData;
+                return !whichData ? undefined : { data: RTFCast(whichData), str: Field.toString(DocCast(whichData) ?? StrCast(whichData)) };
             },
             incomingValue => {
                 if (this._editorView && this._applyingChange !== this.fieldKey) {
@@ -1256,11 +1262,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                             this._editorView.updateState(EditorState.fromJSON(this.config, updatedState));
                             this.tryUpdateScrollHeight();
                         }
-                    } else {
+                    } else if (this._editorView.state.doc.textContent !== incomingValue?.str) {
                         selectAll(this._editorView.state, tx => this._editorView?.dispatch(tx.insertText(incomingValue?.str ?? '')));
                     }
                 }
-            }
+            },
+            { fireImmediately: true }
         );
 
         this._disposers.search = reaction(
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index bdf600475..200896e25 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -48,7 +48,7 @@ export namespace Field {
         const valFunc = (field: Field): string => {
             const res =
                 field instanceof ComputedField && showComputedValue
-                    ? field._lastComputedResult
+                    ? field.value(doc)
                     : field instanceof ComputedField
                       ? `:=${field.script.originalScript.replace(/dashCallChat\(_setCacheResult_, this, `(.*)`\)/, '(($1))')}`
                       : field instanceof ScriptField
diff --git a/src/fields/ScriptField.ts b/src/fields/ScriptField.ts
index 9021c8896..8b51088b2 100644
--- a/src/fields/ScriptField.ts
+++ b/src/fields/ScriptField.ts
@@ -198,14 +198,22 @@ export class ComputedField extends ScriptField {
     }
 
     _lastComputedResult: FieldResult;
-    value = computedFn((doc: Doc) => this._valueOutsideReaction(doc));
-    _valueOutsideReaction = (doc: Doc) => {
-        this._lastComputedResult =
-            this._cachedResult ?? (this.script.compiled && this.script.run({ this: doc, self: doc, value: '', _setCacheResult_: this.setCacheResult, _last_: this._lastComputedResult, _readOnly_: true }, console.log).result);
-        return this._lastComputedResult;
-    };
+    value = (doc:Doc) => (this._lastComputedResult = this._cachedResult ?? 
+        computedFn((doc: Doc) =>
+            this.script.compiled &&
+            this.script.run( {
+                    this: doc, 
+                    //value: '',
+                    _setCacheResult_: this.setCacheResult,
+                    _last_: this._lastComputedResult,
+                    _readOnly_: true,
+                },
+                console.log
+            ).result
+        )(doc)
+    ); // prettier-ignore
 
-    [ToValue](doc: Doc)   { if (ComputedField.useComputed) return { value: this._valueOutsideReaction(doc) };  } // prettier-ignore
+    [ToValue](doc: Doc)   { if (ComputedField.useComputed) return { value: this.value(doc) };  } // prettier-ignore
     [Copy](): ObjectField { return new ComputedField(this.script, this.setterscript, this.rawscript); } // prettier-ignore
 
     public static MakeFunction(script: string, params: object = {}, capturedVariables?: { [name: string]: Doc | string | number | boolean }, setterscript?: string) {
-- 
cgit v1.2.3-70-g09d2

