From 48dbde117728e4b36bae11f0f16a796059ecf7ee Mon Sep 17 00:00:00 2001
From: dg314 <david_grossman@brown.edu>
Date: Sat, 17 Jul 2021 15:13:39 -0400
Subject: initial search panel

---
 src/client/views/MainView.tsx | 11 +++++------
 1 file changed, 5 insertions(+), 6 deletions(-)

(limited to 'src/client/views/MainView.tsx')

diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 4eeb1fc95..bc9c6f4d2 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -180,8 +180,8 @@ export class MainView extends React.Component {
             const targClass = targets[0].className.toString();
             if (SearchBox.Instance._searchbarOpen || SearchBox.Instance.open) {
                 const check = targets.some((thing) =>
-                    (thing.className === "collectionSchemaView-searchContainer" || (thing as any)?.dataset.icon === "filter" ||
-                        thing.className === "collectionSchema-header-menuOptions"));
+                (thing.className === "collectionSchemaView-searchContainer" || (thing as any)?.dataset.icon === "filter" ||
+                    thing.className === "collectionSchema-header-menuOptions"));
                 !check && SearchBox.Instance.resetSearch(true);
             }
             !targClass.includes("contextMenu") && ContextMenu.Instance.closeMenu();
@@ -388,7 +388,6 @@ export class MainView extends React.Component {
                     SettingsManager.Instance.open();
                     break;
                 case "Catalog":
-                    SearchBox.Instance._searchFullDB = "My Stuff";
                     SearchBox.Instance.enter(undefined);
                     break;
                 case "Help":
@@ -523,7 +522,7 @@ export class MainView extends React.Component {
         </svg>;
     }
 
-    @computed get search() {
+    /*@computed get search() {
         TraceMobx();
         return <div className="mainView-searchPanel">
             <SearchBox Document={CurrentUserUtils.MySearchPanelDoc}
@@ -555,7 +554,7 @@ export class MainView extends React.Component {
                 ContainingCollectionView={undefined}
                 ContainingCollectionDoc={undefined} />
         </div>;
-    }
+    }*/
 
     @computed get invisibleWebBox() { // see note under the makeLink method in HypothesisUtils.ts
         return !DocumentLinksButton.invisibleWebDoc ? null :
@@ -604,7 +603,7 @@ export class MainView extends React.Component {
             <GroupManager />
             <GoogleAuthenticationManager />
             <DocumentDecorations boundsLeft={this.leftOffset} boundsTop={this.topOffset} />
-            {this.search}
+            {/*this.search*/}
             <CollectionMenu />
             {LinkDescriptionPopup.descriptionPopup ? <LinkDescriptionPopup /> : null}
             {DocumentLinksButton.LinkEditorDocView ? <LinkMenu docView={DocumentLinksButton.LinkEditorDocView} changeFlyout={emptyFunction} /> : (null)}
-- 
cgit v1.2.3-70-g09d2


From fc1559fd74bde212f418b27806b68a76748153ad Mon Sep 17 00:00:00 2001
From: dg314 <david_grossman@brown.edu>
Date: Thu, 29 Jul 2021 15:12:03 -0400
Subject: search panel updates

---
 src/client/util/CurrentUserUtils.ts    |  11 +-
 src/client/views/MainView.tsx          |   6 +-
 src/client/views/search/SearchBox.scss | 173 ++++--------
 src/client/views/search/SearchBox.tsx  | 491 +++++++++++++++------------------
 4 files changed, 291 insertions(+), 390 deletions(-)

(limited to 'src/client/views/MainView.tsx')

diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 596ccd9a6..31384da3b 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -947,17 +947,12 @@ export class CurrentUserUtils {
 
     // Search sidebar is where searches within the document are performed
     static setupSearchSidebar(doc: Doc) {
-        if (doc.mySearchPanelDoc === undefined) {
-            doc.mySearchPanelDoc = new PrefetchProxy(Docs.Create.SearchDocument({
-                _width: 500, _height: 300, backgroundColor: "dimGray", ignoreClick: true, _searchDoc: true,
+        if (doc.mySearchPanel === undefined) {
+            doc.mySearchPanel = new PrefetchProxy(Docs.Create.SearchDocument({
+                backgroundColor: "dimGray", ignoreClick: true, _searchDoc: true,
                 childDropAction: "alias", _lockedPosition: true, _viewType: CollectionViewType.Schema, title: "sidebar search stack", system: true
             })) as any as Doc;
         }
-        if (doc.mySearchPanel === undefined) {
-            const searchPanelDoc = Cast(doc.mySearchPanelDoc, Doc, null);
-            //const newUpload = CurrentUserUtils.ficon({ onClick: ScriptField.MakeScript("importDocument()"), toolTip: "Import External document", _stayInCollection: true, _hideContextMenu: true, title: "Import", icon: "upload", system: true });
-            doc.mySearchPanel = new PrefetchProxy(Docs.Create.StackingDocument([searchPanelDoc], { title: "Search", _yMargin: 20, ignoreClick: true, _chromeHidden: true, _stayInCollection: true, _hideContextMenu: true, _lockedPosition: true, system: true }));
-        }
     }
 
     static setupClickEditorTemplates(doc: Doc) {
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index bc9c6f4d2..0d957518b 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -178,12 +178,12 @@ export class MainView extends React.Component {
         const targets = document.elementsFromPoint(e.x, e.y);
         if (targets.length) {
             const targClass = targets[0].className.toString();
-            if (SearchBox.Instance._searchbarOpen || SearchBox.Instance.open) {
+            /*if (SearchBox.Instance._resultsOpen) {
                 const check = targets.some((thing) =>
                 (thing.className === "collectionSchemaView-searchContainer" || (thing as any)?.dataset.icon === "filter" ||
                     thing.className === "collectionSchema-header-menuOptions"));
                 !check && SearchBox.Instance.resetSearch(true);
-            }
+            }*/
             !targClass.includes("contextMenu") && ContextMenu.Instance.closeMenu();
             !["timeline-menu-desc", "timeline-menu-item", "timeline-menu-input"].includes(targClass) && TimelineMenu.Instance.closeMenu();
         }
@@ -192,7 +192,7 @@ export class MainView extends React.Component {
     initEventListeners = () => {
         window.addEventListener("drop", e => e.preventDefault(), false);  // prevent default behavior of navigating to a new web page
         window.addEventListener("dragover", e => e.preventDefault(), false);
-        document.addEventListener("pointermove", action(e => SearchBox.Instance._undoBackground = UndoManager.batchCounter ? "#000000a8" : undefined));
+        //document.addEventListener("pointermove", action(e => SearchBox.Instance._undoBackground = UndoManager.batchCounter ? "#000000a8" : undefined));
         document.addEventListener("pointerdown", this.globalPointerDown);
         document.addEventListener("click", (e: MouseEvent) => {
             if (!e.cancelBubble) {
diff --git a/src/client/views/search/SearchBox.scss b/src/client/views/search/SearchBox.scss
index 4f5b7e41a..f8e994da7 100644
--- a/src/client/views/search/SearchBox.scss
+++ b/src/client/views/search/SearchBox.scss
@@ -2,141 +2,84 @@
 @import "./NaviconButton.scss";
 
 .searchBox-container {
-    display: flex;
-    flex-direction: column;
     width: 100%;
     height: 100%;
-    position: relative;
     font-size: 10px;
     line-height: 1;
-    overflow-y: auto;
-    overflow-x: visible;
-    background: lightgrey;
-    overflow: visible;
+    background: none;
     z-index: 1000;
+    padding: 0px;
+    cursor: default;
 
     .searchBox-bar {
-        height: $searchpanel-height;
+        width: 100%;
+        height: 35px;
         display: flex;
         justify-content: center;
         align-items: center;
-        background-color: black;
+        background-color: none;
+        padding: 5px;
 
-        .searchBox-lozenges {
-            position: absolute;
-            left: 15;
-            display: flex;
-
-            .searchBox-lozenge-user,
-            .searchBox-lozenge-dashboard,
-            .searchBox-lozenge {
-                height: 18px;
-                padding: 4px;
-                margin-right: 5px;
-                display: flex;
-                align-items: center;
-                border: grey 1px solid;
-                .searchBox-logoff,
-                .searchBox-dashboards {
-                    border-radius: 3px;
-                    background: olivedrab;
-                    color: white;
-                    display: none;
-                    margin-left: 5px;
-                    padding: 1px 2px 1px 2px;
-                    cursor: pointer;
-                }
-                .searchBox-logoff {
-                    background: red;
-                }
-
-                .searchBox-dashSelect{
-                    border: none;
-                    background-color: transparent;
+        .searchBox-type {
+            display: block;
+            width: 50px;
+            outline: none;
+            padding: 1px 5px 1px 5px;
+            color: black;
+            height: 25px;
+            border: 1px solid black;
+        }
 
-                    &:hover {
-                        cursor: pointer;
-                    }
-                }
-            }
-            .searchBox-lozenge-user:hover {
-                .searchBox-logoff {
-                    display:inline-block;
-                }
-            }
-            .searchBox-lozenge-dashboard:hover {
-                .searchBox-dashboards {
-                    display:inline-block;
-                }
-            }
+        .searchBox-input {
+            display: block;
+            width: calc(100% - 50px);
+            outline: none;
+            padding: 1px 5px 1px 5px;
+            color: black;
+            height: 25px;
+            border: 1px solid black;
         }
-        .searchBox-query {
-            position: relative;
+    }
+
+    .searchBox-results-container {
+        display: flex;
+        flex-direction: column;
+        width: 100%;
+        height: 100%;
+        justify-content: "center";
+    
+        .searchBox-results-count {
             display: flex;
-            width: 450;
+            color: gray;
+            margin-left: 5px;
         }
-        .searchBox-barChild {
+    
+        .searchBox-results-scroll-view {
+            margin-top: 10px;
+            display: inline-block;
+            width: 100%;
+            height: calc(100% - 55px);
+            overflow-y: scroll;
 
-            &.searchBox-collection {
-                flex: 0 1 auto;
-                margin-left: 2px;
-                margin-right: 2px
-            }
+            .searchBox-results-scroll-view-result {
+                display: inline-block;
+                vertical-align: middle;
+                width: 100%;
+                height: 40px;
+                cursor: pointer;
+                font-size: 15px;
+                padding: 10px;
 
-            &.searchBox-input {
-                margin:5px;
-                border-radius:20px;
-                border:black;    
-                display: block;
-                width: 130px;
-                -webkit-transition: width 0.4s;
-                transition: width 0.4s;
-                align-self: stretch;
-                outline:none;
-                &:focus {
-                    width: 500px;
-                    outline:none;
+                &.searchBox-results-scroll-view-result-selected {
+                    background: gray;
                 }
-            }
-            &.searchBox-filter {
-                align-self: stretch;
-                button{
-                    transform:none;
-                    &:hover {
-                        transform: none;
-                    }
-                }
-            }
 
-            &.searchBox-submit {
-                margin-left: 2px;
-                margin-right: 2px
-            }
-
-            &.searchBox-close {
-                color: $light-color;
-                max-height: $searchpanel-height;
+                .titletitle {
+                    display: relative;
+                    float: left;
+                    left: 50px;
+                }
             }
         }
-    }   
-}
-
-.searchBox-results {
-    display: flex;
-    flex-direction: column;
-    top: 300px;
-    display: flex;
-    flex-direction: column;
-    height: 100%;
-    overflow: visible;
-
-    .no-result {
-        width: 500px;
-        background: $light-color-secondary;
-        padding: 10px;
-        height: 50px;
-        text-transform: uppercase;
-        text-align: left;
-        font-weight: bold;
     }
 }
\ No newline at end of file
diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx
index cc4590969..374a754bf 100644
--- a/src/client/views/search/SearchBox.tsx
+++ b/src/client/views/search/SearchBox.tsx
@@ -3,7 +3,7 @@ import { Tooltip } from '@material-ui/core';
 import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import { Doc, DocListCast, Field, Opt, DocListCastAsync, DataSym, HeightSym } from '../../../fields/Doc';
+import { Doc, DocListCast, Field, Opt, DocListCastAsync, DataSym, HeightSym, FieldsSym } from '../../../fields/Doc';
 import { documentSchema } from "../../../fields/documentSchemas";
 import { Copy, Id, ToString } from '../../../fields/FieldSymbols';
 import { List } from '../../../fields/List';
@@ -26,6 +26,9 @@ import "./SearchBox.scss";
 import { undoBatch } from "../../util/UndoManager";
 import { DocServer } from "../../DocServer";
 import { MainView } from "../MainView";
+import { DocumentManager } from "../../util/DocumentManager";
+import { CollectionSchemaBooleanCell } from "../collections/CollectionSchemaCells";
+import { transpileModule } from "typescript";
 
 export const searchSchema = createSchema({ Document: Doc });
 
@@ -38,33 +41,18 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc
     public static Instance: SearchBox;
 
     private _allIcons: string[] = [DocumentType.INK, DocumentType.AUDIO, DocumentType.COL, DocumentType.IMG, DocumentType.LINK, DocumentType.PDF, DocumentType.RTF, DocumentType.VID, DocumentType.WEB];
-    private _numResultsPerPage = 500;
-    private _numTotalResults = -1;
-    private _endIndex = -1;
-    private _lockPromise?: Promise<void>;
     private _resultsSet = new Map<Doc, number>();
     private _inputRef = React.createRef<HTMLInputElement>();
-    private _maxSearchIndex: number = 0;
-    private _curRequest?: Promise<any> = undefined;
     private _disposers: { [name: string]: IReactionDisposer } = {};
     private _blockedTypes = [DocumentType.PRESELEMENT, DocumentType.KVP, DocumentType.FILTER, DocumentType.SEARCH, DocumentType.SEARCHITEM, DocumentType.FONTICON, DocumentType.BUTTON, DocumentType.SCRIPTING];
 
-    private docsforfilter: Doc[] | undefined = [];
-    private realTotalResults: number = 0;
-    private newsearchstring = "";
-
-    @observable _undoBackground: string | undefined = "";
+    @observable _searchString = "";
+    @observable _docTypeString = "all";
     @observable _icons: string[] = this._allIcons;
     @observable _results: [Doc, string[], string[]][] = [];
-    @observable _visibleElements: JSX.Element[] = [];
-    @observable _visibleDocuments: Doc[] = [];
+    @observable _selectedResult: Doc | undefined = undefined;
     @observable _deletedDocsStatus: boolean = false;
     @observable _onlyAliases: boolean = true;
-    @observable _searchbarOpen = false;
-    @observable _noResults = "";
-    @observable _pageStart = 0;
-    @observable open = false;
-    @observable children = 0;
     @computed get filter() { return this._results?.length && (this.currentSelectedCollection?.props.Document._searchFilterDocs || this.currentSelectedCollection?.props.Document._docFilters); }
 
     constructor(props: any) {
@@ -76,8 +64,6 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc
         if (this._inputRef.current) {
             this._inputRef.current.focus();
         }
-        this._disposers.filters = reaction(() => this.props.Document._docFilters,
-            (filters: any) => this.setSearchFilter(this.currentSelectedCollection, !this.filter ? undefined : this.docsforfilter));
     });
 
     componentWillUnmount() {
@@ -86,32 +72,25 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc
 
     @computed get currentSelectedCollection() { return CollectionDockingView.Instance; }
 
-    onChange = action((e: React.ChangeEvent<HTMLInputElement>) => {
-        this.newsearchstring = e.target.value;
-        if (e.target.value === "") {
-            console.log("Reset start");
-            this.docsforfilter = undefined;
-            this.setSearchFilter(this.currentSelectedCollection, undefined);
-            this.resetSearch(false);
-
-            this.open = false;
-            this._results = [];
-            this._resultsSet.clear();
-            this._visibleElements = [];
-            this._numTotalResults = -1;
-            this._endIndex = -1;
-            this._curRequest = undefined;
-            this._maxSearchIndex = 0;
-        }
+    onInputChange = action((e: React.ChangeEvent<HTMLInputElement>) => {
+        this._searchString = e.target.value;
+        this.submitSearch();
     });
 
-    enter = action((e: React.KeyboardEvent | undefined) => {
+    onSelectChange = action((e: React.ChangeEvent<HTMLSelectElement>) => {
+        this._docTypeString = e.target.value;
+        this.submitSearch();
+    });
+
+    /*enter = action((e: React.KeyboardEvent | undefined) => {
         if (!e || e.key === "Enter") {
-            this.layoutDoc._searchString = this.newsearchstring;
-            this._pageStart = 0;
-            this.open = true;
             this.submitSearch();
         }
+    });*/
+
+    onResultClick = action((doc: Doc) => {
+        this.selectElement(doc);
+        this._selectedResult = doc;
     });
 
     @action
@@ -126,21 +105,6 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc
         return finalDocs;
     }
 
-    static async foreachRecursiveDocAsync(docs: Doc[], func: (doc: Doc) => void) {
-        let newarray: Doc[] = [];
-        while (docs.length > 0) {
-            newarray = [];
-            await Promise.all(docs.filter(d => d).map(async d => {
-                const fieldKey = Doc.LayoutFieldKey(d);
-                const annos = !Field.toString(Doc.LayoutField(d) as Field).includes("CollectionView");
-                const data = d[annos ? fieldKey + "-annotations" : fieldKey];
-                const docs = await DocListCastAsync(data);
-                docs && newarray.push(...docs);
-                func(d);
-            }));
-            docs = newarray;
-        }
-    }
     static foreachRecursiveDoc(docs: Doc[], func: (doc: Doc) => void) {
         let newarray: Doc[] = [];
         while (docs.length > 0) {
@@ -172,15 +136,7 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc
             });
 
             this._results = found;
-            this.docsforfilter = this._results.map(r => r[0]);
-            this.setSearchFilter(selectedCollection, this.filter && found.length ? this.docsforfilter : undefined);
-            this._numTotalResults = found.length;
-            this.realTotalResults = found.length;
         }
-        else {
-            this._noResults = "No collection selected :(";
-        }
-
     }
 
     static documentKeys(doc: Doc) {
@@ -197,246 +153,253 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc
 
     @action
     submitSearch = async () => {
-        this.resetSearch(false);
-
-        //this.props.Document._docFilters = new List();
-        this._noResults = "";
+        this.resetSearch();
 
         this.dataDoc[this.fieldKey] = new List<Doc>([]);
-        this.children = 0;
-        let query = StrCast(this.layoutDoc._searchString);
+        let query = StrCast(this._searchString);
         Doc.SetSearchQuery(query);
         this._results = [];
         this._resultsSet.clear();
-        this._visibleElements = [];
-        this._visibleDocuments = [];
 
         if (query) {
-            this._endIndex = 12;
-            this._maxSearchIndex = 0;
-            this._numTotalResults = -1;
             this.searchCollection(query);
-            runInAction(() => {
-                this.open = this._searchbarOpen = true;
-                this.resultsScrolled();
-            });
         }
     }
 
-    getAllResults = async (query: string) => {
-        return SearchUtil.Search(query, true, { fq: this.filterQuery, start: 0, rows: 10000000 });
-    }
+    resetSearch = action(() => {
+        this._results.forEach(result => {
+            Doc.UnBrushDoc(result[0]);
+            Doc.ClearSearchMatches();
+        });
+    });
 
-    private get filterQuery() {
-        const baseExpr = "NOT system_b:true";
-        const authorExpr = undefined;
-        const includeDeleted = this._deletedDocsStatus ? "" : " NOT deleted_b:true";
-        const typeExpr = this._onlyAliases ? "NOT {!join from=id to=proto_i}type_t:*" : `(type_t:* OR {!join from=id to=proto_i}type_t:*) ${this._blockedTypes.map(type => `NOT ({!join from=id to=proto_i}type_t:${type}) AND NOT type_t:${type}`).join(" AND ")}`;
-        // fq: type_t:collection OR {!join from=id to=proto_i}type_t:collection   q:text_t:hello
-        return [baseExpr, authorExpr, includeDeleted, typeExpr].filter(q => q).join(" AND ").replace(/AND $/, "");
+    selectElement = async (doc: Doc) => {
+        await DocumentManager.Instance.jumpToDocument(doc, true); // documents open in new tab instead of on right    
     }
 
-    @computed get primarySort() {
-        const suffixMap = (type: ColumnType) => {
-            switch (type) {
-                case ColumnType.Date: return "_d";
-                case ColumnType.String: return "_t";
-                case ColumnType.Boolean: return "_b";
-                case ColumnType.Number: return "_n";
+    render() {
+        const results = this._results.map(result => {
+            var className = "searchBox-results-scroll-view-result";
+
+            if (this._selectedResult == result[0]) {
+                className += " searchBox-results-scroll-view-result-selected"
             }
-        };
-        const headers = Cast(this.props.Document._schemaHeaders, listSpec(SchemaHeaderField), []);
-        return headers.reduce((p: Opt<string>, header: SchemaHeaderField) => p || (header.desc !== undefined && suffixMap(header.type) ? (header.heading + suffixMap(header.type) + (header.desc ? " desc" : " asc")) : undefined), undefined);
-    }
 
-    searchDatabase = async (query: string) => {
-        this._lockPromise && (await this._lockPromise);
-        this._lockPromise = new Promise(async res => {
-            while (this._results.length <= this._endIndex && (this._numTotalResults === -1 || this._maxSearchIndex < this._numTotalResults)) {
-                this._curRequest = SearchUtil.Search(query, true, { onlyAliases: true, allowAliases: true, /*sort: this.primarySort,*/ fq: this.filterQuery, start: 0, rows: this._numResultsPerPage, hl: "on", "hl.fl": "*", }).then(action(async (res: SearchUtil.DocSearchResult) => {
-                    // happens at the beginning
-                    this.realTotalResults = res.numFound <= 0 ? 0 : res.numFound;
-                    if (res.numFound !== this._numTotalResults && this._numTotalResults === -1) {
-                        this._numTotalResults = res.numFound;
-                    }
-                    const highlighting = res.highlighting || {};
-                    const highlightList = res.docs.map(doc => highlighting[doc[Id]]);
-                    const lines = new Map<string, string[]>();
-                    res.docs.map((doc, i) => lines.set(doc[Id], res.lines[i]));
-                    const docs = res.docs;
-                    const highlights: typeof res.highlighting = {};
-                    docs.forEach((doc, index) => highlights[doc[Id]] = highlightList[index]);
-                    const filteredDocs = this.filterDocsByType(docs);
-
-                    runInAction(() => filteredDocs.forEach((doc, i) => {
-                        const index = this._resultsSet.get(doc);
-                        const highlight = highlights[doc[Id]];
-                        const line = lines.get(doc[Id]) || [];
-                        const hlights = highlight ? Object.keys(highlight).map(key => key.substring(0, key.length - 2)).filter(k => k) : [];
-                        // if (this.findCommonElements(hlights)) {
-                        // }
-                        if (index === undefined) {
-                            this._resultsSet.set(doc, this._results.length);
-                            this._results.push([doc, hlights, line]);
-                        } else {
-                            this._results[index][1].push(...hlights);
-                            this._results[index][2].push(...line);
-                        }
-                    }));
-
-                    this._curRequest = undefined;
-                }));
-                this._maxSearchIndex += this._numResultsPerPage;
-
-                await this._curRequest;
+            if (this._docTypeString == "all" || this._docTypeString == result[0].type) {
+                return (<div key={result[0][Id]} onClick={() => this.onResultClick(result[0])} className={className}><div className="titletitle">{result[0].title}</div></div>)
             }
 
-            this.resultsScrolled();
+            return null;
+        })
 
-            const selectedCollection = this.currentSelectedCollection;//SelectionManager.SelectedDocuments()[0];
-            this.docsforfilter = this._results.map(r => r[0]);
-            this.setSearchFilter(selectedCollection, this.filter ? this.docsforfilter : undefined);
-            res();
-        });
-        return this._lockPromise;
+        results.filter(result => result)
+
+        return (
+            <div style={{ pointerEvents: "all" }} className="searchBox-container">
+                <div className="searchBox-bar">
+                    <select name="type" id="searchBox-type" className="searchBox-type" onChange={this.onSelectChange}>
+                        <option value="all">All</option>
+                        <option value="rtf">Text</option>
+                        <option value="img">Image</option>
+                        <option value="pdf">PDF</option>
+                    </select>
+                    <input defaultValue={""} autoComplete="off" onChange={this.onInputChange} type="text" placeholder="Search..." id="search-input" ref={this._inputRef} className="searchBox-input" />
+                </div >
+                <div className="searchBox-results-container">
+                    <div className="searchBox-results-count">
+                        {`${results.length}` + " result" + (results.length == 1 ? "" : "s")}
+                    </div>
+                    <div className="searchBox-results-scroll-view">
+                        {results}
+                    </div>
+                </div>
+            </div >
+        );
     }
+}
+
+
+
+/*
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { Tooltip } from '@material-ui/core';
+import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import { Doc, DocListCast, Field, Opt, DocListCastAsync, DataSym, HeightSym, FieldsSym } from '../../../fields/Doc';
+import { documentSchema } from "../../../fields/documentSchemas";
+import { Copy, Id, ToString } from '../../../fields/FieldSymbols';
+import { List } from '../../../fields/List';
+import { createSchema, listSpec, makeInterface } from '../../../fields/Schema';
+import { SchemaHeaderField } from '../../../fields/SchemaHeaderField';
+import { Cast, NumCast, StrCast } from '../../../fields/Types';
+import { emptyFunction, returnFalse, returnZero, setupMoveUpEvents, Utils } from '../../../Utils';
+import { Docs } from '../../documents/Documents';
+import { DocumentType } from "../../documents/DocumentTypes";
+import { CurrentUserUtils } from "../../util/CurrentUserUtils";
+import { SetupDrag } from '../../util/DragManager';
+import { SearchUtil } from '../../util/SearchUtil';
+import { Transform } from '../../util/Transform';
+import { CollectionDockingView } from "../collections/CollectionDockingView";
+import { CollectionSchemaView, ColumnType } from "../collections/CollectionSchemaView";
+import { CollectionViewType } from '../collections/CollectionView';
+import { ViewBoxBaseComponent } from "../DocComponent";
+import { FieldView, FieldViewProps } from '../nodes/FieldView';
+import "./SearchBox.scss";
+import { undoBatch } from "../../util/UndoManager";
+import { DocServer } from "../../DocServer";
+import { MainView } from "../MainView";
+import { SelectionManager } from "../../util/SelectionManager";
+import { CollectionSchemaBooleanCell } from "../collections/CollectionSchemaCells";
+import { transpileModule } from "typescript";
+import { DocumentManager } from "../../util/DocumentManager";
+
+export const searchSchema = createSchema({ Document: Doc });
+
+type SearchBoxDocument = makeInterface<[typeof documentSchema, typeof searchSchema]>;
+const SearchBoxDocument = makeInterface(documentSchema, searchSchema);
+
+@observer
+export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDocument>(SearchBoxDocument) {
+    public static LayoutString(fieldKey: string) { return FieldView.LayoutString(SearchBox, fieldKey); }
+    public static Instance: SearchBox;
+
+    @observable _searchString = "";
+    @observable _docTypeString = "all";
+    @observable _results: [Doc, string[], string[]][] = [];
+    @observable _selectedResult: Doc | undefined = undefined;
+    @observable _deletedDocsStatus: boolean = false;
+    @observable _onlyAliases: boolean = true;
 
-    @action.bound
-    openSearch(e: React.SyntheticEvent) {
-        e.stopPropagation();
-        this._results.forEach(result => Doc.BrushDoc(result[0]));
+    constructor(props: any) {
+        super(props);
+        SearchBox.Instance = this;
     }
 
-    resetSearch = action((close: boolean) => {
-        this._results.forEach(result => {
-            Doc.UnBrushDoc(result[0]);
-            Doc.ClearSearchMatches();
-        });
-        close && (this.open = this._searchbarOpen = false);
+    onInputChange = action((e: React.ChangeEvent<HTMLInputElement>) => {
+        this._searchString = e.target.value;
+        this.submitSearch();
     });
 
-    @action.bound
-    closeResults() {
-        this._results = [];
-        this._resultsSet.clear();
-        this._visibleElements = [];
-        this._visibleDocuments = [];
-        this._numTotalResults = -1;
-        this._endIndex = -1;
-        this._curRequest = undefined;
-    }
+    onSelectChange = action((e: React.ChangeEvent<HTMLSelectElement>) => {
+        this._docTypeString = e.target.value;
+        this.submitSearch();
+    });
 
-    @action
-    resultsScrolled = (e?: React.UIEvent<HTMLDivElement>) => {
-        this._endIndex = 30;
-        const headers = new Set<string>(["title", "author", "text", "type", "data", "*lastModified", "context"]);
+    onResultClick = action((doc: Doc) => {
+        this.selectElement(doc);
+        this._selectedResult = doc;
+    });
 
-        if (this._numTotalResults <= this._maxSearchIndex) {
-            this._numTotalResults = this._results.length;
+    static foreachRecursiveDoc(docs: Doc[], func: (doc: Doc) => void) {
+        const blockedTypes = [DocumentType.PRESELEMENT, DocumentType.KVP, DocumentType.FILTER, DocumentType.SEARCH, DocumentType.SEARCHITEM, DocumentType.FONTICON, DocumentType.BUTTON, DocumentType.SCRIPTING];
+        let newarray: Doc[] = [];
+        while (docs.length > 0) {
+            newarray = [];
+            docs.filter(d => d).forEach(d => {
+                const dtype = StrCast(d.type, "string") as DocumentType;
+                if (dtype && !blockedTypes.includes(dtype)) {
+                    const fieldKey = Doc.LayoutFieldKey(d);
+                    const annos = !Field.toString(Doc.LayoutField(d) as Field).includes("CollectionView");
+                    const data = d[annos ? fieldKey + "-annotations" : fieldKey];
+                    data && newarray.push(...DocListCast(data));
+                    func(d);
+                }
+            });
+            docs = newarray;
         }
+    }
 
-        // only hit right at the beginning
-        // visibleElements is all of the elements (even the ones you can't see)
-        if (this._visibleElements.length !== this._numTotalResults) {
-            // undefined until a searchitem is put in there
-            this._visibleElements = Array<JSX.Element>(this._numTotalResults === -1 ? 0 : this._numTotalResults);
-            this._visibleDocuments = Array<Doc>(this._numTotalResults === -1 ? 0 : this._numTotalResults);
-        }
-        let max = this._numResultsPerPage;
-        max > this._results.length ? max = this._results.length : console.log("");
-        for (let i = this._pageStart; i < max; i++) {
-            //if the index is out of the window then put a placeholder in
-            //should ones that have already been found get set to placeholders?
-
-            let result: [Doc, string[], string[]] | undefined = undefined;
-
-            result = this._results[i];
-            if (result) {
-                const highlights = Array.from([...Array.from(new Set(result[1]).values())]);
-                const lines = new List<string>(result[2]);
-                highlights.forEach((item) => headers.add(item));
-                Doc.SetSearchMatch(result[0], { searchMatch: 1 });
-                if (i < this._visibleDocuments.length) {
-                    this._visibleDocuments[i] = result[0];
-                    Doc.BrushDoc(result[0]);
-                    Doc.AddDocToList(this.dataDoc, this.props.fieldKey, result[0]);
-                    this.children++;
+    @action
+    searchCollection(query: string) {
+        const selectedCollection = CollectionDockingView.Instance;
+        query = query.toLowerCase();
+
+        if (selectedCollection !== undefined) {
+            console.log("hello111")
+            // this._currentSelectedCollection = selectedCollection;
+            const docs = DocListCast(selectedCollection.dataDoc[Doc.LayoutFieldKey(selectedCollection.dataDoc)]);
+            const found: [Doc, string[], string[]][] = [];
+            SearchBox.foreachRecursiveDoc(docs, (doc: Doc) => {
+                console.log("HELLO")
+                if (this._docTypeString == "all" || this._docTypeString == doc.type) {
+                    const hlights = new Set<string>();
+                    SearchBox.documentKeys(doc).forEach(key => Field.toString(doc[key] as Field).toLowerCase().includes(query) && hlights.add(key));
+                    Array.from(hlights.keys()).length > 0 && found.push([doc, Array.from(hlights.keys()), []]);
                 }
-            }
-        }
-        if (this.props.Document._schemaHeaders === undefined) {
-            this.props.Document._schemaHeaders = new List<SchemaHeaderField>([new SchemaHeaderField("title", "#f1efeb")]);
-        }
-        if (this._maxSearchIndex >= this._numTotalResults) {
-            this._visibleElements.length = this._results.length;
-            this._visibleDocuments.length = this._results.length;
+            });
+
+            this._results = found;
+            //this.setSearchFilter(selectedCollection, this.filter && found.length ? this._docsforfilter : undefined);
         }
     }
 
-    getTransform = () => this.props.ScreenToLocalTransform().translate(-5, -65);// listBox padding-left and pres-box-cont minHeight
-    panelHeight = () => this.props.PanelHeight();
-    selectElement = (doc: Doc) => { /* this.gotoDocument(this.childDocs.indexOf(doc), NumCasst(this.layoutDoc._itemIndex)); */ };
-    returnHeight = () => NumCast(this.layoutDoc._height);
-    returnLength = () => Math.min(window.innerWidth, 51 + 205 * Cast(this.props.Document._schemaHeaders, listSpec(SchemaHeaderField), []).length);
-
-    setSearchFilter = action((collectionView: { props: { Document: Doc } }, docsForFilter: Doc[] | undefined) => {
-        if (collectionView) {
-            const docFilters = Cast(this.props.Document._docFilters, listSpec("string"), null);
-            collectionView.props.Document._searchFilterDocs = docsForFilter?.length ? new List<Doc>(docsForFilter) : undefined;
-            collectionView.props.Document._docFilters = docsForFilter?.length && docFilters?.length ? new List<string>(docFilters) : undefined;
+    static documentKeys(doc: Doc) {
+        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
+        Doc.GetAllPrototypes(doc).map(proto => Object.keys(proto).forEach(key => keys[key] = false));
+        return Array.from(Object.keys(keys));
+    }
+
+    @action
+    submitSearch = async () => {
+        Doc.ClearSearchMatches();
+        this._results = [];
+
+        this.dataDoc[this.fieldKey] = new List<Doc>([]);
+        let query = StrCast(this._searchString);
+        Doc.SetSearchQuery(query);
+        this._results = [];
+
+        if (query) {
+            this.searchCollection(query);
         }
-    });
+    }
+
+    selectElement = async (doc: Doc) => {
+        await DocumentManager.Instance.jumpToDocument(doc, true); // documents open in new tab instead of on right
+    }
 
     render() {
-        const myDashboards = DocListCast(CurrentUserUtils.MyDashboards.data);
+        const results = this._results.map(result => {
+            var className = "searchBox-results-scroll-view-result";
+
+            if (this._selectedResult == result[0]) {
+                className += " searchBox-results-scroll-view-result-selected"
+            }
+
+            return (<div key={result[0][Id]} onClick={() => this.onResultClick(result[0])} className={className}><div className="titletitle">{result[0].title}</div></div>)
+        })
 
         return (
             <div style={{ pointerEvents: "all" }} className="searchBox-container">
-                <div className="searchBox-bar" style={{ background: SearchBox.Instance._undoBackground }}>
-                    <div className="searchBox-query" >
-                        <input defaultValue={""} autoComplete="off" onChange={this.onChange} type="text" placeholder="Search..." id="search-input" ref={this._inputRef}
-                            className="searchBox-barChild searchBox-input" onKeyPress={this.enter}
-                            style={{ padding: 1, paddingLeft: 20, paddingRight: 60, color: "black", height: 20, width: 250 }} />
-                        <div style={{ display: "flex", alignItems: "center" }}>
-                            <div style={{ position: "absolute", left: 10 }}>
-                                <FontAwesomeIcon icon={"search"} size="lg"
-                                    style={{ color: "black", padding: 1, position: "relative" }} />
-                            </div>
-                            <div style={{ position: "absolute", left: Doc.UserDoc().noviceMode ? 220 : 200, width: 30, zIndex: 9000, color: "grey", background: "white", }}>
-                                {`${this._results.length}` + " of " + `${this.realTotalResults}`}
-                            </div>
-                        </div>
-                    </div >
+                <div className="searchBox-bar">
+                    <select name="type" id="searchBox-type" className="searchBox-type" onChange={this.onSelectChange}>
+                        <option value="all">All</option>
+                        <option value="rtf">Text</option>
+                        <option value="img">Img</option>
+                        <option value="pdf">PDF</option>
+                        <option value="video">Vid</option>
+                        <option value="audio">Aud</option>
+                        <option value="inks">Ink</option>
+                        <option value="col">Col</option>
+                    </select>
+                    <input defaultValue={""} autoComplete="off" onChange={this.onInputChange} type="text" placeholder="Search..." id="search-input" className="searchBox-input" />
                 </div >
-                {!this._searchbarOpen ? (null) :
-                    <div style={{ zIndex: 20000, color: "black" }} ref={(r) => r?.focus()}>
-                        <div style={{ display: "flex", justifyContent: "center", }}>
-                            <div style={{ display: this.open ? "flex" : "none", overflow: "auto", position: "absolute" }}>
-                                <CollectionSchemaView {...this.props}
-                                    CollectionView={undefined}
-                                    addDocument={returnFalse}
-                                    Document={this.props.Document}
-                                    moveDocument={returnFalse}
-                                    removeDocument={returnFalse}
-                                    PanelHeight={this.open ? this.returnHeight : returnZero}
-                                    PanelWidth={this.open ? this.returnLength : returnZero}
-                                    scrollOverflow={length > window.innerWidth || this.children > 6 ? true : false}
-                                    focus={this.selectElement}
-                                    ScreenToLocalTransform={Transform.Identity}
-                                />
-                                <div style={{ position: "absolute", right: 5, bottom: 7, width: 15, height: 15, }}
-                                    onPointerDown={e => setupMoveUpEvents(this, e, (e: PointerEvent, down: number[], delta: number[]) => {
-                                        this.props.Document._height = NumCast(this.props.Document._height) + delta[1];
-                                        return false;
-                                    }, returnFalse, emptyFunction)}
-                                >
-                                    <FontAwesomeIcon icon="grip-lines" size="lg" />
-                                </div>
-                            </div>
-                        </div>
+                <div className="searchBox-results-container">
+                    <div className="searchBox-results-count">
+                        {`${this._results.length}` + " result" + (this._results.length == 1 ? "" : "s")}
                     </div>
-                }
+                    <div className="searchBox-results-scroll-view">
+                        {results}
+                    </div>
+                </div>
             </div >
         );
     }
-}
\ No newline at end of file
+}*/
\ No newline at end of file
-- 
cgit v1.2.3-70-g09d2


From 2ec093982c06965086326df73d365e1b54f6a2a2 Mon Sep 17 00:00:00 2001
From: dg314 <david_grossman@brown.edu>
Date: Wed, 4 Aug 2021 03:19:16 -0400
Subject: search panel updates

---
 src/client/util/CurrentUserUtils.ts    | 27 ++++++-----
 src/client/views/GlobalKeyHandler.ts   |  7 ++-
 src/client/views/MainView.tsx          | 37 --------------
 src/client/views/search/SearchBox.scss | 19 ++++++--
 src/client/views/search/SearchBox.tsx  | 89 +++++++++++++++++-----------------
 5 files changed, 81 insertions(+), 98 deletions(-)

(limited to 'src/client/views/MainView.tsx')

diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 31384da3b..734b96b1e 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -46,6 +46,7 @@ export class CurrentUserUtils {
     //TODO tfs: these should be temporary...
     private static mainDocId: string | undefined;
 
+    public static searchBtn: Doc;
     public static get id() { return this.curr_id; }
     public static get MainDocId() { return this.mainDocId; }
     public static set MainDocId(id: string | undefined) { this.mainDocId = id; }
@@ -533,15 +534,6 @@ export class CurrentUserUtils {
         ];
     }
 
-    /*static setupSearchPanel(doc: Doc) {
-        if (doc.mySearchPanelDoc === undefined) {
-            doc.mySearchPanelDoc = new PrefetchProxy(Docs.Create.SearchDocument({
-                _width: 500, _height: 300, backgroundColor: "dimGray", ignoreClick: true, _searchDoc: true,
-                childDropAction: "alias", _lockedPosition: true, _viewType: CollectionViewType.Schema, title: "sidebar search stack", system: true
-            })) as any as Doc;
-        }
-    }*/
-
     static async setupMenuPanel(doc: Doc, sharingDocumentId: string, linkDatabaseId: string) {
         if (doc.menuStack === undefined) {
             await this.setupSharingSidebar(doc, sharingDocumentId, linkDatabaseId);  // sets up the right sidebar collection for mobile upload documents and sharing
@@ -562,10 +554,23 @@ export class CurrentUserUtils {
                     _height: 60,
                     watchedDocuments,
                     onClick: ScriptField.MakeScript(click, { scriptContext: "any" })
-                }));
+                })
+            );
+
+            menuBtns.forEach(menuBtn => {
+                if (menuBtn.title == "Search") {
+                    this.searchBtn = menuBtn;
+                }
+            });
             // hack -- last button is assumed to be the userDoc
             menuBtns[menuBtns.length - 1].hidden = ComputedField.MakeFunction("IsNoviceMode()");
 
+            menuBtns.forEach(menuBtn => {
+                if (menuBtn.title == "Search") {
+                    doc.searchBtn = menuBtn
+                }
+            })
+
             doc.menuStack = new PrefetchProxy(Docs.Create.StackingDocument(menuBtns, {
                 title: "menuItemPanel",
                 childDropAction: "alias",
@@ -950,7 +955,7 @@ export class CurrentUserUtils {
         if (doc.mySearchPanel === undefined) {
             doc.mySearchPanel = new PrefetchProxy(Docs.Create.SearchDocument({
                 backgroundColor: "dimGray", ignoreClick: true, _searchDoc: true,
-                childDropAction: "alias", _lockedPosition: true, _viewType: CollectionViewType.Schema, title: "sidebar search stack", system: true
+                childDropAction: "alias", _lockedPosition: true, _viewType: CollectionViewType.Schema, title: "Search Sidebar", system: true
             })) as any as Doc;
         }
     }
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index d92cba3eb..2a3cb36c7 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -27,7 +27,6 @@ import { LightboxView } from "./LightboxView";
 import { MainView } from "./MainView";
 import { DocumentLinksButton } from "./nodes/DocumentLinksButton";
 import { AnchorMenu } from "./pdf/AnchorMenu";
-import { SearchBox } from "./search/SearchBox";
 import { CurrentUserUtils } from "../util/CurrentUserUtils";
 import { SettingsManager } from "../util/SettingsManager";
 
@@ -225,7 +224,11 @@ export class KeyManager {
                 PromiseValue(Cast(Doc.UserDoc()["tabs-button-tools"], Doc)).then(pv => pv && (pv.onClick as ScriptField).script.run({ this: pv }));
                 break;
             case "f":
-                SearchBox.Instance.enter(undefined);
+                const searchBtn = Doc.UserDoc().searchBtn as Doc;
+
+                if (searchBtn) {
+                    MainView.Instance.selectMenu(searchBtn);
+                }
                 break;
             case "o":
                 const target = SelectionManager.Views()[0];
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 0d957518b..35222e91f 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -387,9 +387,6 @@ export class MainView extends React.Component {
                 case "Settings":
                     SettingsManager.Instance.open();
                     break;
-                case "Catalog":
-                    SearchBox.Instance.enter(undefined);
-                    break;
                 case "Help":
                     break;
                 default:
@@ -522,40 +519,6 @@ export class MainView extends React.Component {
         </svg>;
     }
 
-    /*@computed get search() {
-        TraceMobx();
-        return <div className="mainView-searchPanel">
-            <SearchBox Document={CurrentUserUtils.MySearchPanelDoc}
-                DataDoc={CurrentUserUtils.MySearchPanelDoc}
-                fieldKey="data"
-                dropAction="move"
-                isSelected={returnTrue}
-                isContentActive={returnTrue}
-                select={returnTrue}
-                setHeight={returnFalse}
-                addDocument={undefined}
-                addDocTab={this.addDocTabFunc}
-                pinToPres={emptyFunction}
-                rootSelected={returnTrue}
-                styleProvider={DefaultStyleProvider}
-                layerProvider={undefined}
-                removeDocument={undefined}
-                ScreenToLocalTransform={Transform.Identity}
-                PanelWidth={this.getPWidth}
-                PanelHeight={this.getPHeight}
-                renderDepth={0}
-                focus={DocUtils.DefaultFocus}
-                docViewPath={returnEmptyDoclist}
-                whenChildContentsActiveChanged={emptyFunction}
-                bringToFront={emptyFunction}
-                docFilters={returnEmptyFilter}
-                docRangeFilters={returnEmptyFilter}
-                searchFilterDocs={returnEmptyDoclist}
-                ContainingCollectionView={undefined}
-                ContainingCollectionDoc={undefined} />
-        </div>;
-    }*/
-
     @computed get invisibleWebBox() { // see note under the makeLink method in HypothesisUtils.ts
         return !DocumentLinksButton.invisibleWebDoc ? null :
             <div className="mainView-invisibleWebRef" ref={DocumentLinksButton.invisibleWebRef}>
diff --git a/src/client/views/search/SearchBox.scss b/src/client/views/search/SearchBox.scss
index f8e994da7..e4d1ac6a3 100644
--- a/src/client/views/search/SearchBox.scss
+++ b/src/client/views/search/SearchBox.scss
@@ -22,17 +22,18 @@
 
         .searchBox-type {
             display: block;
-            width: 50px;
+            width: 55px;
             outline: none;
             padding: 1px 5px 1px 5px;
             color: black;
             height: 25px;
             border: 1px solid black;
+            border-right: 0px;
         }
 
         .searchBox-input {
             display: block;
-            width: calc(100% - 50px);
+            width: calc(100% - 55px);
             outline: none;
             padding: 1px 5px 1px 5px;
             color: black;
@@ -74,10 +75,20 @@
                     background: gray;
                 }
 
-                .titletitle {
+                .searchBox-result-title {
                     display: relative;
                     float: left;
-                    left: 50px;
+                    width: calc(100% - 60px);
+                    text-align: left;
+                }
+
+                .searchBox-result-type {
+                    font-size: 12px;
+                    display: relative;
+                    float: right;
+                    width: 60px;
+                    text-align: right;
+                    color: #333;
                 }
             }
         }
diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx
index 374a754bf..3cbb4b3b1 100644
--- a/src/client/views/search/SearchBox.tsx
+++ b/src/client/views/search/SearchBox.tsx
@@ -35,29 +35,30 @@ export const searchSchema = createSchema({ Document: Doc });
 type SearchBoxDocument = makeInterface<[typeof documentSchema, typeof searchSchema]>;
 const SearchBoxDocument = makeInterface(documentSchema, searchSchema);
 
+const selectValues = ["all", "rtf", "image", "pdf", "web", "video", "audio", "collection"]
+
 @observer
 export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDocument>(SearchBoxDocument) {
     public static LayoutString(fieldKey: string) { return FieldView.LayoutString(SearchBox, fieldKey); }
     public static Instance: SearchBox;
 
-    private _allIcons: string[] = [DocumentType.INK, DocumentType.AUDIO, DocumentType.COL, DocumentType.IMG, DocumentType.LINK, DocumentType.PDF, DocumentType.RTF, DocumentType.VID, DocumentType.WEB];
     private _resultsSet = new Map<Doc, number>();
     private _inputRef = React.createRef<HTMLInputElement>();
-    private _disposers: { [name: string]: IReactionDisposer } = {};
-    private _blockedTypes = [DocumentType.PRESELEMENT, DocumentType.KVP, DocumentType.FILTER, DocumentType.SEARCH, DocumentType.SEARCHITEM, DocumentType.FONTICON, DocumentType.BUTTON, DocumentType.SCRIPTING];
 
     @observable _searchString = "";
     @observable _docTypeString = "all";
-    @observable _icons: string[] = this._allIcons;
     @observable _results: [Doc, string[], string[]][] = [];
     @observable _selectedResult: Doc | undefined = undefined;
     @observable _deletedDocsStatus: boolean = false;
     @observable _onlyAliases: boolean = true;
-    @computed get filter() { return this._results?.length && (this.currentSelectedCollection?.props.Document._searchFilterDocs || this.currentSelectedCollection?.props.Document._docFilters); }
 
     constructor(props: any) {
         super(props);
         SearchBox.Instance = this;
+        this._searchString = "reset_search";
+        this.submitSearch();
+        this._searchString = "";
+        this.submitSearch();
     }
 
     componentDidMount = action(() => {
@@ -67,11 +68,9 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc
     });
 
     componentWillUnmount() {
-        Object.values(this._disposers).forEach(disposer => disposer?.());
+        this.resetSearch;
     }
 
-    @computed get currentSelectedCollection() { return CollectionDockingView.Instance; }
-
     onInputChange = action((e: React.ChangeEvent<HTMLInputElement>) => {
         this._searchString = e.target.value;
         this.submitSearch();
@@ -79,32 +78,13 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc
 
     onSelectChange = action((e: React.ChangeEvent<HTMLSelectElement>) => {
         this._docTypeString = e.target.value;
-        this.submitSearch();
     });
 
-    /*enter = action((e: React.KeyboardEvent | undefined) => {
-        if (!e || e.key === "Enter") {
-            this.submitSearch();
-        }
-    });*/
-
     onResultClick = action((doc: Doc) => {
         this.selectElement(doc);
         this._selectedResult = doc;
     });
 
-    @action
-    filterDocsByType(docs: Doc[]) {
-        const finalDocs: Doc[] = [];
-        docs.forEach(doc => {
-            const layoutresult = StrCast(doc.type, "string") as DocumentType;
-            if (layoutresult && !this._blockedTypes.includes(layoutresult) && this._icons.includes(layoutresult)) {
-                finalDocs.push(doc);
-            }
-        });
-        return finalDocs;
-    }
-
     static foreachRecursiveDoc(docs: Doc[], func: (doc: Doc) => void) {
         let newarray: Doc[] = [];
         while (docs.length > 0) {
@@ -120,23 +100,39 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc
         }
     }
 
+    static formatType(type: String): String {
+        if (type == "pdf") {
+            return "PDF";
+        }
+        else if (type == "image") {
+            return "Img";
+        }
+
+        return type.charAt(0).toUpperCase() + type.substring(1, 3);
+    }
+
     @action
     searchCollection(query: string) {
-        const selectedCollection = this.currentSelectedCollection;//SelectionManager.SelectedDocuments()[0];
+        const selectedCollection = CollectionDockingView.Instance;
+        const blockedTypes = [DocumentType.PRESELEMENT, DocumentType.KVP, DocumentType.FILTER, DocumentType.SEARCH, DocumentType.SEARCHITEM, DocumentType.FONTICON, DocumentType.BUTTON, DocumentType.SCRIPTING];
         query = query.toLowerCase();
 
+        this._results = []
+
         if (selectedCollection !== undefined) {
-            // this._currentSelectedCollection = selectedCollection;
             const docs = DocListCast(selectedCollection.dataDoc[Doc.LayoutFieldKey(selectedCollection.dataDoc)]);
-            const found: [Doc, string[], string[]][] = [];
             SearchBox.foreachRecursiveDoc(docs, (doc: Doc) => {
-                const hlights = new Set<string>();
-                SearchBox.documentKeys(doc).forEach(key => Field.toString(doc[key] as Field).toLowerCase().includes(query) && hlights.add(key));
-                Array.from(hlights.keys()).length > 0 && found.push([doc, Array.from(hlights.keys()), []]);
+                const dtype = StrCast(doc.type, "string") as DocumentType;
+                if (dtype && !blockedTypes.includes(dtype)) {
+                    const hlights = new Set<string>();
+                    SearchBox.documentKeys(doc).forEach(key => Field.toString(doc[key] as Field).toLowerCase().includes(query) && hlights.add(key));
+                    Array.from(hlights.keys()).length > 0 && this._results.push([doc, Array.from(hlights.keys()), []]);
+                }
             });
-
-            this._results = found;
         }
+
+        this._results = Array.from(new Set(this._results))
+        this._selectedResult = undefined
     }
 
     static documentKeys(doc: Doc) {
@@ -155,7 +151,7 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc
     submitSearch = async () => {
         this.resetSearch();
 
-        this.dataDoc[this.fieldKey] = new List<Doc>([]);
+        //this.dataDoc[this.fieldKey] = new List<Doc>([]);
         let query = StrCast(this._searchString);
         Doc.SetSearchQuery(query);
         this._results = [];
@@ -169,6 +165,7 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc
     resetSearch = action(() => {
         this._results.forEach(result => {
             Doc.UnBrushDoc(result[0]);
+            Doc.UnHighlightDoc(result[0]);
             Doc.ClearSearchMatches();
         });
     });
@@ -178,6 +175,8 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc
     }
 
     render() {
+        var validResults = 0;
+
         const results = this._results.map(result => {
             var className = "searchBox-results-scroll-view-result";
 
@@ -186,28 +185,30 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc
             }
 
             if (this._docTypeString == "all" || this._docTypeString == result[0].type) {
-                return (<div key={result[0][Id]} onClick={() => this.onResultClick(result[0])} className={className}><div className="titletitle">{result[0].title}</div></div>)
+                validResults++;
+                return (<div key={result[0][Id]} onClick={() => this.onResultClick(result[0])} className={className}><div className="searchBox-result-title">{result[0].title}</div><div className="searchBox-result-type">{SearchBox.formatType(StrCast(result[0].type))}</div></div>)
             }
 
             return null;
         })
 
-        results.filter(result => result)
+        results.filter(result => result);
+
+        const selectOptions = selectValues.map(value => {
+            return <option key={value} value={value}>{SearchBox.formatType(value)}</option>
+        })
 
         return (
             <div style={{ pointerEvents: "all" }} className="searchBox-container">
                 <div className="searchBox-bar">
                     <select name="type" id="searchBox-type" className="searchBox-type" onChange={this.onSelectChange}>
-                        <option value="all">All</option>
-                        <option value="rtf">Text</option>
-                        <option value="img">Image</option>
-                        <option value="pdf">PDF</option>
+                        {selectOptions}
                     </select>
-                    <input defaultValue={""} autoComplete="off" onChange={this.onInputChange} type="text" placeholder="Search..." id="search-input" ref={this._inputRef} className="searchBox-input" />
+                    <input defaultValue={""} autoComplete="off" onChange={this.onInputChange} type="text" placeholder="Search..." id="search-input" className="searchBox-input" ref={this._inputRef} />
                 </div >
                 <div className="searchBox-results-container">
                     <div className="searchBox-results-count">
-                        {`${results.length}` + " result" + (results.length == 1 ? "" : "s")}
+                        {`${validResults}` + " result" + (validResults == 1 ? "" : "s")}
                     </div>
                     <div className="searchBox-results-scroll-view">
                         {results}
-- 
cgit v1.2.3-70-g09d2


From d511c116330c3a125e6557aa5be70392aca4914b Mon Sep 17 00:00:00 2001
From: dg314 <david_grossman@brown.edu>
Date: Thu, 5 Aug 2021 16:38:00 -0400
Subject: comments

---
 src/client/views/MainView.tsx                      |   6 -
 .../views/collections/CollectionSchemaHeaders.tsx  |   2 +-
 src/client/views/search/SearchBox.scss             |  22 +-
 src/client/views/search/SearchBox.tsx              | 382 +++++++++------------
 4 files changed, 177 insertions(+), 235 deletions(-)

(limited to 'src/client/views/MainView.tsx')

diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 35222e91f..97c88930e 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -178,12 +178,6 @@ export class MainView extends React.Component {
         const targets = document.elementsFromPoint(e.x, e.y);
         if (targets.length) {
             const targClass = targets[0].className.toString();
-            /*if (SearchBox.Instance._resultsOpen) {
-                const check = targets.some((thing) =>
-                (thing.className === "collectionSchemaView-searchContainer" || (thing as any)?.dataset.icon === "filter" ||
-                    thing.className === "collectionSchema-header-menuOptions"));
-                !check && SearchBox.Instance.resetSearch(true);
-            }*/
             !targClass.includes("contextMenu") && ContextMenu.Instance.closeMenu();
             !["timeline-menu-desc", "timeline-menu-item", "timeline-menu-input"].includes(targClass) && TimelineMenu.Instance.closeMenu();
         }
diff --git a/src/client/views/collections/CollectionSchemaHeaders.tsx b/src/client/views/collections/CollectionSchemaHeaders.tsx
index 3b52e6408..c98dcd1d0 100644
--- a/src/client/views/collections/CollectionSchemaHeaders.tsx
+++ b/src/client/views/collections/CollectionSchemaHeaders.tsx
@@ -424,7 +424,7 @@ export class KeysDropdown extends React.Component<KeysDropdownProps> {
                         e.target.checked === true ? Doc.setDocFilter(this.props.Document, this._key, key, "check") : Doc.setDocFilter(this.props.Document, this._key, key, "remove");
                         e.target.checked === true ? this.closeResultsVisibility = "contents" : console.log("");
                         e.target.checked === true ? this.props.col.setColor("green") : this.updateFilter();
-                        e.target.checked === true && SearchBox.Instance.filter === true ? Doc.setDocFilter(docs[0], this._key, key, "check") : Doc.setDocFilter(docs[0], this._key, key, "remove");
+                        e.target.checked === true ? Doc.setDocFilter(docs[0], this._key, key, "check") : Doc.setDocFilter(docs[0], this._key, key, "remove");
                     }}
                     checked={bool}
                 />
diff --git a/src/client/views/search/SearchBox.scss b/src/client/views/search/SearchBox.scss
index e4d1ac6a3..e7526dac8 100644
--- a/src/client/views/search/SearchBox.scss
+++ b/src/client/views/search/SearchBox.scss
@@ -66,13 +66,13 @@
                 display: inline-block;
                 vertical-align: middle;
                 width: 100%;
-                height: 40px;
+                height: 50px;
                 cursor: pointer;
                 font-size: 15px;
-                padding: 10px;
+                padding: 11px;
 
                 &.searchBox-results-scroll-view-result-selected {
-                    background: gray;
+                    background: #999;
                 }
 
                 .searchBox-result-title {
@@ -84,11 +84,25 @@
 
                 .searchBox-result-type {
                     font-size: 12px;
+                    margin-top: 6px;
                     display: relative;
                     float: right;
                     width: 60px;
                     text-align: right;
-                    color: #333;
+                    color: #222;
+                }
+
+                .searchBox-result-keys {
+                    font-size: 10px;
+                    margin-top: 1px;
+                    display: relative;
+                    float: left;
+                    width: 100%;
+                    text-align: left;
+                    color: #555;
+                    white-space: nowrap;
+                    overflow: hidden;
+                    text-overflow: ellipsis;
                 }
             }
         }
diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx
index 3cbb4b3b1..8ef09aa56 100644
--- a/src/client/views/search/SearchBox.tsx
+++ b/src/client/views/search/SearchBox.tsx
@@ -27,66 +27,105 @@ import { undoBatch } from "../../util/UndoManager";
 import { DocServer } from "../../DocServer";
 import { MainView } from "../MainView";
 import { DocumentManager } from "../../util/DocumentManager";
-import { CollectionSchemaBooleanCell } from "../collections/CollectionSchemaCells";
-import { transpileModule } from "typescript";
 
 export const searchSchema = createSchema({ Document: Doc });
 
 type SearchBoxDocument = makeInterface<[typeof documentSchema, typeof searchSchema]>;
 const SearchBoxDocument = makeInterface(documentSchema, searchSchema);
 
-const selectValues = ["all", "rtf", "image", "pdf", "web", "video", "audio", "collection"]
-
+/**
+ * This is the SearchBox component. It represents the search box input and results in
+ * the search panel on the left side of the screen.
+ */
 @observer
 export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDocument>(SearchBoxDocument) {
     public static LayoutString(fieldKey: string) { return FieldView.LayoutString(SearchBox, fieldKey); }
     public static Instance: SearchBox;
 
-    private _resultsSet = new Map<Doc, number>();
     private _inputRef = React.createRef<HTMLInputElement>();
+    private _selectedCollection = CollectionDockingView.Instance;
 
     @observable _searchString = "";
     @observable _docTypeString = "all";
-    @observable _results: [Doc, string[], string[]][] = [];
+    @observable _results: [Doc, string[]][] = [];
     @observable _selectedResult: Doc | undefined = undefined;
     @observable _deletedDocsStatus: boolean = false;
     @observable _onlyAliases: boolean = true;
 
+    /**
+     * This is the constructor for the SearchBox class.
+     */
     constructor(props: any) {
         super(props);
         SearchBox.Instance = this;
-        this._searchString = "reset_search";
-        this.submitSearch();
-        this._searchString = "";
-        this.submitSearch();
     }
 
+    /** 
+     * This method is called when the SearchBox component is first mounted. When the user opens
+     * the search panel, the search input box is automatically selected. This allows the user to
+     * type in the search input box immediately, without needing clicking on it first.
+     */
     componentDidMount = action(() => {
         if (this._inputRef.current) {
             this._inputRef.current.focus();
         }
     });
 
+    /**
+     * This method is called when the SearchBox component is about to be unmounted. When the user
+     * closes the search panel, the search and its results are reset.
+     */
     componentWillUnmount() {
-        this.resetSearch;
+        this.resetSearch();
     }
 
+    /**
+     * This method is called when the text in the search input box is modified by the user. The
+     * _searchString is updated to the new value of the text in the input box and submitSearch
+     * is called to update the search results accordingly.
+     * 
+     * (Note: There is no longer a need to press enter to submit a search. Any update to the input
+     * causes a search to be submitted automatically.)
+     */
     onInputChange = action((e: React.ChangeEvent<HTMLInputElement>) => {
         this._searchString = e.target.value;
         this.submitSearch();
     });
 
+    /**
+     * This method is called when the option in the select drop-down menu is changed. The
+     * _docTypeString is updated to the new value of the option in the drop-down menu. This
+     * is used to filter the results of the search to documents of a specific type.
+     * 
+     * (Note: This doesn't affect the results array, so there is no need to submit a new
+     * search here. The results of the search on the _searchString query are simply filtered
+     * by type directly before rendering them.)
+     */
     onSelectChange = action((e: React.ChangeEvent<HTMLSelectElement>) => {
         this._docTypeString = e.target.value;
     });
 
+    /**
+     * @param {Doc} doc - doc of the search result that has been clicked on
+     *
+     * This method is called when the user clicks on a search result. The _selectedResult is
+     * updated accordingly and the doc is highlighted with the selectElement method.
+     */
     onResultClick = action((doc: Doc) => {
         this.selectElement(doc);
         this._selectedResult = doc;
     });
 
-    static foreachRecursiveDoc(docs: Doc[], func: (doc: Doc) => void) {
+    /**
+     * @param {Doc[]} docs - docs to be searched through recursively
+     * @param {number, Doc => void} func - function to be called on each doc
+     *
+     * This method iterates through an array of docs and all docs within those docs, calling
+     * the function func on each doc.
+     */
+    static foreachRecursiveDoc(docs: Doc[], func: (depth: number, doc: Doc) => void) {
         let newarray: Doc[] = [];
+        var depth = 0;
         while (docs.length > 0) {
             newarray = [];
             docs.filter(d => d).forEach(d => {
@@ -94,12 +133,45 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc
                 const annos = !Field.toString(Doc.LayoutField(d) as Field).includes("CollectionView");
                 const data = d[annos ? fieldKey + "-annotations" : fieldKey];
                 data && newarray.push(...DocListCast(data));
-                func(d);
+                func(depth, d);
             });
             docs = newarray;
+            depth++;
         }
     }
 
+    /**
+     * @param {Doc[]} docs - docs to be searched through recursively
+     * @param {number, Doc => void} func - function to be called on each doc
+     *
+     * This method iterates asynchronously through an array of docs and all docs within those
+     * docs, calling the function func on each doc.
+     */
+    static async foreachRecursiveDocAsync(docs: Doc[], func: (depth: number, doc: Doc) => void) {
+        let newarray: Doc[] = [];
+        var depth = 0;
+        while (docs.length > 0) {
+            newarray = [];
+            await Promise.all(docs.filter(d => d).map(async d => {
+                const fieldKey = Doc.LayoutFieldKey(d);
+                const annos = !Field.toString(Doc.LayoutField(d) as Field).includes("CollectionView");
+                const data = d[annos ? fieldKey + "-annotations" : fieldKey];
+                const docs = await DocListCastAsync(data);
+                docs && newarray.push(...docs);
+                func(depth, d);
+            }));
+            docs = newarray;
+            depth++;
+        }
+    }
+
+    /**
+     * @param {String} type - string representing the type of a doc
+     *
+     * This method converts a doc type string of any length to a 3-letter doc type string in
+     * which the first letter is capitalized. This is used when displaying the type on the
+     * right side of each search result.
+     */
     static formatType(type: String): String {
         if (type == "pdf") {
             return "PDF";
@@ -111,57 +183,75 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc
         return type.charAt(0).toUpperCase() + type.substring(1, 3);
     }
 
+    /**
+     * @param {String} query - search query string
+     *
+     * This method searches the CollectionDockingView instance for a certain query and puts
+     * the matching results in the results array. Docs are considered to be matching results
+     * when the query is a substring of many different pieces of its metadata (title, text,
+     * author, etc).
+     */
     @action
     searchCollection(query: string) {
-        const selectedCollection = CollectionDockingView.Instance;
         const blockedTypes = [DocumentType.PRESELEMENT, DocumentType.KVP, DocumentType.FILTER, DocumentType.SEARCH, DocumentType.SEARCHITEM, DocumentType.FONTICON, DocumentType.BUTTON, DocumentType.SCRIPTING];
+        const blockedKeys = ["x", "y", "proto", "width", "autoHeight", "acl-Override", "acl-Public", "context", "zIndex", "height", "text-scrollHeight", "text-height", "cloneFieldFilter", "isPrototype", "text-annotations",
+            "dragFactory-count", "text-noTemplate", "aliases", "system", "layoutKey", "baseProto", "xMargin", "yMargin", "links", "layout", "layout_keyValue", "fitWidth", "viewType", "title-custom",
+            "panX", "panY", "viewScale"]
         query = query.toLowerCase();
 
         this._results = []
+        this._selectedResult = undefined
 
-        if (selectedCollection !== undefined) {
-            const docs = DocListCast(selectedCollection.dataDoc[Doc.LayoutFieldKey(selectedCollection.dataDoc)]);
-            SearchBox.foreachRecursiveDoc(docs, (doc: Doc) => {
+        if (this._selectedCollection !== undefined) {
+            const docs = DocListCast(this._selectedCollection.dataDoc[Doc.LayoutFieldKey(this._selectedCollection.dataDoc)]);
+            const docIDs: String[] = []
+            SearchBox.foreachRecursiveDoc(docs, (depth: number, doc: Doc) => {
                 const dtype = StrCast(doc.type, "string") as DocumentType;
-                if (dtype && !blockedTypes.includes(dtype)) {
+                if (dtype && !blockedTypes.includes(dtype) && !docIDs.includes(doc[Id]) && depth > 0) {
                     const hlights = new Set<string>();
                     SearchBox.documentKeys(doc).forEach(key => Field.toString(doc[key] as Field).toLowerCase().includes(query) && hlights.add(key));
-                    Array.from(hlights.keys()).length > 0 && this._results.push([doc, Array.from(hlights.keys()), []]);
+                    blockedKeys.forEach(key => {
+                        hlights.delete(key);
+                    })
+                    Array.from(hlights.keys()).length > 0 && this._results.push([doc, Array.from(hlights.keys())]);
                 }
+                docIDs.push(doc[Id])
             });
         }
-
-        this._results = Array.from(new Set(this._results))
-        this._selectedResult = undefined
     }
 
+    /**
+     * @param {Doc} doc - doc for which keys are returned
+     * 
+     * This method returns a list of a document doc's keys.
+     */
     static documentKeys(doc: Doc) {
         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
         Doc.GetAllPrototypes(doc).map(proto => Object.keys(proto).forEach(key => keys[key] = false));
         return Array.from(Object.keys(keys));
     }
 
+    /**
+     * This method submits a search with the _searchString as its query and updates
+     * the results array accordingly.
+     */
     @action
     submitSearch = async () => {
         this.resetSearch();
 
-        //this.dataDoc[this.fieldKey] = new List<Doc>([]);
         let query = StrCast(this._searchString);
         Doc.SetSearchQuery(query);
         this._results = [];
-        this._resultsSet.clear();
 
         if (query) {
             this.searchCollection(query);
         }
     }
 
+    /**
+     * This method resets the search by iterating through each result and removing all
+     * brushes and highlights. All search matches are cleared as well.
+     */
     resetSearch = action(() => {
         this._results.forEach(result => {
             Doc.UnBrushDoc(result[0]);
@@ -170,10 +260,32 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc
         });
     });
 
+    /**
+     * @param {Doc} doc - doc to be selected
+     * 
+     * This method selects a doc by either jumping to it (centering/zooming in on it)
+     * or opening it in a new tab.
+     */
     selectElement = async (doc: Doc) => {
-        await DocumentManager.Instance.jumpToDocument(doc, true); // documents open in new tab instead of on right    
+        await DocumentManager.Instance.jumpToDocument(doc, true);
+    }
+
+    /**
+     * This method returns a JSX list of the options in the select drop-down menu, which
+     * is used to filter the types of documents that appear in the search results.
+     */
+    @computed
+    public get selectOptions() {
+        const selectValues = ["all", "rtf", "image", "pdf", "web", "video", "audio", "collection"]
+
+        return selectValues.map(value => {
+            return <option key={value} value={value}>{SearchBox.formatType(value)}</option>
+        })
     }
 
+    /**
+     * This method renders the search input box, select drop-down menu, and search results.
+     */
     render() {
         var validResults = 0;
 
@@ -186,7 +298,19 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc
 
             if (this._docTypeString == "all" || this._docTypeString == result[0].type) {
                 validResults++;
-                return (<div key={result[0][Id]} onClick={() => this.onResultClick(result[0])} className={className}><div className="searchBox-result-title">{result[0].title}</div><div className="searchBox-result-type">{SearchBox.formatType(StrCast(result[0].type))}</div></div>)
+                return (
+                    <div key={result[0][Id]} onClick={() => this.onResultClick(result[0])} className={className}>
+                        <div className="searchBox-result-title">
+                            {result[0].title}
+                        </div>
+                        <div className="searchBox-result-type">
+                            {SearchBox.formatType(StrCast(result[0].type))}
+                        </div>
+                        <div className="searchBox-result-keys">
+                            {result[1].join(", ")}
+                        </div>
+                    </div>
+                )
             }
 
             return null;
@@ -194,15 +318,11 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc
 
         results.filter(result => result);
 
-        const selectOptions = selectValues.map(value => {
-            return <option key={value} value={value}>{SearchBox.formatType(value)}</option>
-        })
-
         return (
             <div style={{ pointerEvents: "all" }} className="searchBox-container">
                 <div className="searchBox-bar">
                     <select name="type" id="searchBox-type" className="searchBox-type" onChange={this.onSelectChange}>
-                        {selectOptions}
+                        {this.selectOptions}
                     </select>
                     <input defaultValue={""} autoComplete="off" onChange={this.onInputChange} type="text" placeholder="Search..." id="search-input" className="searchBox-input" ref={this._inputRef} />
                 </div >
@@ -217,190 +337,4 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc
             </div >
         );
     }
-}
-
-
-
-/*
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { Tooltip } from '@material-ui/core';
-import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx';
-import { observer } from 'mobx-react';
-import * as React from 'react';
-import { Doc, DocListCast, Field, Opt, DocListCastAsync, DataSym, HeightSym, FieldsSym } from '../../../fields/Doc';
-import { documentSchema } from "../../../fields/documentSchemas";
-import { Copy, Id, ToString } from '../../../fields/FieldSymbols';
-import { List } from '../../../fields/List';
-import { createSchema, listSpec, makeInterface } from '../../../fields/Schema';
-import { SchemaHeaderField } from '../../../fields/SchemaHeaderField';
-import { Cast, NumCast, StrCast } from '../../../fields/Types';
-import { emptyFunction, returnFalse, returnZero, setupMoveUpEvents, Utils } from '../../../Utils';
-import { Docs } from '../../documents/Documents';
-import { DocumentType } from "../../documents/DocumentTypes";
-import { CurrentUserUtils } from "../../util/CurrentUserUtils";
-import { SetupDrag } from '../../util/DragManager';
-import { SearchUtil } from '../../util/SearchUtil';
-import { Transform } from '../../util/Transform';
-import { CollectionDockingView } from "../collections/CollectionDockingView";
-import { CollectionSchemaView, ColumnType } from "../collections/CollectionSchemaView";
-import { CollectionViewType } from '../collections/CollectionView';
-import { ViewBoxBaseComponent } from "../DocComponent";
-import { FieldView, FieldViewProps } from '../nodes/FieldView';
-import "./SearchBox.scss";
-import { undoBatch } from "../../util/UndoManager";
-import { DocServer } from "../../DocServer";
-import { MainView } from "../MainView";
-import { SelectionManager } from "../../util/SelectionManager";
-import { CollectionSchemaBooleanCell } from "../collections/CollectionSchemaCells";
-import { transpileModule } from "typescript";
-import { DocumentManager } from "../../util/DocumentManager";
-
-export const searchSchema = createSchema({ Document: Doc });
-
-type SearchBoxDocument = makeInterface<[typeof documentSchema, typeof searchSchema]>;
-const SearchBoxDocument = makeInterface(documentSchema, searchSchema);
-
-@observer
-export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDocument>(SearchBoxDocument) {
-    public static LayoutString(fieldKey: string) { return FieldView.LayoutString(SearchBox, fieldKey); }
-    public static Instance: SearchBox;
-
-    @observable _searchString = "";
-    @observable _docTypeString = "all";
-    @observable _results: [Doc, string[], string[]][] = [];
-    @observable _selectedResult: Doc | undefined = undefined;
-    @observable _deletedDocsStatus: boolean = false;
-    @observable _onlyAliases: boolean = true;
-
-    constructor(props: any) {
-        super(props);
-        SearchBox.Instance = this;
-    }
-
-    onInputChange = action((e: React.ChangeEvent<HTMLInputElement>) => {
-        this._searchString = e.target.value;
-        this.submitSearch();
-    });
-
-    onSelectChange = action((e: React.ChangeEvent<HTMLSelectElement>) => {
-        this._docTypeString = e.target.value;
-        this.submitSearch();
-    });
-
-    onResultClick = action((doc: Doc) => {
-        this.selectElement(doc);
-        this._selectedResult = doc;
-    });
-
-    static foreachRecursiveDoc(docs: Doc[], func: (doc: Doc) => void) {
-        const blockedTypes = [DocumentType.PRESELEMENT, DocumentType.KVP, DocumentType.FILTER, DocumentType.SEARCH, DocumentType.SEARCHITEM, DocumentType.FONTICON, DocumentType.BUTTON, DocumentType.SCRIPTING];
-        let newarray: Doc[] = [];
-        while (docs.length > 0) {
-            newarray = [];
-            docs.filter(d => d).forEach(d => {
-                const dtype = StrCast(d.type, "string") as DocumentType;
-                if (dtype && !blockedTypes.includes(dtype)) {
-                    const fieldKey = Doc.LayoutFieldKey(d);
-                    const annos = !Field.toString(Doc.LayoutField(d) as Field).includes("CollectionView");
-                    const data = d[annos ? fieldKey + "-annotations" : fieldKey];
-                    data && newarray.push(...DocListCast(data));
-                    func(d);
-                }
-            });
-            docs = newarray;
-        }
-    }
-
-    @action
-    searchCollection(query: string) {
-        const selectedCollection = CollectionDockingView.Instance;
-        query = query.toLowerCase();
-
-        if (selectedCollection !== undefined) {
-            console.log("hello111")
-            // this._currentSelectedCollection = selectedCollection;
-            const docs = DocListCast(selectedCollection.dataDoc[Doc.LayoutFieldKey(selectedCollection.dataDoc)]);
-            const found: [Doc, string[], string[]][] = [];
-            SearchBox.foreachRecursiveDoc(docs, (doc: Doc) => {
-                console.log("HELLO")
-                if (this._docTypeString == "all" || this._docTypeString == doc.type) {
-                    const hlights = new Set<string>();
-                    SearchBox.documentKeys(doc).forEach(key => Field.toString(doc[key] as Field).toLowerCase().includes(query) && hlights.add(key));
-                    Array.from(hlights.keys()).length > 0 && found.push([doc, Array.from(hlights.keys()), []]);
-                }
-            });
-
-            this._results = found;
-            //this.setSearchFilter(selectedCollection, this.filter && found.length ? this._docsforfilter : undefined);
-        }
-    }
-
-    static documentKeys(doc: Doc) {
-        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
-        Doc.GetAllPrototypes(doc).map(proto => Object.keys(proto).forEach(key => keys[key] = false));
-        return Array.from(Object.keys(keys));
-    }
-
-    @action
-    submitSearch = async () => {
-        Doc.ClearSearchMatches();
-        this._results = [];
-
-        this.dataDoc[this.fieldKey] = new List<Doc>([]);
-        let query = StrCast(this._searchString);
-        Doc.SetSearchQuery(query);
-        this._results = [];
-
-        if (query) {
-            this.searchCollection(query);
-        }
-    }
-
-    selectElement = async (doc: Doc) => {
-        await DocumentManager.Instance.jumpToDocument(doc, true); // documents open in new tab instead of on right
-    }
-
-    render() {
-        const results = this._results.map(result => {
-            var className = "searchBox-results-scroll-view-result";
-
-            if (this._selectedResult == result[0]) {
-                className += " searchBox-results-scroll-view-result-selected"
-            }
-
-            return (<div key={result[0][Id]} onClick={() => this.onResultClick(result[0])} className={className}><div className="titletitle">{result[0].title}</div></div>)
-        })
-
-        return (
-            <div style={{ pointerEvents: "all" }} className="searchBox-container">
-                <div className="searchBox-bar">
-                    <select name="type" id="searchBox-type" className="searchBox-type" onChange={this.onSelectChange}>
-                        <option value="all">All</option>
-                        <option value="rtf">Text</option>
-                        <option value="img">Img</option>
-                        <option value="pdf">PDF</option>
-                        <option value="video">Vid</option>
-                        <option value="audio">Aud</option>
-                        <option value="inks">Ink</option>
-                        <option value="col">Col</option>
-                    </select>
-                    <input defaultValue={""} autoComplete="off" onChange={this.onInputChange} type="text" placeholder="Search..." id="search-input" className="searchBox-input" />
-                </div >
-                <div className="searchBox-results-container">
-                    <div className="searchBox-results-count">
-                        {`${this._results.length}` + " result" + (this._results.length == 1 ? "" : "s")}
-                    </div>
-                    <div className="searchBox-results-scroll-view">
-                        {results}
-                    </div>
-                </div>
-            </div >
-        );
-    }
-}*/
\ No newline at end of file
+}
\ No newline at end of file
-- 
cgit v1.2.3-70-g09d2


From e7bbdd3b489fea1c508af53345cd0d1f31685cb9 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Fri, 6 Aug 2021 18:03:38 -0400
Subject: collabortion fixes: added new acl for allowing people to edit their
 own text within the same note, fixed playground fields to write to the server
 without updating other clients.

---
 src/client/DocServer.ts                            |  5 +-
 src/client/documents/Documents.ts                  | 10 ++--
 src/client/util/CurrentUserUtils.ts                |  6 +-
 src/client/util/SharingManager.tsx                 | 13 ++--
 src/client/views/DocComponent.tsx                  |  6 +-
 src/client/views/MainView.tsx                      |  2 +-
 src/client/views/MarqueeAnnotator.tsx              |  4 +-
 src/client/views/PropertiesView.tsx                | 14 ++---
 .../collections/collectionFreeForm/MarqueeView.tsx |  4 +-
 .../views/nodes/formattedText/FormattedTextBox.tsx | 12 +++-
 .../formattedText/ProsemirrorExampleTransfer.ts    | 70 ++++++++++++++--------
 src/fields/Doc.ts                                  |  6 +-
 src/fields/util.ts                                 | 22 ++++---
 13 files changed, 105 insertions(+), 69 deletions(-)

(limited to 'src/client/views/MainView.tsx')

diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts
index 59278d2af..d9ae7d64c 100644
--- a/src/client/DocServer.ts
+++ b/src/client/DocServer.ts
@@ -59,7 +59,10 @@ export namespace DocServer {
     export var PlaygroundFields: string[];
     export function setPlaygroundFields(livePlaygroundFields: string[]) {
         DocServer.PlaygroundFields = livePlaygroundFields;
-        livePlaygroundFields.forEach(f => DocServer.setFieldWriteMode(f, DocServer.WriteMode.Playground));
+        livePlaygroundFields.forEach(f => DocServer.setFieldWriteMode(f, DocServer.WriteMode.LivePlayground));
+    }
+    export function IsPlaygroundField(field: string) {
+        return DocServer.PlaygroundFields?.includes(field.replace(/^_/, ""));
     }
 
     export function setFieldWriteMode(field: string, writeMode: WriteMode) {
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 24f777e88..93f0880a4 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -577,7 +577,7 @@ export namespace Docs {
             dataProps.creationDate = new DateField;
             dataProps[`${fieldKey}-lastModified`] = new DateField;
             dataProps["acl-Override"] = "None";
-            dataProps["acl-Public"] = Doc.UserDoc()?.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Add;
+            dataProps["acl-Public"] = Doc.UserDoc()?.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Augment;
 
             dataProps[fieldKey] = data;
 
@@ -588,7 +588,7 @@ export namespace Docs {
 
             viewProps.author = Doc.CurrentUserEmail;
             viewProps["acl-Override"] = "None";
-            viewProps["acl-Public"] = Doc.UserDoc()?.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Add;
+            viewProps["acl-Public"] = Doc.UserDoc()?.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Augment;
             const viewDoc = Doc.assign(Doc.MakeDelegate(dataDoc, delegId), viewProps, true, true);
             ![DocumentType.LINK, DocumentType.MARKER, DocumentType.LABEL].includes(viewDoc.type as any) && DocUtils.MakeLinkToActiveAudio(() => viewDoc);
 
@@ -699,7 +699,7 @@ export namespace Docs {
             I.author = Doc.CurrentUserEmail;
             I.rotation = 0;
             I.data = new InkField(points);
-            I["acl-Public"] = Doc.UserDoc()?.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Add;
+            I["acl-Public"] = Doc.UserDoc()?.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Augment;
             I["acl-Override"] = "None";
             I[Initializing] = false;
             return I;
@@ -1038,8 +1038,8 @@ export namespace DocUtils {
             title: ComputedField.MakeFunction("generateLinkTitle(self)") as any,
             "anchor1-useLinkSmallAnchor": source.doc.useLinkSmallAnchor ? true : undefined,
             "anchor2-useLinkSmallAnchor": target.doc.useLinkSmallAnchor ? true : undefined,
-            "acl-Public": SharingPermissions.Add,
-            "_acl-Public": SharingPermissions.Add,
+            "acl-Public": SharingPermissions.Augment,
+            "_acl-Public": SharingPermissions.Augment,
             layout_linkView: Cast(Cast(Doc.UserDoc()["template-button-link"], Doc, null).dragFactory, Doc, null),
             linkDisplay: true, hidden: true,
             linkRelationship,
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 1af6607a7..14c43fb1c 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -917,7 +917,7 @@ export class CurrentUserUtils {
                 linkDocs = new Doc(linkDatabaseId, true);
                 (linkDocs as Doc).author = Doc.CurrentUserEmail;
                 (linkDocs as Doc).data = new List<Doc>([]);
-                (linkDocs as Doc)["acl-Public"] = SharingPermissions.Add;
+                (linkDocs as Doc)["acl-Public"] = SharingPermissions.Augment;
             }
             doc.myLinkDatabase = new PrefetchProxy(linkDocs);
         }
@@ -926,10 +926,10 @@ export class CurrentUserUtils {
             if (!sharedDocs) {
                 sharedDocs = Docs.Create.TreeDocument([], {
                     title: "My SharedDocs", childDropAction: "alias", system: true, contentPointerEvents: "all", childLimitHeight: 0, _yMargin: 50, _gridGap: 15,
-                    _showTitle: "title", ignoreClick: true, _lockedPosition: true, "acl-Public": SharingPermissions.Add, "_acl-Public": SharingPermissions.Add,
+                    _showTitle: "title", ignoreClick: true, _lockedPosition: true, "acl-Public": SharingPermissions.Augment, "_acl-Public": SharingPermissions.Augment,
                     _chromeHidden: true, boxShadow: "0 0",
                 }, sharingDocumentId + "outer", sharingDocumentId);
-                (sharedDocs as Doc)["acl-Public"] = (sharedDocs as Doc)[DataSym]["acl-Public"] = SharingPermissions.Add;
+                (sharedDocs as Doc)["acl-Public"] = (sharedDocs as Doc)[DataSym]["acl-Public"] = SharingPermissions.Augment;
             }
             if (sharedDocs instanceof Doc) {
                 Doc.GetProto(sharedDocs).userColor = sharedDocs.userColor || "rgb(202, 202, 202)";
diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx
index d283510b7..a8972b988 100644
--- a/src/client/util/SharingManager.tsx
+++ b/src/client/util/SharingManager.tsx
@@ -5,7 +5,7 @@ import { observer } from "mobx-react";
 import * as React from "react";
 import Select from "react-select";
 import * as RequestPromise from "request-promise";
-import { AclAddonly, AclAdmin, AclEdit, AclPrivate, AclReadonly, AclSym, AclUnset, DataSym, Doc, DocListCast, DocListCastAsync, Opt } from "../../fields/Doc";
+import { AclAugment, AclAdmin, AclEdit, AclPrivate, AclReadonly, AclSym, AclUnset, DataSym, Doc, DocListCast, DocListCastAsync, Opt, AclSelfEdit } from "../../fields/Doc";
 import { List } from "../../fields/List";
 import { Cast, NumCast, StrCast } from "../../fields/Types";
 import { distributeAcls, GetEffectiveAcl, normalizeEmail, SharingPermissions, TraceMobx } from "../../fields/util";
@@ -85,7 +85,8 @@ export class SharingManager extends React.Component<{}> {
     private AclMap = new Map<symbol, string>([
         [AclPrivate, SharingPermissions.None],
         [AclReadonly, SharingPermissions.View],
-        [AclAddonly, SharingPermissions.Add],
+        [AclAugment, SharingPermissions.Augment],
+        [AclSelfEdit, SharingPermissions.SelfEdit],
         [AclEdit, SharingPermissions.Edit],
         [AclAdmin, SharingPermissions.Admin]
     ]);
@@ -101,7 +102,7 @@ export class SharingManager extends React.Component<{}> {
             this.targetDoc = target_doc || target?.props.Document;
             DictationOverlay.Instance.hasActiveModal = true;
             this.isOpen = this.targetDoc !== undefined;
-            this.permissions = SharingPermissions.Add;
+            this.permissions = SharingPermissions.Augment;
         });
     }
 
@@ -366,10 +367,10 @@ export class SharingManager extends React.Component<{}> {
         const dropdownValues: string[] = Object.values(SharingPermissions);
         if (!uniform) dropdownValues.unshift("-multiple-");
         if (override) dropdownValues.unshift("None");
-        return dropdownValues.filter(permission => permission !== SharingPermissions.View).map(permission =>
+        return dropdownValues.filter(permission => !Doc.UserDoc().noviceMode || ![SharingPermissions.View, SharingPermissions.SelfEdit].includes(permission as any)).map(permission =>
             (
                 <option key={permission} value={permission}>
-                    {permission === SharingPermissions.Add ? "Can Augment" : permission}
+                    {permission}
                 </option>
             )
         );
@@ -546,7 +547,7 @@ export class SharingManager extends React.Component<{}> {
                             </select>
                         ) : (
                                 <div className={"permissions-dropdown"}>
-                                    {permissions === SharingPermissions.Add ? "Can Augment" : permissions}
+                                    {permissions}
                                 </div>
                             )}
                     </div>
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index fc36c7e43..99c695a4a 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -1,4 +1,4 @@
-import { Doc, Opt, DataSym, AclReadonly, AclAddonly, AclPrivate, AclEdit, AclSym, DocListCastAsync, DocListCast, AclAdmin } from '../../fields/Doc';
+import { Doc, Opt, DataSym, AclReadonly, AclAugment, AclPrivate, AclEdit, AclSym, DocListCast, AclAdmin, AclSelfEdit } from '../../fields/Doc';
 import { Touchable } from './Touchable';
 import { computed, action, observable } from 'mobx';
 import { Cast, BoolCast, ScriptCast } from '../../fields/Types';
@@ -131,7 +131,7 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T
         removeDocument(doc: Doc | Doc[], annotationKey?: string, leavePushpin?: boolean): boolean {
             const effectiveAcl = GetEffectiveAcl(this.dataDoc);
             const indocs = doc instanceof Doc ? [doc] : doc;
-            const docs = indocs.filter(doc => effectiveAcl === AclEdit || effectiveAcl === AclAdmin || GetEffectiveAcl(doc) === AclAdmin);
+            const docs = indocs.filter(doc => [AclEdit, AclAdmin].includes(effectiveAcl) || GetEffectiveAcl(doc) === AclAdmin);
             if (docs.length) {
                 setTimeout(() => docs.map(doc => { // this allows 'addDocument' to see the annotationOn field in order to create a pushin
                     Doc.SetInPlace(doc, "isPushpin", undefined, true);
@@ -199,7 +199,7 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T
                         });
                     }
 
-                    if (effectiveAcl === AclAddonly) {
+                    if (effectiveAcl === AclAugment) {
                         added.map(doc => {
                             if ([AclAdmin, AclEdit].includes(GetEffectiveAcl(doc))) inheritParentAcls(CurrentUserUtils.ActiveDashboard, doc);
                             doc.context = this.props.Document;
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 005e46836..8f37172a0 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -107,7 +107,7 @@ export class MainView extends React.Component {
         new InkStrokeProperties();
         this._sidebarContent.proto = undefined;
         if (!MainView.Live) {
-            DocServer.setPlaygroundFields(["x", "y", "dataTransition", "_autoHeight", "_showSidebar", "showSidebar", "_sidebarWidthPercent", "_width", "_height", "width", "height", "_viewTransition", "_panX", "_panY", "_viewScale", "_scrollTop", "hidden", "_curPage", "_viewType", "_chromeHidden", "nativeWidth", "_nativeWidth"]); // can play with these fields on someone else's
+            DocServer.setPlaygroundFields(["dataTransition", "autoHeight", "showSidebar", "sidebarWidthPercent", "viewTransition", "panX", "panY", "viewScale", "scrollTop", "hidden", "curPage", "viewType", "chromeHidden", "nativeWidth"]); // can play with these fields on someone else's
         }
         DocServer.GetRefField("rtfProto").then(proto => (proto instanceof Doc) && reaction(() => StrCast(proto.BROADCAST_MESSAGE), msg => msg && alert(msg)));
 
diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx
index 805cda95c..a3a3bce56 100644
--- a/src/client/views/MarqueeAnnotator.tsx
+++ b/src/client/views/MarqueeAnnotator.tsx
@@ -1,6 +1,6 @@
 import { action, observable, ObservableMap, runInAction } from "mobx";
 import { observer } from "mobx-react";
-import { AclAddonly, AclAdmin, AclEdit, DataSym, Doc, Opt } from "../../fields/Doc";
+import { AclAugment, AclAdmin, AclEdit, DataSym, Doc, Opt, AclSelfEdit } from "../../fields/Doc";
 import { Id } from "../../fields/FieldSymbols";
 import { List } from "../../fields/List";
 import { NumCast } from "../../fields/Types";
@@ -156,7 +156,7 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> {
     highlight = (color: string, isLinkButton: boolean, savedAnnotations?: ObservableMap<number, HTMLDivElement[]>) => {
         // creates annotation documents for current highlights
         const effectiveAcl = GetEffectiveAcl(this.props.rootDoc[DataSym]);
-        const annotationDoc = [AclAddonly, AclEdit, AclAdmin].includes(effectiveAcl) && this.makeAnnotationDocument(color, isLinkButton, savedAnnotations);
+        const annotationDoc = [AclAugment, AclSelfEdit, AclEdit, AclAdmin].includes(effectiveAcl) && this.makeAnnotationDocument(color, isLinkButton, savedAnnotations);
         !savedAnnotations && annotationDoc && this.props.addDocument(annotationDoc);
         return annotationDoc as Doc ?? undefined;
     }
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index 8136edf04..de437e1df 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -5,7 +5,7 @@ import { intersection } from "lodash";
 import { action, autorun, computed, Lambda, observable, reaction, runInAction } from "mobx";
 import { observer } from "mobx-react";
 import { ColorState, SketchPicker } from "react-color";
-import { AclAddonly, AclAdmin, AclEdit, AclPrivate, AclReadonly, AclSym, AclUnset, DataSym, Doc, Field, HeightSym, Opt, WidthSym } from "../../fields/Doc";
+import { AclAugment, AclAdmin, AclEdit, AclPrivate, AclReadonly, AclSym, AclUnset, DataSym, Doc, Field, HeightSym, Opt, WidthSym, AclSelfEdit } from "../../fields/Doc";
 import { Id } from "../../fields/FieldSymbols";
 import { InkField } from "../../fields/InkField";
 import { ComputedField } from "../../fields/ScriptField";
@@ -342,12 +342,9 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
         return <select className="permissions-select"
             value={permission}
             onChange={e => this.changePermissions(e, user)}>
-            {dropdownValues.filter(permission => permission !== SharingPermissions.View).map(permission => {
-                return (
-                    <option key={permission} value={permission}>
-                        {permission === SharingPermissions.Add ? "Can Augment" : permission}
-                    </option>);
-            })}
+            {dropdownValues.filter(permission =>
+                !Doc.UserDoc().noviceMode || ![SharingPermissions.View, SharingPermissions.SelfEdit].includes(permission as any)).map(permission =>
+                    <option key={permission} value={permission}>  {permission}  </option>)}
         </select>;
     }
 
@@ -402,7 +399,8 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
             [AclUnset, "None"],
             [AclPrivate, SharingPermissions.None],
             [AclReadonly, SharingPermissions.View],
-            [AclAddonly, SharingPermissions.Add],
+            [AclAugment, SharingPermissions.Augment],
+            [AclSelfEdit, SharingPermissions.SelfEdit],
             [AclEdit, SharingPermissions.Edit],
             [AclAdmin, SharingPermissions.Admin]
         ]);
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 846d28214..19da7ea00 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -1,6 +1,6 @@
 import { action, computed, observable } from "mobx";
 import { observer } from "mobx-react";
-import { AclAddonly, AclAdmin, AclEdit, DataSym, Doc, Opt } from "../../../../fields/Doc";
+import { AclAugment, AclAdmin, AclEdit, DataSym, Doc, Opt } from "../../../../fields/Doc";
 import { Id } from "../../../../fields/FieldSymbols";
 import { InkData, InkField, InkTool } from "../../../../fields/InkField";
 import { List } from "../../../../fields/List";
@@ -298,7 +298,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
             this._downX = x;
             this._downY = y;
             const effectiveAcl = GetEffectiveAcl(this.props.Document[DataSym]);
-            if ([AclAdmin, AclEdit, AclAddonly].includes(effectiveAcl)) {
+            if ([AclAdmin, AclEdit, AclAugment].includes(effectiveAcl)) {
                 PreviewCursor.Show(x, y, this.onKeyPress, this.props.addLiveTextDocument, this.props.getTransform, this.props.addDocument, this.props.nudge);
             }
             this.clearSelection();
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index edd7177de..f7bed2fa1 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -11,7 +11,7 @@ import { ReplaceStep } from 'prosemirror-transform';
 import { EditorState, NodeSelection, Plugin, TextSelection, Transaction } from "prosemirror-state";
 import { EditorView } from "prosemirror-view";
 import { DateField } from '../../../../fields/DateField';
-import { AclAdmin, AclEdit, DataSym, Doc, DocListCast, DocListCastAsync, Field, ForceServerWrite, HeightSym, Opt, UpdatingFromServer, WidthSym } from "../../../../fields/Doc";
+import { AclAdmin, AclEdit, AclSelfEdit, DataSym, Doc, DocListCast, DocListCastAsync, Field, ForceServerWrite, HeightSym, Opt, UpdatingFromServer, WidthSym, AclAugment } from "../../../../fields/Doc";
 import { documentSchema } from '../../../../fields/documentSchemas';
 import { Id } from '../../../../fields/FieldSymbols';
 import { InkTool } from '../../../../fields/InkField';
@@ -253,7 +253,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
             const removeSelection = (json: string | undefined) => json?.indexOf("\"storedMarks\"") === -1 ?
                 json?.replace(/"selection":.*/, "") : json?.replace(/"selection":"\"storedMarks\""/, "\"storedMarks\"");
 
-            if (effectiveAcl === AclEdit || effectiveAcl === AclAdmin) {
+            if (effectiveAcl === AclEdit || effectiveAcl === AclAdmin || effectiveAcl === AclSelfEdit) {
                 const accumTags = [] as string[];
                 state.tr.doc.nodesBetween(0, state.doc.content.size, (node: any, pos: number, parent: any) => {
                     if (node.type === schema.nodes.dashField && node.attrs.fieldKey.startsWith("#")) {
@@ -1401,6 +1401,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
             this._rules!.EnteringStyle = false;
         }
         e.stopPropagation();
+        for (var i = state.selection.from; i < state.selection.to; i++) {
+            const node = state.doc.resolve(i);
+            if (node?.marks?.().some(mark => mark.type === schema.marks.user_mark &&
+                mark.attrs.userid !== Doc.CurrentUserEmail) &&
+                [AclAugment, AclSelfEdit].includes(GetEffectiveAcl(this.rootDoc))) {
+                e.preventDefault();
+            }
+        }
         switch (e.key) {
             case "Escape":
                 this._editorView!.dispatch(state.tr.setSelection(TextSelection.create(state.doc, state.selection.from, state.selection.from)));
diff --git a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
index d5c77786c..1f78b2204 100644
--- a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
+++ b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
@@ -7,13 +7,14 @@ import { splitListItem, wrapInList, } from "prosemirror-schema-list";
 import { EditorState, Transaction, TextSelection } from "prosemirror-state";
 import { SelectionManager } from "../../../util/SelectionManager";
 import { NumCast, BoolCast, Cast, StrCast } from "../../../../fields/Types";
-import { Doc, DataSym, DocListCast } from "../../../../fields/Doc";
+import { Doc, DataSym, DocListCast, AclAugment } from "../../../../fields/Doc";
 import { FormattedTextBox } from "./FormattedTextBox";
 import { Id } from "../../../../fields/FieldSymbols";
 import { Docs } from "../../../documents/Documents";
 import { Utils } from "../../../../Utils";
 import { listSpec } from "../../../../fields/Schema";
 import { List } from "../../../../fields/List";
+import { GetEffectiveAcl } from "../../../../fields/util";
 
 const mac = typeof navigator !== "undefined" ? /Mac/.test(navigator.platform) : false;
 
@@ -70,25 +71,39 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
         return false;
     };
 
+    const canEdit = (state: any) => {
+        for (var i = state.selection.from; i < state.selection.to; i++) {
+            const node = state.doc.resolve(i);
+            if (node?.marks?.().some((mark: any) => mark.type === schema.marks.user_mark &&
+                mark.attrs.userid !== Doc.CurrentUserEmail) &&
+                GetEffectiveAcl(props.Document) === AclAugment) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    const toggleEditableMark = (mark: any) => (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => canEdit(state) && toggleMark(mark)(state, dispatch);
+
     //History commands
     bind("Mod-z", undo);
     bind("Shift-Mod-z", redo);
     !mac && bind("Mod-y", redo);
 
     //Commands to modify Mark
-    bind("Mod-b", toggleMark(schema.marks.strong));
-    bind("Mod-B", toggleMark(schema.marks.strong));
+    bind("Mod-b", toggleEditableMark(schema.marks.strong));
+    bind("Mod-B", toggleEditableMark(schema.marks.strong));
 
-    bind("Mod-e", toggleMark(schema.marks.em));
-    bind("Mod-E", toggleMark(schema.marks.em));
+    bind("Mod-e", toggleEditableMark(schema.marks.em));
+    bind("Mod-E", toggleEditableMark(schema.marks.em));
 
-    bind("Mod-*", toggleMark(schema.marks.code));
+    bind("Mod-*", toggleEditableMark(schema.marks.code));
 
-    bind("Mod-u", toggleMark(schema.marks.underline));
-    bind("Mod-U", toggleMark(schema.marks.underline));
+    bind("Mod-u", toggleEditableMark(schema.marks.underline));
+    bind("Mod-U", toggleEditableMark(schema.marks.underline));
 
     //Commands for lists
-    bind("Ctrl-i", wrapInList(schema.nodes.ordered_list));
+    bind("Ctrl-i", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => canEdit(state) && wrapInList(schema.nodes.ordered_list)(state, dispatch as any));
 
     bind("Tab", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
         /// bcz; Argh!!  replace layotuTEmpalteString with a onTab prop conditionally handles Tab);
@@ -96,6 +111,7 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
             if (!props.LayoutTemplateString) return addTextBox(false, true);
             return true;
         }
+        if (!canEdit(state)) return true;
         const ref = state.selection;
         const range = ref.$from.blockRange(ref.$to);
         const marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
@@ -121,6 +137,7 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
     bind("Shift-Tab", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
         /// bcz; Argh!!  replace with a onShiftTab prop conditionally handles Tab);
         if (props.Document._singleLine) return true;
+        if (!canEdit(state)) return true;
         const marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
 
         if (!liftListItem(schema.nodes.list_item)(state.tr, (tx2: Transaction) => {
@@ -140,24 +157,19 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
     });
 
     //Commands to modify BlockType
-    bind("Ctrl->", wrapIn(schema.nodes.blockquote));
-    bind("Alt-\\", setBlockType(schema.nodes.paragraph));
-    bind("Shift-Ctrl-\\", setBlockType(schema.nodes.code_block));
+    bind("Ctrl->", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => canEdit((state) && wrapIn(schema.nodes.blockquote)(state, dispatch as any)));
+    bind("Alt-\\", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => canEdit(state) && setBlockType(schema.nodes.paragraph)(state, dispatch as any));
+    bind("Shift-Ctrl-\\", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => canEdit(state) && setBlockType(schema.nodes.code_block)(state, dispatch as any));
 
-    bind("Ctrl-m", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
-        dispatch(state.tr.replaceSelectionWith(schema.nodes.equation.create({ fieldKey: "math" + Utils.GenerateGuid() })));
-    });
+    bind("Ctrl-m", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => canEdit(state) && dispatch(state.tr.replaceSelectionWith(schema.nodes.equation.create({ fieldKey: "math" + Utils.GenerateGuid() }))));
 
     for (let i = 1; i <= 6; i++) {
-        bind("Shift-Ctrl-" + i, setBlockType(schema.nodes.heading, { level: i }));
+        bind("Shift-Ctrl-" + i, (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => canEdit(state) && setBlockType(schema.nodes.heading, { level: i })(state, dispatch as any));
     }
 
     //Command to create a horizontal break line
     const hr = schema.nodes.horizontal_rule;
-    bind("Mod-_", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
-        dispatch(state.tr.replaceSelectionWith(hr.create()).scrollIntoView());
-        return true;
-    });
+    bind("Mod-_", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => canEdit(state) && dispatch(state.tr.replaceSelectionWith(hr.create()).scrollIntoView()));
 
     //Command to unselect all
     bind("Escape", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
@@ -173,13 +185,15 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
     };
 
     //Command to create a text document to the right of the selected textbox
-    bind("Alt-Enter", (state: EditorState<S>, dispatch: (tx: Transaction<Schema<any, any>>) => void) => addTextBox(false, true));
+    bind("Alt-Enter", () => addTextBox(false, true));
 
     //Command to create a text document to the bottom of the selected textbox
-    bind("Ctrl-Enter", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => addTextBox(true, true));
+    bind("Ctrl-Enter", () => addTextBox(true, true));
 
     // backspace = chainCommands(deleteSelection, joinBackward, selectNodeBackward);
     bind("Backspace", (state: EditorState<S>, dispatch: (tx: Transaction<Schema<any, any>>) => void) => {
+        if (!canEdit(state)) return true;
+
         if (!deleteSelection(state, (tx: Transaction<Schema<any, any>>) => {
             dispatch(updateBullets(tx, schema));
         })) {
@@ -200,6 +214,9 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
     //command to break line
     bind("Enter", (state: EditorState<S>, dispatch: (tx: Transaction<Schema<any, any>>) => void) => {
         if (addTextBox(true, false)) return true;
+
+        if (!canEdit(state)) return true;
+
         const trange = state.selection.$from.blockRange(state.selection.$to);
         const path = (state.selection.$from as any).path;
         const depth = trange ? liftTarget(trange) : undefined;
@@ -238,18 +255,19 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
 
     //Command to create a blank space
     bind("Space", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
+        if (!canEdit(state)) return true;
         const marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
         dispatch(splitMetadata(marks, state.tr));
         return false;
     });
 
-    bind("Alt-ArrowUp", joinUp);
-    bind("Alt-ArrowDown", joinDown);
-    bind("Mod-BracketLeft", lift);
+    bind("Alt-ArrowUp", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => canEdit(state) && joinUp(state, dispatch as any));
+    bind("Alt-ArrowDown", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => canEdit(state) && joinDown(state, dispatch as any));
+    bind("Mod-BracketLeft", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => canEdit(state) && lift(state, dispatch as any));
 
     const cmd = chainCommands(exitCode, (state, dispatch) => {
         if (dispatch) {
-            dispatch(state.tr.replaceSelectionWith(schema.nodes.hard_break.create()).scrollIntoView());
+            canEdit(state) && dispatch(state.tr.replaceSelectionWith(schema.nodes.hard_break.create()).scrollIntoView());
             return true;
         }
         return false;
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 0cbfaf067..85ea3cfa9 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -90,7 +90,8 @@ export const DirectLinksSym = Symbol("DirectLinks");
 export const AclUnset = Symbol("AclUnset");
 export const AclPrivate = Symbol("AclOwnerOnly");
 export const AclReadonly = Symbol("AclReadOnly");
-export const AclAddonly = Symbol("AclAddonly");
+export const AclAugment = Symbol("AclAugment");
+export const AclSelfEdit = Symbol("AclSelfEdit");
 export const AclEdit = Symbol("AclEdit");
 export const AclAdmin = Symbol("AclAdmin");
 export const UpdatingFromServer = Symbol("UpdatingFromServer");
@@ -102,7 +103,8 @@ const AclMap = new Map<string, symbol>([
     ["None", AclUnset],
     [SharingPermissions.None, AclPrivate],
     [SharingPermissions.View, AclReadonly],
-    [SharingPermissions.Add, AclAddonly],
+    [SharingPermissions.Augment, AclAugment],
+    [SharingPermissions.SelfEdit, AclSelfEdit],
     [SharingPermissions.Edit, AclEdit],
     [SharingPermissions.Admin, AclAdmin]
 ]);
diff --git a/src/fields/util.ts b/src/fields/util.ts
index 526e5af72..2bb6b45c2 100644
--- a/src/fields/util.ts
+++ b/src/fields/util.ts
@@ -1,5 +1,5 @@
 import { UndoManager } from "../client/util/UndoManager";
-import { Doc, FieldResult, UpdatingFromServer, LayoutSym, AclPrivate, AclEdit, AclReadonly, AclAddonly, AclSym, DataSym, DocListCast, AclAdmin, HeightSym, WidthSym, updateCachedAcls, AclUnset, DocListCastAsync, ForceServerWrite, Initializing } from "./Doc";
+import { Doc, FieldResult, UpdatingFromServer, LayoutSym, AclPrivate, AclEdit, AclReadonly, AclAugment, AclSym, DataSym, DocListCast, AclAdmin, HeightSym, WidthSym, updateCachedAcls, AclUnset, DocListCastAsync, ForceServerWrite, Initializing, AclSelfEdit } from "./Doc";
 import { SerializationHelper } from "../client/util/SerializationHelper";
 import { ProxyField, PrefetchProxy } from "./Proxy";
 import { RefField } from "./RefField";
@@ -14,6 +14,7 @@ import CursorField from "./CursorField";
 import { List } from "./List";
 import { SnappingManager } from "../client/util/SnappingManager";
 import { computedFn } from "mobx-utils";
+import { RichTextField } from "./RichTextField";
 
 function _readOnlySetter(): never {
     throw new Error("Documents can't be modified in read-only mode");
@@ -77,7 +78,9 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number
     const fromServer = target[UpdatingFromServer];
     const sameAuthor = fromServer || (receiver.author === Doc.CurrentUserEmail);
     const writeToDoc = sameAuthor || effectiveAcl === AclEdit || effectiveAcl === AclAdmin || (writeMode !== DocServer.WriteMode.LiveReadonly);
-    const writeToServer = (sameAuthor || effectiveAcl === AclEdit || effectiveAcl === AclAdmin || writeMode === DocServer.WriteMode.Default) && !DocServer.Control.isReadOnly();// && !playgroundMode;
+    const writeToServer =
+        (sameAuthor || effectiveAcl === AclEdit || effectiveAcl === AclAdmin || (effectiveAcl === AclSelfEdit && (value instanceof RichTextField))) &&
+        !DocServer.Control.isReadOnly();
 
     if (writeToDoc) {
         if (value === undefined) {
@@ -157,9 +160,10 @@ export function inheritParentAcls(parent: Doc, child: Doc) {
  */
 export enum SharingPermissions {
     Admin = "Admin",
-    Edit = "Can Edit",
-    Add = "Can Augment",
-    View = "Can View",
+    Edit = "Edit",
+    SelfEdit = "Self Edit",
+    Augment = "Augment",
+    View = "View",
     None = "Not Shared"
 }
 
@@ -176,7 +180,7 @@ export function GetEffectiveAcl(target: any, user?: string): symbol {
 
 function getPropAcl(target: any, prop: string | symbol | number) {
     if (prop === UpdatingFromServer || prop === Initializing || target[UpdatingFromServer] || prop === AclSym) return AclAdmin;  // requesting the UpdatingFromServer prop or AclSym must always go through to keep the local DB consistent
-    if (prop && DocServer.PlaygroundFields?.includes(prop.toString())) return AclEdit; // playground props are always editable
+    if (prop && DocServer.IsPlaygroundField(prop.toString())) return AclEdit; // playground props are always editable
     return GetEffectiveAcl(target);
 }
 
@@ -192,7 +196,8 @@ function getEffectiveAcl(target: any, user?: string): symbol {
         HierarchyMapping = HierarchyMapping || new Map<symbol, number>([
             [AclPrivate, 0],
             [AclReadonly, 1],
-            [AclAddonly, 2],
+            [AclAugment, 2],
+            [AclSelfEdit, 2.5],
             [AclEdit, 3],
             [AclAdmin, 4]
         ]);
@@ -235,6 +240,7 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc
         ["Not Shared", 0],
         ["Can View", 1],
         ["Can Augment", 2],
+        ["Self Edit", 2.5],
         ["Can Edit", 3],
         ["Admin", 4]
     ]);
@@ -294,7 +300,7 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc
 export function setter(target: any, in_prop: string | symbol | number, value: any, receiver: any): boolean {
     let prop = in_prop;
     const effectiveAcl = getPropAcl(target, prop);
-    if (effectiveAcl !== AclEdit && effectiveAcl !== AclAdmin) return true;
+    if (effectiveAcl !== AclEdit && effectiveAcl !== AclAdmin && !(effectiveAcl === AclSelfEdit && value instanceof RichTextField)) return true;
     // if you're trying to change an acl but don't have Admin access / you're trying to change it to something that isn't an acceptable acl, you can't
     if (typeof prop === "string" && prop.startsWith("acl") && (effectiveAcl !== AclAdmin || ![...Object.values(SharingPermissions), undefined, "None"].includes(value))) return true;
     // if (typeof prop === "string" && prop.startsWith("acl") && !["Can Edit", "Can Augment", "Can View", "Not Shared", undefined].includes(value)) return true;
-- 
cgit v1.2.3-70-g09d2


From 8480bcbe9e5ddea4bac64273c2e6581d36bb49de Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Fri, 6 Aug 2021 18:25:55 -0400
Subject: switched back to Playground fields and included
 width/height/natiive(w,h) to playgorund list

---
 src/client/DocServer.ts       | 2 +-
 src/client/views/MainView.tsx | 4 +++-
 2 files changed, 4 insertions(+), 2 deletions(-)

(limited to 'src/client/views/MainView.tsx')

diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts
index d9ae7d64c..5712a1d64 100644
--- a/src/client/DocServer.ts
+++ b/src/client/DocServer.ts
@@ -59,7 +59,7 @@ export namespace DocServer {
     export var PlaygroundFields: string[];
     export function setPlaygroundFields(livePlaygroundFields: string[]) {
         DocServer.PlaygroundFields = livePlaygroundFields;
-        livePlaygroundFields.forEach(f => DocServer.setFieldWriteMode(f, DocServer.WriteMode.LivePlayground));
+        livePlaygroundFields.forEach(f => DocServer.setFieldWriteMode(f, DocServer.WriteMode.Playground));
     }
     export function IsPlaygroundField(field: string) {
         return DocServer.PlaygroundFields?.includes(field.replace(/^_/, ""));
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 8f37172a0..2be3c3135 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -107,7 +107,9 @@ export class MainView extends React.Component {
         new InkStrokeProperties();
         this._sidebarContent.proto = undefined;
         if (!MainView.Live) {
-            DocServer.setPlaygroundFields(["dataTransition", "autoHeight", "showSidebar", "sidebarWidthPercent", "viewTransition", "panX", "panY", "viewScale", "scrollTop", "hidden", "curPage", "viewType", "chromeHidden", "nativeWidth"]); // can play with these fields on someone else's
+            DocServer.setPlaygroundFields(["dataTransition", "autoHeight", "showSidebar", "sidebarWidthPercent", "viewTransition",
+                "panX", "panY", "width", "height", "nativeWidth", "nativeHeight",
+                "viewScale", "scrollTop", "hidden", "curPage", "viewType", "chromeHidden", "nativeWidth"]); // can play with these fields on someone else's
         }
         DocServer.GetRefField("rtfProto").then(proto => (proto instanceof Doc) && reaction(() => StrCast(proto.BROADCAST_MESSAGE), msg => msg && alert(msg)));
 
-- 
cgit v1.2.3-70-g09d2


From 81ae5175aebf7e14a781a73d4a7cbaec0b8f3e4a Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 9 Aug 2021 14:26:14 -0400
Subject: fixed augment only text to resize correctly with autoheight. fixed
 acls to cache correctly for new documents. fixed links to be shared across
 users.  fixed links to always default to public.

---
 src/client/DocServer.ts                            |  2 +-
 src/client/documents/Documents.ts                  |  8 ++-
 src/client/util/CurrentUserUtils.ts                | 16 +++---
 src/client/util/LinkManager.ts                     | 67 ++++++++++++++--------
 src/client/util/SettingsManager.tsx                |  3 +-
 src/client/util/SharingManager.tsx                 |  4 +-
 src/client/views/MainView.tsx                      |  2 +-
 .../views/nodes/formattedText/FormattedTextBox.tsx | 20 ++++---
 src/fields/util.ts                                 |  4 +-
 9 files changed, 79 insertions(+), 47 deletions(-)

(limited to 'src/client/views/MainView.tsx')

diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts
index 5712a1d64..f4e3a22cf 100644
--- a/src/client/DocServer.ts
+++ b/src/client/DocServer.ts
@@ -1,6 +1,6 @@
 import * as io from 'socket.io-client';
 import { MessageStore, YoutubeQueryTypes, GestureContent, MobileInkOverlayContent, UpdateMobileInkOverlayPositionContent, MobileDocumentUploadContent } from "./../server/Message";
-import { Opt, Doc, UpdatingFromServer } from '../fields/Doc';
+import { Opt, Doc, UpdatingFromServer, updateCachedAcls } from '../fields/Doc';
 import { Utils, emptyFunction } from '../Utils';
 import { SerializationHelper } from './util/SerializationHelper';
 import { RefField } from '../fields/RefField';
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 93f0880a4..48886aa3b 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -1,7 +1,7 @@
 import { action, runInAction } from "mobx";
 import { basename, extname } from "path";
 import { DateField } from "../../fields/DateField";
-import { Doc, DocListCast, DocListCastAsync, Field, HeightSym, Opt, WidthSym, Initializing } from "../../fields/Doc";
+import { Doc, DocListCast, DocListCastAsync, Field, HeightSym, Opt, WidthSym, Initializing, updateCachedAcls } from "../../fields/Doc";
 import { Id } from "../../fields/FieldSymbols";
 import { HtmlField } from "../../fields/HtmlField";
 import { InkField } from "../../fields/InkField";
@@ -577,7 +577,7 @@ export namespace Docs {
             dataProps.creationDate = new DateField;
             dataProps[`${fieldKey}-lastModified`] = new DateField;
             dataProps["acl-Override"] = "None";
-            dataProps["acl-Public"] = Doc.UserDoc()?.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Augment;
+            dataProps["acl-Public"] = options["acl-Public"] ? options["acl-Public"] : Doc.UserDoc()?.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Augment;
 
             dataProps[fieldKey] = data;
 
@@ -588,13 +588,15 @@ export namespace Docs {
 
             viewProps.author = Doc.CurrentUserEmail;
             viewProps["acl-Override"] = "None";
-            viewProps["acl-Public"] = Doc.UserDoc()?.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Augment;
+            viewProps["acl-Public"] = options["_acl-Public"] ? options["_acl-Public"] : Doc.UserDoc()?.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Augment;
             const viewDoc = Doc.assign(Doc.MakeDelegate(dataDoc, delegId), viewProps, true, true);
             ![DocumentType.LINK, DocumentType.MARKER, DocumentType.LABEL].includes(viewDoc.type as any) && DocUtils.MakeLinkToActiveAudio(() => viewDoc);
 
             !Doc.IsSystem(dataDoc) && ![DocumentType.MARKER, DocumentType.KVP, DocumentType.LINK, DocumentType.LINKANCHOR].includes(proto.type as any) &&
                 !dataDoc.isFolder && !dataProps.annotationOn && Doc.AddDocToList(Cast(Doc.UserDoc().myFileOrphans, Doc, null), "data", dataDoc);
 
+            updateCachedAcls(dataDoc);
+            updateCachedAcls(viewDoc);
             return viewDoc;
         }
 
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 14c43fb1c..34990e121 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -2,13 +2,14 @@ import { computed, observable, reaction } from "mobx";
 import * as rp from 'request-promise';
 import { DataSym, Doc, DocListCast, DocListCastAsync } from "../../fields/Doc";
 import { Id } from "../../fields/FieldSymbols";
+import { InkTool } from "../../fields/InkField";
 import { List } from "../../fields/List";
 import { PrefetchProxy } from "../../fields/Proxy";
 import { RichTextField } from "../../fields/RichTextField";
 import { listSpec } from "../../fields/Schema";
 import { SchemaHeaderField } from "../../fields/SchemaHeaderField";
 import { ComputedField, ScriptField } from "../../fields/ScriptField";
-import { BoolCast, Cast, NumCast, PromiseValue, StrCast, DateCast } from "../../fields/Types";
+import { BoolCast, Cast, DateCast, NumCast, PromiseValue, StrCast } from "../../fields/Types";
 import { nullAudio } from "../../fields/URLField";
 import { SharingPermissions } from "../../fields/util";
 import { Utils } from "../../Utils";
@@ -19,6 +20,7 @@ import { Networking } from "../Network";
 import { CollectionDockingView } from "../views/collections/CollectionDockingView";
 import { DimUnit } from "../views/collections/collectionMulticolumn/CollectionMulticolumnView";
 import { CollectionView, CollectionViewType } from "../views/collections/CollectionView";
+import { Colors } from "../views/global/globalEnums";
 import { MainView } from "../views/MainView";
 import { FormattedTextBox } from "../views/nodes/formattedText/FormattedTextBox";
 import { LabelBox } from "../views/nodes/LabelBox";
@@ -31,13 +33,10 @@ import { LinkManager } from "./LinkManager";
 import { Scripting } from "./Scripting";
 import { SearchUtil } from "./SearchUtil";
 import { SelectionManager } from "./SelectionManager";
-import { UndoManager } from "./UndoManager";
-import { SnappingManager } from "./SnappingManager";
-import { InkTool } from "../../fields/InkField";
-import { SharingManager } from "./SharingManager";
-import { computedFn } from "mobx-utils";
 import { ColorScheme } from "./SettingsManager";
-import { Colors } from "../views/global/globalEnums";
+import { SharingManager } from "./SharingManager";
+import { SnappingManager } from "./SnappingManager";
+import { UndoManager } from "./UndoManager";
 
 
 export let resolvedPorts: { server: number, socket: number };
@@ -915,6 +914,7 @@ export class CurrentUserUtils {
             let linkDocs = Docs.newAccount ? undefined : await DocServer.GetRefField(linkDatabaseId);
             if (!linkDocs) {
                 linkDocs = new Doc(linkDatabaseId, true);
+                (linkDocs as Doc).title = "LINK DATABASE: " + Doc.CurrentUserEmail;
                 (linkDocs as Doc).author = Doc.CurrentUserEmail;
                 (linkDocs as Doc).data = new List<Doc>([]);
                 (linkDocs as Doc)["acl-Public"] = SharingPermissions.Augment;
@@ -1029,7 +1029,7 @@ export class CurrentUserUtils {
         doc.fontFamily = StrCast(doc.fontFamily, "Arial");
         doc.fontColor = StrCast(doc.fontColor, "black");
         doc.fontHighlight = StrCast(doc.fontHighlight, "");
-        doc.defaultAclPrivate = BoolCast(doc.defaultAclPrivate, true);
+        doc.defaultAclPrivate = BoolCast(doc.defaultAclPrivate, false);
         doc.activeCollectionBackground = StrCast(doc.activeCollectionBackground, "white");
         doc.activeCollectionNestedBackground = Cast(doc.activeCollectionNestedBackground, "string", null);
         doc.noviceMode = BoolCast(doc.noviceMode, true);
diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index 8abfd740c..933b98a8c 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -1,4 +1,4 @@
-import { observable, observe, action } from "mobx";
+import { observable, observe, action, reaction, computed } from "mobx";
 import { computedFn } from "mobx-utils";
 import { DirectLinksSym, Doc, DocListCast, Field, Opt } from "../../fields/Doc";
 import { List } from "../../fields/List";
@@ -26,36 +26,41 @@ type CreateViewFunc = (doc: Doc, followLinkLocation: string, finished?: () => vo
 export class LinkManager {
 
     @observable static _instance: LinkManager;
-    @observable static userDocs: Doc[] = [];
+    @observable static userLinkDBs: Doc[] = [];
     public static currentLink: Opt<Doc>;
     public static get Instance() { return LinkManager._instance; }
+    public static addLinkDB = (linkDb: any) => LinkManager.userLinkDBs.push(linkDb);
+    static links: Doc[] = [];
     constructor() {
         LinkManager._instance = this;
         setTimeout(() => {
-            LinkManager.userDocs = [Doc.LinkDBDoc().data as Doc, ...SharingManager.Instance.users.map(user => user.linkDatabase)];
-            const addLinkToDoc = action((link: Doc): any => {
+            LinkManager.userLinkDBs = [];
+            const addLinkToDoc = (link: Doc) => {
                 const a1 = link?.anchor1;
                 const a2 = link?.anchor2;
-                if (a1 instanceof Promise || a2 instanceof Promise) return PromiseValue(a1).then(a1 => PromiseValue(a2).then(a2 => addLinkToDoc(link)));
-                if (a1 instanceof Doc && a2 instanceof Doc && ((a1.author !== undefined && a2.author !== undefined) || link.author === Doc.CurrentUserEmail)) {
-                    Doc.GetProto(a1)[DirectLinksSym].add(link);
-                    Doc.GetProto(a2)[DirectLinksSym].add(link);
-                    Doc.GetProto(link)[DirectLinksSym].add(link);
-                }
-            });
-            const remLinkFromDoc = action((link: Doc): any => {
+                Promise.all([a1, a2]).then(action(() => {
+                    if (a1 instanceof Doc && a2 instanceof Doc && ((a1.author !== undefined && a2.author !== undefined) || link.author === Doc.CurrentUserEmail)) {
+                        Doc.GetProto(a1)[DirectLinksSym].add(link);
+                        Doc.GetProto(a2)[DirectLinksSym].add(link);
+                        Doc.GetProto(link)[DirectLinksSym].add(link);
+                    }
+                }));
+            }
+            const remLinkFromDoc = (link: Doc) => {
                 const a1 = link?.anchor1;
                 const a2 = link?.anchor2;
-                if (a1 instanceof Promise || a2 instanceof Promise) return PromiseValue(a1).then(a1 => PromiseValue(a2).then(a2 => remLinkFromDoc(link)));
-                if (a1 instanceof Doc && a2 instanceof Doc && ((a1.author !== undefined && a2.author !== undefined) || link.author === Doc.CurrentUserEmail)) {
-                    Doc.GetProto(a1)[DirectLinksSym].delete(link);
-                    Doc.GetProto(a2)[DirectLinksSym].delete(link);
-                    Doc.GetProto(link)[DirectLinksSym].delete(link);
-                }
-            });
-            const watchUserLinks = (userLinks: List<Doc>) => {
+                Promise.all([a1, a2]).then(action(() => {
+                    if (a1 instanceof Doc && a2 instanceof Doc && ((a1.author !== undefined && a2.author !== undefined) || link.author === Doc.CurrentUserEmail)) {
+                        Doc.GetProto(a1)[DirectLinksSym].delete(link);
+                        Doc.GetProto(a2)[DirectLinksSym].delete(link);
+                        Doc.GetProto(link)[DirectLinksSym].delete(link);
+                    }
+                }));
+            }
+            const watchUserLinkDB = (userLinkDBDoc: Doc) => {
+                LinkManager.links.push(...DocListCast(userLinkDBDoc.data));
                 const toRealField = (field: Field) => field instanceof ProxyField ? field.value() : field;  // see List.ts.  data structure is not a simple list of Docs, but a list of ProxyField/Fields
-                observe(userLinks, change => {
+                observe(userLinkDBDoc.data as Doc, change => {  // observe pushes/splices on a user link DB 'data' field (should only happen for local changes)
                     switch (change.type as any) {
                         case "splice":
                             (change as any).added.forEach((link: any) => addLinkToDoc(toRealField(link)));
@@ -64,13 +69,29 @@ export class LinkManager {
                         case "update": //let oldValue = change.oldValue;
                     }
                 }, true);
+                observe(userLinkDBDoc, "data",  // obsever when a new array of links is assigned as the link DB 'data' field (should happen whenever a remote user adds/removes a link)
+                    change => {
+                        switch (change.type as any) {
+                            case "update":
+                                Promise.all([...(change.oldValue as any as Doc[] || []), ...(change.newValue as any as Doc[] || [])]).then(doclist => {
+                                    const oldDocs = doclist.slice(0, (change.oldValue as any as Doc[] || []).length);
+                                    const newDocs = doclist.slice((change.oldValue as any as Doc[] || []).length, doclist.length);
+
+                                    const added = newDocs?.filter(link => !(oldDocs || []).includes(link));
+                                    const removed = oldDocs?.filter(link => !(newDocs || []).includes(link));
+                                    added?.forEach((link: any) => addLinkToDoc(toRealField(link)));
+                                    removed?.forEach((link: any) => remLinkFromDoc(toRealField(link)));
+                                });
+                        }
+                    }, true);
             };
-            observe(LinkManager.userDocs, change => {
+            observe(LinkManager.userLinkDBs, change => {
                 switch (change.type as any) {
-                    case "splice": (change as any).added.forEach(watchUserLinks); break;
+                    case "splice": (change as any).added.forEach(watchUserLinkDB); break;
                     case "update": //let oldValue = change.oldValue;
                 }
             }, true);
+            LinkManager.addLinkDB(Doc.LinkDBDoc());
         });
     }
 
diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx
index 3987497b8..bd91db779 100644
--- a/src/client/util/SettingsManager.tsx
+++ b/src/client/util/SettingsManager.tsx
@@ -268,7 +268,8 @@ export class SettingsManager extends React.Component<{}> {
                 <div className="tab-column-content">
                     <button onClick={() => GroupManager.Instance?.open()}>Manage groups</button>
                     <div className="default-acl">
-                        <input className="acl-check" type="checkbox" checked={BoolCast(Doc.UserDoc()?.defaultAclPrivate)} onChange={action(() => Doc.UserDoc().defaultAclPrivate = !Doc.UserDoc().defaultAclPrivate)} />
+                        <input className="acl-check" type="checkbox" checked={BoolCast(Doc.UserDoc()?.defaultAclPrivate)}
+                            onChange={action(() => Doc.UserDoc().defaultAclPrivate = !Doc.UserDoc().defaultAclPrivate)} />
                         <div className="acl-text">Default access private</div>
                     </div>
                 </div>
diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx
index a8972b988..6c4556250 100644
--- a/src/client/util/SharingManager.tsx
+++ b/src/client/util/SharingManager.tsx
@@ -23,6 +23,7 @@ import { GroupManager, UserOptions } from "./GroupManager";
 import { GroupMemberView } from "./GroupMemberView";
 import { SelectionManager } from "./SelectionManager";
 import "./SharingManager.scss";
+import { LinkManager } from "./LinkManager";
 
 export interface User {
     email: string;
@@ -154,10 +155,11 @@ export class SharingManager extends React.Component<{}> {
                 }
             });
             return Promise.all(evaluating).then(() => {
-                runInAction(() => {
+                runInAction(async () => {
                     for (const sharer of sharingDocs) {
                         if (!this.users.find(user => user.user.email === sharer.user.email)) {
                             this.users.push(sharer);
+                            LinkManager.addLinkDB(sharer.linkDatabase);
                         }
                     }
                 });
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 2be3c3135..3d7446cbe 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -108,7 +108,7 @@ export class MainView extends React.Component {
         this._sidebarContent.proto = undefined;
         if (!MainView.Live) {
             DocServer.setPlaygroundFields(["dataTransition", "autoHeight", "showSidebar", "sidebarWidthPercent", "viewTransition",
-                "panX", "panY", "width", "height", "nativeWidth", "nativeHeight",
+                "panX", "panY", "width", "height", "nativeWidth", "nativeHeight", "text-scrollHeight", "text-height",
                 "viewScale", "scrollTop", "hidden", "curPage", "viewType", "chromeHidden", "nativeWidth"]); // can play with these fields on someone else's
         }
         DocServer.GetRefField("rtfProto").then(proto => (proto instanceof Doc) && reaction(() => StrCast(proto.BROADCAST_MESSAGE), msg => msg && alert(msg)));
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index f7bed2fa1..9bba15b28 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -431,6 +431,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
         this.ProseRef = ele;
         this._dropDisposer?.();
         ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.layoutDoc));
+        // if (this.autoHeight) this.tryUpdateScrollHeight(); 
     }
 
     @undoBatch
@@ -1438,16 +1439,19 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
             }
         }
     }
-    tryUpdateScrollHeight() {
+    tryUpdateScrollHeight = () => {
         if (!LightboxView.LightboxDoc || LightboxView.IsLightboxDocView(this.props.docViewPath())) {
             const margins = 2 * NumCast(this.layoutDoc._yMargin, this.props.yPadding || 0);
-            const proseHeight = !this.ProseRef ? 0 : Array.from(this.ProseRef.children[0].children).reduce((p, child) => p + Number(getComputedStyle(child).height.replace("px", "")), margins);
-            const scrollHeight = this.ProseRef && Math.min(NumCast(this.layoutDoc.docMaxAutoHeight, proseHeight), proseHeight);
-            if (scrollHeight && this.props.renderDepth && !this.props.dontRegisterView) {  // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation
-                const setScrollHeight = () => this.rootDoc[this.fieldKey + "-scrollHeight"] = scrollHeight;
-                if (this.rootDoc === this.layoutDoc.doc || this.layoutDoc.resolvedDataDoc) {
-                    setScrollHeight();
-                } else setTimeout(setScrollHeight, 10); // if we have a template that hasn't been resolved yet, we can't set the height or we'd be setting it on the unresolved template.  So set a timeout and hope its arrived...
+            const children = this.ProseRef?.children.length ? Array.from(this.ProseRef.children[0].children) : undefined;
+            if (children) {
+                const proseHeight = !this.ProseRef ? 0 : children.reduce((p, child) => p + Number(getComputedStyle(child).height.replace("px", "")), margins);
+                const scrollHeight = this.ProseRef && Math.min(NumCast(this.layoutDoc.docMaxAutoHeight, proseHeight), proseHeight);
+                if (scrollHeight && this.props.renderDepth && !this.props.dontRegisterView) {  // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation
+                    const setScrollHeight = () => this.rootDoc[this.fieldKey + "-scrollHeight"] = scrollHeight;
+                    if (this.rootDoc === this.layoutDoc.doc || this.layoutDoc.resolvedDataDoc) {
+                        setScrollHeight();
+                    } else setTimeout(setScrollHeight, 10); // if we have a template that hasn't been resolved yet, we can't set the height or we'd be setting it on the unresolved template.  So set a timeout and hope its arrived...
+                }
             }
         }
     }
diff --git a/src/fields/util.ts b/src/fields/util.ts
index 2bb6b45c2..439c4d333 100644
--- a/src/fields/util.ts
+++ b/src/fields/util.ts
@@ -141,7 +141,9 @@ export function denormalizeEmail(email: string) {
 export function inheritParentAcls(parent: Doc, child: Doc) {
     const dataDoc = parent[DataSym];
     for (const key of Object.keys(dataDoc)) {
-        key.startsWith("acl") && distributeAcls(key, dataDoc[key], child);
+        // if the default acl mode is private, then don't inherit the acl-Public permission, but set it to private.
+        const permission = (key === "acl-Public" && Doc.UserDoc().defaultAclPrivate) ? AclPrivate : dataDoc[key];
+        key.startsWith("acl") && distributeAcls(key, permission, child);
     }
 }
 
-- 
cgit v1.2.3-70-g09d2


From ed39e4080b79ceba5c358e1359c1a1a44490f0dc Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 10 Aug 2021 10:59:59 -0400
Subject: hideMInimap is playground field.  added Open Alias to dashboard tab
 menu.   Allow duplicates in List fields (not sure if this is problematic but
 caused problems with URL history)

---
 src/client/views/MainView.tsx             | 2 +-
 src/client/views/collections/TreeView.tsx | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

(limited to 'src/client/views/MainView.tsx')

diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 3d7446cbe..b0b8d7f41 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -108,7 +108,7 @@ export class MainView extends React.Component {
         this._sidebarContent.proto = undefined;
         if (!MainView.Live) {
             DocServer.setPlaygroundFields(["dataTransition", "autoHeight", "showSidebar", "sidebarWidthPercent", "viewTransition",
-                "panX", "panY", "width", "height", "nativeWidth", "nativeHeight", "text-scrollHeight", "text-height",
+                "panX", "panY", "width", "height", "nativeWidth", "nativeHeight", "text-scrollHeight", "text-height", "hideMinimap",
                 "viewScale", "scrollTop", "hidden", "curPage", "viewType", "chromeHidden", "nativeWidth"]); // can play with these fields on someone else's
         }
         DocServer.GetRefField("rtfProto").then(proto => (proto instanceof Doc) && reaction(() => StrCast(proto.BROADCAST_MESSAGE), msg => msg && alert(msg)));
diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx
index e33c39d20..206e48aac 100644
--- a/src/client/views/collections/TreeView.tsx
+++ b/src/client/views/collections/TreeView.tsx
@@ -511,7 +511,7 @@ export class TreeView extends React.Component<TreeViewProps> {
             Doc.IsSystem(this.doc) ? [] :
                 this.props.treeView.fileSysMode && this.doc === Doc.GetProto(this.doc) ?
                     [{ script: ScriptField.MakeFunction(`openOnRight(getAlias(self))`)!, label: "Open Alias" }, makeFolder] :
-                    [{ script: ScriptField.MakeFunction(`DocFocusOrOpen(self)`)!, label: "Focus or Open" }];
+                    [{ script: ScriptField.MakeFunction(`openOnRight(getAlias(self))`)!, label: "Open Alias" }, { script: ScriptField.MakeFunction(`DocFocusOrOpen(self)`)!, label: "Focus or Open" }];
     }
     onChildClick = () => this.props.onChildClick?.() ?? (this._editTitleScript?.() || ScriptCast(this.doc.treeChildClick));
 
-- 
cgit v1.2.3-70-g09d2


From 4d21696c1b49c2c383adcd01974c18461123b83c Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 17 Aug 2021 16:55:15 -0400
Subject: made dashboard context menu items context sensitive to whether
 they're called from MyDashboards, MySharedDocs or elsewhere.  Made
 treeViewOpen a playground field so that users don't interfere with each
 other.   Made Docking views not generate alias of their tabs when shared.

---
 src/client/util/CurrentUserUtils.ts                | 35 +++++++++++-----------
 src/client/views/MainView.tsx                      |  2 +-
 .../views/collections/CollectionDockingView.tsx    |  2 +-
 .../views/collections/CollectionTreeView.tsx       | 13 ++++++--
 src/client/views/collections/CollectionView.tsx    | 24 +++++++++------
 src/client/views/collections/TreeView.tsx          | 25 +++++++++++-----
 src/client/views/nodes/DocumentView.tsx            |  2 +-
 7 files changed, 63 insertions(+), 40 deletions(-)

(limited to 'src/client/views/MainView.tsx')

diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 34990e121..85762a73f 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -765,8 +765,13 @@ export class CurrentUserUtils {
                 _lockedPosition: true, boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "same", treeViewType: "fileSystem", isFolder: true, system: true
             }));
             const newDashboard = ScriptField.MakeScript(`createNewDashboard(Doc.UserDoc())`);
-            (doc.myDashboards as any as Doc).contextMenuScripts = new List<ScriptField>([newDashboard!]);
-            (doc.myDashboards as any as Doc).contextMenuLabels = new List<string>(["Create New Dashboard"]);
+            const toggleTheme = ScriptField.MakeScript(`Doc.UserDoc().darkScheme = !Doc.UserDoc().darkScheme`);
+            const toggleComic = ScriptField.MakeScript(`toggleComicMode()`);
+            const snapshotDashboard = ScriptField.MakeScript(`snapshotDashboard()`);
+            const shareDashboard = ScriptField.MakeScript(`shareDashboard(self)`);
+            const removeDashboard = ScriptField.MakeScript('removeDashboard(self)');
+            (doc.myDashboards as any as Doc).childContextMenuScripts = new List<ScriptField>([newDashboard!, toggleTheme!, toggleComic!, snapshotDashboard!, shareDashboard!, removeDashboard!]);
+            (doc.myDashboards as any as Doc).childContextMenuLabels = new List<string>(["Create New Dashboard", "Toggle Theme Colors", "Toggle Comic Mode", "Snapshot Dashboard", "Share Dashboard", "Remove Dashboard"]);
         }
         return doc.myDashboards as any as Doc;
     }
@@ -933,6 +938,11 @@ export class CurrentUserUtils {
             }
             if (sharedDocs instanceof Doc) {
                 Doc.GetProto(sharedDocs).userColor = sharedDocs.userColor || "rgb(202, 202, 202)";
+                const addToDashboards = ScriptField.MakeScript(`addToDashboards(self)`);
+                const dashboardFilter = ScriptField.MakeFunction(`doc._viewType === '${CollectionViewType.Docking}'`, { doc: Doc.name });
+                sharedDocs.childContextMenuFilters = new List<ScriptField>([dashboardFilter!,]);
+                sharedDocs.childContextMenuScripts = new List<ScriptField>([addToDashboards!,]);
+                sharedDocs.childContextMenuLabels = new List<string>(["Add to Dashboards",]);
             }
             doc.mySharedDocs = new PrefetchProxy(sharedDocs);
         }
@@ -1228,15 +1238,6 @@ export class CurrentUserUtils {
 
         Doc.AddDocToList(myPresentations, "data", presentation);
         userDoc.activePresentation = presentation;
-        const toggleTheme = ScriptField.MakeScript(`Doc.UserDoc().darkScheme = !Doc.UserDoc().darkScheme`);
-        const toggleComic = ScriptField.MakeScript(`toggleComicMode()`);
-        const snapshotDashboard = ScriptField.MakeScript(`snapshotDashboard()`);
-        const createDashboard = ScriptField.MakeScript(`createNewDashboard()`);
-        const shareDashboard = ScriptField.MakeScript(`shareDashboard(self)`);
-        const addToDashboards = ScriptField.MakeScript(`addToDashboards(self)`);
-        const removeDashboard = ScriptField.MakeScript('removeDashboard(self)');
-        dashboardDoc.contextMenuScripts = new List<ScriptField>([toggleTheme!, toggleComic!, snapshotDashboard!, createDashboard!, shareDashboard!, addToDashboards!, removeDashboard!]);
-        dashboardDoc.contextMenuLabels = new List<string>(["Toggle Theme Colors", "Toggle Comic Mode", "Snapshot Dashboard", "Create Dashboard", "Share Dashboard", "Add to Dashboards", "Remove Dashboard"]);
 
         Doc.AddDocToList(dashboards, "data", dashboardDoc);
         CurrentUserUtils.openDashboard(userDoc, dashboardDoc);
@@ -1306,17 +1307,15 @@ Scripting.addGlobal(async function addToDashboards(dashboard: Doc) {
     const allDocs = await DocListCastAsync(dashboard[DataSym]["data-all"]);
 
     // moves the data-all field from the datadoc to the layoutdoc, necessary for off screen docs tab to function properly
-    dashboard["data-all"] = new List<Doc>(allDocs);
-    dashboardAlias["data-all"] = new List<Doc>((allDocs || []).map(doc => Doc.MakeAlias(doc)));
-
-    const dockingConfig = JSON.parse(StrCast(dashboardAlias.dockingConfig));
-    dockingConfig.content = [];
-    dashboardAlias.dockingConfig = JSON.stringify(dockingConfig);
+    // dashboard["data-all"] = new List<Doc>(allDocs);
+    // dashboardAlias["data-all"] = new List<Doc>((allDocs || []).map(doc => Doc.MakeAlias(doc)));
 
+    // const dockingConfig = JSON.parse(StrCast(dashboardAlias.dockingConfig));
+    // dashboardAlias.dockingConfig = JSON.stringify(dockingConfig);
 
     dashboardAlias.data = new List<Doc>(DocListCast(dashboard.data).map(tabFolder => Doc.MakeAlias(tabFolder)));
     DocListCast(dashboardAlias.data).forEach(doc => doc.dashboard = dashboardAlias);
-    DocListCast(dashboardAlias.data)[0].data = new List<Doc>();
+    //new List<Doc>();
     DocListCast(dashboardAlias.data)[1].data = ComputedField.MakeFunction(`dynamicOffScreenDocs(self.dashboard)`) as any;
     Doc.AddDocToList(CurrentUserUtils.MyDashboards, "data", dashboardAlias);
     CurrentUserUtils.openDashboard(Doc.UserDoc(), dashboardAlias);
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index b0b8d7f41..4d2a0a4c4 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -107,7 +107,7 @@ export class MainView extends React.Component {
         new InkStrokeProperties();
         this._sidebarContent.proto = undefined;
         if (!MainView.Live) {
-            DocServer.setPlaygroundFields(["dataTransition", "autoHeight", "showSidebar", "sidebarWidthPercent", "viewTransition",
+            DocServer.setPlaygroundFields(["dataTransition", "treeViewOpen", "autoHeight", "showSidebar", "sidebarWidthPercent", "viewTransition",
                 "panX", "panY", "width", "height", "nativeWidth", "nativeHeight", "text-scrollHeight", "text-height", "hideMinimap",
                 "viewScale", "scrollTop", "hidden", "curPage", "viewType", "chromeHidden", "nativeWidth"]); // can play with these fields on someone else's
         }
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index c0d39b2a2..cae08e1f4 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -170,7 +170,7 @@ export class CollectionDockingView extends CollectionSubView(doc => doc) {
                 const aliasDocList = DocListCast(alias["data-all"]);
                 // if aliasDocList contains the alias, don't do anything
                 // otherwise add the original or an alias depending on whether the doc you're looking at is the current doc or a different alias
-                !DocListCast(document.aliases).some(a => aliasDocList.includes(a)) && Doc.AddDocToList(alias, "data-all", alias !== instance.props.Document ? Doc.MakeAlias(document) : document);
+                !DocListCast(document.aliases).some(a => aliasDocList.includes(a)) && Doc.AddDocToList(alias, "data-all", document);//alias !== instance.props.Document ? Doc.MakeAlias(document) : document);
             });
         }
         const docContentConfig = CollectionDockingView.makeDocumentConfig(document, panelName);
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index 3eece0086..c0553ca60 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -1,10 +1,10 @@
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { action, computed, reaction, IReactionDisposer, observable } from "mobx";
 import { observer } from "mobx-react";
-import { DataSym, Doc, DocListCast, HeightSym, Opt, WidthSym } from '../../../fields/Doc';
+import { DataSym, Doc, DocListCast, HeightSym, Opt, WidthSym, StrListCast } from '../../../fields/Doc';
 import { Id } from '../../../fields/FieldSymbols';
 import { List } from '../../../fields/List';
-import { Document } from '../../../fields/Schema';
+import { Document, listSpec } from '../../../fields/Schema';
 import { ScriptField } from '../../../fields/ScriptField';
 import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
 import { TraceMobx } from '../../../fields/util';
@@ -215,6 +215,11 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
             />
         </div>;
     }
+    childContextMenuItems = () => {
+        const customScripts = Cast(this.doc.childContextMenuScripts, listSpec(ScriptField), []);
+        const filterScripts = Cast(this.doc.childContextMenuFilters, listSpec(ScriptField), []);
+        return StrListCast(this.doc.childContextMenuLabels).map((label, i) => ({ script: customScripts[i], filter: filterScripts[i], label }));
+    }
     @computed get treeViewElements() {
         TraceMobx();
         const dropAction = StrCast(this.doc.childDropAction) as dropActionType;
@@ -247,7 +252,9 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
             this.whenChildContentsActiveChanged,
             this.props.dontRegisterView || Cast(this.props.Document.childDontRegisterViews, "boolean", null),
             this.observerHeight,
-            this.unobserveHeight);
+            this.unobserveHeight,
+            this.childContextMenuItems()
+        );
     }
     @computed get titleBar() {
         const hideTitle = this.props.treeViewHideTitle || this.doc.treeViewHideTitle;
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index e65ebf075..a82128e47 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -70,6 +70,7 @@ export interface CollectionViewProps extends FieldViewProps {
     childDocumentsActive?: () => boolean;// whether child documents can be dragged if collection can be dragged (eg., in a when a Pile document is in startburst mode)
     childFitWidth?: () => boolean;
     childOpacity?: () => number;
+    childContextMenuItems?: () => { script: ScriptField, label: string }[];
     childHideTitle?: () => boolean; // whether to hide the documentdecorations title for children
     childHideDecorationTitle?: () => boolean;
     childLayoutTemplate?: () => (Doc | undefined);// specify a layout Doc template to use for children of the collection
@@ -186,15 +187,20 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab
             }
             !Doc.UserDoc().noviceMode && optionItems.push({ description: `${this.rootDoc.isInPlaceContainer ? "Unset" : "Set"} inPlace Container`, event: () => this.rootDoc.isInPlaceContainer = !this.rootDoc.isInPlaceContainer, icon: "project-diagram" });
 
-            optionItems.push({
-                description: "Create Branch", event: async () => this.props.addDocTab(await BranchCreate(this.rootDoc), "add:right"), icon: "project-diagram"
-            });
-            optionItems.push({
-                description: "Pull Master", event: () => BranchTask(this.rootDoc, "pull"), icon: "project-diagram"
-            });
-            optionItems.push({
-                description: "Merge Branches", event: () => BranchTask(this.rootDoc, "merge"), icon: "project-diagram"
-            });
+            if (!Doc.UserDoc().noviceMode) {
+                optionItems.push({
+                    description: "Create Branch", event: async () => this.props.addDocTab(await BranchCreate(this.rootDoc), "add:right"), icon: "project-diagram"
+                });
+                optionItems.push({
+                    description: "Pull Master", event: () => BranchTask(this.rootDoc, "pull"), icon: "project-diagram"
+                });
+                optionItems.push({
+                    description: "Merge Branches", event: () => BranchTask(this.rootDoc, "merge"), icon: "project-diagram"
+                });
+            }
+            if (this.Document._viewType === CollectionViewType.Docking) {
+                optionItems.push({ description: "Create Dashboard", event: () => CurrentUserUtils.createNewDashboard(Doc.UserDoc()), icon: "project-diagram" });
+            }
 
             !options && cm.addItem({ description: "Options...", subitems: optionItems, icon: "hand-point-right" });
 
diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx
index 206e48aac..0598be49b 100644
--- a/src/client/views/collections/TreeView.tsx
+++ b/src/client/views/collections/TreeView.tsx
@@ -1,7 +1,7 @@
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { action, computed, IReactionDisposer, observable, reaction } from "mobx";
 import { observer } from "mobx-react";
-import { DataSym, Doc, DocListCast, DocListCastOrNull, Field, HeightSym, Opt, WidthSym } from '../../../fields/Doc';
+import { DataSym, Doc, DocListCast, DocListCastOrNull, Field, HeightSym, Opt, WidthSym, StrListCast } from '../../../fields/Doc';
 import { Id } from '../../../fields/FieldSymbols';
 import { List } from '../../../fields/List';
 import { RichTextField } from '../../../fields/RichTextField';
@@ -54,6 +54,7 @@ export interface TreeViewProps {
     indentDocument?: (editTitle: boolean) => void;
     outdentDocument?: (editTitle: boolean) => void;
     ScreenToLocalTransform: () => Transform;
+    contextMenuItems: { script: ScriptField, filter: ScriptField, label: string }[];
     dontRegisterView?: boolean;
     styleProvider?: StyleProviderFunc | undefined;
     treeViewHideHeaderFields: () => boolean;
@@ -336,7 +337,7 @@ export class TreeView extends React.Component<TreeViewProps> {
                     this.props.dropAction, this.props.addDocTab, this.titleStyleProvider, this.props.ScreenToLocalTransform, this.props.isContentActive,
                     this.props.panelWidth, this.props.renderDepth, this.props.treeViewHideHeaderFields,
                     [...this.props.renderedIds, doc[Id]], this.props.onCheckedClick, this.props.onChildClick, this.props.skipFields, false, this.props.whenChildContentsActiveChanged,
-                    this.props.dontRegisterView, emptyFunction, emptyFunction);
+                    this.props.dontRegisterView, emptyFunction, emptyFunction, this.childContextMenuItems());
             } else {
                 contentElement = <EditableView key="editableView"
                     contents={contents !== undefined ? Field.toString(contents as Field) : "null"}
@@ -418,7 +419,7 @@ export class TreeView extends React.Component<TreeViewProps> {
                         StrCast(this.doc.childDropAction, this.props.dropAction) as dropActionType, this.props.addDocTab, this.titleStyleProvider, this.props.ScreenToLocalTransform,
                         this.props.isContentActive, this.props.panelWidth, this.props.renderDepth, this.props.treeViewHideHeaderFields,
                         [...this.props.renderedIds, this.doc[Id]], this.props.onCheckedClick, this.props.onChildClick, this.props.skipFields, false, this.props.whenChildContentsActiveChanged,
-                        this.props.dontRegisterView, emptyFunction, emptyFunction)}
+                        this.props.dontRegisterView, emptyFunction, emptyFunction, this.childContextMenuItems())}
             </ul >;
         } else if (this.treeViewExpandedView === "fields") {
             return <ul key={this.doc[Id] + this.doc.title}>
@@ -507,11 +508,19 @@ export class TreeView extends React.Component<TreeViewProps> {
     }
     contextMenuItems = () => {
         const makeFolder = { script: ScriptField.MakeFunction(`scriptContext.makeFolder()`, { scriptContext: "any" })!, label: "New Folder" };
-        return this.doc.isFolder ? [makeFolder] :
+        const openAlias = { script: ScriptField.MakeFunction(`openOnRight(getAlias(self))`)!, label: "Open Alias" };
+        const focusDoc = { script: ScriptField.MakeFunction(`DocFocusOrOpen(self)`)!, label: "Focus or Open" };
+        return [...this.props.contextMenuItems.filter(mi => !mi.filter ? true : mi.filter.script.run({ doc: this.doc })?.result), ... (this.doc.isFolder ? [makeFolder] :
             Doc.IsSystem(this.doc) ? [] :
                 this.props.treeView.fileSysMode && this.doc === Doc.GetProto(this.doc) ?
-                    [{ script: ScriptField.MakeFunction(`openOnRight(getAlias(self))`)!, label: "Open Alias" }, makeFolder] :
-                    [{ script: ScriptField.MakeFunction(`openOnRight(getAlias(self))`)!, label: "Open Alias" }, { script: ScriptField.MakeFunction(`DocFocusOrOpen(self)`)!, label: "Focus or Open" }];
+                    [openAlias, makeFolder] :
+                    this.doc.viewType === CollectionViewType.Docking ? [] :
+                        [openAlias, focusDoc])];
+    }
+    childContextMenuItems = () => {
+        const customScripts = Cast(this.doc.childContextMenuScripts, listSpec(ScriptField), []);
+        const customFilters = Cast(this.doc.childContextMenuFilters, listSpec(ScriptField), []);
+        return StrListCast(this.doc.childContextMenuLabels).map((label, i) => ({ script: customScripts[i], filter: customFilters[i], label }));
     }
     onChildClick = () => this.props.onChildClick?.() ?? (this._editTitleScript?.() || ScriptCast(this.doc.treeChildClick));
 
@@ -817,7 +826,8 @@ export class TreeView extends React.Component<TreeViewProps> {
         whenChildContentsActiveChanged: (isActive: boolean) => void,
         dontRegisterView: boolean | undefined,
         observerHeight: (ref: any) => void,
-        unobserveHeight: (ref: any) => void
+        unobserveHeight: (ref: any) => void,
+        contextMenuItems: ({ script: ScriptField, filter: ScriptField, label: string }[])
     ) {
         const viewSpecScript = Cast(conainerCollection.viewSpecScript, ScriptField);
         if (viewSpecScript) {
@@ -882,6 +892,7 @@ export class TreeView extends React.Component<TreeViewProps> {
                 parentTreeView={parentTreeView}
                 observeHeight={observerHeight}
                 unobserveHeight={unobserveHeight}
+                contextMenuItems={contextMenuItems}
             />;
         });
     }
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 745d58656..a29f2f662 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -664,7 +664,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
         if (!cm || (e as any)?.nativeEvent?.SchemaHandled) return;
 
         const customScripts = Cast(this.props.Document.contextMenuScripts, listSpec(ScriptField), []);
-        Cast(this.props.Document.contextMenuLabels, listSpec("string"), []).forEach((label, i) =>
+        StrListCast(this.Document.contextMenuLabels).forEach((label, i) =>
             cm.addItem({ description: label, event: () => customScripts[i]?.script.run({ documentView: this, this: this.layoutDoc, scriptContext: this.props.scriptContext, self: this.rootDoc }), icon: "sticky-note" }));
         this.props.contextMenuItems?.().forEach(item =>
             item.label && cm.addItem({ description: item.label, event: () => item.script.script.run({ this: this.layoutDoc, scriptContext: this.props.scriptContext, self: this.rootDoc }), icon: "sticky-note" }));
-- 
cgit v1.2.3-70-g09d2

