aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Utils.ts5
-rw-r--r--src/client/DocServer.ts4
-rw-r--r--src/client/documents/Documents.ts2
-rw-r--r--src/client/northstar/dash-nodes/HistogramBinPrimitiveCollection.ts4
-rw-r--r--src/client/northstar/dash-nodes/HistogramBoxPrimitives.tsx4
-rw-r--r--src/client/util/ProseMirrorEditorView.tsx8
-rw-r--r--src/client/util/RichTextMenu.tsx13
-rw-r--r--src/client/views/CollectionMulticolumnView.scss7
-rw-r--r--src/client/views/CollectionMulticolumnView.tsx57
-rw-r--r--src/client/views/EditableView.tsx2
-rw-r--r--src/client/views/MainView.tsx2
-rw-r--r--src/client/views/ScriptBox.tsx13
-rw-r--r--src/client/views/Templates.tsx11
-rw-r--r--src/client/views/collections/CollectionDockingView.tsx11
-rw-r--r--src/client/views/collections/CollectionSchemaHeaders.tsx3
-rw-r--r--src/client/views/collections/CollectionStackingView.tsx41
-rw-r--r--src/client/views/collections/CollectionSubView.tsx6
-rw-r--r--src/client/views/collections/CollectionTreeView.tsx30
-rw-r--r--src/client/views/collections/CollectionView.tsx15
-rw-r--r--src/client/views/collections/CollectionViewChromes.tsx7
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx2
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx20
-rw-r--r--src/client/views/linking/LinkMenuGroup.tsx2
-rw-r--r--src/client/views/nodes/CollectionFreeFormDocumentView.tsx25
-rw-r--r--src/client/views/nodes/DocumentContentsView.tsx2
-rw-r--r--src/client/views/nodes/DocumentView.scss13
-rw-r--r--src/client/views/nodes/DocumentView.tsx21
-rw-r--r--src/client/views/nodes/FormattedTextBox.tsx25
-rw-r--r--src/client/views/nodes/ImageBox.tsx69
-rw-r--r--src/new_fields/Doc.ts75
-rw-r--r--src/new_fields/documentSchemas.ts6
-rw-r--r--src/pen-gestures/ndollar.ts66
-rw-r--r--src/server/ApiManagers/SessionManager.ts33
-rw-r--r--src/server/DashSession/DashSessionAgent.ts65
-rw-r--r--src/server/IDatabase.ts24
-rw-r--r--src/server/MemoryDatabase.ts100
-rw-r--r--src/server/RouteManager.ts10
-rw-r--r--src/server/Websocket/Websocket.ts10
-rw-r--r--src/server/authentication/models/current_user_utils.ts2
-rw-r--r--src/server/database.ts19
-rw-r--r--src/server/index.ts18
-rw-r--r--src/server/server_Initialization.ts4
-rw-r--r--src/server/session/README.txt11
-rw-r--r--src/server/session/agents/applied_session_agent.ts56
-rw-r--r--src/server/session/agents/monitor.ts367
-rw-r--r--src/server/session/agents/server_worker.ts165
-rw-r--r--src/server/session/utilities/ipc.ts52
-rw-r--r--src/server/session/utilities/repl.ts128
-rw-r--r--src/server/session/utilities/session_config.ts129
-rw-r--r--src/server/session/utilities/utilities.ts31
-rw-r--r--src/typings/index.d.ts2
51 files changed, 554 insertions, 1243 deletions
diff --git a/src/Utils.ts b/src/Utils.ts
index 04fe6750b..7bf05a6fc 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -328,8 +328,8 @@ export function timenow() {
return now.toLocaleDateString() + ' ' + h + ':' + m + ' ' + ampm;
}
-export function aggregateBounds(boundsList: { x: number, y: number, width: number, height: number }[]) {
- return boundsList.reduce((bounds, b) => {
+export function aggregateBounds(boundsList: { x: number, y: number, width: number, height: number }[], xpad: number, ypad: number) {
+ const bounds = boundsList.reduce((bounds, b) => {
const [sptX, sptY] = [b.x, b.y];
const [bptX, bptY] = [sptX + b.width, sptY + b.height];
return {
@@ -337,6 +337,7 @@ export function aggregateBounds(boundsList: { x: number, y: number, width: numbe
r: Math.max(bptX, bounds.r), b: Math.max(bptY, bounds.b)
};
}, { x: Number.MAX_VALUE, y: Number.MAX_VALUE, r: -Number.MAX_VALUE, b: -Number.MAX_VALUE });
+ return { x: bounds.x !== Number.MAX_VALUE ? bounds.x - xpad : bounds.x, y: bounds.y !== Number.MAX_VALUE ? bounds.y - ypad : bounds.y, r: bounds.r !== -Number.MAX_VALUE ? bounds.r + xpad : bounds.r, b: bounds.b !== -Number.MAX_VALUE ? bounds.b + ypad : bounds.b };
}
export function intersectRect(r1: { left: number, top: number, width: number, height: number },
r2: { left: number, top: number, width: number, height: number }) {
diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts
index 12fed3e46..d793b56af 100644
--- a/src/client/DocServer.ts
+++ b/src/client/DocServer.ts
@@ -272,7 +272,7 @@ export namespace DocServer {
const fieldMap: { [id: string]: RefField } = {};
const proms: Promise<void>[] = [];
for (const field of fields) {
- if (field !== undefined) {
+ if (field !== undefined && field !== null) {
// deserialize
const prom = SerializationHelper.Deserialize(field).then(deserialized => {
fieldMap[field.id] = deserialized;
@@ -439,4 +439,4 @@ export namespace DocServer {
function respondToDelete(ids: string | string[]) {
_respondToDelete(ids);
}
-} \ No newline at end of file
+}
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index e149963b9..be678d765 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -355,7 +355,7 @@ export namespace Docs {
AudioBox.ActiveRecordings.map(d => DocUtils.MakeLink({ doc: viewDoc }, { doc: d }, "audio link", "link to audio: " + d.title));
- return Doc.assign(viewDoc, delegateProps);
+ return Doc.assign(viewDoc, delegateProps, true);
}
/**
diff --git a/src/client/northstar/dash-nodes/HistogramBinPrimitiveCollection.ts b/src/client/northstar/dash-nodes/HistogramBinPrimitiveCollection.ts
index 3e9145a1b..6b36ffc9e 100644
--- a/src/client/northstar/dash-nodes/HistogramBinPrimitiveCollection.ts
+++ b/src/client/northstar/dash-nodes/HistogramBinPrimitiveCollection.ts
@@ -3,7 +3,7 @@ import { AttributeTransformationModel } from "../../northstar/core/attribute/Att
import { ChartType } from '../../northstar/model/binRanges/VisualBinRange';
import { AggregateFunction, Bin, Brush, DoubleValueAggregateResult, HistogramResult, MarginAggregateParameters, MarginAggregateResult } from "../../northstar/model/idea/idea";
import { ModelHelpers } from "../../northstar/model/ModelHelpers";
-import { LABColor } from '../../northstar/utils/LABcolor';
+import { LABColor } from '../../northstar/utils/LABColor';
import { PIXIRectangle } from "../../northstar/utils/MathUtil";
import { StyleConstants } from "../../northstar/utils/StyleContants";
import { HistogramBox } from "./HistogramBox";
@@ -237,4 +237,4 @@ export class HistogramBinPrimitiveCollection {
// }
return StyleConstants.HIGHLIGHT_COLOR;
}
-} \ No newline at end of file
+}
diff --git a/src/client/northstar/dash-nodes/HistogramBoxPrimitives.tsx b/src/client/northstar/dash-nodes/HistogramBoxPrimitives.tsx
index 5a16b3782..66d91cc1d 100644
--- a/src/client/northstar/dash-nodes/HistogramBoxPrimitives.tsx
+++ b/src/client/northstar/dash-nodes/HistogramBoxPrimitives.tsx
@@ -5,7 +5,7 @@ import { Utils as DashUtils, emptyFunction } from '../../../Utils';
import { FilterModel } from "../../northstar/core/filter/FilterModel";
import { ModelHelpers } from "../../northstar/model/ModelHelpers";
import { ArrayUtil } from "../../northstar/utils/ArrayUtil";
-import { LABColor } from '../../northstar/utils/LABcolor';
+import { LABColor } from '../../northstar/utils/LABColor';
import { PIXIRectangle } from "../../northstar/utils/MathUtil";
import { StyleConstants } from "../../northstar/utils/StyleContants";
import { HistogramBinPrimitiveCollection, HistogramBinPrimitive } from "./HistogramBinPrimitiveCollection";
@@ -119,4 +119,4 @@ export class HistogramBoxPrimitives extends React.Component<HistogramPrimitivesP
</svg>
</div>;
}
-} \ No newline at end of file
+}
diff --git a/src/client/util/ProseMirrorEditorView.tsx b/src/client/util/ProseMirrorEditorView.tsx
index 3e5fd0604..b42adfbb4 100644
--- a/src/client/util/ProseMirrorEditorView.tsx
+++ b/src/client/util/ProseMirrorEditorView.tsx
@@ -18,23 +18,23 @@ export class ProseMirrorEditorView extends React.Component<ProseMirrorEditorView
private _editorView?: EditorView;
_createEditorView = (element: HTMLDivElement | null) => {
- if (element != null) {
+ if (element !== null) {
this._editorView = new EditorView(element, {
state: this.props.editorState,
dispatchTransaction: this.dispatchTransaction,
});
}
- };
+ }
dispatchTransaction = (tx: any) => {
// In case EditorView makes any modification to a state we funnel those
// modifications up to the parent and apply to the EditorView itself.
const editorState = this.props.editorState.apply(tx);
- if (this._editorView != null) {
+ if (this._editorView) {
this._editorView.updateState(editorState);
}
this.props.onEditorState(editorState);
- };
+ }
focus() {
if (this._editorView) {
diff --git a/src/client/util/RichTextMenu.tsx b/src/client/util/RichTextMenu.tsx
index 639772faa..419d7caf9 100644
--- a/src/client/util/RichTextMenu.tsx
+++ b/src/client/util/RichTextMenu.tsx
@@ -175,7 +175,7 @@ export default class RichTextMenu extends AntimodeMenu {
setMark = (mark: Mark, state: EditorState<any>, dispatch: any) => {
if (mark) {
const node = (state.selection as NodeSelection).node;
- if (node ?.type === schema.nodes.ordered_list) {
+ if (node?.type === schema.nodes.ordered_list) {
let attrs = node.attrs;
if (mark.type === schema.marks.pFontFamily) attrs = { ...attrs, setFontFamily: mark.attrs.family };
if (mark.type === schema.marks.pFontSize) attrs = { ...attrs, setFontSize: mark.attrs.fontSize };
@@ -294,8 +294,8 @@ export default class RichTextMenu extends AntimodeMenu {
e.preventDefault();
e.stopPropagation();
self.view && self.view.focus();
- self.view && command && command(self.view!.state, self.view!.dispatch, self.view);
- self.view && onclick && onclick(self.view!.state, self.view!.dispatch, self.view);
+ self.view && command && command(self.view.state, self.view.dispatch, self.view);
+ self.view && onclick && onclick(self.view.state, self.view.dispatch, self.view);
self.setActiveMarkButtons(self.getActiveMarksOnSelection());
}
@@ -602,7 +602,7 @@ export default class RichTextMenu extends AntimodeMenu {
const link = this.currentLink ? this.currentLink : "";
- const button = <FontAwesomeIcon icon="link" size="lg" />
+ const button = <FontAwesomeIcon icon="link" size="lg" />;
const dropdownContent =
<div className="dropdown link-menu">
@@ -684,8 +684,9 @@ export default class RichTextMenu extends AntimodeMenu {
}
} else {
if (node) {
- const extension = this.linkExtend(this.view!.state.selection.$anchor, href);
- this.view!.dispatch(this.view!.state.tr.removeMark(extension.from, extension.to, this.view!.state.schema.marks.link));
+ const { tr, schema, selection } = this.view.state;
+ const extension = this.linkExtend(selection.$anchor, href);
+ this.view.dispatch(tr.removeMark(extension.from, extension.to, schema.marks.link));
}
}
}
diff --git a/src/client/views/CollectionMulticolumnView.scss b/src/client/views/CollectionMulticolumnView.scss
new file mode 100644
index 000000000..84e80da4a
--- /dev/null
+++ b/src/client/views/CollectionMulticolumnView.scss
@@ -0,0 +1,7 @@
+.collectionMulticolumnView_outer,
+.collectionMulticolumnView_contents {
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+}
+
diff --git a/src/client/views/CollectionMulticolumnView.tsx b/src/client/views/CollectionMulticolumnView.tsx
index 94e86c048..3231c0da1 100644
--- a/src/client/views/CollectionMulticolumnView.tsx
+++ b/src/client/views/CollectionMulticolumnView.tsx
@@ -1,40 +1,73 @@
import { observer } from 'mobx-react';
-import { makeInterface } from '../../new_fields/Schema';
+import { makeInterface, listSpec } from '../../new_fields/Schema';
import { documentSchema } from '../../new_fields/documentSchemas';
-import { CollectionSubView, SubCollectionViewProps } from './collections/CollectionSubView';
+import { CollectionSubView } from './collections/CollectionSubView';
import { DragManager } from '../util/DragManager';
import * as React from "react";
-import { Doc } from '../../new_fields/Doc';
-import { NumCast } from '../../new_fields/Types';
+import { Doc, DocListCast } from '../../new_fields/Doc';
+import { NumCast, Cast, StrCast } from '../../new_fields/Types';
+import { List } from '../../new_fields/List';
+import { ContentFittingDocumentView } from './nodes/ContentFittingDocumentView';
+import { Utils } from '../../Utils';
+import { Transform } from '../util/Transform';
+import "./collectionMulticolumnView.scss";
type MulticolumnDocument = makeInterface<[typeof documentSchema]>;
const MulticolumnDocument = makeInterface(documentSchema);
@observer
export default class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocument) {
-
- constructor(props: Readonly<SubCollectionViewProps>) {
- super(props);
+ private _dropDisposer?: DragManager.DragDropDisposer;
+ private get configuration() {
const { Document } = this.props;
- Document.multicolumnData = new Doc();
+ if (!Document.multicolumnData) {
+ Document.multicolumnData = new List<Doc>();
+ }
+ return DocListCast(this.Document.multicolumnData);
}
- private _dropDisposer?: DragManager.DragDropDisposer;
- protected createDropTarget = (ele: HTMLDivElement) => { //used for stacking and masonry view
+ protected createDropTarget = (ele: HTMLDivElement) => {
this._dropDisposer && this._dropDisposer();
if (ele) {
this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this));
}
}
+ getTransform = (ele: React.RefObject<HTMLDivElement>) => () => {
+ if (!ele.current) return Transform.Identity();
+ const { scale, translateX, translateY } = Utils.GetScreenTransform(ele.current);
+ return new Transform(-translateX, -translateY, 1 / scale);
+ }
+
public isCurrent(doc: Doc) { return !doc.isMinimized && (Math.abs(NumCast(doc.displayTimecode, -1) - NumCast(this.Document.currentTimecode, -1)) < 1.5 || NumCast(doc.displayTimecode, -1) === -1); }
render() {
+ const { PanelWidth } = this.props;
return (
<div className={"collectionMulticolumnView_outer"}>
<div className={"collectionMulticolumnView_contents"}>
- {this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map(({ layout, data }) => {
-
+ {this.configuration.map(config => {
+ const { target, columnWidth } = config;
+ if (target instanceof Doc) {
+ let computedWidth: number = 0;
+ const widthSpecifier = Cast(columnWidth, "number");
+ let matches: RegExpExecArray | null;
+ if (widthSpecifier !== undefined) {
+ computedWidth = widthSpecifier;
+ } else if ((matches = /([\d.]+)\%/.exec(StrCast(columnWidth))) !== null) {
+ computedWidth = Number(matches[1]) / 100 * PanelWidth();
+ }
+ return (!computedWidth ? (null) :
+ <ContentFittingDocumentView
+ {...this.props}
+ Document={target}
+ DataDocument={undefined}
+ PanelWidth={() => computedWidth}
+ getTransform={this.props.ScreenToLocalTransform}
+ />
+ );
+ }
+ return (null);
})}
</div>
</div>
diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx
index 54def38b5..faf02b946 100644
--- a/src/client/views/EditableView.tsx
+++ b/src/client/views/EditableView.tsx
@@ -36,7 +36,7 @@ export interface EditableProps {
resetValue: () => void;
value: string,
onChange: (e: React.ChangeEvent, { newValue }: { newValue: string }) => void,
- autosuggestProps: Autosuggest.AutosuggestProps<string>
+ autosuggestProps: Autosuggest.AutosuggestProps<string, any>
};
oneLine?: boolean;
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 305966160..91c7f909b 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -349,7 +349,7 @@ export class MainView extends React.Component {
addDocTabFunc = (doc: Doc, data: Opt<Doc>, where: string, libraryPath?: Doc[]): boolean => {
return where === "close" ? CollectionDockingView.CloseRightSplit(doc) :
doc.dockingConfig ? this.openWorkspace(doc) :
- CollectionDockingView.AddRightSplit(doc, undefined, undefined, libraryPath);
+ CollectionDockingView.AddRightSplit(doc, undefined, libraryPath);
}
mainContainerXf = () => new Transform(0, -this._buttonBarHeight, 1);
diff --git a/src/client/views/ScriptBox.tsx b/src/client/views/ScriptBox.tsx
index ded2329b4..d24256886 100644
--- a/src/client/views/ScriptBox.tsx
+++ b/src/client/views/ScriptBox.tsx
@@ -82,28 +82,19 @@ export class ScriptBox extends React.Component<ScriptBoxProps> {
);
}
//let l = docList(this.source[0].data).length; if (l) { let ind = this.target[0].index !== undefined ? (this.target[0].index+1) % l : 0; this.target[0].index = ind; this.target[0].proto = getProto(docList(this.source[0].data)[ind]);}
- public static EditButtonScript(title: string, doc: Doc, fieldKey: string, clientX: number, clientY: number, prewrapper?: string, postwrapper?: string) {
+ public static EditButtonScript(title: string, doc: Doc, fieldKey: string, clientX: number, clientY: number, contextParams?: { [name: string]: string }) {
let overlayDisposer: () => void = emptyFunction;
const script = ScriptCast(doc[fieldKey]);
let originalText: string | undefined = undefined;
if (script) {
originalText = script.script.originalScript;
- if (prewrapper && originalText.startsWith(prewrapper)) {
- originalText = originalText.substr(prewrapper.length);
- }
- if (postwrapper && originalText.endsWith(postwrapper)) {
- originalText = originalText.substr(0, originalText.length - postwrapper.length);
- }
}
// tslint:disable-next-line: no-unnecessary-callback-wrapper
const params: string[] = [];
const setParams = (p: string[]) => params.splice(0, params.length, ...p);
const scriptingBox = <ScriptBox initialText={originalText} setParams={setParams} onCancel={overlayDisposer} onSave={(text, onError) => {
- if (prewrapper) {
- text = prewrapper + text + (postwrapper ? postwrapper : "");
- }
const script = CompileScript(text, {
- params: { this: Doc.name },
+ params: { this: Doc.name, ...contextParams },
typecheck: false,
editable: true,
transformer: DocumentIconContainer.getTransformer()
diff --git a/src/client/views/Templates.tsx b/src/client/views/Templates.tsx
index ef78b60d4..8af8a6280 100644
--- a/src/client/views/Templates.tsx
+++ b/src/client/views/Templates.tsx
@@ -56,8 +56,17 @@ export namespace Templates {
<div style="width:100%;overflow:auto">{layout}</div>
</div>
</div>` );
+ export const TitleHover = new Template("TitleHover", TemplatePosition.InnerTop,
+ `<div>
+ <div style="height:25px; width:100%; background-color: rgba(0, 0, 0, .4); color: white; z-index: 100">
+ <span style="text-align:center;width:100%;font-size:20px;position:absolute;overflow:hidden;white-space:nowrap;text-overflow:ellipsis">{props.Document.title}</span>
+ </div>
+ <div style="height:calc(100% - 25px);">
+ <div style="width:100%;overflow:auto">{layout}</div>
+ </div>
+ </div>` );
- export const TemplateList: Template[] = [Title, Caption];
+ export const TemplateList: Template[] = [Title, TitleHover, Caption];
export function sortTemplates(a: Template, b: Template) {
if (a.Position < b.Position) { return -1; }
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 151b84c50..6c50ea0f2 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -34,6 +34,7 @@ import { DocumentType } from '../../documents/DocumentTypes';
import { ComputedField } from '../../../new_fields/ScriptField';
import { InteractionUtils } from '../../util/InteractionUtils';
import { TraceMobx } from '../../../new_fields/util';
+import { Scripting } from '../../util/Scripting';
library.add(faFile);
const _global = (window /* browser */ || global /* node */) as any;
@@ -177,7 +178,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
//
@undoBatch
@action
- public static AddRightSplit(document: Doc, dataDoc: Doc | undefined, minimize: boolean = false, libraryPath?: Doc[]) {
+ public static AddRightSplit(document: Doc, dataDoc: Doc | undefined, libraryPath?: Doc[]) {
if (!CollectionDockingView.Instance) return false;
const instance = CollectionDockingView.Instance;
const newItemStackConfig = {
@@ -202,11 +203,6 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
collayout.config.width = 50;
newContentItem.config.width = 50;
}
- if (minimize) {
- // bcz: this makes the drag image show up better, but it also messes with fixed layout sizes
- // newContentItem.config.width = 10;
- // newContentItem.config.height = 10;
- }
newContentItem.callDownwards('_$init');
instance.layoutChanged();
return true;
@@ -674,7 +670,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
if (doc.dockingConfig) {
return MainView.Instance.openWorkspace(doc);
} else if (location === "onRight") {
- return CollectionDockingView.AddRightSplit(doc, dataDoc, undefined, libraryPath);
+ return CollectionDockingView.AddRightSplit(doc, dataDoc, libraryPath);
} else if (location === "close") {
return CollectionDockingView.CloseRightSplit(doc);
} else {
@@ -724,3 +720,4 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
</div >);
}
}
+Scripting.addGlobal(function openOnRight(doc: any) { CollectionDockingView.AddRightSplit(doc, undefined); });
diff --git a/src/client/views/collections/CollectionSchemaHeaders.tsx b/src/client/views/collections/CollectionSchemaHeaders.tsx
index 0114342b9..92dc8780e 100644
--- a/src/client/views/collections/CollectionSchemaHeaders.tsx
+++ b/src/client/views/collections/CollectionSchemaHeaders.tsx
@@ -286,7 +286,6 @@ class KeysDropdown extends React.Component<KeysDropdownProps> {
}
@undoBatch
- @action
onKeyDown = (e: React.KeyboardEvent): void => {
if (e.key === "Enter") {
const keyOptions = this._searchTerm === "" ? this.props.possibleKeys : this.props.possibleKeys.filter(key => key.toUpperCase().indexOf(this._searchTerm.toUpperCase()) > -1);
@@ -296,7 +295,7 @@ class KeysDropdown extends React.Component<KeysDropdownProps> {
if (!exactFound && this._searchTerm !== "" && this.props.canAddNew) {
this.onSelect(this._searchTerm);
} else {
- this._searchTerm = this._key;
+ this.setSearchTerm(this._key);
}
}
}
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index c1e36272c..886a9c870 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -22,7 +22,6 @@ import { CollectionStackingViewFieldColumn } from "./CollectionStackingViewField
import { CollectionSubView } from "./CollectionSubView";
import { ContextMenu } from "../ContextMenu";
import { ContextMenuProps } from "../ContextMenuItem";
-import { ScriptBox } from "../ScriptBox";
import { CollectionMasonryViewFieldRow } from "./CollectionMasonryViewFieldRow";
import { TraceMobx } from "../../../new_fields/util";
import { CollectionViewType } from "./CollectionView";
@@ -40,9 +39,9 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
@observable _scroll = 0; // used to force the document decoration to update when scrolling
@computed get sectionHeaders() { return Cast(this.props.Document.sectionHeaders, listSpec(SchemaHeaderField)); }
@computed get sectionFilter() { return StrCast(this.props.Document.sectionFilter); }
- @computed get filteredChildren() { return this.childDocs.filter(d => !d.isMinimized); }
+ @computed get filteredChildren() { return this.childDocs.filter(d => !d.isMinimized).map(d => (Doc.GetLayoutDataDocPair(this.props.Document, this.props.DataDoc, this.props.fieldKey, d).layout as Doc) || d); }
@computed get xMargin() { return NumCast(this.props.Document.xMargin, 2 * this.gridGap); }
- @computed get yMargin() { return Math.max(this.props.Document.showTitle ? 30 : 0, NumCast(this.props.Document.yMargin, 2 * this.gridGap)); }
+ @computed get yMargin() { return Math.max(this.props.Document.showTitle && !this.props.Document.showTitleHover ? 30 : 0, NumCast(this.props.Document.yMargin, 2 * this.gridGap)); }
@computed get gridGap() { return NumCast(this.props.Document.gridGap, 10); }
@computed get isStackingView() { return BoolCast(this.props.Document.singleColumn, true); }
@computed get numGroupColumns() { return this.isStackingView ? Math.max(1, this.Sections.size + (this.showAddAGroup ? 1 : 0)) : 1; }
@@ -53,22 +52,18 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
}
@computed get NodeWidth() { return this.props.PanelWidth() - this.gridGap; }
- childDocHeight(child: Doc) { return this.getDocHeight(Doc.GetLayoutDataDocPair(this.props.Document, this.props.DataDoc, this.props.fieldKey, child).layout); }
-
children(docs: Doc[]) {
this._docXfs.length = 0;
return docs.map((d, i) => {
- const pair = Doc.GetLayoutDataDocPair(this.props.Document, this.props.DataDoc, this.props.fieldKey, d);
- const layoutDoc = pair.layout ? Doc.Layout(pair.layout) : d;
- const width = () => Math.min(layoutDoc.nativeWidth && !layoutDoc.ignoreAspect && !this.props.Document.fillColumn ? layoutDoc[WidthSym]() : Number.MAX_VALUE, this.columnWidth / this.numGroupColumns);
- const height = () => this.getDocHeight(layoutDoc);
+ const width = () => Math.min(d.nativeWidth && !d.ignoreAspect && !this.props.Document.fillColumn ? d[WidthSym]() : Number.MAX_VALUE, this.columnWidth / this.numGroupColumns);
+ const height = () => this.getDocHeight(d);
const dref = React.createRef<HTMLDivElement>();
- const dxf = () => this.getDocTransform(layoutDoc, dref.current!);
+ const dxf = () => this.getDocTransform(d, dref.current!);
this._docXfs.push({ dxf: dxf, width: width, height: height });
const rowSpan = Math.ceil((height() + this.gridGap) / this.gridGap);
const style = this.isStackingView ? { width: width(), marginTop: i === 0 ? 0 : this.gridGap, height: height() } : { gridRowEnd: `span ${rowSpan}` };
return <div className={`collectionStackingView-${this.isStackingView ? "columnDoc" : "masonryDoc"}`} key={d[Id]} ref={dref} style={style} >
- {this.getDisplayDoc(pair.layout || d, pair.data, dxf, width)}
+ {this.getDisplayDoc(d, (d.resolvedDataDoc as Doc) || d, dxf, width)}
</div>;
});
}
@@ -115,7 +110,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
const res = this.props.ContentScaling() * sectionsList.reduce((maxHght, s) => {
const r1 = Math.max(maxHght,
(this.Sections.size ? 50 : 0) + s.reduce((height, d, i) => {
- const val = height + this.childDocHeight(d) + (i === s.length - 1 ? this.yMargin : this.gridGap);
+ const val = height + this.getDocHeight(d) + (i === s.length - 1 ? this.yMargin : this.gridGap);
return val;
}, this.yMargin));
return r1;
@@ -157,7 +152,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
}
overlays = (doc: Doc) => {
- return doc.type === DocumentType.IMG || doc.type === DocumentType.VID ? { title: StrCast(this.props.Document.showTitles), caption: StrCast(this.props.Document.showCaptions) } : {};
+ return doc.type === DocumentType.IMG || doc.type === DocumentType.VID ? { title: StrCast(this.props.Document.showTitles), titleHover: StrCast(this.props.Document.showTitleHovers), caption: StrCast(this.props.Document.showCaptions) } : {};
}
@computed get onChildClickHandler() { return ScriptCast(this.Document.onChildClick); }
@@ -292,7 +287,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
sectionStacking = (heading: SchemaHeaderField | undefined, docList: Doc[]) => {
const key = this.sectionFilter;
let type: "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" | undefined = undefined;
- const types = docList.length ? docList.map(d => typeof d[key]) : this.childDocs.map(d => typeof d[key]);
+ const types = docList.length ? docList.map(d => typeof d[key]) : this.filteredChildren.map(d => typeof d[key]);
if (types.map((i, idx) => types.indexOf(i) === idx).length === 1) {
type = types[0];
}
@@ -317,15 +312,17 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
const y = this._scroll; // required for document decorations to update when the text box container is scrolled
const { scale, translateX, translateY } = Utils.GetScreenTransform(dref);
const outerXf = Utils.GetScreenTransform(this._masonryGridRef!);
+ const scaling = 1 / Math.min(1, this.props.PanelHeight() / this.layoutDoc[HeightSym]());
const offset = this.props.ScreenToLocalTransform().transformDirection(outerXf.translateX - translateX, outerXf.translateY - translateY);
- return this.props.ScreenToLocalTransform().
- translate(offset[0], offset[1] + (this.props.ChromeHeight && this.props.ChromeHeight() < 0 ? this.props.ChromeHeight() : 0));
+ const offsetx = (doc[WidthSym]() - doc[WidthSym]() / scaling) / 2;
+ const offsety = (this.props.ChromeHeight && this.props.ChromeHeight() < 0 ? this.props.ChromeHeight() : 0);
+ return this.props.ScreenToLocalTransform().translate(offset[0] - offsetx, offset[1] + offsety).scale(scaling);
}
sectionMasonry = (heading: SchemaHeaderField | undefined, docList: Doc[]) => {
const key = this.sectionFilter;
let type: "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" | undefined = undefined;
- const types = docList.length ? docList.map(d => typeof d[key]) : this.childDocs.map(d => typeof d[key]);
+ const types = docList.length ? docList.map(d => typeof d[key]) : this.filteredChildren.map(d => typeof d[key]);
if (types.map((i, idx) => types.indexOf(i) === idx).length === 1) {
type = types[0];
}
@@ -374,11 +371,6 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
subItems.push({ description: `${this.props.Document.showTitles ? "Hide Titles" : "Show Titles"}`, event: () => this.props.Document.showTitles = !this.props.Document.showTitles ? "title" : "", icon: "plus" });
subItems.push({ description: `${this.props.Document.showCaptions ? "Hide Captions" : "Show Captions"}`, event: () => this.props.Document.showCaptions = !this.props.Document.showCaptions ? "caption" : "", icon: "plus" });
ContextMenu.Instance.addItem({ description: "Stacking Options ...", subitems: subItems, icon: "eye" });
-
- const existingOnClick = ContextMenu.Instance.findByDescription("OnClick...");
- const onClicks: ContextMenuProps[] = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : [];
- onClicks.push({ description: "Edit onChildClick script", icon: "edit", event: (obj: any) => ScriptBox.EditButtonScript("On Child Clicked...", this.props.Document, "onChildClick", obj.x, obj.y) });
- !existingOnClick && ContextMenu.Instance.addItem({ description: "OnClick...", subitems: onClicks, icon: "hand-point-right" });
}
}
@@ -402,6 +394,11 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
<div className="collectionStackingMasonry-cont" >
<div className={this.isStackingView ? "collectionStackingView" : "collectionMasonryView"}
ref={this.createRef}
+ style={{
+ transform: `scale(${Math.min(1, this.props.PanelHeight() / this.layoutDoc[HeightSym]())})`,
+ height: `${100 * 1 / Math.min(this.props.PanelWidth() / this.layoutDoc[WidthSym](), this.props.PanelHeight() / this.layoutDoc[HeightSym]())}%`,
+ transformOrigin: "top"
+ }}
onScroll={action((e: React.UIEvent<HTMLDivElement>) => this._scroll = e.currentTarget.scrollTop)}
onDrop={this.onDrop.bind(this)}
onContextMenu={this.onContextMenu}
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 062521690..5753dd34e 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -85,8 +85,10 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
return this.props.annotationsKey ? (this.extensionDoc ? this.extensionDoc[this.props.annotationsKey] : undefined) : this.dataDoc[this.props.fieldKey];
}
- get childLayoutPairs() {
- return this.childDocs.map(cd => Doc.GetLayoutDataDocPair(this.props.Document, this.props.DataDoc, this.props.fieldKey, cd)).filter(pair => pair.layout).map(pair => ({ layout: pair.layout!, data: pair.data! }));
+ get childLayoutPairs(): { layout: Doc; data: Doc; }[] {
+ const { Document, DataDoc, fieldKey } = this.props;
+ const validPairs = this.childDocs.map(doc => Doc.GetLayoutDataDocPair(Document, DataDoc, fieldKey, doc)).filter(pair => pair.layout);
+ return validPairs.map(({ data, layout }) => ({ data: data!, layout: layout! })); // this mapping is a bit of a hack to coerce types
}
get childDocList() {
return Cast(this.dataField, listSpec(Doc));
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index 79fc477ab..3356aed68 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -28,6 +28,7 @@ import { CollectionSubView } from "./CollectionSubView";
import "./CollectionTreeView.scss";
import React = require("react");
import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils';
+import { ScriptBox } from '../ScriptBox';
export interface TreeViewProps {
@@ -57,6 +58,7 @@ export interface TreeViewProps {
hideHeaderFields: () => boolean;
preventTreeViewOpen: boolean;
renderedIds: string[];
+ onCheckedClick?: ScriptField;
}
library.add(faTrashAlt);
@@ -286,7 +288,7 @@ class TreeView extends React.Component<TreeViewProps> {
DocListCast(contents), this.props.treeViewId, doc, undefined, key, this.props.containingCollection, this.props.prevSibling, addDoc, remDoc, this.move,
this.props.dropAction, this.props.addDocTab, this.props.pinToPres, this.props.ScreenToLocalTransform, this.props.outerXf, this.props.active,
this.props.panelWidth, this.props.ChromeHeight, this.props.renderDepth, this.props.hideHeaderFields, this.props.preventTreeViewOpen,
- [...this.props.renderedIds, doc[Id]], this.props.libraryPath);
+ [...this.props.renderedIds, doc[Id]], this.props.libraryPath, this.props.onCheckedClick);
} else {
contentElement = <EditableView
key="editableView"
@@ -331,7 +333,7 @@ class TreeView extends React.Component<TreeViewProps> {
this.templateDataDoc, expandKey, this.props.containingCollection, this.props.prevSibling, addDoc, remDoc, this.move,
this.props.dropAction, this.props.addDocTab, this.props.pinToPres, this.props.ScreenToLocalTransform,
this.props.outerXf, this.props.active, this.props.panelWidth, this.props.ChromeHeight, this.props.renderDepth, this.props.hideHeaderFields, this.props.preventTreeViewOpen,
- [...this.props.renderedIds, this.props.document[Id]], this.props.libraryPath)}
+ [...this.props.renderedIds, this.props.document[Id]], this.props.libraryPath, this.props.onCheckedClick)}
</ul >;
} else if (this.treeViewExpandedView === "fields") {
return <ul><div ref={this._dref} style={{ display: "inline-block" }} key={this.props.document[Id] + this.props.document.title}>
@@ -366,8 +368,13 @@ class TreeView extends React.Component<TreeViewProps> {
@action
bulletClick = (e: React.MouseEvent) => {
- if (this.props.document.onClick) {
- ScriptCast(this.props.document.onClick).script.run({ this: this.props.document.isTemplateField && this.props.dataDoc ? this.props.dataDoc : this.props.document }, console.log);
+ if (this.props.onCheckedClick) {
+ this.props.document.treeViewChecked = this.props.document.treeViewChecked === "check" ? "x" : this.props.document.treeViewChecked === "x" ? undefined : "check";
+ ScriptCast(this.props.onCheckedClick).script.run({
+ this: this.props.document.isTemplateField && this.props.dataDoc ? this.props.dataDoc : this.props.document,
+ heading: this.props.containingCollection.title,
+ checked: this.props.document.treeViewChecked === "check" ? false : this.props.document.treeViewChecked === "x" ? "x" : "none"
+ }, console.log);
} else {
this.treeViewOpen = !this.treeViewOpen;
}
@@ -376,8 +383,9 @@ class TreeView extends React.Component<TreeViewProps> {
@computed
get renderBullet() {
- return <div className="bullet" title="view inline" onClick={this.bulletClick} style={{ color: StrCast(this.props.document.color, "black"), opacity: 0.4 }}>
- {<FontAwesomeIcon icon={!this.treeViewOpen ? (this.childDocs ? "caret-square-right" : "caret-right") : (this.childDocs ? "caret-square-down" : "caret-down")} />}
+ const checked = this.props.onCheckedClick ? (this.props.document.treeViewChecked ? this.props.document.treeViewChecked : "unchecked") : undefined;
+ return <div className="bullet" title="view inline" onClick={this.bulletClick} style={{ color: StrCast(this.props.document.color, checked === "unchecked" ? "white" : "black"), opacity: 0.4 }}>
+ {<FontAwesomeIcon icon={checked === "check" ? "check" : (checked === "x" ? "times" : checked === "unchecked" ? "square" : !this.treeViewOpen ? (this.childDocs ? "caret-square-right" : "caret-right") : (this.childDocs ? "caret-square-down" : "caret-down"))} />}
</div>;
}
/**
@@ -457,7 +465,8 @@ class TreeView extends React.Component<TreeViewProps> {
hideHeaderFields: () => boolean,
preventTreeViewOpen: boolean,
renderedIds: string[],
- libraryPath: Doc[] | undefined
+ libraryPath: Doc[] | undefined,
+ onCheckedClick: ScriptField | undefined
) {
const viewSpecScript = Cast(containingCollection.viewSpecScript, ScriptField);
if (viewSpecScript) {
@@ -549,6 +558,7 @@ class TreeView extends React.Component<TreeViewProps> {
key={child[Id]}
indentDocument={indent}
outdentDocument={outdent}
+ onCheckedClick={onCheckedClick}
renderDepth={renderDepth}
deleteDoc={remove}
addDocument={addDocument}
@@ -617,6 +627,10 @@ export class CollectionTreeView extends CollectionSubView(Document) {
layoutItems.push({ description: (this.props.Document.hideHeaderFields ? "Show" : "Hide") + " Header Fields", event: () => this.props.Document.hideHeaderFields = !this.props.Document.hideHeaderFields, icon: "paint-brush" });
ContextMenu.Instance.addItem({ description: "Treeview Options ...", subitems: layoutItems, icon: "eye" });
}
+ const existingOnClick = ContextMenu.Instance.findByDescription("OnClick...");
+ const onClicks: ContextMenuProps[] = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : [];
+ onClicks.push({ description: "Edit onChecked Script", icon: "edit", event: (obj: any) => ScriptBox.EditButtonScript("On Checked Changed ...", this.props.Document, "onCheckedClick", obj.x, obj.y, { heading: "boolean", checked: "boolean" }) });
+ !existingOnClick && ContextMenu.Instance.addItem({ description: "OnClick...", subitems: onClicks, icon: "hand-point-right" });
}
outerXf = () => Utils.GetScreenTransform(this._mainEle!);
onTreeDrop = (e: React.DragEvent) => this.onDrop(e, {});
@@ -661,7 +675,7 @@ export class CollectionTreeView extends CollectionSubView(Document) {
TreeView.GetChildElements(this.childDocs, this.props.Document[Id], this.props.Document, this.props.DataDoc, this.props.fieldKey, this.props.ContainingCollectionDoc, undefined, addDoc, this.remove,
moveDoc, dropAction, this.props.addDocTab, this.props.pinToPres, this.props.ScreenToLocalTransform,
this.outerXf, this.props.active, this.props.PanelWidth, this.props.ChromeHeight, this.props.renderDepth, () => BoolCast(this.props.Document.hideHeaderFields),
- BoolCast(this.props.Document.preventTreeViewOpen), [], this.props.LibraryPath)
+ BoolCast(this.props.Document.preventTreeViewOpen), [], this.props.LibraryPath, ScriptCast(this.props.Document.onCheckedClick))
}
</ul>
</div >
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 88023783b..21371dd39 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -34,6 +34,8 @@ import { FieldViewProps, FieldView } from '../nodes/FieldView';
import { Touchable } from '../Touchable';
import { TraceMobx } from '../../../new_fields/util';
import { Utils } from '../../../Utils';
+import { ScriptBox } from '../ScriptBox';
+import CollectionMulticolumnView from '../CollectionMulticolumnView';
const path = require('path');
library.add(faTh, faTree, faSquare, faProjectDiagram, faSignature, faThList, faFingerprint, faColumns, faEllipsisV, faImage, faEye as any, faCopy);
@@ -47,7 +49,8 @@ export enum CollectionViewType {
Masonry,
Pivot,
Linear,
- Staff
+ Staff,
+ Multicolumn
}
export namespace CollectionViewType {
@@ -60,7 +63,8 @@ export namespace CollectionViewType {
["stacking", CollectionViewType.Stacking],
["masonry", CollectionViewType.Masonry],
["pivot", CollectionViewType.Pivot],
- ["linear", CollectionViewType.Linear]
+ ["linear", CollectionViewType.Linear],
+ ["multicolumn", CollectionViewType.Multicolumn]
]);
export const valueOf = (value: string) => stringMapping.get(value.toLowerCase());
@@ -172,6 +176,7 @@ export class CollectionView extends Touchable<FieldViewProps> {
case CollectionViewType.Docking: return (<CollectionDockingView key="collview" {...props} />);
case CollectionViewType.Tree: return (<CollectionTreeView key="collview" {...props} />);
case CollectionViewType.Staff: return (<CollectionStaffView chromeCollapsed={true} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} />);
+ case CollectionViewType.Multicolumn: return (<CollectionMulticolumnView chromeCollapsed={true} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} />);
case CollectionViewType.Linear: { return (<CollectionLinearView key="collview" {...props} />); }
case CollectionViewType.Stacking: { this.props.Document.singleColumn = true; return (<CollectionStackingView key="collview" {...props} />); }
case CollectionViewType.Masonry: { this.props.Document.singleColumn = false; return (<CollectionStackingView key="collview" {...props} />); }
@@ -213,6 +218,7 @@ export class CollectionView extends Touchable<FieldViewProps> {
}, icon: "ellipsis-v"
});
subItems.push({ description: "Staff", event: () => this.props.Document.viewType = CollectionViewType.Staff, icon: "music" });
+ subItems.push({ description: "Multicolumn", event: () => this.props.Document.viewType = CollectionViewType.Multicolumn, icon: "columns" });
subItems.push({ description: "Masonry", event: () => this.props.Document.viewType = CollectionViewType.Masonry, icon: "columns" });
subItems.push({ description: "Pivot", event: () => this.props.Document.viewType = CollectionViewType.Pivot, icon: "columns" });
switch (this.props.Document.viewType) {
@@ -233,6 +239,11 @@ export class CollectionView extends Touchable<FieldViewProps> {
const moreItems = more && "subitems" in more ? more.subitems : [];
moreItems.push({ description: "Export Image Hierarchy", icon: "columns", event: () => ImageUtils.ExportHierarchyToFileSystem(this.props.Document) });
!more && ContextMenu.Instance.addItem({ description: "More...", subitems: moreItems, icon: "hand-point-right" });
+
+ const existingOnClick = ContextMenu.Instance.findByDescription("OnClick...");
+ const onClicks = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : [];
+ onClicks.push({ description: "Edit onChildClick script", icon: "edit", event: (obj: any) => ScriptBox.EditButtonScript("On Child Clicked...", this.props.Document, "onChildClick", obj.x, obj.y) });
+ !existingOnClick && ContextMenu.Instance.addItem({ description: "OnClick...", subitems: onClicks, icon: "hand-point-right" });
}
}
diff --git a/src/client/views/collections/CollectionViewChromes.tsx b/src/client/views/collections/CollectionViewChromes.tsx
index 184504e5a..996c7671e 100644
--- a/src/client/views/collections/CollectionViewChromes.tsx
+++ b/src/client/views/collections/CollectionViewChromes.tsx
@@ -217,9 +217,10 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
`(${keyRestrictionScript}) ${dateRestrictionScript.length ? "&&" : ""} ${dateRestrictionScript}` :
"true";
- const docFilter = StrCast(this.props.CollectionView.props.Document.docFilter);
- const finalScript = docFilter && !fullScript.startsWith("(())") ? `${fullScript} ${docFilter ? "&&" : ""} (${docFilter})` :
- docFilter ? docFilter : fullScript;
+ const docFilter = Cast(this.props.CollectionView.props.Document.docFilter, listSpec("string"), []);
+ const docFilterText = Doc.MakeDocFilter(docFilter);
+ const finalScript = docFilterText && !fullScript.startsWith("(())") ? `${fullScript} ${docFilterText ? "&&" : ""} (${docFilterText})` :
+ docFilterText ? docFilterText : fullScript;
this.props.CollectionView.props.Document.viewSpecScript = ScriptField.MakeFunction(finalScript, { doc: Doc.name });
}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
index a965a6cc9..8c8da63cc 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
@@ -78,7 +78,7 @@ export function computePivotLayout(poolData: ObservableMap<string, any>, pivotDo
x,
y: pivotAxisWidth + 50,
width: pivotAxisWidth * expander * numCols,
- height: 100,
+ height: NumCast(pivotDoc.pivotFontSize, 10),
fontSize: NumCast(pivotDoc.pivotFontSize, 10)
});
for (const doc of val) {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 936c4413f..7985e541f 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -9,7 +9,7 @@ import { Id } from "../../../../new_fields/FieldSymbols";
import { InkTool, InkField, InkData } from "../../../../new_fields/InkField";
import { createSchema, makeInterface } from "../../../../new_fields/Schema";
import { ScriptField } from "../../../../new_fields/ScriptField";
-import { BoolCast, Cast, DateCast, NumCast, StrCast } from "../../../../new_fields/Types";
+import { BoolCast, Cast, DateCast, NumCast, StrCast, ScriptCast } from "../../../../new_fields/Types";
import { CurrentUserUtils } from "../../../../server/authentication/models/current_user_utils";
import { aggregateBounds, emptyFunction, intersectRect, returnOne, Utils } from "../../../../Utils";
import { DocServer } from "../../../DocServer";
@@ -56,6 +56,8 @@ export const panZoomSchema = createSchema({
useClusters: "boolean",
isRuleProvider: "boolean",
fitToBox: "boolean",
+ xPadding: "number", // pixels of padding on left/right of collectionfreeformview contents when fitToBox is set
+ yPadding: "number", // pixels of padding on left/right of collectionfreeformview contents when fitToBox is set
panTransformType: "string",
scrollHeight: "number",
fitX: "number",
@@ -82,7 +84,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
@computed get fitToContent() { return (this.props.fitToBox || this.Document.fitToBox) && !this.isAnnotationOverlay; }
@computed get parentScaling() { return this.props.ContentScaling && this.fitToContent && !this.isAnnotationOverlay ? this.props.ContentScaling() : 1; }
- @computed get contentBounds() { return aggregateBounds(this._layoutElements.filter(e => e.bounds && !e.bounds.z).map(e => e.bounds!)); }
+ @computed get contentBounds() { return aggregateBounds(this._layoutElements.filter(e => e.bounds && !e.bounds.z).map(e => e.bounds!), NumCast(this.layoutDoc.xPadding, 10), NumCast(this.layoutDoc.yPadding, 10)); }
@computed get nativeWidth() { return this.Document.fitToContent ? 0 : this.Document.nativeWidth || 0; }
@computed get nativeHeight() { return this.fitToContent ? 0 : this.Document.nativeHeight || 0; }
private get isAnnotationOverlay() { return this.props.isAnnotationOverlay; }
@@ -710,6 +712,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
getScale = () => this.Document.scale || 1;
@computed get libraryPath() { return this.props.LibraryPath ? [...this.props.LibraryPath, this.props.Document] : []; }
+ @computed get onChildClickHandler() { return ScriptCast(this.Document.onChildClick); }
getChildDocumentViewProps(childLayout: Doc, childData?: Doc): DocumentViewProps {
return {
@@ -719,7 +722,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
LibraryPath: this.libraryPath,
layoutKey: undefined,
ruleProvider: this.Document.isRuleProvider && childLayout.type !== DocumentType.TEXT ? this.props.Document : this.props.ruleProvider, //bcz: hack! - currently ruleProviders apply to documents in nested colleciton, not direct children of themselves
- onClick: undefined, // this.props.onClick, // bcz: check this out -- I don't think we want to inherit click handlers, or we at least need a way to ignore them
+ //onClick: undefined, // this.props.onClick, // bcz: check this out -- I don't think we want to inherit click handlers, or we at least need a way to ignore them
+ onClick: this.onChildClickHandler,
ScreenToLocalTransform: childLayout.z ? this.getTransformOverlay : this.getTransform,
renderDepth: this.props.renderDepth + 1,
PanelWidth: childLayout[WidthSym],
@@ -800,9 +804,11 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
}
this.childLayoutPairs.filter((pair, i) => this.isCurrent(pair.layout)).forEach(pair =>
computedElementData.elements.push({
- ele: <CollectionFreeFormDocumentView key={pair.layout[Id]} dataProvider={this.childDataProvider}
+ ele: <CollectionFreeFormDocumentView key={pair.layout[Id]} {...this.getChildDocumentViewProps(pair.layout, pair.data)}
+ dataProvider={this.childDataProvider}
ruleProvider={this.Document.isRuleProvider ? this.props.Document : this.props.ruleProvider}
- jitterRotation={NumCast(this.props.Document.jitterRotation)} {...this.getChildDocumentViewProps(pair.layout, pair.data)} />,
+ jitterRotation={NumCast(this.props.Document.jitterRotation)}
+ fitToBox={this.props.fitToBox || this.Document.freeformLayoutEngine === "pivot"} />,
bounds: this.childDataProvider(pair.layout)
}));
@@ -988,8 +994,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
</MarqueeView>;
}
@computed get contentScaling() {
- let hscale = this.nativeHeight ? this.props.PanelHeight() / this.nativeHeight : 1;
- let wscale = this.nativeWidth ? this.props.PanelWidth() / this.nativeWidth : 1;
+ const hscale = this.nativeHeight ? this.props.PanelHeight() / this.nativeHeight : 1;
+ const wscale = this.nativeWidth ? this.props.PanelWidth() / this.nativeWidth : 1;
return wscale < hscale ? wscale : hscale;
}
render() {
diff --git a/src/client/views/linking/LinkMenuGroup.tsx b/src/client/views/linking/LinkMenuGroup.tsx
index abd17ec4d..ace9a9e4c 100644
--- a/src/client/views/linking/LinkMenuGroup.tsx
+++ b/src/client/views/linking/LinkMenuGroup.tsx
@@ -47,7 +47,7 @@ export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> {
document.removeEventListener("pointerup", this.onLinkButtonUp);
const targets = this.props.group.map(l => LinkManager.Instance.getOppositeAnchor(l, this.props.sourceDoc)).filter(d => d) as Doc[];
- DragManager.StartLinkTargetsDrag(this._drag.current!, e.x, e.y, this.props.sourceDoc, targets);
+ DragManager.StartLinkTargetsDrag(this._drag.current, e.x, e.y, this.props.sourceDoc, targets);
}
e.stopPropagation();
}
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 2e598a14d..614a68e7a 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -1,4 +1,4 @@
-import { random } from "animejs";
+import anime from "animejs";
import { computed, IReactionDisposer, observable, reaction, trace } from "mobx";
import { observer } from "mobx-react";
import { Doc, HeightSym, WidthSym } from "../../../new_fields/Doc";
@@ -11,6 +11,8 @@ import { DocumentView, DocumentViewProps } from "./DocumentView";
import React = require("react");
import { PositionDocument } from "../../../new_fields/documentSchemas";
import { TraceMobx } from "../../../new_fields/util";
+import { returnFalse } from "../../../Utils";
+import { ContentFittingDocumentView } from "./ContentFittingDocumentView";
export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
dataProvider?: (doc: Doc) => { x: number, y: number, width: number, height: number, z: number, transition?: string } | undefined;
@@ -20,13 +22,14 @@ export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
height?: number;
jitterRotation: number;
transition?: string;
+ fitToBox?: boolean;
}
@observer
export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeFormDocumentViewProps, PositionDocument>(PositionDocument) {
_disposer: IReactionDisposer | undefined = undefined;
get displayName() { return "CollectionFreeFormDocumentView(" + this.props.Document.title + ")"; } // this makes mobx trace() statements more descriptive
- get transform() { return `scale(${this.props.ContentScaling()}) translate(${this.X}px, ${this.Y}px) rotate(${random(-1, 1) * this.props.jitterRotation}deg)`; }
+ get transform() { return `scale(${this.props.ContentScaling()}) translate(${this.X}px, ${this.Y}px) rotate(${anime.random(-1, 1) * this.props.jitterRotation}deg)`; }
get X() { return this._animPos !== undefined ? this._animPos[0] : this.renderScriptDim ? this.renderScriptDim.x : this.props.x !== undefined ? this.props.x : this.dataProvider ? this.dataProvider.x : (this.Document.x || 0); }
get Y() { return this._animPos !== undefined ? this._animPos[1] : this.renderScriptDim ? this.renderScriptDim.y : this.props.y !== undefined ? this.props.y : this.dataProvider ? this.dataProvider.y : (this.Document.y || 0); }
get width() { return this.renderScriptDim ? this.renderScriptDim.width : this.props.width !== undefined ? this.props.width : this.props.dataProvider && this.dataProvider ? this.dataProvider.width : this.layoutDoc[WidthSym](); }
@@ -83,8 +86,8 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
@observable _animPos: number[] | undefined = undefined;
- finalPanelWidth = () => this.dataProvider ? this.dataProvider.width : this.panelWidth();
- finalPanelHeight = () => this.dataProvider ? this.dataProvider.height : this.panelHeight();
+ finalPanelWidth = () => (this.dataProvider ? this.dataProvider.width : this.panelWidth());
+ finalPanelHeight = () => (this.dataProvider ? this.dataProvider.height : this.panelHeight());
render() {
TraceMobx();
@@ -104,24 +107,22 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
zIndex: this.Document.zIndex || 0,
}} >
- <DocumentView {...this.props}
+
+ {!this.props.fitToBox ? <DocumentView {...this.props}
dragDivName={"collectionFreeFormDocumentView-container"}
ContentScaling={this.contentScaling}
ScreenToLocalTransform={this.getTransform}
backgroundColor={this.clusterColorFunc}
PanelWidth={this.finalPanelWidth}
PanelHeight={this.finalPanelHeight}
- />
- {/* <ContentFittingDocumentView {...this.props}
- //dragDivName={"collectionFreeFormDocumentView-container"}
- //ContentScaling={this.contentScaling}
+ /> : <ContentFittingDocumentView {...this.props}
+ DataDocument={this.props.DataDoc}
getTransform={this.getTransform}
active={returnFalse}
focus={(doc: Doc) => this.props.focus(doc, false)}
- // backgroundColor={this.clusterColorFunc}
PanelWidth={this.finalPanelWidth}
PanelHeight={this.finalPanelHeight}
- /> */}
+ />}
</div>;
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index 8f6bfc8e1..66886165e 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -82,7 +82,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
return this.props.DataDoc;
}
get layoutDoc() {
- return this.props.DataDoc === undefined ? Doc.expandTemplateLayout(Doc.Layout(this.props.Document), this.props.Document) : Doc.Layout(this.props.Document);
+ return Doc.expandTemplateLayout(Doc.Layout(this.props.Document), this.props.Document);
}
CreateBindings(): JsxBindings {
diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss
index f44c6dd3b..2ce56c73d 100644
--- a/src/client/views/nodes/DocumentView.scss
+++ b/src/client/views/nodes/DocumentView.scss
@@ -55,7 +55,7 @@
position: absolute;
}
- .documentView-titleWrapper {
+ .documentView-titleWrapper, .documentView-titleWrapper-hover {
overflow: hidden;
color: white;
transform-origin: top left;
@@ -68,6 +68,9 @@
text-overflow: ellipsis;
white-space: pre;
}
+ .documentView-titleWrapper-hover {
+ display:none;
+ }
.documentView-searchHighlight {
position: absolute;
@@ -85,4 +88,12 @@
}
}
+}
+
+.documentView-node:hover, .documentView-node-topmost:hover {
+ > .documentView-styleWrapper {
+ > .documentView-titleWrapper-hover {
+ display:inline-block;
+ }
+ }
} \ No newline at end of file
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 10d2e2b3e..b2c2ccff5 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -29,7 +29,6 @@ import { CollectionDockingView } from "../collections/CollectionDockingView";
import { CollectionView } from "../collections/CollectionView";
import { ContextMenu } from "../ContextMenu";
import { ContextMenuProps } from '../ContextMenuItem';
-import { DictationOverlay } from '../DictationOverlay';
import { DocComponent } from "../DocComponent";
import { EditableView } from '../EditableView';
import { OverlayView } from '../OverlayView';
@@ -64,7 +63,7 @@ export interface DocumentViewProps {
moveDocument?: (doc: Doc, targetCollection: Doc | undefined, addDocument: (document: Doc) => boolean) => boolean;
ScreenToLocalTransform: () => Transform;
renderDepth: number;
- showOverlays?: (doc: Doc) => { title?: string, caption?: string };
+ showOverlays?: (doc: Doc) => { title?: string, titleHover?: string, caption?: string };
ContentScaling: () => number;
ruleProvider: Doc | undefined;
PanelWidth: () => number;
@@ -196,7 +195,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
ScriptBox.EditButtonScript("On Button Clicked ...", this.props.Document, "onClick", e.clientX, e.clientY);
} else if (this.props.Document.isButton === "Selector") { // this should be moved to an OnClick script
FormattedTextBoxComment.Hide();
- this.Document.links?.[0] instanceof Doc && (Doc.UserDoc().SelectedDocs = new List([Doc.LinkOtherAnchor(this.Document.links[0]!, this.props.Document)]));
+ this.Document.links?.[0] instanceof Doc && (Doc.UserDoc().SelectedDocs = new List([Doc.LinkOtherAnchor(this.Document.links[0], this.props.Document)]));
} else if (this.Document.isButton) {
SelectionManager.SelectDoc(this, e.ctrlKey); // don't think this should happen if a button action is actually triggered.
this.buttonClick(e.altKey, e.ctrlKey);
@@ -741,7 +740,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
chromeHeight = () => {
const showOverlays = this.props.showOverlays ? this.props.showOverlays(this.Document) : undefined;
const showTitle = showOverlays && "title" in showOverlays ? showOverlays.title : StrCast(this.layoutDoc.showTitle);
- return (showTitle ? 25 : 0) + 1;
+ const showTitleHover = showOverlays && "titleHover" in showOverlays ? showOverlays.titleHover : StrCast(this.layoutDoc.showTitleHover);
+ return (showTitle && !showTitleHover ? 0 : 0) + 1;
}
@computed get finalLayoutKey() { return this.props.layoutKey || "layout"; }
@@ -751,6 +751,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
return (<DocumentContentsView ContainingCollectionView={this.props.ContainingCollectionView}
ContainingCollectionDoc={this.props.ContainingCollectionDoc}
Document={this.props.Document}
+ DataDoc={this.props.DataDoc}
fitToBox={this.props.fitToBox}
LibraryPath={this.props.LibraryPath}
addDocument={this.props.addDocument}
@@ -777,8 +778,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
isSelected={this.isSelected}
select={this.select}
onClick={this.onClickHandler}
- layoutKey={this.finalLayoutKey}
- DataDoc={this.props.DataDoc} />);
+ layoutKey={this.finalLayoutKey} />);
}
linkEndpoint = (linkDoc: Doc) => Doc.LinkEndpoint(linkDoc, this.props.Document);
@@ -795,6 +795,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
TraceMobx();
const showOverlays = this.props.showOverlays ? this.props.showOverlays(this.Document) : undefined;
const showTitle = showOverlays && "title" in showOverlays ? showOverlays.title : StrCast(this.getLayoutPropStr("showTitle"));
+ const showTitleHover = showOverlays && "titleHover" in showOverlays ? showOverlays.titleHover : StrCast(this.getLayoutPropStr("showTitleHover"));
const showCaption = showOverlays && "caption" in showOverlays ? showOverlays.caption : this.getLayoutPropStr("showCaption");
const showTextTitle = showTitle && StrCast(this.layoutDoc.layout).indexOf("FormattedTextBox") !== -1 ? showTitle : undefined;
const searchHighlight = (!this.Document.searchFields ? (null) :
@@ -810,15 +811,15 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
/>
</div>);
const titleView = (!showTitle ? (null) :
- <div className="documentView-titleWrapper" style={{
+ <div className={`documentView-titleWrapper${showTitleHover ? "-hover" : ""}`} style={{
position: showTextTitle ? "relative" : "absolute",
pointerEvents: SelectionManager.GetIsDragging() ? "none" : "all",
}}>
<EditableView ref={this._titleRef}
- contents={this.Document[showTitle]}
+ contents={(this.props.DataDoc || this.props.Document)[showTitle]}
display={"block"} height={72} fontSize={12}
- GetValue={() => StrCast(this.Document[showTitle])}
- SetValue={undoBatch((value: string) => (Doc.GetProto(this.Document)[showTitle] = value) ? true : true)}
+ GetValue={() => StrCast((this.props.DataDoc || this.props.Document)[showTitle])}
+ SetValue={undoBatch((value: string) => (Doc.GetProto(this.props.DataDoc || this.props.Document)[showTitle] = value) ? true : true)}
/>
</div>);
return <>
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 8e28cf928..60842bcb0 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -906,15 +906,16 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
this.tryUpdateHeight();
// see if we need to preserve the insertion point
- const prosediv = this.ProseRef ?.children ?.[0] as any;
- const keeplocation = prosediv ?.keeplocation;
+ const prosediv = this.ProseRef?.children?.[0] as any;
+ const keeplocation = prosediv?.keeplocation;
prosediv && (prosediv.keeplocation = undefined);
- const pos = this._editorView ?.state.selection.$from.pos || 1;
- keeplocation && setTimeout(() => this._editorView ?.dispatch(this._editorView ?.state.tr.setSelection(TextSelection.create(this._editorView.state.doc, pos))));
+ const pos = this._editorView?.state.selection.$from.pos || 1;
+ keeplocation && setTimeout(() => this._editorView?.dispatch(this._editorView?.state.tr.setSelection(TextSelection.create(this._editorView.state.doc, pos))));
// jump rich text menu to this textbox
- if (this._ref.current) {
- const x = Math.min(Math.max(this._ref.current!.getBoundingClientRect().left, 0), window.innerWidth - RichTextMenu.Instance.width);
+ const { current } = this._ref;
+ if (current) {
+ const x = Math.min(Math.max(current.getBoundingClientRect().left, 0), window.innerWidth - RichTextMenu.Instance.width);
const y = this._ref.current!.getBoundingClientRect().top - RichTextMenu.Instance.height - 50;
RichTextMenu.Instance.jumpTo(x, y);
}
@@ -933,13 +934,13 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
if ((this._editorView!.root as any).getSelection().isCollapsed) { // this is a hack to allow the cursor to be placed at the end of a document when the document ends in an inline dash comment. Apparently Chrome on Windows has a bug/feature which breaks this when clicking after the end of the text.
const pcords = this._editorView!.posAtCoords({ left: e.clientX, top: e.clientY });
const node = pcords && this._editorView!.state.doc.nodeAt(pcords.pos); // get what prosemirror thinks the clicked node is (if it's null, then we didn't click on any text)
- if (pcords && node ?.type === this._editorView!.state.schema.nodes.dashComment) {
+ if (pcords && node?.type === this._editorView!.state.schema.nodes.dashComment) {
this._editorView!.dispatch(this._editorView!.state.tr.setSelection(TextSelection.create(this._editorView!.state.doc, pcords.pos + 2)));
e.preventDefault();
}
if (!node && this.ProseRef) {
const lastNode = this.ProseRef.children[this.ProseRef.children.length - 1].children[this.ProseRef.children[this.ProseRef.children.length - 1].children.length - 1]; // get the last prosemirror div
- if (e.clientY > lastNode.getBoundingClientRect().bottom) { // if we clicked below the last prosemirror div, then set the selection to be the end of the document
+ if (e.clientY > lastNode?.getBoundingClientRect().bottom) { // if we clicked below the last prosemirror div, then set the selection to be the end of the document
this._editorView!.dispatch(this._editorView!.state.tr.setSelection(TextSelection.create(this._editorView!.state.doc, this._editorView!.state.doc.content.size)));
}
}
@@ -996,7 +997,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
for (let off = 1; off < 100; off++) {
const pos = this._editorView!.posAtCoords({ left: x + off, top: y });
const node = pos && this._editorView!.state.doc.nodeAt(pos.pos);
- if (node ?.type === schema.nodes.list_item) {
+ if (node?.type === schema.nodes.list_item) {
list_node = node;
break;
}
@@ -1087,7 +1088,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
}
if (e.key === "Escape") {
this._editorView!.dispatch(state.tr.setSelection(TextSelection.create(state.doc, state.selection.from, state.selection.from)));
- (document.activeElement as any).blur ?.();
+ (document.activeElement as any).blur?.();
SelectionManager.DeselectAll();
}
e.stopPropagation();
@@ -1109,7 +1110,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
@action
tryUpdateHeight(limitHeight?: number) {
- let scrollHeight = this._ref.current ?.scrollHeight;
+ let scrollHeight = this._ref.current?.scrollHeight;
if (!this.layoutDoc.animateToPos && this.layoutDoc.autoHeight && scrollHeight &&
getComputedStyle(this._ref.current!.parentElement!).top === "0px") { // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation
if (limitHeight && scrollHeight > limitHeight) {
@@ -1171,7 +1172,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
{this.props.Document.hideSidebar ? (null) : this.sidebarWidthPercent === "0%" ?
<div className="formattedTextBox-sidebar-handle" onPointerDown={this.sidebarDown} onClick={e => this.toggleSidebar()} /> :
<div className={"formattedTextBox-sidebar" + (InkingControl.Instance.selectedTool !== InkTool.None ? "-inking" : "")}
- style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${StrCast(this.extensionDoc ?.backgroundColor, "transparent")}` }}>
+ style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${StrCast(this.extensionDoc?.backgroundColor, "transparent")}` }}>
<CollectionFreeFormView {...this.props}
PanelHeight={this.props.PanelHeight}
PanelWidth={() => this.sidebarWidth}
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 09e627078..634555012 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -214,37 +214,23 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
}
_curSuffix = "_m";
- _resized = false;
- resize = (srcpath: string) => {
- requestImageSize(srcpath)
+ _resized = "";
+ resize = (imgPath: string) => {
+ requestImageSize(imgPath)
.then((size: any) => {
const rotation = NumCast(this.dataDoc.rotation) % 180;
const realsize = rotation === 90 || rotation === 270 ? { height: size.width, width: size.height } : size;
const aspect = realsize.height / realsize.width;
if (this.Document.width && (Math.abs(1 - NumCast(this.Document.height) / NumCast(this.Document.width) / (realsize.height / realsize.width)) > 0.1)) {
setTimeout(action(() => {
- this._resized = true;
- this.Document.height = this.Document[WidthSym]() * aspect;
- this.Document.nativeHeight = realsize.height;
- this.Document.nativeWidth = realsize.width;
+ if (this.pathInfos.srcpath === imgPath && (!this.layoutDoc.isTemplateDoc || this.dataDoc !== this.layoutDoc)) {
+ this._resized = imgPath;
+ this.Document.height = this.Document[WidthSym]() * aspect;
+ this.Document.nativeHeight = realsize.height;
+ this.Document.nativeWidth = realsize.width;
+ }
}), 0);
- } else this._resized = true;
- })
- .catch((err: any) => console.log(err));
- }
- fadesize = (srcpath: string) => {
- requestImageSize(srcpath)
- .then((size: any) => {
- const rotation = NumCast(this.dataDoc.rotation) % 180;
- const realsize = rotation === 90 || rotation === 270 ? { height: size.width, width: size.height } : size;
- const aspect = realsize.height / realsize.width;
- if (this.Document.width && (Math.abs(1 - NumCast(this.Document.height) / NumCast(this.Document.width) / (realsize.height / realsize.width)) > 0.1)) {
- setTimeout(action(() => {
- this.Document.height = this.Document[WidthSym]() * aspect;
- this.Document.nativeHeight = realsize.height;
- this.Document.nativeWidth = realsize.width;
- }), 0);
- }
+ } else this._resized = imgPath;
})
.catch((err: any) => console.log(err));
}
@@ -285,18 +271,16 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
return !tags ? (null) : (<img id={"google-tags"} src={"/assets/google_tags.png"} />);
}
- @computed get content() {
- TraceMobx();
- const extensionDoc = this.extensionDoc;
- if (!extensionDoc) return (null);
- // let transform = this.props.ScreenToLocalTransform().inverse();
+ @computed get nativeSize() {
const pw = typeof this.props.PanelWidth === "function" ? this.props.PanelWidth() : typeof this.props.PanelWidth === "number" ? (this.props.PanelWidth as any) as number : 50;
- // var [sptX, sptY] = transform.transformPoint(0, 0);
- // let [bptX, bptY] = transform.transformPoint(pw, this.props.PanelHeight());
- // let w = bptX - sptX;
-
const nativeWidth = (this.Document.nativeWidth || pw);
const nativeHeight = (this.Document.nativeHeight || 1);
+ return { nativeWidth, nativeHeight };
+ }
+
+ @computed get pathInfos() {
+ const extensionDoc = this.extensionDoc!;
+ const { nativeWidth, nativeHeight } = this.nativeSize;
let paths = [[Utils.CorsProxy("http://www.cs.brown.edu/~bcz/noImage.png"), nativeWidth / nativeHeight]];
// this._curSuffix = "";
// if (w > 20) {
@@ -308,15 +292,24 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
// else if (this._largeRetryCount < 10) this._curSuffix = "_l";
if (field instanceof ImageField) paths = [[this.choosePath(field.url), nativeWidth / nativeHeight]];
paths.push(...altpaths);
- // }
- const rotation = NumCast(this.Document.rotation, 0);
- const aspect = (rotation % 180) ? this.Document[HeightSym]() / this.Document[WidthSym]() : 1;
- const shift = (rotation % 180) ? (nativeHeight - nativeWidth / aspect) / 2 : 0;
const srcpath = paths[Math.min(paths.length - 1, (this.Document.curPage || 0))][0] as string;
const srcaspect = paths[Math.min(paths.length - 1, (this.Document.curPage || 0))][1] as number;
const fadepath = paths[Math.min(paths.length - 1, 1)][0] as string;
+ return { srcpath, srcaspect, fadepath };
+ }
+
+ @computed get content() {
+ TraceMobx();
+ const extensionDoc = this.extensionDoc;
+ if (!extensionDoc) return (null);
+
+ const { srcpath, srcaspect, fadepath } = this.pathInfos;
+ const { nativeWidth, nativeHeight } = this.nativeSize;
+ const rotation = NumCast(this.Document.rotation, 0);
+ const aspect = (rotation % 180) ? this.Document[HeightSym]() / this.Document[WidthSym]() : 1;
+ const shift = (rotation % 180) ? (nativeHeight - nativeWidth / aspect) / 2 : 0;
- !this.Document.ignoreAspect && !this._resized && this.resize(srcpath);
+ !this.Document.ignoreAspect && this._resized !== srcpath && this.resize(srcpath);
return <div className="imageBox-cont" key={this.props.Document[Id]} ref={this.createDropTarget} onContextMenu={this.specificContextMenu}>
<div className="imageBox-fader" >
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index 8117453e7..e0ab5d97c 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -290,14 +290,13 @@ export namespace Doc {
* @param fields the fields to project onto the target. Its type signature defines a mapping from some string key
* to a potentially undefined field, where each entry in this mapping is optional.
*/
- export function assign<K extends string>(doc: Doc, fields: Partial<Record<K, Opt<Field>>>) {
+ export function assign<K extends string>(doc: Doc, fields: Partial<Record<K, Opt<Field>>>, skipUndefineds: boolean = false) {
for (const key in fields) {
if (fields.hasOwnProperty(key)) {
const value = fields[key];
- // Do we want to filter out undefineds?
- // if (value !== undefined) {
- doc[key] = value;
- // }
+ if (!skipUndefineds || value !== undefined) { // Do we want to filter out undefineds?
+ doc[key] = value;
+ }
}
}
return doc;
@@ -406,8 +405,9 @@ export namespace Doc {
}
export function MakeAlias(doc: Doc) {
const alias = !GetT(doc, "isPrototype", "boolean", true) ? Doc.MakeCopy(doc) : Doc.MakeDelegate(doc);
- if (alias.layout instanceof Doc) {
- alias.layout = Doc.MakeAlias(alias.layout);
+ const layout = Doc.Layout(alias);
+ if (layout instanceof Doc && layout !== alias) {
+ Doc.SetLayout(alias, Doc.MakeAlias(layout));
}
const aliasNumber = Doc.GetProto(doc).aliasNumber = NumCast(Doc.GetProto(doc).aliasNumber) + 1;
alias.title = ComputedField.MakeFunction(`renameAlias(this, ${aliasNumber})`);
@@ -455,6 +455,7 @@ export namespace Doc {
if (resolvedDataDoc && Doc.WillExpandTemplateLayout(childDocLayout, resolvedDataDoc)) {
const extensionDoc = fieldExtensionDoc(resolvedDataDoc, StrCast(childDocLayout.templateField, StrCast(childDocLayout.title)));
layoutDoc = Doc.expandTemplateLayout(childDocLayout, extensionDoc !== resolvedDataDoc ? extensionDoc : undefined);
+ setTimeout(() => layoutDoc && (layoutDoc.resolvedDataDoc = resolvedDataDoc), 0);
} else layoutDoc = childDocLayout;
return { layout: layoutDoc, data: resolvedDataDoc };
}
@@ -468,7 +469,7 @@ export namespace Doc {
//
export function fieldExtensionDoc(doc: Doc, fieldKey: string) {
const extension = doc[fieldKey + "_ext"];
- if (extension === undefined) {
+ if (doc instanceof Doc && extension === undefined) {
setTimeout(() => CreateDocumentExtensionForField(doc, fieldKey), 0);
}
return extension ? extension as Doc : undefined;
@@ -572,13 +573,15 @@ export namespace Doc {
return;
}
- const layoutCustomLayout = Doc.MakeDelegate(templateDoc);
+ if ((target[targetKey] as Doc)?.proto !== templateDoc) {
+ const layoutCustomLayout = Doc.MakeDelegate(templateDoc);
- titleTarget && (Doc.GetProto(target).title = titleTarget);
- Doc.GetProto(target).type = DocumentType.TEMPLATE;
- target.onClick = templateDoc.onClick instanceof ObjectField && templateDoc.onClick[Copy]();
+ titleTarget && (Doc.GetProto(target).title = titleTarget);
+ Doc.GetProto(target).type = DocumentType.TEMPLATE;
+ target.onClick = templateDoc.onClick instanceof ObjectField && templateDoc.onClick[Copy]();
- Doc.GetProto(target)[targetKey] = layoutCustomLayout;
+ Doc.GetProto(target)[targetKey] = layoutCustomLayout;
+ }
target.layoutKey = targetKey;
return target;
}
@@ -655,6 +658,7 @@ export namespace Doc {
// the document containing the view layout information - will be the Document itself unless the Document has
// a layout field. In that case, all layout information comes from there unless overriden by Document
export function Layout(doc: Doc) { return Doc.LayoutField(doc) instanceof Doc ? doc[StrCast(doc.layoutKey, "layout")] as Doc : doc; }
+ export function SetLayout(doc: Doc, layout: Doc | string) { doc[StrCast(doc.layoutKey, "layout")] = layout; }
export function LayoutField(doc: Doc) { return doc[StrCast(doc.layoutKey, "layout")]; }
const manager = new DocData();
export function SearchQuery(): string { return manager._searchQuery; }
@@ -742,8 +746,19 @@ export namespace Doc {
source.dragFactory instanceof Doc && source.dragFactory.isTemplateDoc ? source.dragFactory :
source && source.layout instanceof Doc && source.layout.isTemplateDoc ? source.layout : undefined;
}
-}
+ export function MakeDocFilter(docFilters: string[]) {
+ let docFilterText = "";
+ for (let i = 0; i < docFilters.length; i += 3) {
+ const key = docFilters[i];
+ const value = docFilters[i + 1];
+ const modifiers = docFilters[i + 2];
+ const scriptText = `${modifiers === "x" ? "!" : ""}matchFieldValue(doc, "${key}", "${value}")`;
+ docFilterText = docFilterText ? docFilterText + " || " + scriptText : scriptText;
+ }
+ return docFilterText ? "(" + docFilterText + ")" : "";
+ }
+}
Scripting.addGlobal(function renameAlias(doc: any, n: any) { return StrCast(Doc.GetProto(doc).title).replace(/\([0-9]*\)/, "") + `(${n})`; });
Scripting.addGlobal(function getProto(doc: any) { return Doc.GetProto(doc); });
@@ -761,9 +776,31 @@ Scripting.addGlobal(function selectedDocs(container: Doc, excludeCollections: bo
const docs = DocListCast(Doc.UserDoc().SelectedDocs).filter(d => !Doc.AreProtosEqual(d, container) && !d.annotationOn && d.type !== DocumentType.DOCUMENT && d.type !== DocumentType.KVP && (!excludeCollections || !Cast(d.data, listSpec(Doc), null)));
return docs.length ? new List(docs) : prevValue;
});
-Scripting.addGlobal(function setDocFilter(container: Doc, key: string, value: any, type: string, contains: boolean = true) {
- const scriptText = `${contains ? "" : "!"}(((doc.${key} && (doc.${key} as ${type})${type === "string" ? ".includes" : "<="}(${value}))) ||
- ((doc.data_ext && doc.data_ext.${key}) && (doc.data_ext.${key} as ${type})${type === "string" ? ".includes" : "<="}(${value}))))`;
- container.docFilter = scriptText;
- container.viewSpecScript = ScriptField.MakeFunction(scriptText, { doc: Doc.name });
+Scripting.addGlobal(function matchFieldValue(doc: Doc, key: string, value: any) {
+ const fieldVal = doc[key] ? doc[key] : doc[key + "_ext"];
+ if (StrCast(fieldVal, null) !== undefined) return StrCast(fieldVal) === value;
+ if (NumCast(fieldVal, null) !== undefined) return NumCast(fieldVal) === value;
+ if (Cast(fieldVal, listSpec("string"), []).length) {
+ const vals = Cast(fieldVal, listSpec("string"), []);
+ return vals.some(v => v === value);
+ }
+ return false;
+});
+Scripting.addGlobal(function setDocFilter(container: Doc, key: string, value: any, modifiers: string) {
+ const docFilters = Cast(container.docFilter, listSpec("string"), []);
+ let found = false;
+ for (let i = 0; i < docFilters.length && !found; i += 3) {
+ if (docFilters[i] === key && docFilters[i + 1] === value) {
+ found = true;
+ docFilters.splice(i, 3);
+ }
+ }
+ if (!found || modifiers !== "none") {
+ docFilters.push(key);
+ docFilters.push(value);
+ docFilters.push(modifiers);
+ container.docFilter = new List<string>(docFilters);
+ }
+ const docFilterText = Doc.MakeDocFilter(docFilters);
+ container.viewSpecScript = docFilterText ? ScriptField.MakeFunction(docFilterText, { doc: Doc.name }) : undefined;
}); \ No newline at end of file
diff --git a/src/new_fields/documentSchemas.ts b/src/new_fields/documentSchemas.ts
index 21e69fbed..909fdc6c3 100644
--- a/src/new_fields/documentSchemas.ts
+++ b/src/new_fields/documentSchemas.ts
@@ -41,7 +41,8 @@ export const documentSchema = createSchema({
searchFields: "string", // the search fields to display when this document matches a search in its metadata
heading: "number", // the logical layout 'heading' of this document (used by rule provider to stylize h1 header elements, from h2, etc)
showCaption: "string", // whether editable caption text is overlayed at the bottom of the document
- showTitle: "string", // whether an editable title banner is displayed at tht top of the document
+ showTitle: "string", // the fieldkey whose contents should be displayed at the top of the document
+ showTitleHover: "string", // the showTitle should be shown only on hover
isButton: "boolean", // whether document functions as a button (overiding native interactions of its content)
ignoreClick: "boolean", // whether documents ignores input clicks (but does not ignore manipulation and other events)
isAnimating: "string", // whether the document is in the midst of animating between two layouts (used by icons to de/iconify documents). value is undefined|"min"|"max"
@@ -49,6 +50,9 @@ export const documentSchema = createSchema({
scrollToLinkID: "string", // id of link being traversed. allows this doc to scroll/highlight/etc its link anchor. scrollToLinkID should be set to undefined by this doc after it sets up its scroll,etc.
strokeWidth: "number",
fontSize: "string",
+ fitToBox: "boolean", // whether freeform view contents should be zoomed/panned to fill the area of the document view
+ xPadding: "number", // pixels of padding on left/right of collectionfreeformview contents when fitToBox is set
+ yPadding: "number", // pixels of padding on left/right of collectionfreeformview contents when fitToBox is set
LODarea: "number", // area (width*height) where CollectionFreeFormViews switch from a label to rendering contents
LODdisable: "boolean", // whether to disbale LOD switching for CollectionFreeFormViews
});
diff --git a/src/pen-gestures/ndollar.ts b/src/pen-gestures/ndollar.ts
index 872c524d6..ef5ca38c6 100644
--- a/src/pen-gestures/ndollar.ts
+++ b/src/pen-gestures/ndollar.ts
@@ -257,16 +257,16 @@ export class NDollarRecognizer {
{
if (!requireSameNoOfStrokes || strokes.length === this.Multistrokes[i].NumStrokes) // optional -- only attempt match when same # of component strokes
{
- for (var j = 0; j < this.Multistrokes[i].Unistrokes.length; j++) // for each unistroke within this multistroke
+ for (const unistroke of this.Multistrokes[i].Unistrokes) // for each unistroke within this multistroke
{
- if (AngleBetweenUnitVectors(candidate.StartUnitVector, this.Multistrokes[i].Unistrokes[j].StartUnitVector) <= AngleSimilarityThreshold) // strokes start in the same direction
+ if (AngleBetweenUnitVectors(candidate.StartUnitVector, unistroke.StartUnitVector) <= AngleSimilarityThreshold) // strokes start in the same direction
{
var d;
if (useProtractor) {
- d = OptimalCosineDistance(this.Multistrokes[i].Unistrokes[j].Vector, candidate.Vector); // Protractor
+ d = OptimalCosineDistance(unistroke.Vector, candidate.Vector); // Protractor
}
else {
- d = DistanceAtBestAngle(candidate.Points, this.Multistrokes[i].Unistrokes[j], -AngleRange, +AngleRange, AnglePrecision); // Golden Section Search (original $N)
+ d = DistanceAtBestAngle(candidate.Points, unistroke, -AngleRange, +AngleRange, AnglePrecision); // Golden Section Search (original $N)
}
if (d < b) {
b = d; // best (least) distance
@@ -283,8 +283,8 @@ export class NDollarRecognizer {
AddGesture = (name: string, useBoundedRotationInvariance: boolean, strokes: any[]) => {
this.Multistrokes[this.Multistrokes.length] = new Multistroke(name, useBoundedRotationInvariance, strokes);
var num = 0;
- for (var i = 0; i < this.Multistrokes.length; i++) {
- if (this.Multistrokes[i].Name === name) {
+ for (const multistroke of this.Multistrokes) {
+ if (multistroke.Name === name) {
num++;
}
}
@@ -322,20 +322,20 @@ function HeapPermute(n: number, order: any[], /*out*/ orders: any[]) {
function MakeUnistrokes(strokes: any, orders: any) {
const unistrokes = new Array(); // array of point arrays
- for (var r = 0; r < orders.length; r++) {
- for (var b = 0; b < Math.pow(2, orders[r].length); b++) // use b's bits for directions
+ for (const order of orders) {
+ for (var b = 0; b < Math.pow(2, order.length); b++) // use b's bits for directions
{
const unistroke = new Array(); // array of points
- for (var i = 0; i < orders[r].length; i++) {
+ for (var i = 0; i < order.length; i++) {
var pts;
if (((b >> i) & 1) === 1) {// is b's bit at index i on?
- pts = strokes[orders[r][i]].slice().reverse(); // copy and reverse
+ pts = strokes[order[i]].slice().reverse(); // copy and reverse
}
else {
- pts = strokes[orders[r][i]].slice(); // copy
+ pts = strokes[order[i]].slice(); // copy
}
- for (var p = 0; p < pts.length; p++) {
- unistroke[unistroke.length] = pts[p]; // append points
+ for (const point of pts) {
+ unistroke[unistroke.length] = point; // append points
}
}
unistrokes[unistrokes.length] = unistroke; // add one unistroke to set
@@ -346,9 +346,9 @@ function MakeUnistrokes(strokes: any, orders: any) {
function CombineStrokes(strokes: any) {
const points = new Array();
- for (var s = 0; s < strokes.length; s++) {
- for (var p = 0; p < strokes[s].length; p++) {
- points[points.length] = new Point(strokes[s][p].X, strokes[s][p].Y);
+ for (const stroke of strokes) {
+ for (const { X, Y } of stroke) {
+ points[points.length] = new Point(X, Y);
}
}
return points;
@@ -384,9 +384,9 @@ function RotateBy(points: any, radians: any) // rotates points around centroid
const cos = Math.cos(radians);
const sin = Math.sin(radians);
const newpoints = new Array();
- for (var i = 0; i < points.length; i++) {
- const qx = (points[i].X - c.X) * cos - (points[i].Y - c.Y) * sin + c.X;
- const qy = (points[i].X - c.X) * sin + (points[i].Y - c.Y) * cos + c.Y;
+ for (const point of points) {
+ const qx = (point.X - c.X) * cos - (point.Y - c.Y) * sin + c.X;
+ const qy = (point.X - c.X) * sin + (point.Y - c.Y) * cos + c.Y;
newpoints[newpoints.length] = new Point(qx, qy);
}
return newpoints;
@@ -396,9 +396,9 @@ function ScaleDimTo(points: any, size: any, ratio1D: any) // scales bbox uniform
const B = BoundingBox(points);
const uniformly = Math.min(B.Width / B.Height, B.Height / B.Width) <= ratio1D; // 1D or 2D gesture test
const newpoints = new Array();
- for (var i = 0; i < points.length; i++) {
- const qx = uniformly ? points[i].X * (size / Math.max(B.Width, B.Height)) : points[i].X * (size / B.Width);
- const qy = uniformly ? points[i].Y * (size / Math.max(B.Width, B.Height)) : points[i].Y * (size / B.Height);
+ for (const { X, Y } of points) {
+ const qx = uniformly ? X * (size / Math.max(B.Width, B.Height)) : X * (size / B.Width);
+ const qy = uniformly ? Y * (size / Math.max(B.Width, B.Height)) : Y * (size / B.Height);
newpoints[newpoints.length] = new Point(qx, qy);
}
return newpoints;
@@ -407,9 +407,9 @@ function TranslateTo(points: any, pt: any) // translates points' centroid
{
const c = Centroid(points);
const newpoints = new Array();
- for (var i = 0; i < points.length; i++) {
- const qx = points[i].X + pt.X - c.X;
- const qy = points[i].Y + pt.Y - c.Y;
+ for (const { X, Y } of points) {
+ const qx = X + pt.X - c.X;
+ const qy = Y + pt.Y - c.Y;
newpoints[newpoints.length] = new Point(qx, qy);
}
return newpoints;
@@ -478,9 +478,9 @@ function DistanceAtAngle(points: any, T: any, radians: any) {
}
function Centroid(points: any) {
var x = 0.0, y = 0.0;
- for (var i = 0; i < points.length; i++) {
- x += points[i].X;
- y += points[i].Y;
+ for (const point of points) {
+ x += point.X;
+ y += point.Y;
}
x /= points.length;
y /= points.length;
@@ -488,11 +488,11 @@ function Centroid(points: any) {
}
function BoundingBox(points: any) {
var minX = +Infinity, maxX = -Infinity, minY = +Infinity, maxY = -Infinity;
- for (var i = 0; i < points.length; i++) {
- minX = Math.min(minX, points[i].X);
- minY = Math.min(minY, points[i].Y);
- maxX = Math.max(maxX, points[i].X);
- maxY = Math.max(maxY, points[i].Y);
+ for (const { X, Y } of points) {
+ minX = Math.min(minX, X);
+ minY = Math.min(minY, Y);
+ maxX = Math.max(maxX, X);
+ maxY = Math.max(maxY, Y);
}
return new Rectangle(minX, minY, maxX - minX, maxY - minY);
}
diff --git a/src/server/ApiManagers/SessionManager.ts b/src/server/ApiManagers/SessionManager.ts
index 21103fdd5..a99aa05e0 100644
--- a/src/server/ApiManagers/SessionManager.ts
+++ b/src/server/ApiManagers/SessionManager.ts
@@ -8,16 +8,16 @@ const permissionError = "You are not authorized!";
export default class SessionManager extends ApiManager {
- private secureSubscriber = (root: string, ...params: string[]) => new RouteSubscriber(root).add("password", ...params);
+ private secureSubscriber = (root: string, ...params: string[]) => new RouteSubscriber(root).add("sessionKey", ...params);
private authorizedAction = (handler: SecureHandler) => {
return (core: AuthorizedCore) => {
const { req, res, isRelease } = core;
- const { password } = req.params;
+ const { sessionKey } = req.params;
if (!isRelease) {
return res.send("This can be run only on the release server.");
}
- if (password !== process.env.session_key) {
+ if (sessionKey !== process.env.session_key) {
return _permission_denied(res, permissionError);
}
return handler(core);
@@ -28,20 +28,11 @@ export default class SessionManager extends ApiManager {
register({
method: Method.GET,
- subscription: this.secureSubscriber("debug", "mode", "recipient?"),
- secureHandler: this.authorizedAction(async ({ req, res }) => {
- const { mode } = req.params;
- if (["passive", "active"].includes(mode)) {
- const recipient = req.params.recipient || DashSessionAgent.notificationRecipient;
- const response = await sessionAgent.serverWorker.sendMonitorAction("debug", { mode, recipient }, true);
- if (response instanceof Error) {
- res.send(response);
- } else {
- res.send(`Your request was successful: the server ${mode === "active" ? "created and compressed a new" : "retrieved and compressed the most recent"} back up. It was sent to ${recipient}.`);
- }
- } else {
- res.send(`Your request failed. '${mode}' is not a valid mode: please choose either 'active' or 'passive'`);
- }
+ subscription: this.secureSubscriber("debug", "to?"),
+ secureHandler: this.authorizedAction(async ({ req: { params }, res }) => {
+ const to = params.to || DashSessionAgent.notificationRecipient;
+ const { error } = await sessionAgent.serverWorker.emit("debug", { to });
+ res.send(error ? error.message : `Your request was successful: the server captured and compressed (but did not save) a new back up. It was sent to ${to}.`);
})
});
@@ -49,12 +40,8 @@ export default class SessionManager extends ApiManager {
method: Method.GET,
subscription: this.secureSubscriber("backup"),
secureHandler: this.authorizedAction(async ({ res }) => {
- const response = await sessionAgent.serverWorker.sendMonitorAction("backup");
- if (response instanceof Error) {
- res.send(response);
- } else {
- res.send("Your request was successful: the server successfully created a new back up.");
- }
+ const { error } = await sessionAgent.serverWorker.emit("backup");
+ res.send(error ? error.message : "Your request was successful: the server successfully created a new back up.");
})
});
diff --git a/src/server/DashSession/DashSessionAgent.ts b/src/server/DashSession/DashSessionAgent.ts
index f3f0a3c3d..c55e01243 100644
--- a/src/server/DashSession/DashSessionAgent.ts
+++ b/src/server/DashSession/DashSessionAgent.ts
@@ -5,12 +5,11 @@ import { Utils } from "../../Utils";
import { WebSocket } from "../Websocket/Websocket";
import { MessageStore } from "../Message";
import { launchServer, onWindows } from "..";
-import { existsSync, mkdirSync, readdirSync, statSync, createWriteStream, readFileSync } from "fs";
+import { readdirSync, statSync, createWriteStream, readFileSync, unlinkSync } from "fs";
import * as Archiver from "archiver";
import { resolve } from "path";
-import { AppliedSessionAgent, ExitHandler } from "../session/agents/applied_session_agent";
-import { Monitor } from "../session/agents/monitor";
-import { ServerWorker } from "../session/agents/server_worker";
+import { AppliedSessionAgent, MessageHandler, ExitHandler, Monitor, ServerWorker } from "resilient-server-session";
+import rimraf = require("rimraf");
/**
* If we're the monitor (master) thread, we should launch the monitor logic for the session.
@@ -26,22 +25,22 @@ export class DashSessionAgent extends AppliedSessionAgent {
* The core method invoked when the single master thread is initialized.
* Installs event hooks, repl commands and additional IPC listeners.
*/
- protected async initializeMonitor(monitor: Monitor) {
+ protected async initializeMonitor(monitor: Monitor, sessionKey: string): Promise<void> {
+ await this.dispatchSessionPassword(sessionKey);
monitor.addReplCommand("pull", [], () => monitor.exec("git pull"));
monitor.addReplCommand("solr", [/start|stop|index/], this.executeSolrCommand);
monitor.addReplCommand("backup", [], this.backup);
- monitor.addReplCommand("debug", [/active|passive/, /\S+\@\S+/], async ([mode, recipient]) => this.dispatchZippedDebugBackup(mode, recipient));
- monitor.addServerMessageListener("backup", this.backup);
- monitor.addServerMessageListener("debug", ({ args: { mode, recipient } }) => this.dispatchZippedDebugBackup(mode, recipient));
- monitor.on(Monitor.IntrinsicEvents.KeyGenerated, this.dispatchSessionPassword);
- monitor.on(Monitor.IntrinsicEvents.CrashDetected, this.dispatchCrashReport);
+ monitor.addReplCommand("debug", [/\S+\@\S+/], async ([to]) => this.dispatchZippedDebugBackup(to));
+ monitor.on("backup", this.backup);
+ monitor.on("debug", async ({ to }) => this.dispatchZippedDebugBackup(to));
+ monitor.coreHooks.onCrashDetected(this.dispatchCrashReport);
}
/**
* The core method invoked when a server worker thread is initialized.
* Installs logic to be executed when the server worker dies.
*/
- protected async initializeServerWorker() {
+ protected async initializeServerWorker(): Promise<ServerWorker> {
const worker = ServerWorker.Create(launchServer); // server initialization delegated to worker
worker.addExitHandler(this.notifyClient);
return worker;
@@ -51,7 +50,7 @@ export class DashSessionAgent extends AppliedSessionAgent {
* Prepares the body of the email with instructions on restoring the transmitted remote database backup locally.
*/
private _remoteDebugInstructions: string | undefined;
- private generateDebugInstructions = (zipName: string, target: string) => {
+ private generateDebugInstructions = (zipName: string, target: string): string => {
if (!this._remoteDebugInstructions) {
this._remoteDebugInstructions = readFileSync(resolve(__dirname, "./templates/remote_debug_instructions.txt"), { encoding: "utf8" });
}
@@ -65,7 +64,7 @@ export class DashSessionAgent extends AppliedSessionAgent {
* Prepares the body of the email with information regarding a crash event.
*/
private _crashInstructions: string | undefined;
- private generateCrashInstructions({ name, message, stack }: Error) {
+ private generateCrashInstructions({ name, message, stack }: Error): string {
if (!this._crashInstructions) {
this._crashInstructions = readFileSync(resolve(__dirname, "./templates/crash_instructions.txt"), { encoding: "utf8" });
}
@@ -80,14 +79,18 @@ export class DashSessionAgent extends AppliedSessionAgent {
* This sends a pseudorandomly generated guid to the configuration's recipients, allowing them alone
* to kill the server via the /kill/:key route.
*/
- private dispatchSessionPassword = async (key: string) => {
+ private dispatchSessionPassword = async (sessionKey: string): Promise<void> => {
const { mainLog } = this.sessionMonitor;
const { notificationRecipient } = DashSessionAgent;
mainLog(green("dispatching session key..."));
const error = await Email.dispatch({
to: notificationRecipient,
subject: "Dash Release Session Admin Authentication Key",
- content: `The key for this session (started @ ${new Date().toUTCString()}) is ${key}.\n\n${this.signature}`
+ content: [
+ `Here's the key for this session (started @ ${new Date().toUTCString()}):`,
+ sessionKey,
+ this.signature
+ ].join("\n\n")
});
if (error) {
this.sessionMonitor.mainLog(red(`dispatch failure @ ${notificationRecipient} (${yellow(error.message)})`));
@@ -100,7 +103,7 @@ export class DashSessionAgent extends AppliedSessionAgent {
/**
* This sends an email with the generated crash report.
*/
- private dispatchCrashReport = async (crashCause: Error) => {
+ private dispatchCrashReport: MessageHandler<{ error: Error }> = async ({ error: crashCause }) => {
const { mainLog } = this.sessionMonitor;
const { notificationRecipient } = DashSessionAgent;
const error = await Email.dispatch({
@@ -109,7 +112,7 @@ export class DashSessionAgent extends AppliedSessionAgent {
content: this.generateCrashInstructions(crashCause)
});
if (error) {
- this.sessionMonitor.mainLog(red(`dispatch failure @ ${notificationRecipient} (${yellow(error.message)})`));
+ this.sessionMonitor.mainLog(red(`dispatch failure @ ${notificationRecipient} ${yellow(`(${error.message})`)}`));
mainLog(red("distribution of crash notification experienced errors"));
} else {
mainLog(green("successfully distributed crash notification to recipients"));
@@ -120,7 +123,7 @@ export class DashSessionAgent extends AppliedSessionAgent {
* Logic for interfacing with Solr. Either starts it,
* stops it, or rebuilds its indicies.
*/
- private executeSolrCommand = async (args: string[]) => {
+ private executeSolrCommand = async (args: string[]): Promise<void> => {
const { exec, mainLog } = this.sessionMonitor;
const action = args[0];
if (action === "index") {
@@ -153,7 +156,7 @@ export class DashSessionAgent extends AppliedSessionAgent {
* Performs a backup of the database, saved to the desktop subdirectory.
* This should work as is only on our specific release server.
*/
- private backup = async () => this.sessionMonitor.exec("backup.bat", { cwd: this.releaseDesktop });
+ private backup = async (): Promise<void> => this.sessionMonitor.exec("backup.bat", { cwd: this.releaseDesktop });
/**
* Compress either a brand new backup or the most recent backup and send it
@@ -161,21 +164,14 @@ export class DashSessionAgent extends AppliedSessionAgent {
* @param mode specifies whether or not to make a new backup before exporting
* @param to the recipient of the email
*/
- private async dispatchZippedDebugBackup(mode: string, to: string) {
+ private async dispatchZippedDebugBackup(to: string): Promise<void> {
const { mainLog } = this.sessionMonitor;
try {
// if desired, complete an immediate backup to send
- if (mode === "active") {
- await this.backup();
- mainLog("backup complete");
- }
+ await this.backup();
+ mainLog("backup complete");
- // ensure the directory for compressed backups exists
const backupsDirectory = `${this.releaseDesktop}/backups`;
- const compressedDirectory = `${this.releaseDesktop}/compressed`;
- if (!existsSync(compressedDirectory)) {
- mkdirSync(compressedDirectory);
- }
// sort all backups by their modified time, and choose the most recent one
const target = readdirSync(backupsDirectory).map(filename => ({
@@ -186,11 +182,12 @@ export class DashSessionAgent extends AppliedSessionAgent {
// create a zip file and to it, write the contents of the backup directory
const zipName = `${target}.zip`;
- const zipPath = `${compressedDirectory}/${zipName}`;
+ const zipPath = `${this.releaseDesktop}/${zipName}`;
+ const targetPath = `${backupsDirectory}/${target}`;
const output = createWriteStream(zipPath);
const zip = Archiver('zip');
zip.pipe(output);
- zip.directory(`${backupsDirectory}/${target}/Dash`, false);
+ zip.directory(`${targetPath}/Dash`, false);
await zip.finalize();
mainLog(`zip finalized with size ${statSync(zipPath).size} bytes, saved to ${zipPath}`);
@@ -202,6 +199,12 @@ export class DashSessionAgent extends AppliedSessionAgent {
attachments: [{ filename: zipName, path: zipPath }]
});
+ // since this is intended to be a zero-footprint operation, clean up
+ // by unlinking both the backup generated earlier in the function and the compressed zip file.
+ // to generate a persistent backup, just run backup.
+ unlinkSync(zipPath);
+ rimraf.sync(targetPath);
+
// indicate success or failure
mainLog(`${error === null ? green("successfully dispatched") : red("failed to dispatch")} ${zipName} to ${cyan(to)}`);
error && mainLog(red(error.message));
diff --git a/src/server/IDatabase.ts b/src/server/IDatabase.ts
new file mode 100644
index 000000000..6a63df485
--- /dev/null
+++ b/src/server/IDatabase.ts
@@ -0,0 +1,24 @@
+import * as mongodb from 'mongodb';
+import { Transferable } from './Message';
+
+export const DocumentsCollection = 'documents';
+export const NewDocumentsCollection = 'newDocuments';
+export interface IDatabase {
+ update(id: string, value: any, callback: (err: mongodb.MongoError, res: mongodb.UpdateWriteOpResult) => void, upsert?: boolean, collectionName?: string): Promise<void>;
+ updateMany(query: any, update: any, collectionName?: string): Promise<mongodb.WriteOpResult>;
+
+ replace(id: string, value: any, callback: (err: mongodb.MongoError, res: mongodb.UpdateWriteOpResult) => void, upsert?: boolean, collectionName?: string): void;
+
+ delete(query: any, collectionName?: string): Promise<mongodb.DeleteWriteOpResultObject>;
+ delete(id: string, collectionName?: string): Promise<mongodb.DeleteWriteOpResultObject>;
+
+ deleteAll(collectionName?: string, persist?: boolean): Promise<any>;
+
+ insert(value: any, collectionName?: string): Promise<void>;
+
+ getDocument(id: string, fn: (result?: Transferable) => void, collectionName?: string): void;
+ getDocuments(ids: string[], fn: (result: Transferable[]) => void, collectionName?: string): void;
+ visit(ids: string[], fn: (result: any) => string[] | Promise<string[]>, collectionName?: string): Promise<void>;
+
+ query(query: { [key: string]: any }, projection?: { [key: string]: 0 | 1 }, collectionName?: string): Promise<mongodb.Cursor>;
+}
diff --git a/src/server/MemoryDatabase.ts b/src/server/MemoryDatabase.ts
new file mode 100644
index 000000000..543f96e7f
--- /dev/null
+++ b/src/server/MemoryDatabase.ts
@@ -0,0 +1,100 @@
+import { IDatabase, DocumentsCollection, NewDocumentsCollection } from './IDatabase';
+import { Transferable } from './Message';
+import * as mongodb from 'mongodb';
+
+export class MemoryDatabase implements IDatabase {
+
+ private db: { [collectionName: string]: { [id: string]: any } } = {};
+
+ private getCollection(collectionName: string) {
+ const collection = this.db[collectionName];
+ if (collection) {
+ return collection;
+ } else {
+ return this.db[collectionName] = {};
+ }
+ }
+
+ public update(id: string, value: any, callback: (err: mongodb.MongoError, res: mongodb.UpdateWriteOpResult) => void, _upsert?: boolean, collectionName = DocumentsCollection): Promise<void> {
+ const collection = this.getCollection(collectionName);
+ const set = "$set";
+ if (set in value) {
+ let currentVal = collection[id] ?? (collection[id] = {});
+ const val = value[set];
+ for (const key in val) {
+ const keys = key.split(".");
+ for (let i = 0; i < keys.length - 1; i++) {
+ const k = keys[i];
+ if (typeof currentVal[k] === "object") {
+ currentVal = currentVal[k];
+ } else {
+ currentVal[k] = {};
+ currentVal = currentVal[k];
+ }
+ }
+ currentVal[keys[keys.length - 1]] = val[key];
+ }
+ } else {
+ collection[id] = value;
+ }
+ callback(null as any, {} as any);
+ return Promise.resolve(undefined);
+ }
+
+ public updateMany(query: any, update: any, collectionName = NewDocumentsCollection): Promise<mongodb.WriteOpResult> {
+ throw new Error("Can't updateMany a MemoryDatabase");
+ }
+
+ public replace(id: string, value: any, callback: (err: mongodb.MongoError, res: mongodb.UpdateWriteOpResult) => void, upsert?: boolean, collectionName = DocumentsCollection): void {
+ this.update(id, value, callback, upsert, collectionName);
+ }
+
+ public delete(query: any, collectionName?: string): Promise<mongodb.DeleteWriteOpResultObject>;
+ public delete(id: string, collectionName?: string): Promise<mongodb.DeleteWriteOpResultObject>;
+ public delete(id: any, collectionName = DocumentsCollection): Promise<mongodb.DeleteWriteOpResultObject> {
+ const i = id.id ?? id;
+ delete this.getCollection(collectionName)[i];
+
+ return Promise.resolve({} as any);
+ }
+
+ public deleteAll(collectionName = DocumentsCollection, _persist = true): Promise<any> {
+ delete this.db[collectionName];
+ return Promise.resolve();
+ }
+
+ public insert(value: any, collectionName = DocumentsCollection): Promise<void> {
+ const id = value.id;
+ this.getCollection(collectionName)[id] = value;
+ return Promise.resolve();
+ }
+
+ public getDocument(id: string, fn: (result?: Transferable) => void, collectionName = NewDocumentsCollection): void {
+ fn(this.getCollection(collectionName)[id]);
+ }
+ public getDocuments(ids: string[], fn: (result: Transferable[]) => void, collectionName = DocumentsCollection): void {
+ fn(ids.map(id => this.getCollection(collectionName)[id]));
+ }
+
+ public async visit(ids: string[], fn: (result: any) => string[] | Promise<string[]>, collectionName = NewDocumentsCollection): Promise<void> {
+ const visited = new Set<string>();
+ while (ids.length) {
+ const count = Math.min(ids.length, 1000);
+ const index = ids.length - count;
+ const fetchIds = ids.splice(index, count).filter(id => !visited.has(id));
+ if (!fetchIds.length) {
+ continue;
+ }
+ const docs = await new Promise<{ [key: string]: any }[]>(res => this.getDocuments(fetchIds, res, collectionName));
+ for (const doc of docs) {
+ const id = doc.id;
+ visited.add(id);
+ ids.push(...(await fn(doc)));
+ }
+ }
+ }
+
+ public query(): Promise<mongodb.Cursor> {
+ throw new Error("Can't query a MemoryDatabase");
+ }
+}
diff --git a/src/server/RouteManager.ts b/src/server/RouteManager.ts
index a8ad81bf7..5afd607fd 100644
--- a/src/server/RouteManager.ts
+++ b/src/server/RouteManager.ts
@@ -68,7 +68,7 @@ export default class RouteManager {
console.log('please remove all duplicate routes before continuing');
}
if (malformedCount) {
- console.log(`please ensure all routes adhere to ^\/$|^\/[A-Za-z]+(\/\:[A-Za-z]+)*$`);
+ console.log(`please ensure all routes adhere to ^\/$|^\/[A-Za-z]+(\/\:[A-Za-z?]+)*$`);
}
process.exit(1);
} else {
@@ -86,7 +86,11 @@ export default class RouteManager {
const { method, subscription, secureHandler: onValidation, publicHandler: onUnauthenticated, errorHandler: onError } = initializer;
const isRelease = this._isRelease;
const supervised = async (req: express.Request, res: express.Response) => {
- const { user, originalUrl: target } = req;
+ let { user } = req;
+ const { originalUrl: target } = req;
+ if (process.env.DB === "MEM" && !user) {
+ user = { id: "guest", email: "", userDocumentId: "guestDocId" };
+ }
const core = { req, res, isRelease };
const tryExecute = async (toExecute: (args: any) => any | Promise<any>, args: any) => {
try {
@@ -128,7 +132,7 @@ export default class RouteManager {
} else {
route = subscriber.build;
}
- if (!/^\/$|^\/[A-Za-z]+(\/\:[A-Za-z]+)*$/g.test(route)) {
+ if (!/^\/$|^\/[A-Za-z]+(\/\:[A-Za-z?]+)*$/g.test(route)) {
this.failedRegistrations.push({
reason: RegistrationError.Malformed,
route
diff --git a/src/server/Websocket/Websocket.ts b/src/server/Websocket/Websocket.ts
index 578147d60..6dda6956e 100644
--- a/src/server/Websocket/Websocket.ts
+++ b/src/server/Websocket/Websocket.ts
@@ -28,7 +28,7 @@ export namespace WebSocket {
function initialize(isRelease: boolean) {
const endpoint = io();
- endpoint.on("connection", function (socket: Socket) {
+ endpoint.on("connection", function(socket: Socket) {
_socket = socket;
socket.use((_packet, next) => {
@@ -83,7 +83,9 @@ export namespace WebSocket {
export async function deleteFields() {
await Database.Instance.deleteAll();
- await Search.clear();
+ if (process.env.DISABLE_SEARCH !== "true") {
+ await Search.clear();
+ }
await Database.Instance.deleteAll('newDocuments');
}
@@ -92,7 +94,9 @@ export namespace WebSocket {
await Database.Instance.deleteAll('newDocuments');
await Database.Instance.deleteAll('sessions');
await Database.Instance.deleteAll('users');
- await Search.clear();
+ if (process.env.DISABLE_SEARCH !== "true") {
+ await Search.clear();
+ }
}
function barReceived(socket: SocketIO.Socket, userEmail: string) {
diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts
index 220c37e2b..36d4cd2f2 100644
--- a/src/server/authentication/models/current_user_utils.ts
+++ b/src/server/authentication/models/current_user_utils.ts
@@ -322,4 +322,4 @@ export class CurrentUserUtils {
};
return recurs([] as Attribute[], schema ? schema.rootAttributeGroup : undefined);
}
-} \ No newline at end of file
+}
diff --git a/src/server/database.ts b/src/server/database.ts
index 6e0771c11..83ce865c6 100644
--- a/src/server/database.ts
+++ b/src/server/database.ts
@@ -5,6 +5,8 @@ import { Utils, emptyFunction } from '../Utils';
import { DashUploadUtils } from './DashUploadUtils';
import { Credentials } from 'google-auth-library';
import { GoogleApiServerUtils } from './apis/google/GoogleApiServerUtils';
+import { IDatabase } from './IDatabase';
+import { MemoryDatabase } from './MemoryDatabase';
import * as mongoose from 'mongoose';
export namespace Database {
@@ -44,7 +46,7 @@ export namespace Database {
}
}
- class Database {
+ class Database implements IDatabase {
public static DocumentsCollection = 'documents';
private MongoClient = mongodb.MongoClient;
private currentWrites: { [id: string]: Promise<void> } = {};
@@ -215,7 +217,7 @@ export namespace Database {
if (!fetchIds.length) {
continue;
}
- const docs = await new Promise<{ [key: string]: any }[]>(res => Instance.getDocuments(fetchIds, res, "newDocuments"));
+ const docs = await new Promise<{ [key: string]: any }[]>(res => this.getDocuments(fetchIds, res, collectionName));
for (const doc of docs) {
const id = doc.id;
visited.add(id);
@@ -262,7 +264,16 @@ export namespace Database {
}
}
- export const Instance = new Database();
+ function getDatabase() {
+ switch (process.env.DB) {
+ case "MEM":
+ return new MemoryDatabase();
+ default:
+ return new Database();
+ }
+ }
+
+ export const Instance: IDatabase = getDatabase();
export namespace Auxiliary {
@@ -331,4 +342,4 @@ export namespace Database {
}
-} \ No newline at end of file
+}
diff --git a/src/server/index.ts b/src/server/index.ts
index 0cce0dc54..313a2f0e2 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -5,7 +5,7 @@ import * as path from 'path';
import { Database } from './database';
import { DashUploadUtils } from './DashUploadUtils';
import RouteSubscriber from './RouteSubscriber';
-import initializeServer from './server_initialization';
+import initializeServer from './server_Initialization';
import RouteManager, { Method, _success, _permission_denied, _error, _invalid, PublicHandler } from './RouteManager';
import * as qs from 'query-string';
import UtilManager from './ApiManagers/UtilManager';
@@ -24,7 +24,7 @@ import { Logger } from "./ProcessFactory";
import { yellow } from "colors";
import { DashSessionAgent } from "./DashSession/DashSessionAgent";
import SessionManager from "./ApiManagers/SessionManager";
-import { AppliedSessionAgent } from "./session/agents/applied_session_agent";
+import { AppliedSessionAgent } from "resilient-server-session";
export const onWindows = process.platform === "win32";
export let sessionAgent: AppliedSessionAgent;
@@ -41,11 +41,13 @@ async function preliminaryFunctions() {
await GoogleCredentialsLoader.loadCredentials();
GoogleApiServerUtils.processProjectCredentials();
await DashUploadUtils.buildFileDirectories();
- await log_execution({
- startMessage: "attempting to initialize mongodb connection",
- endMessage: "connection outcome determined",
- action: Database.tryInitializeConnection
- });
+ if (process.env.DB !== "MEM") {
+ await log_execution({
+ startMessage: "attempting to initialize mongodb connection",
+ endMessage: "connection outcome determined",
+ action: Database.tryInitializeConnection
+ });
+ }
}
/**
@@ -142,4 +144,4 @@ if (process.env.RELEASE) {
(sessionAgent = new DashSessionAgent()).launch();
} else {
launchServer();
-} \ No newline at end of file
+}
diff --git a/src/server/server_Initialization.ts b/src/server/server_Initialization.ts
index cbe070293..9f67c1dda 100644
--- a/src/server/server_Initialization.ts
+++ b/src/server/server_Initialization.ts
@@ -86,7 +86,7 @@ function buildWithMiddleware(server: express.Express) {
resave: true,
cookie: { maxAge: week },
saveUninitialized: true,
- store: new MongoStore({ url: Database.url })
+ store: process.env.DB === "MEM" ? new session.MemoryStore() : new MongoStore({ url: Database.url })
}),
flash(),
expressFlash(),
@@ -152,4 +152,4 @@ function registerCorsProxy(server: express.Express) {
});
}).pipe(res);
});
-} \ No newline at end of file
+}
diff --git a/src/server/session/README.txt b/src/server/session/README.txt
deleted file mode 100644
index ac7d3d4e7..000000000
--- a/src/server/session/README.txt
+++ /dev/null
@@ -1,11 +0,0 @@
-/**
- * These abstractions rely on NodeJS's cluster module, which allows a parent (master) process to share
- * code with its children (workers). A simple `isMaster` flag indicates who is trying to access
- * the code, and thus determines the functionality that actually gets invoked (checked by the caller, not internally).
- *
- * Think of the master thread as a factory, and the workers as the helpers that actually run the server.
- *
- * So, when we run `npm start`, given the appropriate check, initializeMaster() is called in the parent process
- * This will spawn off its own child process (by default, mirrors the execution path of its parent),
- * in which initializeWorker() is invoked.
- */ \ No newline at end of file
diff --git a/src/server/session/agents/applied_session_agent.ts b/src/server/session/agents/applied_session_agent.ts
deleted file mode 100644
index 53293d3bf..000000000
--- a/src/server/session/agents/applied_session_agent.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-import { isMaster } from "cluster";
-import { Monitor } from "./monitor";
-import { ServerWorker } from "./server_worker";
-
-export type ExitHandler = (reason: Error | boolean) => void | Promise<void>;
-
-export abstract class AppliedSessionAgent {
-
- // the following two methods allow the developer to create a custom
- // session and use the built in customization options for each thread
- protected abstract async initializeMonitor(monitor: Monitor): Promise<void>;
- protected abstract async initializeServerWorker(): Promise<ServerWorker>;
-
- private launched = false;
-
- public killSession = (reason: string, graceful = true, errorCode = 0) => {
- const target = isMaster ? this.sessionMonitor : this.serverWorker;
- target.killSession(reason, graceful, errorCode);
- }
-
- private sessionMonitorRef: Monitor | undefined;
- public get sessionMonitor(): Monitor {
- if (!isMaster) {
- this.serverWorker.sendMonitorAction("kill", {
- graceful: false,
- reason: "Cannot access the session monitor directly from the server worker thread.",
- errorCode: 1
- });
- throw new Error();
- }
- return this.sessionMonitorRef!;
- }
-
- private serverWorkerRef: ServerWorker | undefined;
- public get serverWorker(): ServerWorker {
- if (isMaster) {
- throw new Error("Cannot access the server worker directly from the session monitor thread");
- }
- return this.serverWorkerRef!;
- }
-
- public async launch(): Promise<void> {
- if (!this.launched) {
- this.launched = true;
- if (isMaster) {
- await this.initializeMonitor(this.sessionMonitorRef = Monitor.Create());
- this.sessionMonitorRef.finalize();
- } else {
- this.serverWorkerRef = await this.initializeServerWorker();
- }
- } else {
- throw new Error("Cannot launch a session thread more than once per process.");
- }
- }
-
-} \ No newline at end of file
diff --git a/src/server/session/agents/monitor.ts b/src/server/session/agents/monitor.ts
deleted file mode 100644
index e1709f5e6..000000000
--- a/src/server/session/agents/monitor.ts
+++ /dev/null
@@ -1,367 +0,0 @@
-import { ExitHandler } from "./applied_session_agent";
-import { Configuration, configurationSchema, defaultConfig, Identifiers, colorMapping } from "../utilities/session_config";
-import Repl, { ReplAction } from "../utilities/repl";
-import { isWorker, setupMaster, on, Worker, fork } from "cluster";
-import { IPC } from "../utilities/ipc";
-import { red, cyan, white, yellow, blue, green } from "colors";
-import { exec, ExecOptions } from "child_process";
-import { Utils } from "../../../Utils";
-import { validate, ValidationError } from "jsonschema";
-import { Utilities } from "../utilities/utilities";
-import { readFileSync } from "fs";
-import { EventEmitter } from "events";
-
-/**
- * Validates and reads the configuration file, accordingly builds a child process factory
- * and spawns off an initial process that will respawn as predecessors die.
- */
-export class Monitor extends EventEmitter {
-
- private static count = 0;
- private finalized = false;
- private exitHandlers: ExitHandler[] = [];
- private readonly config: Configuration;
- private onMessage: { [message: string]: Monitor.ServerMessageHandler[] | undefined } = {};
- private activeWorker: Worker | undefined;
- private key: string | undefined;
- private repl: Repl;
-
- public static Create() {
- if (isWorker) {
- IPC.dispatchMessage(process, {
- action: {
- message: "kill",
- args: {
- reason: "cannot create a monitor on the worker process.",
- graceful: false,
- errorCode: 1
- }
- }
- });
- process.exit(1);
- } else if (++Monitor.count > 1) {
- console.error(red("cannot create more than one monitor."));
- process.exit(1);
- } else {
- return new Monitor();
- }
- }
-
- /**
- * Kill this session and its active child
- * server process, either gracefully (may wait
- * indefinitely, but at least allows active networking
- * requests to complete) or immediately.
- */
- public killSession = async (reason: string, graceful = true, errorCode = 0) => {
- this.mainLog(cyan(`exiting session ${graceful ? "clean" : "immediate"}ly`));
- this.mainLog(`session exit reason: ${(red(reason))}`);
- await this.executeExitHandlers(true);
- this.killActiveWorker(graceful, true);
- process.exit(errorCode);
- }
-
- /**
- * Execute the list of functions registered to be called
- * whenever the process exits.
- */
- public addExitHandler = (handler: ExitHandler) => this.exitHandlers.push(handler);
-
- /**
- * Extend the default repl by adding in custom commands
- * that can invoke application logic external to this module
- */
- public addReplCommand = (basename: string, argPatterns: (RegExp | string)[], action: ReplAction) => {
- this.repl.registerCommand(basename, argPatterns, action);
- }
-
- public exec = (command: string, options?: ExecOptions) => {
- return new Promise<void>(resolve => {
- exec(command, { ...options, encoding: "utf8" }, (error, stdout, stderr) => {
- if (error) {
- this.execLog(red(`unable to execute ${white(command)}`));
- error.message.split("\n").forEach(line => line.length && this.execLog(red(`(error) ${line}`)));
- } else {
- let outLines: string[], errorLines: string[];
- if ((outLines = stdout.split("\n").filter(line => line.length)).length) {
- outLines.forEach(line => line.length && this.execLog(cyan(`(stdout) ${line}`)));
- }
- if ((errorLines = stderr.split("\n").filter(line => line.length)).length) {
- errorLines.forEach(line => line.length && this.execLog(yellow(`(stderr) ${line}`)));
- }
- }
- resolve();
- });
- });
- }
-
- /**
- * Add a listener at this message. When the monitor process
- * receives a message, it will invoke all registered functions.
- */
- public addServerMessageListener = (message: string, handler: Monitor.ServerMessageHandler) => {
- const handlers = this.onMessage[message];
- if (handlers) {
- handlers.push(handler);
- } else {
- this.onMessage[message] = [handler];
- }
- }
-
- /**
- * Unregister a given listener at this message.
- */
- public removeServerMessageListener = (message: string, handler: Monitor.ServerMessageHandler) => {
- const handlers = this.onMessage[message];
- if (handlers) {
- const index = handlers.indexOf(handler);
- if (index > -1) {
- handlers.splice(index, 1);
- }
- }
- }
-
- /**
- * Unregister all listeners at this message.
- */
- public clearServerMessageListeners = (message: string) => this.onMessage[message] = undefined;
-
- private constructor() {
- super();
-
- console.log(this.timestamp(), cyan("initializing session..."));
- this.config = this.loadAndValidateConfiguration();
-
- // determines whether or not we see the compilation / initialization / runtime output of each child server process
- const output = this.config.showServerOutput ? "inherit" : "ignore";
- setupMaster({ stdio: ["ignore", output, output, "ipc"] });
-
- // handle exceptions in the master thread - there shouldn't be many of these
- // the IPC (inter process communication) channel closed exception can't seem
- // to be caught in a try catch, and is inconsequential, so it is ignored
- process.on("uncaughtException", ({ message, stack }): void => {
- if (message !== "Channel closed") {
- this.mainLog(red(message));
- if (stack) {
- this.mainLog(`uncaught exception\n${red(stack)}`);
- }
- }
- });
-
- // a helpful cluster event called on the master thread each time a child process exits
- on("exit", ({ process: { pid } }, code, signal) => {
- const prompt = `server worker with process id ${pid} has exited with code ${code}${signal === null ? "" : `, having encountered signal ${signal}`}.`;
- this.mainLog(cyan(prompt));
- // to make this a robust, continuous session, every time a child process dies, we immediately spawn a new one
- this.spawn();
- });
-
- this.repl = this.initializeRepl();
- }
-
- public finalize = (): void => {
- if (this.finalized) {
- throw new Error("Session monitor is already finalized");
- }
- this.finalized = true;
- this.emit(Monitor.IntrinsicEvents.KeyGenerated, this.key = Utils.GenerateGuid());
- this.spawn();
- }
-
- /**
- * Generates a blue UTC string associated with the time
- * of invocation.
- */
- private timestamp = () => blue(`[${new Date().toUTCString()}]`);
-
- /**
- * A formatted, identified and timestamped log in color
- */
- public mainLog = (...optionalParams: any[]) => {
- console.log(this.timestamp(), this.config.identifiers.master.text, ...optionalParams);
- }
-
- /**
- * A formatted, identified and timestamped log in color for non-
- */
- private execLog = (...optionalParams: any[]) => {
- console.log(this.timestamp(), this.config.identifiers.exec.text, ...optionalParams);
- }
-
- /**
- * Reads in configuration .json file only once, in the master thread
- * and pass down any variables the pertinent to the child processes as environment variables.
- */
- private loadAndValidateConfiguration = (): Configuration => {
- let config: Configuration;
- try {
- console.log(this.timestamp(), cyan("validating configuration..."));
- config = JSON.parse(readFileSync('./session.config.json', 'utf8'));
- const options = {
- throwError: true,
- allowUnknownAttributes: false
- };
- // ensure all necessary and no excess information is specified by the configuration file
- validate(config, configurationSchema, options);
- config = Utilities.preciseAssign({}, defaultConfig, config);
- } catch (error) {
- if (error instanceof ValidationError) {
- console.log(red("\nSession configuration failed."));
- console.log("The given session.config.json configuration file is invalid.");
- console.log(`${error.instance}: ${error.stack}`);
- process.exit(0);
- } else if (error.code === "ENOENT" && error.path === "./session.config.json") {
- console.log(cyan("Loading default session parameters..."));
- console.log("Consider including a session.config.json configuration file in your project root for customization.");
- config = Utilities.preciseAssign({}, defaultConfig);
- } else {
- console.log(red("\nSession configuration failed."));
- console.log("The following unknown error occurred during configuration.");
- console.log(error.stack);
- process.exit(0);
- }
- } finally {
- const { identifiers } = config!;
- Object.keys(identifiers).forEach(key => {
- const resolved = key as keyof Identifiers;
- const { text, color } = identifiers[resolved];
- identifiers[resolved].text = (colorMapping.get(color) || white)(`${text}:`);
- });
- return config!;
- }
- }
-
- /**
- * Builds the repl that allows the following commands to be typed into stdin of the master thread.
- */
- private initializeRepl = (): Repl => {
- const repl = new Repl({ identifier: () => `${this.timestamp()} ${this.config.identifiers.master.text}` });
- const boolean = /true|false/;
- const number = /\d+/;
- const letters = /[a-zA-Z]+/;
- repl.registerCommand("exit", [/clean|force/], args => this.killSession("manual exit requested by repl", args[0] === "clean", 0));
- repl.registerCommand("restart", [/clean|force/], args => this.killActiveWorker(args[0] === "clean"));
- repl.registerCommand("set", [letters, "port", number, boolean], args => this.setPort(args[0], Number(args[2]), args[3] === "true"));
- repl.registerCommand("set", [/polling/, number, boolean], async args => {
- const newPollingIntervalSeconds = Math.floor(Number(args[1]));
- if (newPollingIntervalSeconds < 0) {
- this.mainLog(red("the polling interval must be a non-negative integer"));
- } else {
- if (newPollingIntervalSeconds !== this.config.polling.intervalSeconds) {
- this.config.polling.intervalSeconds = newPollingIntervalSeconds;
- if (args[2] === "true") {
- return IPC.dispatchMessage(this.activeWorker!, { newPollingIntervalSeconds }, true);
- }
- }
- }
- });
- return repl;
- }
-
- private executeExitHandlers = async (reason: Error | boolean) => Promise.all(this.exitHandlers.map(handler => handler(reason)));
-
- /**
- * Attempts to kill the active worker gracefully, unless otherwise specified.
- */
- private killActiveWorker = (graceful = true, isSessionEnd = false): void => {
- if (this.activeWorker && !this.activeWorker.isDead()) {
- if (graceful) {
- IPC.dispatchMessage(this.activeWorker, { manualExit: { isSessionEnd } });
- } else {
- this.activeWorker.process.kill();
- }
- }
- }
-
- /**
- * Allows the caller to set the port at which the target (be it the server,
- * the websocket, some other custom port) is listening. If an immediate restart
- * is specified, this monitor will kill the active child and re-launch the server
- * at the port. Otherwise, the updated port won't be used until / unless the child
- * dies on its own and triggers a restart.
- */
- private setPort = (port: "server" | "socket" | string, value: number, immediateRestart: boolean): void => {
- if (value > 1023 && value < 65536) {
- this.config.ports[port] = value;
- if (immediateRestart) {
- this.killActiveWorker();
- }
- } else {
- this.mainLog(red(`${port} is an invalid port number`));
- }
- }
-
- /**
- * Kills the current active worker and proceeds to spawn a new worker,
- * feeding in configuration information as environment variables.
- */
- private spawn = (): void => {
- const {
- polling: {
- route,
- failureTolerance,
- intervalSeconds
- },
- ports
- } = this.config;
- this.killActiveWorker();
- this.activeWorker = fork({
- pollingRoute: route,
- pollingFailureTolerance: failureTolerance,
- serverPort: ports.server,
- socketPort: ports.socket,
- pollingIntervalSeconds: intervalSeconds,
- session_key: this.key,
- ipc_suffix: IPC.suffix
- });
- this.mainLog(cyan(`spawned new server worker with process id ${this.activeWorker?.process.pid}`));
- // an IPC message handler that executes actions on the master thread when prompted by the active worker
- IPC.addMessagesHandler(this.activeWorker!, async ({ lifecycle, action }) => {
- if (action) {
- const { message, args } = action as Monitor.Action;
- console.log(this.timestamp(), `${this.config.identifiers.worker.text} action requested (${cyan(message)})`);
- switch (message) {
- case "kill":
- const { reason, graceful, errorCode } = args;
- this.killSession(reason, graceful, errorCode);
- break;
- case "notify_crash":
- this.emit(Monitor.IntrinsicEvents.CrashDetected, args.error);
- break;
- case Monitor.IntrinsicEvents.ServerRunning:
- this.emit(Monitor.IntrinsicEvents.ServerRunning, args.firstTime);
- break;
- case "set_port":
- const { port, value, immediateRestart } = args;
- this.setPort(port, value, immediateRestart);
- break;
- }
- const handlers = this.onMessage[message];
- if (handlers) {
- handlers.forEach(handler => handler({ message, args }));
- }
- }
- if (lifecycle) {
- console.log(this.timestamp(), `${this.config.identifiers.worker.text} lifecycle phase (${lifecycle})`);
- }
- });
- }
-
-}
-
-export namespace Monitor {
-
- export interface Action {
- message: string;
- args: any;
- }
-
- export type ServerMessageHandler = (action: Action) => void | Promise<void>;
-
- export enum IntrinsicEvents {
- KeyGenerated = "key_generated",
- CrashDetected = "crash_detected",
- ServerRunning = "server_running"
- }
-
-} \ No newline at end of file
diff --git a/src/server/session/agents/server_worker.ts b/src/server/session/agents/server_worker.ts
deleted file mode 100644
index e9fdaf923..000000000
--- a/src/server/session/agents/server_worker.ts
+++ /dev/null
@@ -1,165 +0,0 @@
-import { ExitHandler } from "./applied_session_agent";
-import { isMaster } from "cluster";
-import { IPC } from "../utilities/ipc";
-import { red, green, white, yellow } from "colors";
-import { get } from "request-promise";
-import { Monitor } from "./monitor";
-
-/**
- * Effectively, each worker repairs the connection to the server by reintroducing a consistent state
- * if its predecessor has died. It itself also polls the server heartbeat, and exits with a notification
- * email if the server encounters an uncaught exception or if the server cannot be reached.
- */
-export class ServerWorker {
-
- private static count = 0;
- private shouldServerBeResponsive = false;
- private exitHandlers: ExitHandler[] = [];
- private pollingFailureCount = 0;
- private pollingIntervalSeconds: number;
- private pollingFailureTolerance: number;
- private pollTarget: string;
- private serverPort: number;
- private isInitialized = false;
-
- public static Create(work: Function) {
- if (isMaster) {
- console.error(red("cannot create a worker on the monitor process."));
- process.exit(1);
- } else if (++ServerWorker.count > 1) {
- IPC.dispatchMessage(process, {
- action: {
- message: "kill", args: {
- reason: "cannot create more than one worker on a given worker process.",
- graceful: false,
- errorCode: 1
- }
- }
- });
- process.exit(1);
- } else {
- return new ServerWorker(work);
- }
- }
-
- /**
- * Allows developers to invoke application specific logic
- * by hooking into the exiting of the server process.
- */
- public addExitHandler = (handler: ExitHandler) => this.exitHandlers.push(handler);
-
- /**
- * Kill the session monitor (parent process) from this
- * server worker (child process). This will also kill
- * this process (child process).
- */
- public killSession = (reason: string, graceful = true, errorCode = 0) => this.sendMonitorAction("kill", { reason, graceful, errorCode });
-
- /**
- * A convenience wrapper to tell the session monitor (parent process)
- * to carry out the action with the specified message and arguments.
- */
- public sendMonitorAction = (message: string, args?: any, expectResponse = false) => IPC.dispatchMessage(process, { action: { message, args } }, expectResponse);
-
- private constructor(work: Function) {
- this.lifecycleNotification(green(`initializing process... ${white(`[${process.execPath} ${process.execArgv.join(" ")}]`)}`));
-
- const { pollingRoute, serverPort, pollingIntervalSeconds, pollingFailureTolerance } = process.env;
- this.serverPort = Number(serverPort);
- this.pollingIntervalSeconds = Number(pollingIntervalSeconds);
- this.pollingFailureTolerance = Number(pollingFailureTolerance);
- this.pollTarget = `http://localhost:${serverPort}${pollingRoute}`;
-
- this.configureProcess();
- work();
- this.pollServer();
- }
-
- /**
- * Set up message and uncaught exception handlers for this
- * server process.
- */
- private configureProcess = () => {
- // updates the local values of variables to the those sent from master
- IPC.addMessagesHandler(process, async ({ newPollingIntervalSeconds, manualExit }) => {
- if (newPollingIntervalSeconds !== undefined) {
- this.pollingIntervalSeconds = newPollingIntervalSeconds;
- }
- if (manualExit !== undefined) {
- const { isSessionEnd } = manualExit;
- await this.executeExitHandlers(isSessionEnd);
- process.exit(0);
- }
- });
-
- // one reason to exit, as the process might be in an inconsistent state after such an exception
- process.on('uncaughtException', this.proactiveUnplannedExit);
- process.on('unhandledRejection', reason => {
- const appropriateError = reason instanceof Error ? reason : new Error(`unhandled rejection: ${reason}`);
- this.proactiveUnplannedExit(appropriateError);
- });
- }
-
- /**
- * Execute the list of functions registered to be called
- * whenever the process exits.
- */
- private executeExitHandlers = async (reason: Error | boolean) => Promise.all(this.exitHandlers.map(handler => handler(reason)));
-
- /**
- * Notify master thread (which will log update in the console) of initialization via IPC.
- */
- public lifecycleNotification = (event: string) => IPC.dispatchMessage(process, { lifecycle: event });
-
- /**
- * Called whenever the process has a reason to terminate, either through an uncaught exception
- * in the process (potentially inconsistent state) or the server cannot be reached.
- */
- private proactiveUnplannedExit = async (error: Error): Promise<void> => {
- this.shouldServerBeResponsive = false;
- // communicates via IPC to the master thread that it should dispatch a crash notification email
- this.sendMonitorAction("notify_crash", { error });
- await this.executeExitHandlers(error);
- // notify master thread (which will log update in the console) of crash event via IPC
- this.lifecycleNotification(red(`crash event detected @ ${new Date().toUTCString()}`));
- this.lifecycleNotification(red(error.message));
- process.exit(1);
- }
-
- /**
- * This monitors the health of the server by submitting a get request to whatever port / route specified
- * by the configuration every n seconds, where n is also given by the configuration.
- */
- private pollServer = async (): Promise<void> => {
- await new Promise<void>(resolve => {
- setTimeout(async () => {
- try {
- await get(this.pollTarget);
- if (!this.shouldServerBeResponsive) {
- // notify monitor thread that the server is up and running
- this.lifecycleNotification(green(`listening on ${this.serverPort}...`));
- this.sendMonitorAction(Monitor.IntrinsicEvents.ServerRunning, { firstTime: !this.isInitialized });
- this.isInitialized = true;
- }
- this.shouldServerBeResponsive = true;
- } catch (error) {
- // if we expect the server to be unavailable, i.e. during compilation,
- // the listening variable is false, activeExit will return early and the child
- // process will continue
- if (this.shouldServerBeResponsive) {
- if (++this.pollingFailureCount > this.pollingFailureTolerance) {
- this.proactiveUnplannedExit(error);
- } else {
- this.lifecycleNotification(yellow(`the server has encountered ${this.pollingFailureCount} of ${this.pollingFailureTolerance} tolerable failures`));
- }
- }
- } finally {
- resolve();
- }
- }, 1000 * this.pollingIntervalSeconds);
- });
- // controlled, asynchronous infinite recursion achieves a persistent poll that does not submit a new request until the previous has completed
- this.pollServer();
- }
-
-} \ No newline at end of file
diff --git a/src/server/session/utilities/ipc.ts b/src/server/session/utilities/ipc.ts
deleted file mode 100644
index b20f3d337..000000000
--- a/src/server/session/utilities/ipc.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-import { isMaster } from "cluster";
-import { Utils } from "../../../Utils";
-
-export namespace IPC {
-
- export const suffix = isMaster ? Utils.GenerateGuid() : process.env.ipc_suffix;
- const ipc_id = `ipc_id_${suffix}`;
- const response_expected = `response_expected_${suffix}`;
- const is_response = `is_response_${suffix}`;
-
- export async function dispatchMessage(target: NodeJS.EventEmitter & { send?: Function }, message: any, expectResponse = false): Promise<Error | undefined> {
- if (!target.send) {
- return new Error("Cannot dispatch when send is undefined.");
- }
- message[response_expected] = expectResponse;
- if (expectResponse) {
- return new Promise(resolve => {
- const messageId = Utils.GenerateGuid();
- message[ipc_id] = messageId;
- const responseHandler: (args: any) => void = response => {
- const { error } = response;
- if (response[is_response] && response[ipc_id] === messageId) {
- target.removeListener("message", responseHandler);
- resolve(error);
- }
- };
- target.addListener("message", responseHandler);
- target.send!(message);
- });
- } else {
- target.send(message);
- }
- }
-
- export function addMessagesHandler(target: NodeJS.EventEmitter & { send?: Function }, handler: (message: any) => void | Promise<void>): void {
- target.addListener("message", async incoming => {
- let error: Error | undefined;
- try {
- await handler(incoming);
- } catch (e) {
- error = e;
- }
- if (incoming[response_expected] && target.send) {
- const response: any = { error };
- response[ipc_id] = incoming[ipc_id];
- response[is_response] = true;
- target.send(response);
- }
- });
- }
-
-} \ No newline at end of file
diff --git a/src/server/session/utilities/repl.ts b/src/server/session/utilities/repl.ts
deleted file mode 100644
index 643141286..000000000
--- a/src/server/session/utilities/repl.ts
+++ /dev/null
@@ -1,128 +0,0 @@
-import { createInterface, Interface } from "readline";
-import { red, green, white } from "colors";
-
-export interface Configuration {
- identifier: () => string | string;
- onInvalid?: (command: string, validCommand: boolean) => string | string;
- onValid?: (success?: string) => string | string;
- isCaseSensitive?: boolean;
-}
-
-export type ReplAction = (parsedArgs: Array<string>) => any | Promise<any>;
-export interface Registration {
- argPatterns: RegExp[];
- action: ReplAction;
-}
-
-export default class Repl {
- private identifier: () => string | string;
- private onInvalid: ((command: string, validCommand: boolean) => string) | string;
- private onValid: ((success: string) => string) | string;
- private isCaseSensitive: boolean;
- private commandMap = new Map<string, Registration[]>();
- public interface: Interface;
- private busy = false;
- private keys: string | undefined;
-
- constructor({ identifier: prompt, onInvalid, onValid, isCaseSensitive }: Configuration) {
- this.identifier = prompt;
- this.onInvalid = onInvalid || this.usage;
- this.onValid = onValid || this.success;
- this.isCaseSensitive = isCaseSensitive ?? true;
- this.interface = createInterface(process.stdin, process.stdout).on('line', this.considerInput);
- }
-
- private resolvedIdentifier = () => typeof this.identifier === "string" ? this.identifier : this.identifier();
-
- private usage = (command: string, validCommand: boolean) => {
- if (validCommand) {
- const formatted = white(command);
- const patterns = green(this.commandMap.get(command)!.map(({ argPatterns }) => `${formatted} ${argPatterns.join(" ")}`).join('\n'));
- return `${this.resolvedIdentifier()}\nthe given arguments do not match any registered patterns for ${formatted}\nthe list of valid argument patterns is given by:\n${patterns}`;
- } else {
- const resolved = this.keys;
- if (resolved) {
- return resolved;
- }
- const members: string[] = [];
- const keys = this.commandMap.keys();
- let next: IteratorResult<string>;
- while (!(next = keys.next()).done) {
- members.push(next.value);
- }
- return `${this.resolvedIdentifier()} commands: { ${members.sort().join(", ")} }`;
- }
- }
-
- private success = (command: string) => `${this.resolvedIdentifier()} completed local execution of ${white(command)}`;
-
- public registerCommand = (basename: string, argPatterns: (RegExp | string)[], action: ReplAction) => {
- const existing = this.commandMap.get(basename);
- const converted = argPatterns.map(input => input instanceof RegExp ? input : new RegExp(input));
- const registration = { argPatterns: converted, action };
- if (existing) {
- existing.push(registration);
- } else {
- this.commandMap.set(basename, [registration]);
- }
- }
-
- private invalid = (command: string, validCommand: boolean) => {
- console.log(red(typeof this.onInvalid === "string" ? this.onInvalid : this.onInvalid(command, validCommand)));
- this.busy = false;
- }
-
- private valid = (command: string) => {
- console.log(green(typeof this.onValid === "string" ? this.onValid : this.onValid(command)));
- this.busy = false;
- }
-
- private considerInput = async (line: string) => {
- if (this.busy) {
- console.log(red("Busy"));
- return;
- }
- this.busy = true;
- line = line.trim();
- if (this.isCaseSensitive) {
- line = line.toLowerCase();
- }
- const [command, ...args] = line.split(/\s+/g);
- if (!command) {
- return this.invalid(command, false);
- }
- const registered = this.commandMap.get(command);
- if (registered) {
- const { length } = args;
- const candidates = registered.filter(({ argPatterns: { length: count } }) => count === length);
- for (const { argPatterns, action } of candidates) {
- const parsed: string[] = [];
- let matched = true;
- if (length) {
- for (let i = 0; i < length; i++) {
- let matches: RegExpExecArray | null;
- if ((matches = argPatterns[i].exec(args[i])) === null) {
- matched = false;
- break;
- }
- parsed.push(matches[0]);
- }
- }
- if (!length || matched) {
- const result = action(parsed);
- const resolve = () => this.valid(`${command} ${parsed.join(" ")}`);
- if (result instanceof Promise) {
- result.then(resolve);
- } else {
- resolve();
- }
- return;
- }
- }
- this.invalid(command, true);
- } else {
- this.invalid(command, false);
- }
- }
-
-} \ No newline at end of file
diff --git a/src/server/session/utilities/session_config.ts b/src/server/session/utilities/session_config.ts
deleted file mode 100644
index b0e65dde4..000000000
--- a/src/server/session/utilities/session_config.ts
+++ /dev/null
@@ -1,129 +0,0 @@
-import { Schema } from "jsonschema";
-import { yellow, red, cyan, green, blue, magenta, Color, grey, gray, white, black } from "colors";
-
-const colorPattern = /black|red|green|yellow|blue|magenta|cyan|white|gray|grey/;
-
-const identifierProperties: Schema = {
- type: "object",
- properties: {
- text: {
- type: "string",
- minLength: 1
- },
- color: {
- type: "string",
- pattern: colorPattern
- }
- }
-};
-
-const portProperties: Schema = {
- type: "number",
- minimum: 1024,
- maximum: 65535
-};
-
-export const configurationSchema: Schema = {
- id: "/configuration",
- type: "object",
- properties: {
- showServerOutput: { type: "boolean" },
- ports: {
- type: "object",
- properties: {
- server: portProperties,
- socket: portProperties
- },
- required: ["server"],
- additionalProperties: true
- },
- identifiers: {
- type: "object",
- properties: {
- master: identifierProperties,
- worker: identifierProperties,
- exec: identifierProperties
- }
- },
- polling: {
- type: "object",
- additionalProperties: false,
- properties: {
- intervalSeconds: {
- type: "number",
- minimum: 1,
- maximum: 86400
- },
- route: {
- type: "string",
- pattern: /\/[a-zA-Z]*/g
- },
- failureTolerance: {
- type: "number",
- minimum: 0,
- }
- }
- },
- }
-};
-
-type ColorLabel = "yellow" | "red" | "cyan" | "green" | "blue" | "magenta" | "grey" | "gray" | "white" | "black";
-
-export const colorMapping: Map<ColorLabel, Color> = new Map([
- ["yellow", yellow],
- ["red", red],
- ["cyan", cyan],
- ["green", green],
- ["blue", blue],
- ["magenta", magenta],
- ["grey", grey],
- ["gray", gray],
- ["white", white],
- ["black", black]
-]);
-
-interface Identifier {
- text: string;
- color: ColorLabel;
-}
-
-export interface Identifiers {
- master: Identifier;
- worker: Identifier;
- exec: Identifier;
-}
-
-export interface Configuration {
- showServerOutput: boolean;
- identifiers: Identifiers;
- ports: { [description: string]: number };
- polling: {
- route: string;
- intervalSeconds: number;
- failureTolerance: number;
- };
-}
-
-export const defaultConfig: Configuration = {
- showServerOutput: false,
- identifiers: {
- master: {
- text: "__monitor__",
- color: "yellow"
- },
- worker: {
- text: "__server__",
- color: "magenta"
- },
- exec: {
- text: "__exec__",
- color: "green"
- }
- },
- ports: { server: 3000 },
- polling: {
- route: "/",
- intervalSeconds: 30,
- failureTolerance: 0
- }
-}; \ No newline at end of file
diff --git a/src/server/session/utilities/utilities.ts b/src/server/session/utilities/utilities.ts
deleted file mode 100644
index ac8a6590a..000000000
--- a/src/server/session/utilities/utilities.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-export namespace Utilities {
-
- /**
- * At any arbitrary layer of nesting within the configuration objects, any single value that
- * is not specified by the configuration is given the default counterpart. If, within an object,
- * one peer is given by configuration and two are not, the one is preserved while the two are given
- * the default value.
- * @returns the composition of all of the assigned objects, much like Object.assign(), but with more
- * granularity in the overwriting of nested objects
- */
- export function preciseAssign(target: any, ...sources: any[]): any {
- for (const source of sources) {
- preciseAssignHelper(target, source);
- }
- return target;
- }
-
- export function preciseAssignHelper(target: any, source: any) {
- Array.from(new Set([...Object.keys(target), ...Object.keys(source)])).map(property => {
- let targetValue: any, sourceValue: any;
- if (sourceValue = source[property]) {
- if (typeof sourceValue === "object" && typeof (targetValue = target[property]) === "object") {
- preciseAssignHelper(targetValue, sourceValue);
- } else {
- target[property] = sourceValue;
- }
- }
- });
- }
-
-} \ No newline at end of file
diff --git a/src/typings/index.d.ts b/src/typings/index.d.ts
index cc68e8a4d..281bb3217 100644
--- a/src/typings/index.d.ts
+++ b/src/typings/index.d.ts
@@ -2,8 +2,6 @@
declare module 'googlephotos';
declare module 'react-image-lightbox-with-rotate';
-declare module 'kill-port';
-declare module 'ipc-event-emitter';
declare module 'cors';
declare module '@react-pdf/renderer' {