From 2e99ef0512b8f1b5c089637643ff6addf5afd943 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 4 Mar 2024 16:07:52 -0500
Subject: cleaned up a number of things related to autoHeight and
 stacking/masonry/notetaking views

---
 .../views/collections/CollectionStackingView.tsx   | 73 +++++++---------------
 1 file changed, 21 insertions(+), 52 deletions(-)

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

diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index 54314f62c..dc391631a 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -11,12 +11,11 @@ import { listSpec } from '../../../fields/Schema';
 import { SchemaHeaderField } from '../../../fields/SchemaHeaderField';
 import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
 import { TraceMobx } from '../../../fields/util';
-import { emptyFunction, returnEmptyDoclist, returnFalse, returnNone, returnZero, setupMoveUpEvents, smoothScroll, Utils } from '../../../Utils';
+import { DivHeight, emptyFunction, returnEmptyDoclist, returnFalse, returnNone, returnZero, setupMoveUpEvents, smoothScroll, Utils } from '../../../Utils';
 import { Docs, DocUtils } from '../../documents/Documents';
 import { CollectionViewType } from '../../documents/DocumentTypes';
 import { DragManager, dropActionType } from '../../util/DragManager';
 import { SettingsManager } from '../../util/SettingsManager';
-import { SnappingManager } from '../../util/SnappingManager';
 import { Transform } from '../../util/Transform';
 import { undoBatch, UndoManager } from '../../util/UndoManager';
 import { ContextMenu } from '../ContextMenu';
@@ -45,17 +44,16 @@ export type collectionStackingViewProps = {
 
 @observer
 export class CollectionStackingView extends CollectionSubView<Partial<collectionStackingViewProps>>() {
+    _disposers: { [key: string]: IReactionDisposer } = {};
     _masonryGridRef: HTMLDivElement | null = null;
     // used in a column dragger, likely due for the masonry grid view. We want to use this
     _draggerRef = React.createRef<HTMLDivElement>();
-    // Not sure what a pivot field is. Seems like we cause reaction in MobX get rid of it once we exit this view
-    _pivotFieldDisposer?: IReactionDisposer;
-    // Seems like we cause reaction in MobX get rid of our height once we exit this view
-    _layout_autoHeightDisposer?: IReactionDisposer;
     // keeping track of documents. Updated on internal and external drops. What's the difference?
     _docXfs: { height: () => number; width: () => number; stackedDocTransform: () => Transform }[] = [];
     // Doesn't look like this field is being used anywhere. Obsolete?
     _columnStart: number = 0;
+
+    @observable _refList: any[] = [];
     // map of node headers to their heights. Used in Masonry
     @observable _heightMap = new Map<string, number>();
     // Assuming that this is the current css cursor style
@@ -207,27 +205,27 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
         this._props.setContentViewBox?.(this);
 
         // reset section headers when a new filter is inputted
-        this._pivotFieldDisposer = reaction(
+        this._disposers.pivotField = reaction(
             () => this.pivotField,
             () => (this.dataDoc['_' + this.fieldKey + '_columnHeaders'] = new List())
         );
-        this._layout_autoHeightDisposer = reaction(
+        this._disposers.autoHeight = reaction(
             () => this.layoutDoc._layout_autoHeight,
-            layout_autoHeight =>
-                layout_autoHeight &&
-                this._props.setHeight?.(
-                    Math.min(
-                        NumCast(this.layoutDoc._maxHeight, Number.MAX_SAFE_INTEGER),
-                        this.headerMargin + (this.isStackingView ? Math.max(...this.refList.map(r => Number(getComputedStyle(r).height.replace('px', '')))) : this.refList.reduce((p, r) => p + Number(getComputedStyle(r).height.replace('px', '')), 0))
-                    )
-                )
+            layout_autoHeight => layout_autoHeight && this._props.setHeight?.(this.headerMargin + (this.isStackingView ? Math.max(...this._refList.map(DivHeight)) : this._refList.reduce((p, r) => p + DivHeight(r), 0)))
+        );
+        this._disposers.refList = reaction(
+            () => ({ refList: this._refList.slice(), autoHeight: this.layoutDoc._layout_autoHeight && !LightboxView.Contains(this.DocumentView?.()) }),
+            ({ refList, autoHeight }) => {
+                this.observer.disconnect();
+                if (autoHeight) refList.forEach(r => this.observer.observe(r));
+            },
+            { fireImmediately: true }
         );
     }
 
     componentWillUnmount() {
         super.componentWillUnmount();
-        this._pivotFieldDisposer?.();
-        this._layout_autoHeightDisposer?.();
+        Object.keys(this._disposers).forEach(key => this._disposers[key]());
     }
 
     isAnyChildContentActive = () => this._props.isAnyChildContentActive();
@@ -523,7 +521,6 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
         });
     };
     headings = () => Array.from(this.Sections);
-    refList: any[] = [];
     // what a section looks like if we're in stacking view
     sectionStacking = (heading: SchemaHeaderField | undefined, docList: Doc[]) => {
         const key = this.pivotField;
@@ -536,23 +533,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
         }
         return (
             <CollectionStackingViewFieldColumn
-                unobserveHeight={ref => this.refList.splice(this.refList.indexOf(ref), 1)}
-                observeHeight={ref => {
-                    if (ref) {
-                        this.refList.push(ref);
-                        this.observer = new _global.ResizeObserver(
-                            action((entries: any) => {
-                                if (this.layoutDoc._layout_autoHeight && ref && this.refList.length && !SnappingManager.IsDragging) {
-                                    const height = this.headerMargin + Math.min(NumCast(this.layoutDoc._maxHeight, Number.MAX_SAFE_INTEGER), Math.max(...this.refList.map(r => Number(getComputedStyle(r).height.replace('px', '')))));
-                                    if (!LightboxView.Contains(this.DocumentView?.())) {
-                                        this._props.setHeight?.(height);
-                                    }
-                                }
-                            })
-                        );
-                        this.observer.observe(ref);
-                    }
-                }}
+                refList={this._refList}
                 addDocument={this.addDocument}
                 chromeHidden={this.chromeHidden}
                 colHeaderData={this.colHeaderData}
@@ -591,21 +572,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
                 Document={this.Document}
                 chromeHidden={this.chromeHidden}
                 pivotField={this.pivotField}
-                unobserveHeight={ref => this.refList.splice(this.refList.indexOf(ref), 1)}
-                observeHeight={ref => {
-                    if (ref) {
-                        this.refList.push(ref);
-                        this.observer = new _global.ResizeObserver(
-                            action((entries: any) => {
-                                if (this.layoutDoc._layout_autoHeight && ref && this.refList.length && !SnappingManager.IsDragging) {
-                                    const height = this.refList.reduce((p, r) => p + Number(getComputedStyle(r).height.replace('px', '')), 0);
-                                    this._props.setHeight?.(2 * this.headerMargin + height); // bcz: added 2x for header to fix problem with scrollbars appearing in Tools panel
-                                }
-                            })
-                        );
-                        this.observer.observe(ref);
-                    }
-                }}
+                refList={this._refList}
                 key={heading ? heading.heading : ''}
                 rows={rows}
                 headings={this.headings}
@@ -709,7 +676,9 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
     @computed get backgroundEvents() {
         return this._props.isContentActive() === false ? 'none' : undefined;
     }
-    observer: any;
+
+    observer = new _global.ResizeObserver(() => this._props.setHeight?.(this.headerMargin + (this.isStackingView ? Math.max(...this._refList.map(DivHeight)) : this._refList.reduce((p, r) => p + DivHeight(r), 0))));
+
     render() {
         TraceMobx();
         const editableViewProps = {
-- 
cgit v1.2.3-70-g09d2


From bd9170eaa21a2fbccd0912f50d40cefaf0bbe47f Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 6 Mar 2024 12:28:24 -0500
Subject: cleaned up some dropActionType strings.  fixed stackingview scrolling
 when always active.  fixed schemaView keys dropdown menu to be a popup. 
 cleaned up  display of key Finfo's

---
 src/client/documents/Documents.ts                  |  46 +++----
 src/client/views/DashboardView.tsx                 |   9 +-
 src/client/views/TemplateMenu.tsx                  |   2 +-
 .../views/collections/CollectionStackingView.tsx   |  15 ++-
 src/client/views/collections/CollectionSubView.tsx |   5 +-
 src/client/views/collections/TreeView.tsx          |   6 +-
 .../collectionSchema/CollectionSchemaView.scss     | 149 +++++++++++----------
 .../collectionSchema/CollectionSchemaView.tsx      |  68 ++++++----
 src/client/views/nodes/LinkAnchorBox.tsx           |   4 +-
 .../views/nodes/RecordingBox/RecordingBox.tsx      |   4 +-
 .../views/nodes/formattedText/FormattedTextBox.tsx |   4 +-
 11 files changed, 170 insertions(+), 142 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 031560886..c9b5cb915 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -70,14 +70,14 @@ class EmptyBox {
 }
 
 export enum FInfoFieldType {
-    string,
-    boolean,
-    number,
-    Doc,
-    enumeration,
-    date,
-    list,
-    rtf,
+    string = 'string',
+    boolean = 'boolean',
+    number = 'number',
+    Doc = 'Doc',
+    enumeration = 'enum',
+    date = 'date',
+    list = 'list',
+    rtf = 'rich text',
 }
 export class FInfo {
     description: string = '';
@@ -198,20 +198,20 @@ type DATEt = DateInfo | number;
 type DTYPEt = DTypeInfo | string;
 export class DocumentOptions {
     // coordinate and dimensions depending on view
-    x?: NUMt = new NumInfo('x coordinate of document in a freeform view', false);
-    y?: NUMt = new NumInfo('y coordinate of document in a freeform view', false);
+    x?: NUMt = new NumInfo('horizontal coordinate in freeform view', false);
+    y?: NUMt = new NumInfo('vertical coordinate in freeform view', false);
     z?: NUMt = new NumInfo('whether document is in overlay (1) or not (0)', false, false, [1, 0]);
-    overlayX?: NUMt = new NumInfo('x coordinate of document in a overlay view', false);
-    overlayY?: NUMt = new NumInfo('y coordinate of document in a overlay view', false);
-    text?: RTFt = new RtfInfo('rich text of a text doc', true);
+    overlayX?: NUMt = new NumInfo('horizontal coordinate in overlay view', false);
+    overlayY?: NUMt = new NumInfo('vertical coordinate in overlay view', false);
+    text?: RTFt = new RtfInfo('plain or rich text', true);
     _dimMagnitude?: NUMt = new NumInfo("magnitude of collectionMulti{row,col} element's width or height", false);
     _dimUnit?: DIMt = new DimInfo("units of collectionMulti{row,col} element's width or height - 'px' or '*' for pixels or relative units");
-    latitude?: NUMt = new NumInfo('latitude coordinate for map views', false);
-    longitude?: NUMt = new NumInfo('longitude coordinate for map views', false);
+    latitude?: NUMt = new NumInfo('latitude coordinate', false);
+    longitude?: NUMt = new NumInfo('longitude coordinate', false);
     routeCoordinates?: STRt = new StrInfo("stores a route's/direction's coordinates (stringified version)"); // for a route document, this stores the route's coordinates
-    markerType?: STRt = new StrInfo('Defines the marker type for a pushpin document');
-    markerColor?: STRt = new StrInfo('Defines the marker color for a pushpin document');
-    map?: STRt = new StrInfo('text location of map');
+    markerType?: STRt = new StrInfo('marker type for a pushpin document');
+    markerColor?: STRt = new StrInfo('marker color for a pushpin document');
+    map?: STRt = new StrInfo('map location name');
     map_type?: STRt = new StrInfo('type of map view', false);
     map_zoom?: NUMt = new NumInfo('zoom of a map view', false);
     map_pitch?: NUMt = new NumInfo('pitch of a map view', false);
@@ -221,11 +221,11 @@ export class DocumentOptions {
     date_range?: STRt = new StrInfo('date range for calendar', false);
 
     wikiData?: STRt = new StrInfo('WikiData ID related to map location');
-    description?: STRt = new StrInfo('A description of the document');
-    _timecodeToShow?: NUMt = new NumInfo('the time that a document should be displayed (e.g., when an annotation shows up as a video plays)', false);
-    _timecodeToHide?: NUMt = new NumInfo('the time that a document should be hidden', false);
-    _width?: NUMt = new NumInfo('displayed width of a document');
-    _height?: NUMt = new NumInfo('displayed height of document');
+    description?: STRt = new StrInfo('description of document');
+    _timecodeToShow?: NUMt = new NumInfo('media timecode when document should appear (e.g., when an annotation shows up as a video plays)', false);
+    _timecodeToHide?: NUMt = new NumInfo('media timecode when document should disappear', false);
+    _width?: NUMt = new NumInfo("width of document in container's coordinates");
+    _height?: NUMt = new NumInfo("height of document in container's coordiantes");
     data_nativeWidth?: NUMt = new NumInfo('native width of data field contents (e.g., the pixel width of an image)', false);
     data_nativeHeight?: NUMt = new NumInfo('native height of data field contents (e.g., the pixel height of an image)', false);
     linearBtnWidth?: NUMt = new NumInfo('unexpanded width of a linear menu button (button "width" changes when it expands)', false);
diff --git a/src/client/views/DashboardView.tsx b/src/client/views/DashboardView.tsx
index 472d419fc..439d250de 100644
--- a/src/client/views/DashboardView.tsx
+++ b/src/client/views/DashboardView.tsx
@@ -28,6 +28,7 @@ import { Colors } from './global/globalEnums';
 import { MainViewModal } from './MainViewModal';
 import { ButtonType } from './nodes/FontIconBox/FontIconBox';
 import { ObservableReactComponent } from './ObservableReactComponent';
+import { dropActionType } from '../util/DragManager';
 
 enum DashboardGroup {
     MyDashboards,
@@ -403,7 +404,7 @@ export class DashboardView extends ObservableReactComponent<{}> {
             _layout_fitWidth: true,
             _gridGap: 5,
             _forceActive: true,
-            childDragAction: 'embed',
+            childDragAction: dropActionType.embed,
             treeView_TruncateTitleWidth: 150,
             ignoreClick: true,
             contextMenuIcons: new List<string>(['plus']),
@@ -411,7 +412,7 @@ export class DashboardView extends ObservableReactComponent<{}> {
             _lockedPosition: true,
             layout_boxShadow: '0 0',
             childDontRegisterViews: true,
-            dropAction: 'same',
+            dropAction: dropActionType.same,
             isSystem: true,
             layout_explainer: 'All of the calendars that you have created will appear here.',
         };
@@ -450,7 +451,7 @@ export class DashboardView extends ObservableReactComponent<{}> {
             _layout_fitWidth: true,
             _gridGap: 5,
             _forceActive: true,
-            childDragAction: 'embed',
+            childDragAction: dropActionType.embed,
             treeView_TruncateTitleWidth: 150,
             ignoreClick: true,
             layout_headerButton: myTrailsBtn,
@@ -459,7 +460,7 @@ export class DashboardView extends ObservableReactComponent<{}> {
             _lockedPosition: true,
             layout_boxShadow: '0 0',
             childDontRegisterViews: true,
-            dropAction: 'same',
+            dropAction: dropActionType.same,
             isSystem: true,
             layout_explainer: 'All of the trails that you have created will appear here.',
         };
diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx
index 5d9abb460..00195f7d7 100644
--- a/src/client/views/TemplateMenu.tsx
+++ b/src/client/views/TemplateMenu.tsx
@@ -105,7 +105,7 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
         const addedTypes = DocListCast(Cast(Doc.UserDoc()['template_clickFuncs'], Doc, null)?.data);
         const templateMenu: Array<JSX.Element> = [];
         this.props.templates?.forEach((checked, template) => templateMenu.push(<TemplateToggle key={template} template={template} checked={checked} toggle={this.toggleTemplate} />));
-        templateMenu.push(<OtherToggle key={'default'} name={'Default'} checked={templateName === 'layout'} toggle={this.toggleDefault} />);
+        templateMenu.push(<OtherToggle key="default" name={firstDoc.layout instanceof Doc ? StrCast(firstDoc.layout.title) : 'Default'} checked={templateName === 'layout'} toggle={this.toggleDefault} />);
         addedTypes.concat(noteTypes).map(template => (template.treeView_Checked = this.templateIsUsed(firstDoc, template)));
         this._addedKeys &&
             Array.from(this._addedKeys)
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index dc391631a..ea1caf58f 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -233,10 +233,6 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
     moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean): boolean => {
         return this._props.removeDocument?.(doc) && addDocument?.(doc) ? true : false;
     };
-    createRef = (ele: HTMLDivElement | null) => {
-        this._masonryGridRef = ele;
-        this.createDashEventsTarget(ele!); //so the whole grid is the drop target?
-    };
 
     onChildClickHandler = () => this._props.childClickScript || ScriptCast(this.Document.onChildClick);
     @computed get onChildDoubleClickHandler() {
@@ -679,6 +675,8 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
 
     observer = new _global.ResizeObserver(() => this._props.setHeight?.(this.headerMargin + (this.isStackingView ? Math.max(...this._refList.map(DivHeight)) : this._refList.reduce((p, r) => p + DivHeight(r), 0))));
 
+    onPassiveWheel = (e: WheelEvent) => e.stopPropagation();
+    _oldWheel: any;
     render() {
         TraceMobx();
         const editableViewProps = {
@@ -699,7 +697,14 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
                 <div className="collectionStackingMasonry-cont">
                     <div
                         className={this.isStackingView ? 'collectionStackingView' : 'collectionMasonryView'}
-                        ref={this.createRef}
+                        ref={ele => {
+                            this._masonryGridRef = ele;
+                            this.createDashEventsTarget(ele); //so the whole grid is the drop target?
+                            this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel);
+                            this._oldWheel = ele;
+                            // prevent wheel events from passively propagating up through containers and prevents containers from preventDefault which would block scrolling
+                            ele?.addEventListener('wheel', this.onPassiveWheel, { passive: false });
+                        }}
                         style={{
                             overflowY: this.isContentActive() ? 'auto' : 'hidden',
                             background: this._props.styleProvider?.(this.Document, this._props, StyleProp.BackgroundColor),
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index fdbd1cc90..59a695a3d 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -219,12 +219,13 @@ export function CollectionSubView<X>(moreProps?: X) {
                 const targetDocments = DocListCast(this.dataDoc[this._props.fieldKey]);
                 const someMoved = !dropAction && docDragData.draggedDocuments.some(drag => targetDocments.includes(drag));
                 if (someMoved) docDragData.droppedDocuments = docDragData.droppedDocuments.map((drop, i) => (targetDocments.includes(docDragData.draggedDocuments[i]) ? docDragData.draggedDocuments[i] : drop));
-                if ((!dropAction || dropAction === 'inSame' || dropAction === 'same' || dropAction === 'move' || someMoved) && docDragData.moveDocument) {
+                if ((!dropAction || dropAction === dropActionType.inPlace || dropAction === dropActionType.same || dropAction === dropActionType.move || someMoved) && docDragData.moveDocument) {
                     const movedDocs = docDragData.droppedDocuments.filter((d, i) => docDragData.draggedDocuments[i] === d);
                     const addedDocs = docDragData.droppedDocuments.filter((d, i) => docDragData.draggedDocuments[i] !== d);
                     if (movedDocs.length) {
                         const canAdd =
-                            (de.embedKey || dropAction || Doc.AreProtosEqual(Cast(movedDocs[0].annotationOn, Doc, null), this.Document)) && (dropAction !== 'inSame' || docDragData.draggedDocuments.every(d => d.embedContainer === this.Document));
+                            (de.embedKey || dropAction || Doc.AreProtosEqual(Cast(movedDocs[0].annotationOn, Doc, null), this.Document)) &&
+                            (dropAction !== dropActionType.inPlace || docDragData.draggedDocuments.every(d => d.embedContainer === this.Document));
                         const moved = docDragData.moveDocument(movedDocs, this.Document, canAdd ? this.addDocument : returnFalse);
                         added = canAdd || moved ? moved : undefined;
                     } else if (addedDocs.length) {
diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx
index 4f2d8b9c0..285a789e6 100644
--- a/src/client/views/collections/TreeView.tsx
+++ b/src/client/views/collections/TreeView.tsx
@@ -449,10 +449,10 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
 
         const addDoc = inside ? this.localAdd : parentAddDoc;
         const canAdd = !StrCast((inside ? this.Document : this._props.treeViewParent)?.treeView_FreezeChildren).includes('add') || forceAdd;
-        if (canAdd && (dropAction !== 'inSame' || droppedDocuments.every(d => d.embedContainer === this._props.parentTreeView?.Document))) {
-            const move = (!dropAction || canEmbed || dropAction === 'proto' || dropAction === 'move' || dropAction === 'same' || dropAction === 'inSame') && moveDocument;
+        if (canAdd && (dropAction !== dropActionType.inPlace || droppedDocuments.every(d => d.embedContainer === this._props.parentTreeView?.Document))) {
+            const move = (!dropAction || canEmbed || dropAction === dropActionType.proto || dropAction === dropActionType.move || dropAction === dropActionType.same || dropAction === dropActionType.inPlace) && moveDocument;
             this._props.parentTreeView instanceof TreeView && (this._props.parentTreeView.dropping = true);
-            const res = droppedDocuments.reduce((added, d) => (move ? move(d, undefined, addDoc) || (dropAction === 'proto' ? addDoc(d) : false) : addDoc(d)) || added, false);
+            const res = droppedDocuments.reduce((added, d) => (move ? move(d, undefined, addDoc) || (dropAction === dropActionType.proto ? addDoc(d) : false) : addDoc(d)) || added, false);
             this._props.parentTreeView instanceof TreeView && (this._props.parentTreeView.dropping = false);
             return res;
         }
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.scss b/src/client/views/collections/collectionSchema/CollectionSchemaView.scss
index fed4e89cf..ac0bd2378 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaView.scss
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.scss
@@ -38,91 +38,97 @@
                 background: yellow;
             }
         }
+    }
 
-        .schema-column-menu,
-        .schema-filter-menu {
-            background: $light-gray;
-            position: absolute;
-            min-width: 200px;
-            max-width: 400px;
-            display: flex;
-            flex-direction: column;
-            align-items: flex-start;
-            z-index: 1;
-
-            .schema-key-search-input {
-                width: calc(100% - 20px);
-                margin: 10px;
-            }
+    .schema-preview-divider {
+        height: 100%;
+        background: black;
+        cursor: ew-resize;
+    }
+}
 
-            .schema-search-result {
-                cursor: pointer;
-                padding: 5px 10px;
-                width: 100%;
+.schema-column-menu,
+.schema-filter-menu {
+    background: $light-gray;
+    position: relative;
+    min-width: 200px;
+    max-width: 400px;
+    display: flex;
+    flex-direction: column;
+    align-items: flex-start;
+    z-index: 1;
 
-                &:hover {
-                    background-color: $medium-gray;
-                }
+    .schema-key-search-input {
+        width: calc(100% - 20px);
+        margin: 10px;
+    }
 
-                .schema-search-result-type,
-                .schema-search-result-desc {
-                    color: $dark-gray;
-                    font-size: $body-text;
-                }
-            }
+    .schema-search-result {
+        cursor: pointer;
+        padding: 5px 10px;
+        width: 100%;
 
-            .schema-key-search,
-            .schema-new-key-options {
-                width: 100%;
-                display: flex;
-                flex-direction: column;
-                align-items: flex-start;
-            }
+        &:hover {
+            background-color: $medium-gray;
+        }
+        .schema-search-result-type {
+            font-family: 'Courier New', Courier, monospace;
+        }
 
-            .schema-new-key-options {
-                margin: 10px;
-                .schema-key-warning {
-                    color: red;
-                    font-weight: normal;
-                    align-self: center;
-                }
-            }
+        .schema-search-result-type,
+        .schema-search-result-desc {
+            color: $dark-gray;
+            font-size: $body-text;
+        }
+        .schema-search-result-desc {
+            font-style: italic;
+        }
+    }
 
-            .schema-key-list {
-                width: 100%;
-                max-height: 300px;
-                overflow-y: auto;
-            }
+    .schema-key-search,
+    .schema-new-key-options {
+        width: 100%;
+        display: flex;
+        flex-direction: column;
+        align-items: flex-start;
+    }
 
-            .schema-key-type-option {
-                margin: 2px 0px;
+    .schema-new-key-options {
+        margin: 10px;
+        .schema-key-warning {
+            color: red;
+            font-weight: normal;
+            align-self: center;
+        }
+    }
 
-                input {
-                    margin-right: 5px;
-                }
-            }
+    .schema-key-list {
+        width: 100%;
+        max-height: 300px;
+        overflow-y: auto;
+    }
 
-            .schema-key-default-val {
-                margin: 5px 0;
-            }
+    .schema-key-type-option {
+        margin: 2px 0px;
 
-            .schema-column-menu-button {
-                cursor: pointer;
-                padding: 2px 5px;
-                background: $medium-blue;
-                border-radius: 9999px;
-                color: $white;
-                width: fit-content;
-                margin: 5px;
-                align-self: center;
-            }
+        input {
+            margin-right: 5px;
         }
     }
 
-    .schema-preview-divider {
-        height: 100%;
-        background: black;
-        cursor: ew-resize;
+    .schema-key-default-val {
+        margin: 5px 0;
+    }
+
+    .schema-column-menu-button {
+        cursor: pointer;
+        padding: 2px 5px;
+        background: $medium-blue;
+        border-radius: 9999px;
+        color: $white;
+        width: fit-content;
+        margin: 5px;
+        align-self: center;
     }
 }
 
@@ -133,6 +139,7 @@
     .row-menu {
         display: flex;
         justify-content: center;
+        cursor: pointer;
     }
 }
 
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
index eee3836d2..12f0ad5e9 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
@@ -1,5 +1,5 @@
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, makeObservable, observable, ObservableMap, observe } from 'mobx';
+import { action, computed, makeObservable, observable, ObservableMap, observe, trace } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { Doc, DocListCast, Field, NumListCast, Opt, StrListCast } from '../../../../fields/Doc';
@@ -25,6 +25,8 @@ import { CollectionSubView } from '../CollectionSubView';
 import './CollectionSchemaView.scss';
 import { SchemaColumnHeader } from './SchemaColumnHeader';
 import { SchemaRowBox } from './SchemaRowBox';
+import { Popup, PopupTrigger, Type } from 'browndash-components';
+import { SettingsManager } from '../../../util/SettingsManager';
 const { default: { SCHEMA_NEW_NODE_HEIGHT } } = require('../../global/globalCssVariables.module.scss'); // prettier-ignore
 
 export enum ColumnType {
@@ -161,7 +163,7 @@ export class CollectionSchemaView extends CollectionSubView() {
                         (change as any).added.forEach((doc: Doc) => // for each document added 
                             Doc.GetAllPrototypes(doc.value as Doc).forEach(proto => // for all of its prototypes (and itself)
                                 Object.keys(proto).forEach(action(key => // check if any of its keys are new, and add them
-                                    !this.fieldInfos.get(key) && this.fieldInfos.set(key, new FInfo(key, key === 'author'))))));
+                                    !this.fieldInfos.get(key) && this.fieldInfos.set(key, new FInfo("-no description-", key === 'author'))))));
                         break;
                     case 'update': //let oldValue = change.oldValue; // fill this in if the entire child list will ever be reassigned with a new list
                 }
@@ -681,6 +683,7 @@ export class CollectionSchemaView extends CollectionSubView() {
                         } else {
                             this.setKey(this._menuValue, this._newFieldDefault);
                         }
+                        this._columnMenuIndex = undefined;
                     })}>
                     done
                 </div>
@@ -688,12 +691,12 @@ export class CollectionSchemaView extends CollectionSubView() {
         );
     }
 
-    onPassiveWheel = (e: WheelEvent) => {
+    onKeysPassiveWheel = (e: WheelEvent) => {
         // if scrollTop is 0, then don't let wheel trigger scroll on any container (which it would since onScroll won't be triggered on this)
-        if (!this._oldWheel.scrollTop && e.deltaY <= 0) e.preventDefault();
+        if (!this._oldKeysWheel.scrollTop && e.deltaY <= 0) e.preventDefault();
         e.stopPropagation();
     };
-    _oldWheel: any;
+    _oldKeysWheel: any;
     @computed get keysDropdown() {
         return (
             <div className="schema-key-search">
@@ -708,9 +711,9 @@ export class CollectionSchemaView extends CollectionSubView() {
                 <div
                     className="schema-key-list"
                     ref={r => {
-                        this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel);
-                        this._oldWheel = r;
-                        r?.addEventListener('wheel', this.onPassiveWheel, { passive: false });
+                        this._oldKeysWheel?.removeEventListener('wheel', this.onKeysPassiveWheel);
+                        this._oldKeysWheel = r;
+                        r?.addEventListener('wheel', this.onKeysPassiveWheel, { passive: false });
                     }}>
                     {this._menuKeys.map(key => (
                         <div
@@ -721,14 +724,14 @@ export class CollectionSchemaView extends CollectionSubView() {
                             }}>
                             <p>
                                 <span className="schema-search-result-key">
-                                    {key}
-                                    {this.fieldInfos.get(key)!.fieldType ? ', ' : ''}
+                                    <b>{key}</b>
+                                    {this.fieldInfos.get(key)!.fieldType ? ':' : ''}
                                 </span>
                                 <span className="schema-search-result-type" style={{ color: this.fieldInfos.get(key)!.readOnly ? 'red' : 'inherit' }}>
                                     {this.fieldInfos.get(key)!.fieldType}
                                 </span>
+                                <span className="schema-search-result-desc">&nbsp;&nbsp;{this.fieldInfos.get(key)!.description}</span>
                             </p>
-                            <p className="schema-search-result-desc">{this.fieldInfos.get(key)!.description}</p>
                         </div>
                     ))}
                 </div>
@@ -740,16 +743,17 @@ export class CollectionSchemaView extends CollectionSubView() {
         const x = this._columnMenuIndex! == -1 ? 0 : this.displayColumnWidths.reduce((total, curr, index) => total + (index < this._columnMenuIndex! ? curr : 0), CollectionSchemaView._rowMenuWidth);
         return (
             <div className="schema-column-menu" style={{ left: x, minWidth: CollectionSchemaView._minColWidth }}>
-                <input className="schema-key-search-input" type="text" value={this._menuValue} onKeyDown={this.onSearchKeyDown} onChange={this.updateKeySearch} onPointerDown={e => e.stopPropagation()} />
+                <input className="schema-key-search-input" type="text" onKeyDown={this.onSearchKeyDown} onChange={this.updateKeySearch} onPointerDown={e => e.stopPropagation()} />
+                {this._makeNewField ? this.newFieldMenu : this.keysDropdown}
+            </div>
+        );
+    }
+    get renderKeysMenu() {
+        console.log('RNDERMENUT:' + this._columnMenuIndex);
+        return (
+            <div className="schema-column-menu" style={{ left: 0, minWidth: CollectionSchemaView._minColWidth }}>
+                <input className="schema-key-search-input" type="text" onKeyDown={this.onSearchKeyDown} onChange={this.updateKeySearch} onPointerDown={e => e.stopPropagation()} />
                 {this._makeNewField ? this.newFieldMenu : this.keysDropdown}
-                <div
-                    className="schema-column-menu-button"
-                    onPointerDown={action(e => {
-                        e.stopPropagation();
-                        this.closeColumnMenu();
-                    })}>
-                    cancel
-                </div>
             </div>
         );
     }
@@ -833,6 +837,8 @@ export class CollectionSchemaView extends CollectionSubView() {
     isContentActive = () => this._props.isSelected() || this._props.isContentActive();
     screenToLocal = () => this.ScreenToLocalBoxXf().translate(-this.tableWidth, 0);
     previewWidthFunc = () => this.previewWidth;
+    onPassiveWheel = (e: WheelEvent) => e.stopPropagation();
+    _oldWheel: any;
     render() {
         return (
             <div className="collectionSchemaView" ref={(ele: HTMLDivElement | null) => this.createDashEventsTarget(ele)} onDrop={this.onExternalDrop.bind(this)}>
@@ -841,15 +847,23 @@ export class CollectionSchemaView extends CollectionSubView() {
                     className="schema-table"
                     style={{ width: `calc(100% - ${this.previewWidth}px)` }}
                     onWheel={e => this._props.isContentActive() && e.stopPropagation()}
-                    ref={r => {
-                        // prevent wheel events from passively propagating up through containers
-                        r?.addEventListener('wheel', (e: WheelEvent) => {}, { passive: false });
+                    ref={ele => {
+                        // prevent wheel events from passively propagating up through containers and prevents containers from preventDefault which would block scrolling
+                        this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel);
+                        (this._oldWheel = ele)?.addEventListener('wheel', this.onPassiveWheel, { passive: false });
                     }}>
                     <div className="schema-header-row" style={{ height: this.rowHeightFunc() }}>
                         <div className="row-menu" style={{ width: CollectionSchemaView._rowMenuWidth }}>
-                            <div className="schema-header-button" onPointerDown={e => (this._columnMenuIndex === -1 ? this.closeColumnMenu() : this.openColumnMenu(-1, true))}>
-                                <FontAwesomeIcon icon="plus" />
-                            </div>
+                            <Popup
+                                placement="right"
+                                background={SettingsManager.userBackgroundColor}
+                                color={SettingsManager.userColor}
+                                toggle={<FontAwesomeIcon onPointerDown={e => this.openColumnMenu(-1, true)} icon="plus" />}
+                                trigger={PopupTrigger.CLICK}
+                                type={Type.TERT}
+                                isOpen={this._columnMenuIndex !== -1 ? false : undefined}
+                                popup={this.renderKeysMenu}
+                            />
                         </div>
                         {this.columnKeys.map((key, index) => (
                             <SchemaColumnHeader
@@ -870,7 +884,7 @@ export class CollectionSchemaView extends CollectionSubView() {
                             />
                         ))}
                     </div>
-                    {this._columnMenuIndex !== undefined && this.renderColumnMenu}
+                    {this._columnMenuIndex !== undefined && this._columnMenuIndex !== -1 && this.renderColumnMenu}
                     {this._filterColumnIndex !== undefined && this.renderFilterMenu}
                     <CollectionSchemaViewDocs schema={this} childDocs={this.sortedDocsFunc} rowHeight={this.rowHeightFunc} setRef={(ref: HTMLDivElement | null) => (this._tableContentRef = ref)} />
                     {this.layoutDoc.chromeHidden ? null : (
diff --git a/src/client/views/nodes/LinkAnchorBox.tsx b/src/client/views/nodes/LinkAnchorBox.tsx
index 00e1f04c5..0a4325d8c 100644
--- a/src/client/views/nodes/LinkAnchorBox.tsx
+++ b/src/client/views/nodes/LinkAnchorBox.tsx
@@ -5,7 +5,7 @@ import { Utils, emptyFunction, setupMoveUpEvents } from '../../../Utils';
 import { Doc } from '../../../fields/Doc';
 import { NumCast, StrCast } from '../../../fields/Types';
 import { TraceMobx } from '../../../fields/util';
-import { DragManager } from '../../util/DragManager';
+import { DragManager, dropActionType } from '../../util/DragManager';
 import { LinkFollower } from '../../util/LinkFollower';
 import { SelectionManager } from '../../util/SelectionManager';
 import { ViewBoxBaseComponent } from '../DocComponent';
@@ -54,7 +54,7 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() {
             const separation = Math.sqrt((pt[0] - e.clientX) * (pt[0] - e.clientX) + (pt[1] - e.clientY) * (pt[1] - e.clientY));
             if (separation > 100) {
                 const dragData = new DragManager.DocumentDragData([this.Document]);
-                dragData.dropAction = 'embed';
+                dragData.dropAction = dropActionType.embed;
                 dragData.dropPropertiesToRemove = ['link_anchor_1_x', 'link_anchor_1_y', 'link_anchor_2_x', 'link_anchor_2_y', 'onClick'];
                 DragManager.StartDocumentDrag([this._ref.current!], dragData, pt[0], pt[1]);
                 return true;
diff --git a/src/client/views/nodes/RecordingBox/RecordingBox.tsx b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
index f6d94ce05..e38a42b29 100644
--- a/src/client/views/nodes/RecordingBox/RecordingBox.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
@@ -11,7 +11,7 @@ import { Upload } from '../../../../server/SharedMediaTypes';
 import { DocumentType } from '../../../documents/DocumentTypes';
 import { Docs } from '../../../documents/Documents';
 import { DocumentManager } from '../../../util/DocumentManager';
-import { DragManager } from '../../../util/DragManager';
+import { DragManager, dropActionType } from '../../../util/DragManager';
 import { ScriptingGlobals } from '../../../util/ScriptingGlobals';
 import { Presentation } from '../../../util/TrackMovements';
 import { undoBatch } from '../../../util/UndoManager';
@@ -251,7 +251,7 @@ ScriptingGlobals.add(function resumeWorkspaceReplaying(value: Doc, _readOnly_: b
 
 ScriptingGlobals.add(function startRecordingDrag(value: { doc: Doc | string; e: React.PointerEvent }) {
     if (DocCast(value.doc)) {
-        DragManager.StartDocumentDrag([value.e.target as HTMLElement], new DragManager.DocumentDragData([DocCast(value.doc)], 'embed'), value.e.clientX, value.e.clientY);
+        DragManager.StartDocumentDrag([value.e.target as HTMLElement], new DragManager.DocumentDragData([DocCast(value.doc)], dropActionType.embed), value.e.clientX, value.e.clientY);
         value.e.preventDefault();
         return true;
     }
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 2d5239f06..4a1a51558 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -30,7 +30,7 @@ import { Docs, DocUtils } from '../../../documents/Documents';
 import { CollectionViewType } from '../../../documents/DocumentTypes';
 import { DictationManager } from '../../../util/DictationManager';
 import { DocumentManager } from '../../../util/DocumentManager';
-import { DragManager } from '../../../util/DragManager';
+import { DragManager, dropActionType } from '../../../util/DragManager';
 import { MakeTemplate } from '../../../util/DropConverter';
 import { LinkManager } from '../../../util/LinkManager';
 import { RTFMarkup } from '../../../util/RTFMarkup';
@@ -624,7 +624,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
                         docId: draggedDoc[Id],
                         float: 'unset',
                     });
-                    if (!['embed', 'copy'].includes((dropAction ?? '') as any)) {
+                    if (![dropActionType.embed, dropActionType.copy].includes(dropAction ?? dropActionType.move)) {
                         added = dragData.removeDocument?.(draggedDoc) ? true : false;
                     } else {
                         added = true;
-- 
cgit v1.2.3-70-g09d2


From 606088e419f0e146715244d00840349b587c80ba Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Sun, 17 Mar 2024 11:50:15 -0400
Subject: use metakey to edit computedfield result instead of expression in
 schema cell,  set default new field values on data doc. fixed stacking view
 from autoresizing when switching to a different collection view.  changed
 syntax for setting fields in text docs to use ':=' for computed fields.  
 Added call to Chat in computed functions when (( )) is used.  Added caching
 of computed function result when a function called by ComputedField uses the
 _setCacheResult_ method (currently only gptCallChat).

---
 src/client/documents/Documents.ts                  |   2 +-
 src/client/util/SnappingManager.ts                 |   5 +-
 src/client/views/GlobalKeyHandler.ts               |   2 +
 .../views/collections/CollectionStackingView.tsx   |   1 +
 .../collectionFreeForm/CollectionFreeFormView.tsx  |   2 +-
 .../collectionSchema/CollectionSchemaView.tsx      |  11 ++-
 .../collectionSchema/SchemaTableCell.tsx           |  23 +++--
 src/client/views/nodes/ComparisonBox.tsx           |  35 ++++---
 src/client/views/nodes/DocumentView.tsx            |   2 +-
 src/client/views/nodes/FieldView.tsx               |   6 +-
 src/client/views/nodes/KeyValueBox.tsx             |  65 ++++++++-----
 src/client/views/nodes/KeyValuePair.tsx            |   2 +-
 .../views/nodes/formattedText/DashFieldView.tsx    |   2 +-
 .../views/nodes/formattedText/RichTextRules.ts     |  36 +++++---
 src/client/views/nodes/formattedText/nodes_rts.ts  |   1 -
 src/fields/Doc.ts                                  |  21 ++++-
 src/fields/ScriptField.ts                          | 102 +++++++++++++--------
 src/server/public/assets/documentation.png         | Bin 0 -> 4526 bytes
 18 files changed, 203 insertions(+), 115 deletions(-)
 create mode 100644 src/server/public/assets/documentation.png

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index a13edec77..1d7c73306 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -175,7 +175,7 @@ class DateInfo extends FInfo {
 }
 class RtfInfo extends FInfo {
     constructor(d: string, filterable?: boolean) {
-        super(d, true);
+        super(d);
         this.filterable = filterable;
     }
     fieldType? = FInfoFieldType.rtf;
diff --git a/src/client/util/SnappingManager.ts b/src/client/util/SnappingManager.ts
index 40c3f76fb..359140732 100644
--- a/src/client/util/SnappingManager.ts
+++ b/src/client/util/SnappingManager.ts
@@ -9,6 +9,7 @@ export class SnappingManager {
 
     @observable _shiftKey = false;
     @observable _ctrlKey = false;
+    @observable _metaKey = false;
     @observable _isLinkFollowing = false;
     @observable _isDragging: boolean = false;
     @observable _isResizing: Doc | undefined = undefined;
@@ -32,6 +33,7 @@ export class SnappingManager {
     public static get VertSnapLines()  { return this.Instance._vertSnapLines; } // prettier-ignore
     public static get ShiftKey()       { return this.Instance._shiftKey; } // prettier-ignore
     public static get CtrlKey()        { return this.Instance._ctrlKey; } // prettier-ignore
+    public static get MetaKey()        { return this.Instance._metaKey; } // prettier-ignore
     public static get IsLinkFollowing(){ return this.Instance._isLinkFollowing; } // prettier-ignore
     public static get IsDragging()     { return this.Instance._isDragging; } // prettier-ignore
     public static get IsResizing()     { return this.Instance._isResizing; } // prettier-ignore
@@ -39,7 +41,8 @@ export class SnappingManager {
     public static get ExploreMode()    { return this.Instance._exploreMode; } // prettier-ignore
     public static SetShiftKey       = (down: boolean) => runInAction(() => (this.Instance._shiftKey = down)); // prettier-ignore
     public static SetCtrlKey        = (down: boolean) => runInAction(() => (this.Instance._ctrlKey = down)); // prettier-ignore
-    public static SetIsLinkFollowing= (follow: boolean) => runInAction(() => (this.Instance._isLinkFollowing = follow)); // prettier-ignore
+    public static SetMetaKey        = (down: boolean) => runInAction(() => (this.Instance._metaKey = down)); // prettier-ignore
+    public static SetIsLinkFollowing= (follow:boolean)=> runInAction(() => (this.Instance._isLinkFollowing = follow)); // prettier-ignore
     public static SetIsDragging     = (drag: boolean) => runInAction(() => (this.Instance._isDragging = drag)); // prettier-ignore
     public static SetIsResizing     = (doc: Opt<Doc>) => runInAction(() => (this.Instance._isResizing = doc)); // prettier-ignore
     public static SetCanEmbed       = (embed:boolean) => runInAction(() => (this.Instance._canEmbed = embed)); // prettier-ignore
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index e800798ca..667d8dbb0 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -55,10 +55,12 @@ export class KeyManager {
     public handleModifiers = action((e: KeyboardEvent) => {
         if (e.shiftKey) SnappingManager.SetShiftKey(true);
         if (e.ctrlKey) SnappingManager.SetCtrlKey(true);
+        if (e.metaKey) SnappingManager.SetMetaKey(true);
     });
     public unhandleModifiers = action((e: KeyboardEvent) => {
         if (!e.shiftKey) SnappingManager.SetShiftKey(false);
         if (!e.ctrlKey) SnappingManager.SetCtrlKey(false);
+        if (!e.metaKey) SnappingManager.SetMetaKey(false);
     });
 
     public handle = action((e: KeyboardEvent) => {
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index ea1caf58f..2b23935eb 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -225,6 +225,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
 
     componentWillUnmount() {
         super.componentWillUnmount();
+        this.observer.disconnect();
         Object.keys(this._disposers).forEach(key => this._disposers[key]());
     }
 
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 7bfbbf3f9..b2fb5848e 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -249,7 +249,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     // this search order, for example, allows icons of cropped images to find the panx/pany/zoom on the cropped image's data doc instead of the usual layout doc because the zoom/panX/panY define the cropped image
     panX = () => this.freeformData()?.bounds.cx ?? NumCast(this.Document[this.panXFieldKey], NumCast(Cast(this.Document.resolvedDataDoc, Doc, null)?.freeform_panX, 1));
     panY = () => this.freeformData()?.bounds.cy ?? NumCast(this.Document[this.panYFieldKey], NumCast(Cast(this.Document.resolvedDataDoc, Doc, null)?.freeform_panY, 1));
-    zoomScaling = () => this.freeformData()?.scale ?? NumCast(Doc.Layout(this.Document)[this.scaleFieldKey], NumCast(Cast(this.Document.resolvedDataDoc, Doc, null)?.[this.scaleFieldKey], 1));
+    zoomScaling = () => this.freeformData()?.scale ?? NumCast(Doc.Layout(this.Document)[this.scaleFieldKey], 1); //, NumCast(DocCast(this.Document.resolvedDataDoc)?.[this.scaleFieldKey], 1));
     PanZoomCenterXf = () =>
         this._props.isAnnotationOverlay && this.zoomScaling() === 1 ? `` : `translate(${this.cachedCenteringShiftX}px, ${this.cachedCenteringShiftY}px) scale(${this.zoomScaling()}) translate(${-this.panX()}px, ${-this.panY()}px)`;
     ScreenToContentsXf = () => this.screenToFreeformContentsXf.copy();
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
index 12f0ad5e9..4a0ca8fe5 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
@@ -1,5 +1,6 @@
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, makeObservable, observable, ObservableMap, observe, trace } from 'mobx';
+import { Popup, PopupTrigger, Type } from 'browndash-components';
+import { action, computed, makeObservable, observable, ObservableMap, observe } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { Doc, DocListCast, Field, NumListCast, Opt, StrListCast } from '../../../../fields/Doc';
@@ -12,12 +13,13 @@ import { Docs, DocumentOptions, DocUtils, FInfo } from '../../../documents/Docum
 import { DocumentManager } from '../../../util/DocumentManager';
 import { DragManager, dropActionType } from '../../../util/DragManager';
 import { SelectionManager } from '../../../util/SelectionManager';
+import { SettingsManager } from '../../../util/SettingsManager';
 import { undoable, undoBatch } from '../../../util/UndoManager';
 import { ContextMenu } from '../../ContextMenu';
 import { EditableView } from '../../EditableView';
 import { Colors } from '../../global/globalEnums';
 import { DocumentView } from '../../nodes/DocumentView';
-import { FocusViewOptions, FieldViewProps } from '../../nodes/FieldView';
+import { FieldViewProps, FocusViewOptions } from '../../nodes/FieldView';
 import { KeyValueBox } from '../../nodes/KeyValueBox';
 import { ObservableReactComponent } from '../../ObservableReactComponent';
 import { DefaultStyleProvider, StyleProp } from '../../StyleProvider';
@@ -25,8 +27,7 @@ import { CollectionSubView } from '../CollectionSubView';
 import './CollectionSchemaView.scss';
 import { SchemaColumnHeader } from './SchemaColumnHeader';
 import { SchemaRowBox } from './SchemaRowBox';
-import { Popup, PopupTrigger, Type } from 'browndash-components';
-import { SettingsManager } from '../../../util/SettingsManager';
+import { DocData } from '../../../../fields/DocSymbols';
 const { default: { SCHEMA_NEW_NODE_HEIGHT } } = require('../../global/globalCssVariables.module.scss'); // prettier-ignore
 
 export enum ColumnType {
@@ -284,7 +285,7 @@ export class CollectionSchemaView extends CollectionSubView() {
     };
 
     @action
-    addNewKey = (key: string, defaultVal: any) => this.childDocs.forEach(doc => (doc[key] = defaultVal));
+    addNewKey = (key: string, defaultVal: any) => this.childDocs.forEach(doc => (doc[DocData][key] = defaultVal));
 
     @undoBatch
     removeColumn = (index: number) => {
diff --git a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
index ed1b519b4..711ef507c 100644
--- a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
+++ b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
@@ -1,8 +1,11 @@
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { Popup, Size, Type } from 'browndash-components';
 import { action, computed, makeObservable, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import { extname } from 'path';
 import * as React from 'react';
 import DatePicker from 'react-datepicker';
+import 'react-datepicker/dist/react-datepicker.css';
 import Select from 'react-select';
 import { Utils, emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnZero } from '../../../../Utils';
 import { DateField } from '../../../../fields/DateField';
@@ -12,6 +15,8 @@ import { BoolCast, Cast, DateCast, DocCast, FieldValue, StrCast } from '../../..
 import { ImageField } from '../../../../fields/URLField';
 import { FInfo, FInfoFieldType } from '../../../documents/Documents';
 import { DocFocusOrOpen } from '../../../util/DocumentManager';
+import { dropActionType } from '../../../util/DragManager';
+import { SettingsManager } from '../../../util/SettingsManager';
 import { Transform } from '../../../util/Transform';
 import { undoBatch, undoable } from '../../../util/UndoManager';
 import { EditableView } from '../../EditableView';
@@ -24,12 +29,7 @@ import { KeyValueBox } from '../../nodes/KeyValueBox';
 import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox';
 import { ColumnType, FInfotoColType } from './CollectionSchemaView';
 import './CollectionSchemaView.scss';
-import 'react-datepicker/dist/react-datepicker.css';
-import { Popup, Size, Type } from 'browndash-components';
-import { IconLookup, faCaretDown } from '@fortawesome/free-solid-svg-icons';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { SettingsManager } from '../../../util/SettingsManager';
-import { dropActionType } from '../../../util/DragManager';
+import { SnappingManager } from '../../../util/SnappingManager';
 
 export interface SchemaTableCellProps {
     Document: Doc;
@@ -129,10 +129,12 @@ export class SchemaTableCell extends ObservableReactComponent<SchemaTableCellPro
                     contents={undefined}
                     fieldContents={fieldProps}
                     editing={this.selected ? undefined : false}
-                    GetValue={() => Field.toKeyValueString(this._props.Document, this._props.fieldKey)}
+                    GetValue={() => Field.toKeyValueString(this._props.Document, this._props.fieldKey, SnappingManager.MetaKey)}
                     SetValue={undoable((value: string, shiftDown?: boolean, enterKey?: boolean) => {
                         if (shiftDown && enterKey) {
                             this._props.setColumnValues(this._props.fieldKey.replace(/^_/, ''), value);
+                            this._props.finishEdit?.();
+                            return true;
                         }
                         const ret = KeyValueBox.SetField(this._props.Document, this._props.fieldKey.replace(/^_/, ''), value, Doc.IsDataProto(this._props.Document) ? true : undefined);
                         this._props.finishEdit?.();
@@ -345,8 +347,7 @@ export class SchemaBoolCell extends ObservableReactComponent<SchemaTableCellProp
                     onChange={undoBatch((value: React.ChangeEvent<HTMLInputElement> | undefined) => {
                         if ((value?.nativeEvent as any).shiftKey) {
                             this._props.setColumnValues(this._props.fieldKey.replace(/^_/, ''), (color === 'black' ? '=' : '') + value?.target?.checked.toString());
-                        }
-                        KeyValueBox.SetField(this._props.Document, this._props.fieldKey.replace(/^_/, ''), (color === 'black' ? '=' : '') + value?.target?.checked.toString());
+                        } else KeyValueBox.SetField(this._props.Document, this._props.fieldKey.replace(/^_/, ''), (color === 'black' ? '=' : '') + value?.target?.checked.toString());
                     })}
                 />
                 <EditableView
@@ -357,8 +358,10 @@ export class SchemaBoolCell extends ObservableReactComponent<SchemaTableCellProp
                     SetValue={undoBatch((value: string, shiftDown?: boolean, enterKey?: boolean) => {
                         if (shiftDown && enterKey) {
                             this._props.setColumnValues(this._props.fieldKey.replace(/^_/, ''), value);
+                            this._props.finishEdit?.();
+                            return true;
                         }
-                        const set = KeyValueBox.SetField(this._props.Document, this._props.fieldKey.replace(/^_/, ''), value);
+                        const set = KeyValueBox.SetField(this._props.Document, this._props.fieldKey.replace(/^_/, ''), value, Doc.IsDataProto(this._props.Document) ? true : undefined);
                         this._props.finishEdit?.();
                         return set;
                     })}
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index e759030f5..715b23fb6 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -4,6 +4,7 @@ import { observer } from 'mobx-react';
 import * as React from 'react';
 import { emptyFunction, returnFalse, returnNone, returnZero, setupMoveUpEvents } from '../../../Utils';
 import { Doc, Opt } from '../../../fields/Doc';
+import { RichTextField } from '../../../fields/RichTextField';
 import { DocCast, NumCast, RTFCast, StrCast } from '../../../fields/Types';
 import { DocUtils, Docs } from '../../documents/Documents';
 import { DragManager, dropActionType } from '../../util/DragManager';
@@ -13,11 +14,9 @@ import { StyleProp } from '../StyleProvider';
 import './ComparisonBox.scss';
 import { DocumentView } from './DocumentView';
 import { FieldView, FieldViewProps } from './FieldView';
-import { PinProps, PresBox } from './trails';
+import { KeyValueBox } from './KeyValueBox';
 import { FormattedTextBox } from './formattedText/FormattedTextBox';
-import { RichTextField } from '../../../fields/RichTextField';
-import { GPTCallType, gptAPICall } from '../../apis/gpt/GPT';
-import { DocData } from '../../../fields/DocSymbols';
+import { PinProps, PresBox } from './trails';
 
 @observer
 export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implements ViewBoxInterface {
@@ -173,10 +172,17 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                 </div>
             );
         };
+
+        /**
+         * Display the Docs in the before/after fields of the comparison.  This also supports a GPT flash card use case
+         * where if there are no Docs in the slots, but the main fieldKey contains text, then
+         * @param which
+         * @returns
+         */
         const displayDoc = (which: string) => {
             const whichDoc = DocCast(this.dataDoc[which]);
             const targetDoc = DocCast(whichDoc?.annotationOn, whichDoc);
-            const subjectText = RTFCast(this.Document[this.fieldKey])?.Text;
+            const subjectText = RTFCast(this.Document[this.fieldKey])?.Text.trim();
             // if there is no Doc in the first comparison slot, but the comparison box's fieldKey slot has a RichTextField, then render a text box to show the contents of the document's field key slot
             // of if there is no Doc in the second comparison slot, but the second slot has a RichTextField, then render a text box to show the contents of the document's field key slot
             const layoutTemplateString = !targetDoc
@@ -188,15 +194,18 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                 : undefined;
 
             // A bit hacky to try out the concept of using GPT to fill in flashcards -- this whole process should probably be packaged into a script to be more generic.
-            //   If the second slot doesn't have anything in it, but the fieldKey slot has text
-            // and the fieldKey + "_alternate" has a text that incldues "--TEXT--", then
-            // treat the fieldKey + "_altenrate" text as a GPT query parameterized by the fieldKey text
-            // Call GPT to fill in an "answer" value in the second slot.
+            //   If the second slot doesn't have anything in it, but the fieldKey slot has text (e.g., this.text is a string)
+            // and the fieldKey + "_alternate" has text, then treat the _alternate's text as a GPT query (indicated by (( && )) ) that is parameterized (optionally)
+            // by the field references in the text (eg., this.text_alternate is
+            //     "((Provide a one sentence definition for (this) that doesn't use any word in (this.excludeWords) ))"
+            //     where (this) is replaced by the text in the fieldKey slot abd this.excludeWords is repalced by the conetnts of the excludeWords field
+            // A GPT call will put the "answer"  in the second slot of the comparison (eg., text_2)
             if (which.endsWith('2') && !layoutTemplateString && !targetDoc) {
-                const queryText = RTFCast(this.Document[this.fieldKey + '_alternate'])?.Text;
-                if (queryText?.includes('--TEXT--') && subjectText) {
-                    this.Document[DocData][this.fieldKey + '_2'] = '';
-                    gptAPICall(queryText?.replace('--TEXT--', subjectText), GPTCallType.COMPLETION).then(value => (this.Document[DocData][this.fieldKey + '_2'] = value.trim()));
+                var queryText = RTFCast(this.Document[this.fieldKey + '_alternate'])
+                    ?.Text.replace('(this)', subjectText) // TODO: this should be done in KeyValueBox.setField but it doesn't know about the fieldKey ...
+                    .trim();
+                if (subjectText && queryText.match(/\(\(.*\)\)/)) {
+                    KeyValueBox.SetField(this.Document, which, ':=' + queryText, false); // make the second slot be a computed field on the data doc that calls ChatGpt
                 }
             }
             return targetDoc || layoutTemplateString ? (
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index bbeacef88..9848f18e0 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1215,7 +1215,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
             if (layout_fieldKey && layout_fieldKey !== 'layout' && layout_fieldKey !== 'layout_icon') this.Document.deiconifyLayout = layout_fieldKey.replace('layout_', '');
         } else {
             const deiconifyLayout = Cast(this.Document.deiconifyLayout, 'string', null);
-            this.switchViews(deiconifyLayout ? true : false, deiconifyLayout, finalFinished);
+            this.switchViews(deiconifyLayout ? true : false, deiconifyLayout, finalFinished, true);
             this.Document.deiconifyLayout = undefined;
             this._props.bringToFront?.(this.Document);
         }
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index 8a49b4757..4ecaaa283 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -11,6 +11,7 @@ import { ViewBoxInterface } from '../DocComponent';
 import { CollectionFreeFormDocumentView } from './CollectionFreeFormDocumentView';
 import { DocumentView, OpenWhere } from './DocumentView';
 import { PinProps } from './trails';
+import { computed } from 'mobx';
 
 export interface FocusViewOptions {
     willPan?: boolean; // determines whether to pan to target document
@@ -121,9 +122,12 @@ export class FieldView extends React.Component<FieldViewProps> {
     public static LayoutString(fieldType: { name: string }, fieldStr: string) {
         return `<${fieldType.name} {...props} fieldKey={'${fieldStr}'}/>`; //e.g., "<ImageBox {...props} fieldKey={'data'} />"
     }
+    @computed get fieldval() {
+        return this.props.Document[this.props.fieldKey];
+    }
 
     render() {
-        const field = this.props.Document[this.props.fieldKey];
+        const field = this.fieldval;
         // prettier-ignore
         if (field instanceof Doc)  return <p> <b>{field.title?.toString()}</b></p>;
         if (field === undefined) return <p>{'<null>'}</p>;
diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx
index 89a5ac0b8..2257e6455 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -6,7 +6,7 @@ import { Doc, Field, FieldResult } from '../../../fields/Doc';
 import { List } from '../../../fields/List';
 import { RichTextField } from '../../../fields/RichTextField';
 import { ComputedField, ScriptField } from '../../../fields/ScriptField';
-import { DocCast } from '../../../fields/Types';
+import { DocCast, StrCast } from '../../../fields/Types';
 import { ImageField } from '../../../fields/URLField';
 import { Docs } from '../../documents/Documents';
 import { SetupDrag } from '../../util/DragManager';
@@ -71,34 +71,51 @@ export class KeyValueBox extends ObservableReactComponent<FieldViewProps> {
             }
         }
     };
-    public static CompileKVPScript(value: string): KVPScript | undefined {
-        const eq = value.startsWith('=');
-        value = eq ? value.substring(1) : value;
-        const dubEq = value.startsWith(':=') ? 'computed' : value.startsWith('$=') ? 'script' : false;
-        value = dubEq ? value.substring(2) : value;
-        const options: ScriptOptions = { addReturn: true, typecheck: false, params: { this: Doc.name, self: Doc.name, documentView: 'any', _last_: 'any', _readOnly_: 'boolean' }, editable: true };
-        if (dubEq) options.typecheck = false;
-        const script = CompileScript(value, { ...options, transformer: DocumentIconContainer.getTransformer() });
-        return !script.compiled ? undefined : { script, type: dubEq, onDelegate: eq };
+    /**
+     * this compiles a string as a script after parsing off initial characters that determine script parameters
+     * if the script starts with '=', then it will be stored on the delegate of the Doc, otherise on the data doc
+     * if the script then starts with a ':=', then it will be treated as ComputedField,
+     *                                  '$=', then it will just be a Script
+     * @param value
+     * @returns
+     */
+    public static CompileKVPScript(rawvalue: string): KVPScript | undefined {
+        const onDelegate = rawvalue.startsWith('=');
+        rawvalue = onDelegate ? rawvalue.substring(1) : rawvalue;
+        const type: 'computed' | 'script' | false = rawvalue.startsWith(':=') ? 'computed' : rawvalue.startsWith('$=') ? 'script' : false;
+        rawvalue = type ? rawvalue.substring(2) : rawvalue;
+        rawvalue = rawvalue.replace(/.*\(\((.*)\)\)/, 'dashCallChat(_setCacheResult_, this, "$1")');
+        const value = ["'", '"', '`'].includes(rawvalue.length ? rawvalue[0] : '') || !isNaN(rawvalue as any) ? rawvalue : '`' + rawvalue + '`';
+
+        var script = ScriptField.CompileScript(rawvalue, {}, true, undefined, DocumentIconContainer.getTransformer());
+        if (!script.compiled) {
+            script = ScriptField.CompileScript(value, {}, true, undefined, DocumentIconContainer.getTransformer());
+        }
+        return !script.compiled ? undefined : { script, type, onDelegate };
     }
 
-    public static ApplyKVPScript(doc: Doc, key: string, kvpScript: KVPScript, forceOnDelegate?: boolean): boolean {
+    public static ApplyKVPScript(doc: Doc, key: string, kvpScript: KVPScript, forceOnDelegate?: boolean, setResult?: (value: FieldResult) => void) {
         const { script, type, onDelegate } = kvpScript;
         //const target = onDelegate ? Doc.Layout(doc.layout) : Doc.GetProto(doc); // bcz: TODO need to be able to set fields on layout templates
         const target = forceOnDelegate || onDelegate || key.startsWith('_') ? doc : DocCast(doc.proto, doc);
-        let field: Field;
-        if (type === 'computed') {
-            field = new ComputedField(script);
-        } else if (type === 'script') {
-            field = new ScriptField(script);
-        } else {
-            const res = script.run({ this: Doc.Layout(doc), self: doc }, console.log);
-            if (!res.success) {
-                target[key] = script.originalScript;
-                return true;
+        let field: Field | undefined;
+        switch (type) {
+            case 'computed': field = new ComputedField(script); break; // prettier-ignore
+            case 'script': field = new ScriptField(script); break; // prettier-ignore
+            default: {
+                const _setCacheResult_ = (value: FieldResult) => {
+                    field = value as Field;
+                    setResult?.(value);
+                };
+                const res = script.run({ this: Doc.Layout(doc), self: doc, _setCacheResult_ }, console.log);
+                if (!res.success) {
+                    if (key) target[key] = script.originalScript;
+                    return false;
+                }
+                field === undefined && (field = res.result);
             }
-            field = res.result;
         }
+        if (!key) return field;
         if (Field.IsField(field, true) && (key !== 'proto' || field !== target)) {
             target[key] = field;
             return true;
@@ -107,10 +124,10 @@ export class KeyValueBox extends ObservableReactComponent<FieldViewProps> {
     }
 
     @undoBatch
-    public static SetField(doc: Doc, key: string, value: string, forceOnDelegate?: boolean) {
+    public static SetField(doc: Doc, key: string, value: string, forceOnDelegate?: boolean, setResult?: (value: FieldResult) => void) {
         const script = this.CompileKVPScript(value);
         if (!script) return false;
-        return this.ApplyKVPScript(doc, key, script, forceOnDelegate);
+        return this.ApplyKVPScript(doc, key, script, forceOnDelegate, setResult);
     }
 
     onPointerDown = (e: React.PointerEvent): void => {
diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx
index f9e8ce4f3..d59489a78 100644
--- a/src/client/views/nodes/KeyValuePair.tsx
+++ b/src/client/views/nodes/KeyValuePair.tsx
@@ -125,7 +125,7 @@ export class KeyValuePair extends ObservableReactComponent<KeyValuePairProps> {
                                 pinToPres: returnZero,
                             }}
                             GetValue={() => Field.toKeyValueString(this._props.doc, this._props.keyName)}
-                            SetValue={(value: string) => KeyValueBox.SetField(this._props.doc, this._props.keyName, value)}
+                            SetValue={(value: string) => (KeyValueBox.SetField(this._props.doc, this._props.keyName, value) ? true : false)}
                         />
                     </div>
                 </td>
diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx
index 5c4d850ad..62cb460c2 100644
--- a/src/client/views/nodes/formattedText/DashFieldView.tsx
+++ b/src/client/views/nodes/formattedText/DashFieldView.tsx
@@ -137,7 +137,7 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi
                     col={0}
                     deselectCell={emptyFunction}
                     selectCell={emptyFunction}
-                    maxWidth={this._props.hideKey ? undefined : this.return100}
+                    maxWidth={this._props.hideKey ? undefined : this._props.tbox._props.PanelWidth}
                     columnWidth={this._props.hideKey ? () => this._props.tbox._props.PanelWidth() - 20 : returnZero}
                     selectedCell={() => [this._dashDoc!, 0]}
                     fieldKey={this._fieldKey}
diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts
index d5c91fc09..c798ae4b3 100644
--- a/src/client/views/nodes/formattedText/RichTextRules.ts
+++ b/src/client/views/nodes/formattedText/RichTextRules.ts
@@ -1,6 +1,6 @@
 import { ellipsis, emDash, InputRule, smartQuotes, textblockTypeInputRule } from 'prosemirror-inputrules';
 import { NodeSelection, TextSelection } from 'prosemirror-state';
-import { Doc, StrListCast } from '../../../../fields/Doc';
+import { Doc, FieldResult, StrListCast } from '../../../../fields/Doc';
 import { DocData } from '../../../../fields/DocSymbols';
 import { Id } from '../../../../fields/FieldSymbols';
 import { List } from '../../../../fields/List';
@@ -8,13 +8,14 @@ import { NumCast, StrCast } from '../../../../fields/Types';
 import { Utils } from '../../../../Utils';
 import { DocServer } from '../../../DocServer';
 import { Docs, DocUtils } from '../../../documents/Documents';
+import { CollectionViewType } from '../../../documents/DocumentTypes';
+import { CollectionView } from '../../collections/CollectionView';
+import { ContextMenu } from '../../ContextMenu';
+import { KeyValueBox } from '../KeyValueBox';
 import { FormattedTextBox } from './FormattedTextBox';
 import { wrappingInputRule } from './prosemirrorPatches';
 import { RichTextMenu } from './RichTextMenu';
 import { schema } from './schema_rts';
-import { CollectionView } from '../../collections/CollectionView';
-import { CollectionViewType } from '../../../documents/DocumentTypes';
-import { ContextMenu } from '../../ContextMenu';
 
 export class RichTextRules {
     public Document: Doc;
@@ -282,18 +283,28 @@ export class RichTextRules {
                     : tr;
             }),
 
+            new InputRule(new RegExp(/(^|[^=])(\(\(.*\)\))/), (state, match, start, end) => {
+                var count = 0; // ignore first return value which will be the notation that chat is pending a result
+                KeyValueBox.SetField(this.Document, '', match[2], false, (gptval: FieldResult) => {
+                    count && this.TextBox.EditorView?.dispatch(this.TextBox.EditorView!.state.tr.insertText(' ' + (gptval as string)));
+                    count++;
+                });
+                return null;
+            }),
+
             // create a text display of a metadata field on this or another document, or create a hyperlink portal to another document
             // [[<fieldKey> : <Doc>]]
             // [[:docTitle]] => hyperlink
             // [[fieldKey]] => show field
-            // [[fieldKey=value]] => show field and also set its value
+            // [[fieldKey{:,=:}=value]] => show field and also set its value
             // [[fieldKey:docTitle]] => show field of doc
             new InputRule(
-                new RegExp(/\[\[([a-zA-Z_\? \-0-9]*)(=[a-z,A-Z_@\? /\-0-9]*)?(:[a-zA-Z_@:\.\? \-0-9]+)?\]\]$/),
+                new RegExp(/\[\[([a-zA-Z_\? \-0-9]*)((=:|:)?=)([a-z,A-Z_@\?+\-*/\ 0-9\(\)]*)?(:[a-zA-Z_@:\.\? \-0-9]+)?\]\]$/),
                 (state, match, start, end) => {
                     const fieldKey = match[1];
-                    const docTitle = match[3]?.replace(':', '');
-                    const value = match[2]?.substring(1);
+                    const assign = match[2] === '=' ? '' : match[2];
+                    const value = match[4];
+                    const docTitle = match[5]?.replace(':', '');
                     const linkToDoc = (target: Doc) => {
                         const rstate = this.TextBox.EditorView?.state;
                         const selection = rstate?.selection.$from.pos;
@@ -325,13 +336,14 @@ export class RichTextRules {
                         }
                         return state.tr;
                     }
-                    if (value?.includes(',')) {
+                    // if the value has commas assume its an array (unless it's part of a chat gpt call indicated by '((' )
+                    if (value?.includes(',') && !value.startsWith('((')) {
                         const values = value.split(',');
                         const strs = values.some(v => !v.match(/^[-]?[0-9.]$/));
                         this.Document[DocData][fieldKey] = strs ? new List<string>(values) : new List<number>(values.map(v => Number(v)));
-                    } else if (value !== '' && value !== undefined) {
-                        const num = value.match(/^[0-9.]$/);
-                        this.Document[DocData][fieldKey] = value === 'true' ? true : value === 'false' ? false : num ? Number(value) : value;
+                    } else if (value) {
+                        KeyValueBox.SetField(this.Document, fieldKey, assign + value, Doc.IsDataProto(this.Document) ? true : undefined, assign ? undefined: 
+                            (gptval: FieldResult) => this.Document[DocData][fieldKey] = gptval as string ); // prettier-ignore
                     }
                     const target = getTitledDoc(docTitle);
                     const fieldView = state.schema.nodes.dashField.create({ fieldKey, docId: target?.[Id], hideKey: false });
diff --git a/src/client/views/nodes/formattedText/nodes_rts.ts b/src/client/views/nodes/formattedText/nodes_rts.ts
index 4706a97fa..c9115be90 100644
--- a/src/client/views/nodes/formattedText/nodes_rts.ts
+++ b/src/client/views/nodes/formattedText/nodes_rts.ts
@@ -1,4 +1,3 @@
-import * as React from 'react';
 import { DOMOutputSpec, Node, NodeSpec } from 'prosemirror-model';
 import { listItem, orderedList } from 'prosemirror-schema-list';
 import { ParagraphNodeSpec, toParagraphDOM, getParagraphNodeAttrs } from './ParagraphNodeSpec';
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 921d7aa5d..30f5f716c 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -34,14 +34,29 @@ import * as JSZip from 'jszip';
 import { FieldViewProps } from '../client/views/nodes/FieldView';
 export const LinkedTo = '-linkedTo';
 export namespace Field {
-    export function toKeyValueString(doc: Doc, key: string): string {
-        const onDelegate = Object.keys(doc).includes(key.replace(/^_/, ''));
+    /**
+     * Converts a field to its equivalent input string in the key value box such that if the string
+     * is entered into a keyValueBox it will create an equivalent field (except if showComputedValue is set).
+     * @param doc doc containing key
+     * @param key field key to display
+     * @param showComputedValue whether copmuted function should display its value instead of its function
+     * @returns string representation of the field
+     */
+    export function toKeyValueString(doc: Doc, key: string, showComputedValue?: boolean): string {
+        const onDelegate = !Doc.IsDataProto(doc) && Object.keys(doc).includes(key.replace(/^_/, ''));
         const field = ComputedField.WithoutComputed(() => FieldValue(doc[key]));
         return !Field.IsField(field)
             ? key.startsWith('_')
                 ? '='
                 : ''
-            : (onDelegate ? '=' : '') + (field instanceof ComputedField ? `:=${field.script.originalScript}` : field instanceof ScriptField ? `$=${field.script.originalScript}` : Field.toScriptString(field));
+            : (onDelegate ? '=' : '') +
+                  (field instanceof ComputedField && showComputedValue
+                      ? field._lastComputedResult
+                      : field instanceof ComputedField
+                        ? `:=${field.script.originalScript}`
+                        : field instanceof ScriptField
+                          ? `$=${field.script.originalScript}`
+                          : Field.toScriptString(field));
     }
     export function toScriptString(field: Field) {
         switch (typeof field) {
diff --git a/src/fields/ScriptField.ts b/src/fields/ScriptField.ts
index c7fe72ca6..9021c8896 100644
--- a/src/fields/ScriptField.ts
+++ b/src/fields/ScriptField.ts
@@ -1,15 +1,17 @@
+import { action, makeObservable, observable } from 'mobx';
 import { computedFn } from 'mobx-utils';
 import { createSimpleSchema, custom, map, object, primitive, PropSchema, serializable, SKIP } from 'serializr';
 import { DocServer } from '../client/DocServer';
-import { CompiledScript, CompileScript, ScriptOptions } from '../client/util/Scripting';
+import { CompiledScript, CompileScript, ScriptOptions, Transformer } from '../client/util/Scripting';
 import { scriptingGlobal, ScriptingGlobals } from '../client/util/ScriptingGlobals';
 import { autoObject, Deserializable } from '../client/util/SerializationHelper';
 import { numberRange } from '../Utils';
-import { Doc, Field, Opt } from './Doc';
-import { Copy, Id, ToJavascriptString, ToScriptString, ToString, ToValue } from './FieldSymbols';
+import { Doc, Field, FieldResult, Opt } from './Doc';
+import { Copy, FieldChanged, Id, ToJavascriptString, ToScriptString, ToString, ToValue } from './FieldSymbols';
 import { List } from './List';
 import { ObjectField } from './ObjectField';
 import { Cast, StrCast } from './Types';
+import { GPTCallType, gptAPICall } from '../client/apis/gpt/GPT';
 
 function optional(propSchema: PropSchema) {
     return custom(
@@ -85,6 +87,13 @@ export class ScriptField extends ObjectField {
     readonly script: CompiledScript;
     @serializable(object(scriptSchema))
     readonly setterscript: CompiledScript | undefined;
+    @serializable
+    @observable
+    _cachedResult: FieldResult = undefined;
+    setCacheResult = action((value: FieldResult) => {
+        this._cachedResult = value;
+        this[FieldChanged]?.();
+    });
 
     @serializable(autoObject())
     captures?: List<string>;
@@ -122,21 +131,25 @@ export class ScriptField extends ObjectField {
     [ToString]() {
         return this.script.originalScript;
     }
-    public static CompileScript(script: string, params: object = {}, addReturn = false, capturedVariables?: { [name: string]: Doc | string | number | boolean }) {
+    public static CompileScript(script: string, params: object = {}, addReturn = false, capturedVariables?: { [name: string]: Doc | string | number | boolean }, transformer?: Transformer) {
         return CompileScript(script, {
             params: {
                 this: Doc?.name || 'Doc', // this is the doc that executes the script
                 self: Doc?.name || 'Doc', // self is the root doc of the doc that executes the script
+                documentView: 'any',
                 _last_: 'any', // _last_ is the previous value of a computed field when it is being triggered to re-run.
+                _setCacheResult_: 'any', // set the cached value of the function
                 _readOnly_: 'boolean', // _readOnly_ is set when a computed field is executed to indicate that it should not have mobx side-effects.  used for checking the value of a set function (see FontIconBox)
                 ...params,
             },
+            transformer,
             typecheck: false,
             editable: true,
             addReturn: addReturn,
             capturedVariables,
         });
     }
+
     public static MakeFunction(script: string, params: object = {}, capturedVariables?: { [name: string]: Doc | string | number | boolean }) {
         const compiled = ScriptField.CompileScript(script, params, true, capturedVariables);
         return compiled.compiled ? new ScriptField(compiled) : undefined;
@@ -146,29 +159,62 @@ export class ScriptField extends ObjectField {
         const compiled = ScriptField.CompileScript(script, params, false, capturedVariables);
         return compiled.compiled ? new ScriptField(compiled) : undefined;
     }
+    public static CallGpt(queryText: string, setVal: (val: FieldResult) => void, target: Doc) {
+        if (typeof queryText === 'string' && setVal) {
+            while (queryText.match(/\(this\.[a-zA-Z_]*\)/)?.length) {
+                const fieldRef = queryText.split('(this.')[1].replace(/\).*/, '');
+                queryText = queryText.replace(/\(this\.[a-zA-Z_]*\)/, Field.toString(target[fieldRef] as Field));
+            }
+            setVal(`Chat Pending: ${queryText}`);
+            gptAPICall(queryText, GPTCallType.COMPLETION).then(result => {
+                if (queryText.includes('#')) {
+                    const matches = result.match(/-?[0-9][0-9,]+[.]?[0-9]*/);
+                    if (matches?.length) setVal(Number(matches[0].replace(/,/g, '')));
+                } else setVal(result.trim());
+            });
+        }
+    }
 }
 
 @scriptingGlobal
 @Deserializable('computed', deserializeScript)
 export class ComputedField extends ScriptField {
-    _lastComputedResult: any;
-    //TODO maybe add an observable cache based on what is passed in for doc, considering there shouldn't really be that many possible values for doc
-    value = computedFn((doc: Doc) => this._valueOutsideReaction(doc));
-    _valueOutsideReaction = (doc: Doc) => (this._lastComputedResult = this.script.compiled && this.script.run({ this: doc, self: doc, value: '', _last_: this._lastComputedResult, _readOnly_: true }, console.log).result);
-
-    [ToValue](doc: Doc) {
-        return ComputedField.toValue(doc, this);
+    static undefined = '__undefined';
+    static useComputed = true;
+    static DisableComputedFields() { this.useComputed = false; } // prettier-ignore
+    static EnableComputedFields()  { this.useComputed = true; } // prettier-ignore
+    static WithoutComputed<T>(fn: () => T) {
+        this.DisableComputedFields();
+        try {
+            return fn();
+        } finally {
+            this.EnableComputedFields();
+        }
     }
-    [Copy](): ObjectField {
-        return new ComputedField(this.script, this.setterscript, this.rawscript);
+
+    constructor(script: CompiledScript | undefined, setterscript?: CompiledScript, rawscript?: string) {
+        super(script, setterscript, rawscript);
+        makeObservable(this);
     }
 
+    _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;
+    };
+
+    [ToValue](doc: Doc)   { if (ComputedField.useComputed) return { value: this._valueOutsideReaction(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) {
         const compiled = ScriptField.CompileScript(script, params, true, { value: '', ...capturedVariables });
         const compiledsetter = setterscript ? ScriptField.CompileScript(setterscript, { ...params, value: 'any' }, false, capturedVariables) : undefined;
         const compiledsetscript = compiledsetter?.compiled ? compiledsetter : undefined;
         return compiled.compiled ? new ComputedField(compiled, compiledsetscript) : undefined;
     }
+
     public static MakeInterpolatedNumber(fieldKey: string, interpolatorKey: string, doc: Doc, curTimecode: number, defaultVal: Opt<number>) {
         if (!doc[`${fieldKey}_indexed`]) {
             const flist = new List<number>(numberRange(curTimecode + 1).map(i => undefined) as any as number[]);
@@ -206,33 +252,6 @@ export class ComputedField extends ScriptField {
         return (doc[`${fieldKey}`] = getField.compiled ? new ComputedField(getField, setField?.compiled ? setField : undefined) : undefined);
     }
 }
-export namespace ComputedField {
-    let useComputed = true;
-    export function DisableComputedFields() {
-        useComputed = false;
-    }
-
-    export function EnableComputedFields() {
-        useComputed = true;
-    }
-
-    export const undefined = '__undefined';
-
-    export function WithoutComputed<T>(fn: () => T) {
-        DisableComputedFields();
-        try {
-            return fn();
-        } finally {
-            EnableComputedFields();
-        }
-    }
-
-    export function toValue(doc: any, value: any) {
-        if (useComputed) {
-            return { value: value._valueOutsideReaction(doc) };
-        }
-    }
-}
 
 ScriptingGlobals.add(
     function setIndexVal(list: any[], index: number, value: any) {
@@ -258,3 +277,6 @@ ScriptingGlobals.add(
     'returns the value at a given index of a list',
     '(list: any[], index: number)'
 );
+ScriptingGlobals.add(function dashCallChat(setVal: (val: FieldResult) => void, target: Doc, queryText: string) {
+    ScriptField.CallGpt(queryText, setVal, target);
+}, 'calls chat gpt for the query string and then calls setVal with the result');
diff --git a/src/server/public/assets/documentation.png b/src/server/public/assets/documentation.png
new file mode 100644
index 000000000..95c76b198
Binary files /dev/null and b/src/server/public/assets/documentation.png differ
-- 
cgit v1.2.3-70-g09d2

