From ba5b687011526188bb024ddf37c254aaf285c06d Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 27 Apr 2023 16:45:59 -0400
Subject: performance tuning for schema views to avoid re-rendering each table
 cell when child docs change.

---
 .../collectionSchema/CollectionSchemaView.tsx      | 105 +++++++++------------
 1 file changed, 44 insertions(+), 61 deletions(-)

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

diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
index 8cd307adf..50a91a60a 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
@@ -1,9 +1,9 @@
 import React = require('react');
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, observable, ObservableMap, trace, untracked } from 'mobx';
+import { action, computed, observable, ObservableMap, observe, reaction, trace, untracked } from 'mobx';
 import { observer } from 'mobx-react';
 import { computedFn } from 'mobx-utils';
-import { Doc, DocListCast, Field, StrListCast } from '../../../../fields/Doc';
+import { Doc, DocListCast, Field, NumListCast, StrListCast } from '../../../../fields/Doc';
 import { Id } from '../../../../fields/FieldSymbols';
 import { List } from '../../../../fields/List';
 import { listSpec } from '../../../../fields/Schema';
@@ -47,6 +47,7 @@ const defaultColumnKeys: string[] = ['title', 'type', 'author', 'creationDate',
 
 @observer
 export class CollectionSchemaView extends CollectionSubView() {
+    private _keysDisposer: any;
     private _closestDropIndex: number = 0;
     private _previewRef: HTMLDivElement | null = null;
     private _makeNewColumn: boolean = false;
@@ -58,15 +59,13 @@ export class CollectionSchemaView extends CollectionSubView() {
     public static _rowMenuWidth: number = 60;
     public static _previewDividerWidth: number = 4;
     public static _newNodeInputHeight: number = 30;
+    public fieldInfos = new ObservableMap<string, FInfo>();
 
-    @computed get _selectedDocs() {
-        return SelectionManager.Docs().filter(doc => Doc.AreProtosEqual(DocCast(doc.context), this.props.Document));
-    }
+    @observable _menuKeys: string[] = [];
     @observable _rowEles: ObservableMap = new ObservableMap<Doc, HTMLDivElement>();
     @observable _colEles: HTMLDivElement[] = [];
     @observable _displayColumnWidths: number[] | undefined;
     @observable _columnMenuIndex: number | undefined;
-    @observable _fieldInfos: [string, FInfo][] = [];
     @observable _newFieldWarning: string = '';
     @observable _makeNewField: boolean = false;
     @observable _newFieldDefault: any = 0;
@@ -76,34 +75,12 @@ export class CollectionSchemaView extends CollectionSubView() {
     @observable _filterSearchValue: string = '';
     @observable _selectedCell: [Doc, number] | undefined;
 
-    get fieldInfos() {
-        const docs = this.childDocs;
-        const keys: { [key: string]: boolean } = {};
-        // bcz: ugh.  this is untracked since otherwise a large collection of documents will blast the server for all their fields.
-        //  then as each document's fields come back, we update the documents _proxies.  Each time we do this, the whole schema will be
-        //  invalidated and re-rendered.   This workaround will inquire all of the document fields before the options button is clicked.
-        //  then by the time the options button is clicked, all of the fields should be in place.  If a new field is added while this menu
-        //  is displayed (unlikely) it won't show up until something else changes.
-        //TODO Types
-        untracked(() => docs.map(doc => Doc.GetAllPrototypes(doc).map(proto => Object.keys(proto).forEach(key => (keys[key] = false)))));
-
-        let computedKeys: { [key: string]: FInfo } = {};
-
-        Object.entries(this._documentOptions).forEach((pair: [string, FInfo]) => {
-            computedKeys[pair[0]] = pair[1];
-        });
-
-        Object.keys(keys).forEach((key: string) => {
-            if (!(key in computedKeys)) {
-                computedKeys[key] = new FInfo('');
-            }
-        });
-
-        return computedKeys;
+    @computed get _selectedDocs() {
+        return SelectionManager.Docs().filter(doc => Doc.AreProtosEqual(DocCast(doc.context), this.rootDoc));
     }
 
-    get documentKeys() {
-        return Object.keys(this.fieldInfos);
+    @computed get documentKeys() {
+        return Array.from(this.fieldInfos.keys());
     }
 
     @computed get previewWidth() {
@@ -119,20 +96,15 @@ export class CollectionSchemaView extends CollectionSubView() {
     }
 
     @computed get storedColumnWidths() {
-        let widths = Cast(
+        const widths = NumListCast(
             this.layoutDoc.columnWidths,
-            listSpec('number'),
             this.columnKeys.map(() => (this.tableWidth - CollectionSchemaView._rowMenuWidth) / this.columnKeys.length)
         );
 
         const totalWidth = widths.reduce((sum, width) => sum + width, 0);
         if (totalWidth !== this.tableWidth - CollectionSchemaView._rowMenuWidth) {
-            widths = widths.map(w => {
-                const proportion = w / totalWidth;
-                return proportion * (this.tableWidth - CollectionSchemaView._rowMenuWidth);
-            });
+            return widths.map(w => (w / totalWidth) * (this.tableWidth - CollectionSchemaView._rowMenuWidth));
         }
-
         return widths;
     }
 
@@ -148,19 +120,37 @@ export class CollectionSchemaView extends CollectionSubView() {
         return BoolCast(this.layoutDoc.sortDesc);
     }
 
-    rowIndex(doc: Doc) {
-        return this.sortedDocs.docs.indexOf(doc);
-    }
-
+    @action
     componentDidMount() {
         this.props.setContentView?.(this);
         document.addEventListener('keydown', this.onKeyDown);
+
+        Object.entries(this._documentOptions).forEach((pair: [string, FInfo]) => this.fieldInfos.set(pair[0], pair[1]));
+        this._keysDisposer = observe(
+            this.rootDoc[this.fieldKey ?? 'data'] as List<Doc>,
+            change => {
+                switch (change.type as any) {
+                    case 'splice':
+                        // prettier-ignore
+                        (change as any).added.forEach((doc: Doc) => // for each document added 
+                            Doc.GetAllPrototypes(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(''))))));
+                        break;
+                    case 'update': //let oldValue = change.oldValue; // fill this in if the entire child list will ever be reassigned with a new list
+                }
+            },
+            true
+        );
     }
 
     componentWillUnmount() {
+        this._keysDisposer?.();
         document.removeEventListener('keydown', this.onKeyDown);
     }
 
+    rowIndex = (doc: Doc) => this.sortedDocs.docs.indexOf(doc);
+
     @action
     onKeyDown = (e: KeyboardEvent) => {
         if (this._selectedDocs.length > 0) {
@@ -554,8 +544,7 @@ export class CollectionSchemaView extends CollectionSubView() {
     onSearchKeyDown = (e: React.KeyboardEvent) => {
         switch (e.key) {
             case 'Enter':
-                const menuKeys = Object.keys(this._fieldInfos);
-                menuKeys.length > 0 && this._menuValue.length > 0 ? this.setKey(menuKeys[0]) : action(() => (this._makeNewField = true))();
+                this._menuKeys.length > 0 && this._menuValue.length > 0 ? this.setKey(this._menuKeys[0]) : action(() => (this._makeNewField = true))();
                 break;
             case 'Escape':
                 this.closeColumnMenu();
@@ -584,7 +573,7 @@ export class CollectionSchemaView extends CollectionSubView() {
         this._makeNewColumn = false;
         this._columnMenuIndex = index;
         this._menuValue = '';
-        this._fieldInfos = Object.entries(this.fieldInfos);
+        this._menuKeys = this.documentKeys;
         this._makeNewField = false;
         this._newFieldWarning = '';
         this._makeNewField = false;
@@ -630,7 +619,7 @@ export class CollectionSchemaView extends CollectionSubView() {
     @action
     updateKeySearch = (e: React.ChangeEvent<HTMLInputElement>) => {
         this._menuValue = e.target.value;
-        this._fieldInfos = Object.entries(this.fieldInfos).filter(value => value[0].toLowerCase().includes(this._menuValue.toLowerCase()));
+        this._menuKeys = this.documentKeys.filter(value => value.toLowerCase().includes(this._menuValue.toLowerCase()));
     };
 
     getFieldFilters = (field: string) => StrListCast(this.Document._docFilters).filter(filter => filter.split(':')[0] == field);
@@ -732,7 +721,7 @@ export class CollectionSchemaView extends CollectionSubView() {
                             { passive: false }
                         )
                     }>
-                    {this._fieldInfos.map(([key, info]) => (
+                    {this._menuKeys.map(key => (
                         <div
                             className="schema-search-result"
                             onPointerDown={e => {
@@ -742,13 +731,13 @@ export class CollectionSchemaView extends CollectionSubView() {
                             <p>
                                 <span className="schema-search-result-key">
                                     {key}
-                                    {info.fieldType ? ', ' : ''}
+                                    {this.fieldInfos.get(key)!.fieldType ? ', ' : ''}
                                 </span>
-                                <span className="schema-search-result-type" style={{ color: info.readOnly ? 'red' : 'inherit' }}>
-                                    {info.fieldType}
+                                <span className="schema-search-result-type" style={{ color: this.fieldInfos.get(key)!.readOnly ? 'red' : 'inherit' }}>
+                                    {this.fieldInfos.get(key)!.fieldType}
                                 </span>
                             </p>
-                            <p className="schema-search-result-desc">{info.description}</p>
+                            <p className="schema-search-result-desc">{this.fieldInfos.get(key)!.description}</p>
                         </div>
                     ))}
                 </div>
@@ -888,13 +877,7 @@ export class CollectionSchemaView extends CollectionSubView() {
                     </div>
                     {this._columnMenuIndex !== undefined && this.renderColumnMenu}
                     {this._filterColumnIndex !== undefined && this.renderFilterMenu}
-                    <CollectionSchemaViewDocs
-                        schema={this}
-                        childDocs={this.sortedDocsFunc}
-                        setRef={(ref: HTMLDivElement | null) => {
-                            this._tableContentRef = ref;
-                        }}
-                    />
+                    <CollectionSchemaViewDocs schema={this} childDocs={this.sortedDocsFunc} setRef={(ref: HTMLDivElement | null) => (this._tableContentRef = ref)} />
                     <EditableView GetValue={returnEmptyString} SetValue={this.addNewTextDoc} placeholder={"Type ':' for commands"} contents={'+ New Node'} menuCallback={this.menuCallback} height={CollectionSchemaView._newNodeInputHeight} />
                 </div>
                 {this.previewWidth > 0 && <div className="schema-preview-divider" style={{ width: CollectionSchemaView._previewDividerWidth }} onPointerDown={this.onDividerDown}></div>}
@@ -923,7 +906,7 @@ export class CollectionSchemaView extends CollectionSubView() {
                                 moveDocument={this.props.moveDocument}
                                 addDocument={this.addRow}
                                 removeDocument={this.props.removeDocument}
-                                whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged}
+                                whenChildContentsActiveChanged={returnFalse}
                                 addDocTab={this.props.addDocTab}
                                 pinToPres={this.props.pinToPres}
                                 bringToFront={returnFalse}
@@ -978,7 +961,7 @@ class CollectionSchemaViewDocs extends React.Component<CollectionSchemaViewDocsP
                                 bringToFront={emptyFunction}
                                 isDocumentActive={this.props.schema.props.childDocumentsActive?.() ? this.props.schema.props.isDocumentActive : this.props.schema.isContentActive}
                                 isContentActive={emptyFunction}
-                                whenChildContentsActiveChanged={active => this.props.schema.props.whenChildContentsActiveChanged(active)}
+                                whenChildContentsActiveChanged={this.props.schema.props.whenChildContentsActiveChanged}
                                 hideDecorations={true}
                                 hideTitle={true}
                                 hideDocumentButtonBar={true}
-- 
cgit v1.2.3-70-g09d2

