aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBob Zeleznik <zzzman@gmail.com>2020-05-07 01:35:11 -0400
committerBob Zeleznik <zzzman@gmail.com>2020-05-07 01:35:11 -0400
commit51a1f88b4e975946eb5ac8cef75a122e197cd872 (patch)
tree9d56101ca7caf1f3625843d380f4493a16cde1bf
parent7e2b162446a7f384ef2781fd786a97c21d1a172b (diff)
added batch requesting of list items. fixed some performance issues with tree views.
-rw-r--r--src/client/DocServer.ts64
-rw-r--r--src/client/util/Scripting.ts2
-rw-r--r--src/client/views/DocumentButtonBar.tsx32
-rw-r--r--src/client/views/DocumentDecorations.tsx2
-rw-r--r--src/client/views/collections/CollectionDockingView.tsx8
-rw-r--r--src/client/views/collections/CollectionStackingView.tsx10
-rw-r--r--src/client/views/collections/CollectionTreeView.scss1
-rw-r--r--src/client/views/collections/CollectionTreeView.tsx18
-rw-r--r--src/client/views/collections/ParentDocumentSelector.tsx4
-rw-r--r--src/new_fields/List.ts76
-rw-r--r--src/new_fields/Proxy.ts18
-rw-r--r--src/server/authentication/models/current_user_utils.ts13
-rw-r--r--src/server/database.ts2
13 files changed, 161 insertions, 89 deletions
diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts
index 0c9d5f75c..bf9fd232c 100644
--- a/src/client/DocServer.ts
+++ b/src/client/DocServer.ts
@@ -7,6 +7,7 @@ import { RefField } from '../new_fields/RefField';
import { Id, HandleUpdate } from '../new_fields/FieldSymbols';
import GestureOverlay from './views/GestureOverlay';
import MobileInkOverlay from '../mobile/MobileInkOverlay';
+import { runInAction } from 'mobx';
/**
* This class encapsulates the transfer and cross-client synchronization of
@@ -109,6 +110,7 @@ export namespace DocServer {
GUID = identifier;
_socket = OpenSocket(`${protocol}//${hostname}:${port}`);
+ _GetCachedRefField = _GetCachedRefFieldImpl;
_GetRefField = _GetRefFieldImpl;
_GetRefFields = _GetRefFieldsImpl;
_CreateField = _CreateFieldImpl;
@@ -243,12 +245,22 @@ export namespace DocServer {
return Promise.resolve(cached);
}
};
+ const _GetCachedRefFieldImpl = (id: string): Opt<RefField> => {
+ const cached = _cache[id];
+ if (cached !== undefined && !(cached instanceof Promise)) {
+ return cached;
+ }
+ }
let _GetRefField: (id: string) => Promise<Opt<RefField>> = errorFunc;
+ let _GetCachedRefField: (id: string) => Opt<RefField> = errorFunc;
export function GetRefField(id: string): Promise<Opt<RefField>> {
return _GetRefField(id);
}
+ export function GetCachedRefField(id: string): Opt<RefField> {
+ return _GetCachedRefField(id);
+ }
export async function getYoutubeChannels() {
const apiKey = await Utils.EmitCallback(_socket, MessageStore.YoutubeApiQuery, { type: YoutubeQueryTypes.Channels });
@@ -308,32 +320,34 @@ export namespace DocServer {
const deserializeFields = getSerializedFields.then(async fields => {
const fieldMap: { [id: string]: RefField } = {};
const proms: Promise<void>[] = [];
- for (const field of fields) {
- if (field !== undefined && field !== null) {
- // deserialize
- const prom = SerializationHelper.Deserialize(field).then(deserialized => {
- fieldMap[field.id] = deserialized;
-
- //overwrite or delete any promises (that we inserted as flags
- // to indicate that the field was in the process of being fetched). Now everything
- // should be an actual value within or entirely absent from the cache.
- if (deserialized !== undefined) {
- _cache[field.id] = deserialized;
- } else {
- delete _cache[field.id];
- }
- return deserialized;
- });
- // 4) here, for each of the documents we've requested *ourselves* (i.e. weren't promises or found in the cache)
- // we set the value at the field's id to a promise that will resolve to the field.
- // When we find that promises exist at keys in the cache, THIS is where they were set, just by some other caller (method).
- // The mapping in the .then call ensures that when other callers await these promises, they'll
- // get the resolved field
- _cache[field.id] = prom;
- // adds to a list of promises that will be awaited asynchronously
- proms.push(prom);
+ runInAction(() => {
+ for (const field of fields) {
+ if (field !== undefined && field !== null) {
+ // deserialize
+ const prom = SerializationHelper.Deserialize(field).then(deserialized => {
+ fieldMap[field.id] = deserialized;
+
+ //overwrite or delete any promises (that we inserted as flags
+ // to indicate that the field was in the process of being fetched). Now everything
+ // should be an actual value within or entirely absent from the cache.
+ if (deserialized !== undefined) {
+ _cache[field.id] = deserialized;
+ } else {
+ delete _cache[field.id];
+ }
+ return deserialized;
+ });
+ // 4) here, for each of the documents we've requested *ourselves* (i.e. weren't promises or found in the cache)
+ // we set the value at the field's id to a promise that will resolve to the field.
+ // When we find that promises exist at keys in the cache, THIS is where they were set, just by some other caller (method).
+ // The mapping in the .then call ensures that when other callers await these promises, they'll
+ // get the resolved field
+ _cache[field.id] = prom;
+ // adds to a list of promises that will be awaited asynchronously
+ proms.push(prom);
+ }
}
- }
+ });
await Promise.all(proms);
return fieldMap;
});
diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts
index 12628273b..8b7b9c9c7 100644
--- a/src/client/util/Scripting.ts
+++ b/src/client/util/Scripting.ts
@@ -136,7 +136,7 @@ function Run(script: string | undefined, customParams: string[], diagnostics: an
if (batch) {
batch.end();
}
- onError && onError(error);
+ onError?.(error);
return { success: false, error, result: errorVal };
}
};
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index cf57094b2..d58eb5875 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -50,11 +50,9 @@ enum UtilityButtonState {
}
@observer
-export class DocumentButtonBar extends React.Component<{ views: (DocumentView | undefined)[], stack?: any }, {}> {
+export class DocumentButtonBar extends React.Component<{ views: () => (DocumentView | undefined)[], stack?: any }, {}> {
private _linkButton = React.createRef<HTMLDivElement>();
private _dragRef = React.createRef<HTMLDivElement>();
- private _downX = 0;
- private _downY = 0;
private _pullAnimating = false;
private _pushAnimating = false;
private _pullColorAnimating = false;
@@ -71,7 +69,7 @@ export class DocumentButtonBar extends React.Component<{ views: (DocumentView |
public static hasPushedHack = false;
public static hasPulledHack = false;
- constructor(props: { views: (DocumentView | undefined)[] }) {
+ constructor(props: { views: () => (DocumentView | undefined)[] }) {
super(props);
runInAction(() => DocumentButtonBar.Instance = this);
}
@@ -113,7 +111,7 @@ export class DocumentButtonBar extends React.Component<{ views: (DocumentView |
this._pullColorAnimating = false;
});
- get view0() { return this.props.views?.[0]; }
+ get view0() { return this.props.views()?.[0]; }
@action
onLinkButtonMoved = (e: PointerEvent) => {
@@ -265,7 +263,7 @@ export class DocumentButtonBar extends React.Component<{ views: (DocumentView |
const view0 = this.view0;
return !view0 ? (null) : <div title="Show metadata panel" className="documentButtonBar-linkFlyout">
<Flyout anchorPoint={anchorPoints.LEFT_TOP}
- content={<MetadataEntryMenu docs={() => this.props.views.filter(dv => dv).map(dv => dv!.props.Document)} suggestWithFunction /> /* tfs: @bcz This might need to be the data document? */}>
+ content={<MetadataEntryMenu docs={() => this.props.views().filter(dv => dv).map(dv => dv!.props.Document)} suggestWithFunction /> /* tfs: @bcz This might need to be the data document? */}>
<div className={"documentButtonBar-linkButton-" + "empty"} onPointerDown={e => e.stopPropagation()} >
{<FontAwesomeIcon className="documentdecorations-icon" icon="tag" size="sm" />}
</div>
@@ -289,7 +287,7 @@ export class DocumentButtonBar extends React.Component<{ views: (DocumentView |
}
onAliasButtonMoved = () => {
if (this._dragRef.current) {
- const dragDocView = this.props.views[0]!;
+ const dragDocView = this.view0!;
const dragData = new DragManager.DocumentDragData([dragDocView.props.Document]);
const [left, top] = dragDocView.props.ScreenToLocalTransform().inverse().transformPoint(0, 0);
dragData.embedDoc = true;
@@ -308,16 +306,18 @@ export class DocumentButtonBar extends React.Component<{ views: (DocumentView |
get templateButton() {
const view0 = this.view0;
const templates: Map<Template, boolean> = new Map();
+ const views = this.props.views();
Array.from(Object.values(Templates.TemplateList)).map(template =>
- templates.set(template, this.props.views.reduce((checked, doc) => checked || doc?.props.Document["_show" + template.Name] ? true : false, false as boolean)));
- return !view0 ? (null) : <div title="Tap: Customize layout. Drag: Create alias" className="documentButtonBar-linkFlyout" ref={this._dragRef}>
- <Flyout anchorPoint={anchorPoints.LEFT_TOP} onOpen={action(() => this._aliasDown = true)} onClose={action(() => this._aliasDown = false)}
- content={!this._aliasDown ? (null) : <TemplateMenu docViews={this.props.views.filter(v => v).map(v => v as DocumentView)} templates={templates} />}>
- <div className={"documentButtonBar-linkButton-empty"} ref={this._dragRef} onPointerDown={this.onAliasButtonDown} >
- {<FontAwesomeIcon className="documentdecorations-icon" icon="edit" size="sm" />}
- </div>
- </Flyout>
- </div>;
+ templates.set(template, views.reduce((checked, doc) => checked || doc?.props.Document["_show" + template.Name] ? true : false, false as boolean)));
+ return !view0 ? (null) :
+ <div title="Tap: Customize layout. Drag: Create alias" className="documentButtonBar-linkFlyout" ref={this._dragRef}>
+ <Flyout anchorPoint={anchorPoints.LEFT_TOP} onOpen={action(() => this._aliasDown = true)} onClose={action(() => this._aliasDown = false)}
+ content={!this._aliasDown ? (null) : <TemplateMenu docViews={views.filter(v => v).map(v => v as DocumentView)} templates={templates} />}>
+ <div className={"documentButtonBar-linkButton-empty"} ref={this._dragRef} onPointerDown={this.onAliasButtonDown} >
+ {<FontAwesomeIcon className="documentdecorations-icon" icon="edit" size="sm" />}
+ </div>
+ </Flyout>
+ </div>;
}
render() {
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index b89806656..9b6105811 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -499,7 +499,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
</div >
<div className="link-button-container" style={{ left: bounds.x - this._resizeBorderWidth / 2, top: bounds.b + this._resizeBorderWidth / 2 }}>
- <DocumentButtonBar views={SelectionManager.SelectedDocuments()} />
+ <DocumentButtonBar views={SelectionManager.SelectedDocuments} />
</div>
</div >
);
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 6cd5d1b1b..6f5989052 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -517,14 +517,14 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
DragManager.StartDocumentDrag([gearSpan], dragData, e.clientX, e.clientY);
}
};
- let rendered = false;
+
tab.buttonDisposer = reaction(() => ((view: Opt<DocumentView>) => view ? [view] : [])(DocumentManager.Instance.getDocumentView(doc)),
(views) => {
- !rendered && ReactDOM.render(<span title="Drag as document" className="collectionDockingView-dragAsDocument" onPointerDown={onDown} >
- <DockingViewButtonSelector views={views} Stack={stack} />
+ ReactDOM.render(<span title="Drag as document" className="collectionDockingView-dragAsDocument" onPointerDown={onDown} >
+ <DockingViewButtonSelector views={() => views} Stack={stack} />
</span>,
gearSpan);
- rendered = true;
+ tab.buttonDisposer?.();
});
tab.reactComponents = [gearSpan];
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index 4cc2e1a5f..c199988c0 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -57,6 +57,14 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument)
}
@computed get NodeWidth() { return this.props.PanelWidth() - this.gridGap; }
+ constructor(props: any) {
+ super(props);
+
+ if (this.sectionHeaders === undefined) {
+ this.props.Document.sectionHeaders = new List<SchemaHeaderField>();
+ }
+ }
+
children(docs: Doc[], columns?: number) {
TraceMobx();
this._docXfs.length = 0;
@@ -85,7 +93,7 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument)
setTimeout(() => this.props.Document.sectionHeaders = new List<SchemaHeaderField>(), 0);
return new Map<SchemaHeaderField, Doc[]>();
}
- const sectionHeaders: SchemaHeaderField[] = Array.from(this.sectionHeaders);
+ const sectionHeaders = Array.from(this.sectionHeaders);
const fields = new Map<SchemaHeaderField, Doc[]>(sectionHeaders.map(sh => [sh, []] as [SchemaHeaderField, []]));
let changed = false;
this.filteredChildren.map(d => {
diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss
index a00bb6bfb..2aac81146 100644
--- a/src/client/views/collections/CollectionTreeView.scss
+++ b/src/client/views/collections/CollectionTreeView.scss
@@ -81,7 +81,6 @@
position: relative;
text-overflow: ellipsis;
white-space: pre-wrap;
- overflow: hidden;
min-width: 10px;
// width:100%;//width: max-content;
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index 297c11e35..4fb54cc07 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -34,6 +34,7 @@ import "./CollectionTreeView.scss";
import { CollectionViewType } from './CollectionView';
import React = require("react");
import { makeTemplate } from '../../util/DropConverter';
+import { TraceMobx } from '../../../new_fields/util';
export interface TreeViewProps {
@@ -100,7 +101,7 @@ class TreeView extends React.Component<TreeViewProps> {
get defaultExpandedView() { return this.childDocs ? this.fieldKey : StrCast(this.props.document.defaultExpandedView, "fields"); }
@observable _overrideTreeViewOpen = false; // override of the treeViewOpen field allowing the display state to be independent of the document's state
set treeViewOpen(c: boolean) { if (this.props.treeViewPreventOpen) this._overrideTreeViewOpen = c; else this.props.document.treeViewOpen = this._overrideTreeViewOpen = c; }
- @computed get treeViewOpen() { return (!this.props.treeViewPreventOpen && BoolCast(this.props.document.treeViewOpen)) || this._overrideTreeViewOpen; }
+ @computed get treeViewOpen() { return (!this.props.treeViewPreventOpen && !this.props.document.treeViewPreventOpen && BoolCast(this.props.document.treeViewOpen)) || this._overrideTreeViewOpen; }
@computed get treeViewExpandedView() { return StrCast(this.props.document.treeViewExpandedView, this.defaultExpandedView); }
@computed get MAX_EMBED_HEIGHT() { return NumCast(this.props.containingCollection.maxEmbedHeight, 200); }
@computed get dataDoc() { return this.templateDataDoc ? this.templateDataDoc : this.props.document; }
@@ -326,6 +327,7 @@ class TreeView extends React.Component<TreeViewProps> {
rtfHeight = () => this.rtfWidth() < Doc.Layout(this.props.document)?.[WidthSym]() ? Math.min(Doc.Layout(this.props.document)?.[HeightSym](), this.MAX_EMBED_HEIGHT) : this.MAX_EMBED_HEIGHT;
@computed get renderContent() {
+ TraceMobx();
const expandKey = this.treeViewExpandedView === this.fieldKey ? this.fieldKey : this.treeViewExpandedView === "links" ? "links" : undefined;
if (expandKey !== undefined) {
const remDoc = (doc: Doc) => this.remove(doc, expandKey);
@@ -419,13 +421,15 @@ class TreeView extends React.Component<TreeViewProps> {
return [{ script: focusScript!, label: "Focus" }];
}
_docRef = React.createRef<DocumentView>();
+ _editTitleScript: ScriptField | undefined;
/**
* Renders the EditableView title element for placement into the tree.
*/
@computed
get renderTitle() {
+ TraceMobx();
const onItemDown = SetupDrag(this._tref, () => this.dataDoc, this.move, this.props.dropAction, this.props.treeViewId[Id], true);
- const editTitle = ScriptField.MakeFunction("setInPlace(this, 'editTitle', true)");
+ //!this._editTitleScript && (this._editTitleScript = ScriptField.MakeFunction("setInPlace(this, 'editTitle', true)"));
const headerElements = (
<>
@@ -449,7 +453,6 @@ class TreeView extends React.Component<TreeViewProps> {
return <>
<div className="docContainer" ref={this._tref} title="click to edit title" id={`docContainer-${this.props.parentKey}`} onPointerDown={onItemDown}
style={{
- background: Doc.IsHighlighted(this.props.document) ? "orange" : Doc.IsBrushed(this.props.document) ? "#06121212" : "0",
fontWeight: this.props.document.searchMatch ? "bold" : undefined,
textDecoration: Doc.GetT(this.props.document, "title", "string", true) ? "underline" : undefined,
outline: BoolCast(this.props.document.workspaceBrush) ? "dashed 1px #06123232" : undefined,
@@ -461,12 +464,12 @@ class TreeView extends React.Component<TreeViewProps> {
ref={this._docRef}
Document={this.props.document}
DataDoc={undefined}
- LibraryPath={this.props.libraryPath || []}
+ LibraryPath={this.props.libraryPath || emptyPath}
addDocument={undefined}
addDocTab={this.props.addDocTab}
rootSelected={returnTrue}
pinToPres={emptyFunction}
- onClick={this.props.onChildClick || editTitle}
+ onClick={this.props.onChildClick || this._editTitleScript}
dropAction={this.props.dropAction}
moveDocument={this.move}
removeDocument={this.removeDoc}
@@ -478,7 +481,7 @@ class TreeView extends React.Component<TreeViewProps> {
NativeWidth={returnZero}
contextMenuItems={this.contextMenuItems}
renderDepth={1}
- focus={emptyFunction}
+ focus={returnTrue}
parentActive={returnTrue}
whenActiveChanged={emptyFunction}
bringToFront={emptyFunction}
@@ -493,8 +496,9 @@ class TreeView extends React.Component<TreeViewProps> {
}
render() {
+ TraceMobx();
const sorting = this.props.document[`${this.fieldKey}-sortAscending`];
- setTimeout(() => runInAction(() => untracked(() => this._overrideTreeViewOpen = this.treeViewOpen)), 0);
+ //setTimeout(() => runInAction(() => untracked(() => this._overrideTreeViewOpen = this.treeViewOpen)), 0);
return <div className="treeViewItem-container" ref={this.createTreeDropTarget}>
<li className="collection-child">
<div className="treeViewItem-header" ref={this._header} onClick={e => {
diff --git a/src/client/views/collections/ParentDocumentSelector.tsx b/src/client/views/collections/ParentDocumentSelector.tsx
index 10c6ead1a..6e4f801c0 100644
--- a/src/client/views/collections/ParentDocumentSelector.tsx
+++ b/src/client/views/collections/ParentDocumentSelector.tsx
@@ -94,7 +94,7 @@ export class ParentDocSelector extends React.Component<SelectorProps> {
}
@observer
-export class DockingViewButtonSelector extends React.Component<{ views: DocumentView[], Stack: any }> {
+export class DockingViewButtonSelector extends React.Component<{ views: () => DocumentView[], Stack: any }> {
customStylesheet(styles: any) {
return {
...styles,
@@ -120,7 +120,7 @@ export class DockingViewButtonSelector extends React.Component<{ views: Document
if (getComputedStyle(this._ref.current!).width !== "100%") {
e.stopPropagation(); e.preventDefault();
}
- this.props.views[0]?.select(false);
+ this.props.views()[0]?.select(false);
}} className="buttonSelector">
<Flyout anchorPoint={anchorPoints.LEFT_TOP} content={this.flyout} stylesheet={this.customStylesheet}>
<FontAwesomeIcon icon={"cog"} size={"sm"} />
diff --git a/src/new_fields/List.ts b/src/new_fields/List.ts
index a43f11e82..fdabea365 100644
--- a/src/new_fields/List.ts
+++ b/src/new_fields/List.ts
@@ -2,12 +2,13 @@ import { Deserializable, autoObject, afterDocDeserialize } from "../client/util/
import { Field } from "./Doc";
import { setter, getter, deleteProperty, updateFunction } from "./util";
import { serializable, alias, list } from "serializr";
-import { observable, action } from "mobx";
+import { observable, action, runInAction } from "mobx";
import { ObjectField } from "./ObjectField";
import { RefField } from "./RefField";
import { ProxyField } from "./Proxy";
-import { Self, Update, Parent, OnUpdate, SelfProxy, ToScriptString, ToString, Copy } from "./FieldSymbols";
+import { Self, Update, Parent, OnUpdate, SelfProxy, ToScriptString, ToString, Copy, Id } from "./FieldSymbols";
import { Scripting } from "../client/util/Scripting";
+import { DocServer } from "../client/DocServer";
const listHandlers: any = {
/// Mutator methods
@@ -54,11 +55,13 @@ const listHandlers: any = {
return res;
},
sort(cmpFunc: any) {
+ this[Self].__realFields(); // coerce retrieving entire array
const res = this[Self].__fields.sort(cmpFunc ? (first: any, second: any) => cmpFunc(toRealField(first), toRealField(second)) : undefined);
this[Update]();
return res;
},
splice: action(function (this: any, start: number, deleteCount: number, ...items: any[]) {
+ this[Self].__realFields(); // coerce retrieving entire array
items = items.map(toObjectField);
const list = this[Self];
for (let i = 0; i < items.length; i++) {
@@ -94,102 +97,102 @@ const listHandlers: any = {
},
/// Accessor methods
concat: action(function (this: any, ...items: any[]) {
+ this[Self].__realFields();
return this[Self].__fields.map(toRealField).concat(...items);
}),
includes(valueToFind: any, fromIndex: number) {
- const fields = this[Self].__fields;
if (valueToFind instanceof RefField) {
- return fields.map(toRealField).includes(valueToFind, fromIndex);
+ return this[Self].__realFields().includes(valueToFind, fromIndex);
} else {
- return fields.includes(valueToFind, fromIndex);
+ return this[Self].__fields.includes(valueToFind, fromIndex);
}
},
indexOf(valueToFind: any, fromIndex: number) {
- const fields = this[Self].__fields;
if (valueToFind instanceof RefField) {
- return fields.map(toRealField).indexOf(valueToFind, fromIndex);
+ return this[Self].__realFields().indexOf(valueToFind, fromIndex);
} else {
- return fields.indexOf(valueToFind, fromIndex);
+ return this[Self].__fields.indexOf(valueToFind, fromIndex);
}
},
join(separator: any) {
+ this[Self].__realFields();
return this[Self].__fields.map(toRealField).join(separator);
},
lastIndexOf(valueToFind: any, fromIndex: number) {
- const fields = this[Self].__fields;
if (valueToFind instanceof RefField) {
- return fields.map(toRealField).lastIndexOf(valueToFind, fromIndex);
+ return this[Self].__realFields().lastIndexOf(valueToFind, fromIndex);
} else {
- return fields.lastIndexOf(valueToFind, fromIndex);
+ return this[Self].__fields.lastIndexOf(valueToFind, fromIndex);
}
},
slice(begin: number, end: number) {
+ this[Self].__realFields();
return this[Self].__fields.slice(begin, end).map(toRealField);
},
/// Iteration methods
entries() {
- return this[Self].__fields.map(toRealField).entries();
+ return this[Self].__realFields().entries();
},
every(callback: any, thisArg: any) {
- return this[Self].__fields.map(toRealField).every(callback, thisArg);
+ return this[Self].__realFields().every(callback, thisArg);
// TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
// If we don't want to support the array parameter, we should use this version instead
// return this[Self].__fields.every((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg);
},
filter(callback: any, thisArg: any) {
- return this[Self].__fields.map(toRealField).filter(callback, thisArg);
+ return this[Self].__realFields().filter(callback, thisArg);
// TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
// If we don't want to support the array parameter, we should use this version instead
// return this[Self].__fields.filter((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg);
},
find(callback: any, thisArg: any) {
- return this[Self].__fields.map(toRealField).find(callback, thisArg);
+ return this[Self].__realFields().find(callback, thisArg);
// TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
// If we don't want to support the array parameter, we should use this version instead
// return this[Self].__fields.find((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg);
},
findIndex(callback: any, thisArg: any) {
- return this[Self].__fields.map(toRealField).findIndex(callback, thisArg);
+ return this[Self].__realFields().findIndex(callback, thisArg);
// TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
// If we don't want to support the array parameter, we should use this version instead
// return this[Self].__fields.findIndex((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg);
},
forEach(callback: any, thisArg: any) {
- return this[Self].__fields.map(toRealField).forEach(callback, thisArg);
+ return this[Self].__realFields().forEach(callback, thisArg);
// TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
// If we don't want to support the array parameter, we should use this version instead
// return this[Self].__fields.forEach((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg);
},
map(callback: any, thisArg: any) {
- return this[Self].__fields.map(toRealField).map(callback, thisArg);
+ return this[Self].__realFields().map(callback, thisArg);
// TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
// If we don't want to support the array parameter, we should use this version instead
// return this[Self].__fields.map((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg);
},
reduce(callback: any, initialValue: any) {
- return this[Self].__fields.map(toRealField).reduce(callback, initialValue);
+ return this[Self].__realFields().reduce(callback, initialValue);
// TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
// If we don't want to support the array parameter, we should use this version instead
// return this[Self].__fields.reduce((acc:any, element:any, index:number, array:any) => callback(acc, toRealField(element), index, array), initialValue);
},
reduceRight(callback: any, initialValue: any) {
- return this[Self].__fields.map(toRealField).reduceRight(callback, initialValue);
+ return this[Self].__realFields().reduceRight(callback, initialValue);
// TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
// If we don't want to support the array parameter, we should use this version instead
// return this[Self].__fields.reduceRight((acc:any, element:any, index:number, array:any) => callback(acc, toRealField(element), index, array), initialValue);
},
some(callback: any, thisArg: any) {
- return this[Self].__fields.map(toRealField).some(callback, thisArg);
+ return this[Self].__realFields().some(callback, thisArg);
// TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
// If we don't want to support the array parameter, we should use this version instead
// return this[Self].__fields.some((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg);
},
values() {
- return this[Self].__fields.map(toRealField).values();
+ return this[Self].__realFields().values();
},
[Symbol.iterator]() {
- return this[Self].__fields.map(toRealField).values();
+ return this[Self].__realFields().values();
}
};
@@ -254,6 +257,31 @@ class ListImpl<T extends Field> extends ObjectField {
[key: number]: T | (T extends RefField ? Promise<T> : never);
+ // this requests all ProxyFields at the same time to avoid the overhead
+ // of separate network requests and separate updates to the React dom.
+ private __realFields() {
+ const waiting = this.__fields.filter(f => f instanceof ProxyField && f.promisedValue());
+ const promised = waiting.map(f => f instanceof ProxyField ? f.promisedValue() : "");
+ // if we find any ProxyFields that don't have a current value, then
+ // start the server request for all of them
+ if (promised.length) {
+ const promise = DocServer.GetRefFields(promised);
+ // as soon as we get the fields from the server, set all the list values in one
+ // action to generate one React dom update.
+ promise.then(fields => runInAction(() => {
+ waiting.map((w, i) => w instanceof ProxyField && w.setValue(fields[promised[i]]));
+ }));
+ // we also have to mark all lists items with this promise so that any calls to them
+ // will await the batch request.
+ // This counts on the handler for 'promise' in the call above being invoked before the
+ // handler for 'promise' in the lines below.
+ waiting.map((w, i) => {
+ w instanceof ProxyField && w.setPromise(promise.then(fields => fields[promised[i]]));
+ });
+ }
+ return this.__fields.map(toRealField);
+ }
+
@serializable(alias("fields", list(autoObject(), { afterDeserialize: afterDocDeserialize })))
private get __fields() {
return this.___fields;
@@ -283,7 +311,7 @@ class ListImpl<T extends Field> extends ObjectField {
// console.log(diff);
const update = this[OnUpdate];
// update && update(diff);
- update && update();
+ update?.();
}
private [Self] = this;
diff --git a/src/new_fields/Proxy.ts b/src/new_fields/Proxy.ts
index d50c0f14e..555faaad0 100644
--- a/src/new_fields/Proxy.ts
+++ b/src/new_fields/Proxy.ts
@@ -1,7 +1,7 @@
import { Deserializable } from "../client/util/SerializationHelper";
import { FieldWaiting } from "./Doc";
import { primitive, serializable } from "serializr";
-import { observable, action } from "mobx";
+import { observable, action, runInAction } from "mobx";
import { DocServer } from "../client/DocServer";
import { RefField } from "./RefField";
import { ObjectField } from "./ObjectField";
@@ -61,6 +61,11 @@ export class ProxyField<T extends RefField> extends ObjectField {
return undefined;
}
if (!this.promise) {
+ const cached = DocServer.GetCachedRefField(this.fieldId);
+ if (cached !== undefined) {
+ runInAction(() => this.cache = cached as any);
+ return cached as any;
+ }
this.promise = DocServer.GetRefField(this.fieldId).then(action((field: any) => {
this.promise = undefined;
this.cache = field;
@@ -70,6 +75,17 @@ export class ProxyField<T extends RefField> extends ObjectField {
}
return this.promise as any;
}
+ promisedValue(): string { return !this.cache && !this.failed && !this.promise ? this.fieldId : ""; }
+ setPromise(promise: any) {
+ this.promise = promise;
+ }
+ @action
+ setValue(field: any) {
+ this.promise = undefined;
+ this.cache = field;
+ if (field === undefined) this.failed = true;
+ return field;
+ }
}
export namespace ProxyField {
diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts
index e7b448358..6e06fe931 100644
--- a/src/server/authentication/models/current_user_utils.ts
+++ b/src/server/authentication/models/current_user_utils.ts
@@ -21,8 +21,7 @@ import { FormattedTextBox } from "../../../client/views/nodes/formattedText/Form
import { MainView } from "../../../client/views/MainView";
import { DocumentType } from "../../../client/documents/DocumentTypes";
import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField";
-import { faBoxOpen } from "@fortawesome/free-solid-svg-icons";
-import { CollectionMulticolumnView, DimUnit } from "../../../client/views/collections/collectionMulticolumn/CollectionMulticolumnView";
+import { DimUnit } from "../../../client/views/collections/collectionMulticolumn/CollectionMulticolumnView";
export class CurrentUserUtils {
private static curr_id: string;
@@ -282,8 +281,12 @@ export class CurrentUserUtils {
doc["template-icon-view-col"] as Doc, doc["template-icon-view-rtf"] as Doc], { title: "icon templates", _height: 75 }));
} else {
const templateIconsDoc = Cast(doc["template-icons"], Doc, null);
- DocListCastAsync(templateIconsDoc).then(list => templateIconsDoc.data = new List<Doc>([doc["template-icon-view"] as Doc, doc["template-icon-view-img"] as Doc,
- doc["template-icon-view-col"] as Doc, doc["template-icon-view-rtf"] as Doc]));
+ const requiredTypes = [doc["template-icon-view"] as Doc, doc["template-icon-view-img"] as Doc,
+ doc["template-icon-view-col"] as Doc, doc["template-icon-view-rtf"] as Doc];
+ DocListCastAsync(templateIconsDoc.data).then(async curIcons => {
+ await Promise.all(curIcons!);
+ requiredTypes.map(ntype => Doc.AddDocToList(templateIconsDoc, "data", ntype));
+ });
}
return doc["template-icons"] as Doc;
}
@@ -501,7 +504,7 @@ export class CurrentUserUtils {
static setupDocumentCollection(doc: Doc) {
if (doc.myDocuments === undefined) {
doc.myDocuments = new PrefetchProxy(Docs.Create.TreeDocument([], {
- title: "DOCUMENTS", _height: 42, forceActive: true, boxShadow: "0 0", treeViewPreventOpen: true, lockedPosition: true,
+ title: "DOCUMENTS", _height: 42, forceActive: true, boxShadow: "0 0", treeViewPreventOpen: false, lockedPosition: true,
}));
}
return doc.myDocuments as Doc;
diff --git a/src/server/database.ts b/src/server/database.ts
index cfd707825..580f7f919 100644
--- a/src/server/database.ts
+++ b/src/server/database.ts
@@ -291,7 +291,7 @@ export namespace Database {
}
}
- export const Instance: IDatabase = getDatabase();
+ export const Instance = getDatabase();
export namespace Auxiliary {