aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorbob <bcz@cs.brown.edu>2019-12-16 10:40:45 -0500
committerbob <bcz@cs.brown.edu>2019-12-16 10:40:45 -0500
commit9c3acfbd6ad518cd1afcc05a521480d93c07f13a (patch)
treea6c6bbbfc2250e7c6d1f1d4b9757e55128597edf /src
parent7584c5bed0de45f2f656fb289ff67463fb1958ef (diff)
parent001cea83f6c57914e0e76a75a94f3e06e542a419 (diff)
Merge branch 'master' of https://github.com/browngraphicslab/Dash-Web
Diffstat (limited to 'src')
-rw-r--r--src/client/documents/DocumentTypes.ts3
-rw-r--r--src/client/documents/Documents.ts20
-rw-r--r--src/client/util/DragManager.ts23
-rw-r--r--src/client/util/Import & Export/DirectoryImportBox.tsx7
-rw-r--r--src/client/util/RichTextRules.ts5
-rw-r--r--src/client/util/RichTextSchema.tsx6
-rw-r--r--src/client/util/SearchUtil.ts4
-rw-r--r--src/client/util/SelectionManager.ts6
-rw-r--r--src/client/views/DocComponent.tsx1
-rw-r--r--src/client/views/collections/CollectionSubView.tsx4
-rw-r--r--src/client/views/collections/ParentDocumentSelector.tsx2
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss2
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx21
-rw-r--r--src/client/views/nodes/ContentFittingDocumentView.scss5
-rw-r--r--src/client/views/nodes/ContentFittingDocumentView.tsx24
-rw-r--r--src/client/views/nodes/DocumentBox.scss6
-rw-r--r--src/client/views/nodes/DocumentBox.tsx55
-rw-r--r--src/client/views/nodes/DocumentContentsView.tsx3
-rw-r--r--src/client/views/nodes/DocumentView.tsx23
-rw-r--r--src/client/views/nodes/FormattedTextBox.tsx1
-rw-r--r--src/client/views/nodes/ImageBox.tsx1
-rw-r--r--src/client/views/nodes/KeyValueBox.tsx2
-rw-r--r--src/client/views/nodes/PDFBox.tsx61
-rw-r--r--src/client/views/pdf/PDFMenu.tsx36
-rw-r--r--src/client/views/pdf/PDFViewer.tsx8
-rw-r--r--src/client/views/search/SearchBox.tsx5
-rw-r--r--src/new_fields/Doc.ts21
-rw-r--r--src/new_fields/ScriptField.ts5
-rw-r--r--src/new_fields/documentSchemas.ts4
-rw-r--r--src/server/ApiManagers/PDFManager.ts72
-rw-r--r--src/server/ChildProcessUtilities/daemon/session.ts190
-rw-r--r--src/server/ProcessFactory.ts (renamed from src/server/ChildProcessUtilities/ProcessFactory.ts)25
-rw-r--r--src/server/authentication/models/current_user_utils.ts1
-rw-r--r--src/server/index.ts2
-rw-r--r--src/server/session_manager/config.ts33
-rw-r--r--src/server/session_manager/input_manager.ts101
-rw-r--r--src/server/session_manager/logs/current_daemon_pid.log1
-rw-r--r--src/server/session_manager/logs/current_server_pid.log1
-rw-r--r--src/server/session_manager/logs/current_session_manager_pid.log1
-rw-r--r--src/server/session_manager/session_manager.ts199
40 files changed, 620 insertions, 370 deletions
diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts
index f6dd0c346..8f96b2fa6 100644
--- a/src/client/documents/DocumentTypes.ts
+++ b/src/client/documents/DocumentTypes.ts
@@ -25,5 +25,6 @@ export enum DocumentType {
COLOR = "color",
DOCULINK = "doculink",
PDFANNO = "pdfanno",
- INK = "ink"
+ INK = "ink",
+ DOCUMENT = "document"
} \ No newline at end of file
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 7df08c7e6..50c51ace1 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -48,6 +48,7 @@ import { PresElementBox } from "../views/presentationview/PresElementBox";
import { QueryBox } from "../views/nodes/QueryBox";
import { ColorBox } from "../views/nodes/ColorBox";
import { DocuLinkBox } from "../views/nodes/DocuLinkBox";
+import { DocumentBox } from "../views/nodes/DocumentBox";
import { InkingStroke } from "../views/InkingStroke";
import { InkField } from "../../new_fields/InkField";
const requestImageSize = require('../util/request-image-size');
@@ -112,7 +113,7 @@ export interface DocumentOptions {
dropConverter?: ScriptField; // script to run when documents are dropped on this Document.
strokeWidth?: number;
color?: string;
- limitHeight?:number; // maximum height for newly created (eg, from pasting) text documents
+ limitHeight?: number; // maximum height for newly created (eg, from pasting) text documents
// [key: string]: Opt<Field>;
}
@@ -171,6 +172,10 @@ export namespace Docs {
layout: { view: KeyValueBox, dataField: data },
options: { height: 150 }
}],
+ [DocumentType.DOCUMENT, {
+ layout: { view: DocumentBox, dataField: data },
+ options: { height: 250 }
+ }],
[DocumentType.VID, {
layout: { view: VideoBox, dataField: data },
options: { currentTimecode: 0 },
@@ -181,7 +186,7 @@ export namespace Docs {
}],
[DocumentType.PDF, {
layout: { view: PDFBox, dataField: data },
- options: { nativeWidth: 1200, curPage: 1 }
+ options: { curPage: 1 }
}],
[DocumentType.ICON, {
layout: { view: IconBox, dataField: data },
@@ -482,6 +487,10 @@ export namespace Docs {
return InstanceFromProto(Prototypes.get(DocumentType.KVP), document, { title: document.title + ".kvp", ...options });
}
+ export function DocumentDocument(document?: Doc, options: DocumentOptions = {}) {
+ return InstanceFromProto(Prototypes.get(DocumentType.DOCUMENT), document, { title: document ? document.title + "" : "container", ...options });
+ }
+
export function FreeformDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) {
return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { chromeStatus: "collapsed", schemaColumns: new List([new SchemaHeaderField("title", "#f1efeb")]), ...options, viewType: CollectionViewType.Freeform }, id);
}
@@ -644,17 +653,20 @@ export namespace Docs {
let ctor: ((path: string, options: DocumentOptions) => (Doc | Promise<Doc | undefined>)) | undefined = undefined;
if (type.indexOf("image") !== -1) {
ctor = Docs.Create.ImageDocument;
+ if (!options.width) options.width = 300;
}
if (type.indexOf("video") !== -1) {
ctor = Docs.Create.VideoDocument;
+ if (!options.width) options.width = 600;
+ if (!options.height) options.height = options.width * 2 / 3;
}
if (type.indexOf("audio") !== -1) {
ctor = Docs.Create.AudioDocument;
}
if (type.indexOf("pdf") !== -1) {
ctor = Docs.Create.PdfDocument;
- options.nativeWidth = 927;
- options.nativeHeight = 1200;
+ if (!options.width) options.width = 400;
+ if (!options.height) options.height = options.width * 1200 / 927;
}
if (type.indexOf("excel") !== -1) {
ctor = Docs.Create.DBDocument;
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 326262895..df2f5fe3c 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -18,7 +18,7 @@ import { convertDropDataToButtons } from "./DropConverter";
export type dropActionType = "alias" | "copy" | undefined;
export function SetupDrag(
_reference: React.RefObject<HTMLElement>,
- docFunc: () => Doc | Promise<Doc>,
+ docFunc: () => Doc | Promise<Doc> | undefined,
moveFunc?: DragManager.MoveFunction,
dropAction?: dropActionType,
treeViewId?: string,
@@ -32,13 +32,15 @@ export function SetupDrag(
document.removeEventListener("pointermove", onRowMove);
document.removeEventListener('pointerup', onRowUp);
const doc = await docFunc();
- const dragData = new DragManager.DocumentDragData([doc]);
- dragData.dropAction = dropAction;
- dragData.moveDocument = moveFunc;
- dragData.treeViewId = treeViewId;
- dragData.dontHideOnDrop = dontHideOnDrop;
- DragManager.StartDocumentDrag([_reference.current!], dragData, e.x, e.y);
- dragStarted && dragStarted();
+ if (doc) {
+ const dragData = new DragManager.DocumentDragData([doc]);
+ dragData.dropAction = dropAction;
+ dragData.moveDocument = moveFunc;
+ dragData.treeViewId = treeViewId;
+ dragData.dontHideOnDrop = dontHideOnDrop;
+ DragManager.StartDocumentDrag([_reference.current!], dragData, e.x, e.y);
+ dragStarted && dragStarted();
+ }
};
const onRowUp = (): void => {
document.removeEventListener("pointermove", onRowMove);
@@ -49,12 +51,13 @@ export function SetupDrag(
e.stopPropagation();
if (e.shiftKey && CollectionDockingView.Instance) {
e.persist();
- CollectionDockingView.Instance.StartOtherDrag({
+ const dragDoc = await docFunc();
+ dragDoc && CollectionDockingView.Instance.StartOtherDrag({
pageX: e.pageX,
pageY: e.pageY,
preventDefault: emptyFunction,
button: 0
- }, [await docFunc()]);
+ }, [dragDoc]);
} else {
document.addEventListener("pointermove", onRowMove);
document.addEventListener("pointerup", onRowUp);
diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx
index e6a215b2c..5b5bffd8c 100644
--- a/src/client/util/Import & Export/DirectoryImportBox.tsx
+++ b/src/client/util/Import & Export/DirectoryImportBox.tsx
@@ -122,12 +122,7 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
await Promise.all(uploads.map(async ({ name, type, clientAccessPath, exifData }) => {
const path = Utils.prepend(clientAccessPath);
- const options = {
- nativeWidth: 300,
- width: 300,
- title: name
- };
- const document = await Docs.Get.DocumentFromType(type, path, options);
+ const document = await Docs.Get.DocumentFromType(type, path, { width: 300, title: name });
const { data, error } = exifData;
if (document) {
Doc.GetProto(document).exif = error || Docs.Get.DocumentHierarchyFromJson(data);
diff --git a/src/client/util/RichTextRules.ts b/src/client/util/RichTextRules.ts
index 364c85165..f5dd459fc 100644
--- a/src/client/util/RichTextRules.ts
+++ b/src/client/util/RichTextRules.ts
@@ -85,8 +85,9 @@ export const inpRules = {
const value = state.doc.textBetween(start, end);
if (value) {
DocServer.GetRefField(value).then(docx => {
- const doc = ((docx instanceof Doc) && docx) || Docs.Create.FreeformDocument([], { title: value, width: 500, height: 500 }, value);
- DocUtils.Publish(doc, value, returnFalse, returnFalse);
+ const target = ((docx instanceof Doc) && docx) || Docs.Create.FreeformDocument([], { title: value, width: 500, height: 500 }, value);
+ DocUtils.Publish(target, value, returnFalse, returnFalse);
+ DocUtils.MakeLink({ doc: (schema as any).Document }, { doc: target }, "portal link", "");
});
const link = state.schema.marks.link.create({ href: Utils.prepend("/doc/" + value), location: "onRight", title: value });
return state.tr.addMark(start, end, link);
diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index 543f45731..7a048ed0d 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -741,7 +741,11 @@ export class DashDocView {
self._dashDoc = dashDoc;
dashDoc.hideSidebar = true;
if (node.attrs.width !== dashDoc.width + "px" || node.attrs.height !== dashDoc.height + "px") {
- view.dispatch(view.state.tr.setNodeMarkup(getPos(), null, { ...node.attrs, width: dashDoc.width + "px", height: dashDoc.height + "px" }));
+ try { // bcz: an exception will be thrown if two aliases are open at the same time when a doc view comment is made
+ view.dispatch(view.state.tr.setNodeMarkup(getPos(), null, { ...node.attrs, width: dashDoc.width + "px", height: dashDoc.height + "px" }));
+ } catch (e) {
+ console.log(e);
+ }
}
this._reactionDisposer && this._reactionDisposer();
this._reactionDisposer = reaction(() => dashDoc[HeightSym]() + dashDoc[WidthSym](), () => {
diff --git a/src/client/util/SearchUtil.ts b/src/client/util/SearchUtil.ts
index 7a9176bec..1fda82880 100644
--- a/src/client/util/SearchUtil.ts
+++ b/src/client/util/SearchUtil.ts
@@ -64,7 +64,7 @@ export namespace SearchUtil {
const textDocs = newIds.map((id: string) => textDocMap[id]).map(doc => doc as Doc);
for (let i = 0; i < textDocs.length; i++) {
const testDoc = textDocs[i];
- if (testDoc instanceof Doc && testDoc.type !== DocumentType.KVP && theDocs.findIndex(d => Doc.AreProtosEqual(d, testDoc)) === -1) {
+ if (testDoc instanceof Doc && testDoc.type !== DocumentType.KVP && testDoc.type !== DocumentType.EXTENSION && theDocs.findIndex(d => Doc.AreProtosEqual(d, testDoc)) === -1) {
theDocs.push(Doc.GetProto(testDoc));
theLines.push(newLines[i].map(line => line.replace(query, query.toUpperCase())));
}
@@ -74,7 +74,7 @@ export namespace SearchUtil {
const docs = ids.map((id: string) => docMap[id]).map(doc => doc as Doc);
for (let i = 0; i < ids.length; i++) {
const testDoc = docs[i];
- if (testDoc instanceof Doc && testDoc.type !== DocumentType.KVP && (options.allowAliases || theDocs.findIndex(d => Doc.AreProtosEqual(d, testDoc)) === -1)) {
+ if (testDoc instanceof Doc && testDoc.type !== DocumentType.KVP && testDoc.type !== DocumentType.EXTENSION && (options.allowAliases || theDocs.findIndex(d => Doc.AreProtosEqual(d, testDoc)) === -1)) {
theDocs.push(testDoc);
theLines.push([]);
}
diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts
index e01216e0f..cb7a69295 100644
--- a/src/client/util/SelectionManager.ts
+++ b/src/client/util/SelectionManager.ts
@@ -2,6 +2,8 @@ import { observable, action, runInAction, ObservableMap } from "mobx";
import { Doc } from "../../new_fields/Doc";
import { DocumentView } from "../views/nodes/DocumentView";
import { computedFn } from "mobx-utils";
+import { List } from "../../new_fields/List";
+import { Scripting } from "./Scripting";
export namespace SelectionManager {
@@ -27,6 +29,7 @@ export namespace SelectionManager {
manager.SelectedDocuments.clear();
manager.SelectedDocuments.set(docView, true);
}
+ Doc.UserDoc().SelectedDocs = new List(SelectionManager.SelectedDocuments().map(dv => dv.props.Document));
}
@action
DeselectDoc(docView: DocumentView): void {
@@ -78,3 +81,6 @@ export namespace SelectionManager {
return Array.from(manager.SelectedDocuments.keys());
}
}
+
+Scripting.addGlobal(function selectDoc(doc: any) { Doc.UserDoc().SelectedDocs = new List([doc]); });
+
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index c7ddee7ea..7fbad4638 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -59,6 +59,7 @@ export function DocAnnotatableComponent<P extends DocAnnotatableProps, T>(schema
@computed get layoutDoc() { return Doc.Layout(this.props.Document); }
@computed get dataDoc() { return (this.props.DataDoc && this.props.Document.isTemplateField ? this.props.DataDoc : Doc.GetProto(this.props.Document)) as Doc; }
@computed get extensionDoc() { return Doc.fieldExtensionDoc(this.dataDoc, this.props.fieldKey); }
+ @computed get extensionDocSync() { return Doc.fieldExtensionDocSync(this.dataDoc, this.props.fieldKey); }
@computed get annotationsKey() { return "annotations"; }
@action.bound
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 5c7794cc0..062521690 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -262,7 +262,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
.then(result => {
const type = result["content-type"];
if (type) {
- Docs.Get.DocumentFromType(type, str, { ...options, width: 300, nativeWidth: type.indexOf("video") !== -1 ? 600 : 300 })
+ Docs.Get.DocumentFromType(type, str, options)
.then(doc => doc && this.props.addDocument(doc));
}
});
@@ -281,7 +281,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
const dropFileName = file ? file.name : "-empty-";
promises.push(Networking.PostFormDataToServer("/upload", formData).then(results => {
results.map(action(({ clientAccessPath }: any) => {
- const full = { ...options, nativeWidth: type.indexOf("video") !== -1 ? 600 : 300, width: 300, title: dropFileName };
+ const full = { ...options, width: 300, title: dropFileName };
const pathname = Utils.prepend(clientAccessPath);
Docs.Get.DocumentFromType(type, pathname, full).then(doc => {
doc && (Doc.GetProto(doc).fileUpload = basename(pathname).replace("upload_", "").replace(/\.[a-z0-9]*$/, ""));
diff --git a/src/client/views/collections/ParentDocumentSelector.tsx b/src/client/views/collections/ParentDocumentSelector.tsx
index e00d8e83b..8346e8099 100644
--- a/src/client/views/collections/ParentDocumentSelector.tsx
+++ b/src/client/views/collections/ParentDocumentSelector.tsx
@@ -81,7 +81,7 @@ export class SelectorContextMenu extends React.Component<SelectorProps> {
@observer
export class ParentDocSelector extends React.Component<SelectorProps> {
render() {
- let flyout = (
+ const flyout = (
<div className="parentDocumentSelector-flyout" style={{}} title=" ">
<SelectorContextMenu {...this.props} />
</div>
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
index 070d4aa65..f8a9e49d8 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
@@ -52,6 +52,8 @@
left: 0;
width: 100%;
height: 100%;
+ align-items: center;
+ display: flex;
}
// selection border...?
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 89c1df598..fc507bf5e 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -882,6 +882,19 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
eles.push(<CollectionFreeFormRemoteCursors {...this.props} key="remoteCursors" />);
return eles;
}
+ @computed get placeholder() {
+ return <div style={{ background: "gray", width: "100%", height: "100%", display: "flex", alignItems: "center" }}>
+ <span style={{ fontSize: 96, background: "gray", margin: "auto", display: "flex" }}>{this.props.Document.title}></span></div>;
+ }
+ @computed get marqueeView() {
+ return <MarqueeView {...this.props} extensionDoc={this.extensionDoc!} activeDocuments={this.getActiveDocuments} selectDocuments={this.selectDocuments} addDocument={this.addDocument}
+ addLiveTextDocument={this.addLiveTextBox} getContainerTransform={this.getContainerTransform} getTransform={this.getTransform} isAnnotationOverlay={this.isAnnotationOverlay}>
+ <CollectionFreeFormViewPannableContents centeringShiftX={this.centeringShiftX} centeringShiftY={this.centeringShiftY}
+ easing={this.easing} zoomScaling={this.zoomScaling} panX={this.panX} panY={this.panY}>
+ {this.children}
+ </CollectionFreeFormViewPannableContents>
+ </MarqueeView>;
+ }
render() {
TraceMobx();
// update the actual dimensions of the collection so that they can inquired (e.g., by a minimap)
@@ -895,13 +908,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
<div className={"collectionfreeformview-container"} ref={this.createDropTarget} onWheel={this.onPointerWheel}//pointerEvents: SelectionManager.GetIsDragging() ? "all" : undefined,
style={{ pointerEvents: SelectionManager.GetIsDragging() ? "all" : undefined, height: this.isAnnotationOverlay ? (this.props.Document.scrollHeight ? this.Document.scrollHeight : "100%") : this.props.PanelHeight() }}
onPointerDown={this.onPointerDown} onPointerMove={this.onCursorMove} onDrop={this.onDrop.bind(this)} onContextMenu={this.onContextMenu} onTouchStart={this.onTouchStart}>
- <MarqueeView {...this.props} extensionDoc={this.extensionDoc} activeDocuments={this.getActiveDocuments} selectDocuments={this.selectDocuments} addDocument={this.addDocument}
- addLiveTextDocument={this.addLiveTextBox} getContainerTransform={this.getContainerTransform} getTransform={this.getTransform} isAnnotationOverlay={this.isAnnotationOverlay}>
- <CollectionFreeFormViewPannableContents centeringShiftX={this.centeringShiftX} centeringShiftY={this.centeringShiftY}
- easing={this.easing} zoomScaling={this.zoomScaling} panX={this.panX} panY={this.panY}>
- {this.children}
- </CollectionFreeFormViewPannableContents>
- </MarqueeView>
+ {!BoolCast(this.Document.LODdisable) && !this.props.isAnnotationOverlay && this.props.renderDepth > 0 && this.Document[WidthSym]() * this.Document[HeightSym]() / this.props.ScreenToLocalTransform().Scale / this.props.ScreenToLocalTransform().Scale < NumCast(this.Document.LODarea, 100000) ? this.placeholder : this.marqueeView}
<CollectionFreeFormOverlayView elements={this.elementFunc} />
</div>;
}
diff --git a/src/client/views/nodes/ContentFittingDocumentView.scss b/src/client/views/nodes/ContentFittingDocumentView.scss
index 796e67269..2801af441 100644
--- a/src/client/views/nodes/ContentFittingDocumentView.scss
+++ b/src/client/views/nodes/ContentFittingDocumentView.scss
@@ -2,10 +2,11 @@
.contentFittingDocumentView {
position: relative;
- height: auto !important;
+ display: flex;
+ align-items: center;
.contentFittingDocumentView-previewDoc {
- position: absolute;
+ position: relative;
display: inline;
}
diff --git a/src/client/views/nodes/ContentFittingDocumentView.tsx b/src/client/views/nodes/ContentFittingDocumentView.tsx
index 5e3306a11..e15790c9d 100644
--- a/src/client/views/nodes/ContentFittingDocumentView.tsx
+++ b/src/client/views/nodes/ContentFittingDocumentView.tsx
@@ -31,9 +31,9 @@ interface ContentFittingDocumentViewProps {
CollectionDoc?: Doc;
onClick?: ScriptField;
getTransform: () => Transform;
- addDocument: (document: Doc) => boolean;
- moveDocument: (document: Doc, target: Doc | undefined, addDoc: ((doc: Doc) => boolean)) => boolean;
- removeDocument: (document: Doc) => boolean;
+ addDocument?: (document: Doc) => boolean;
+ moveDocument?: (document: Doc, target: Doc | undefined, addDoc: ((doc: Doc) => boolean)) => boolean;
+ removeDocument?: (document: Doc) => boolean;
active: (outsideReaction: boolean) => boolean;
whenActiveChanged: (isActive: boolean) => void;
addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => boolean;
@@ -47,8 +47,8 @@ interface ContentFittingDocumentViewProps {
export class ContentFittingDocumentView extends React.Component<ContentFittingDocumentViewProps>{
public get displayName() { return "DocumentView(" + this.props.Document?.title + ")"; } // this makes mobx trace() statements more descriptive
private get layoutDoc() { return this.props.Document && Doc.Layout(this.props.Document); }
- private get nativeWidth() { return NumCast(this.layoutDoc!.nativeWidth, this.props.PanelWidth()); }
- private get nativeHeight() { return NumCast(this.layoutDoc!.nativeHeight, this.props.PanelHeight()); }
+ private get nativeWidth() { return NumCast(this.layoutDoc?.nativeWidth, this.props.PanelWidth()); }
+ private get nativeHeight() { return NumCast(this.layoutDoc?.nativeHeight, this.props.PanelHeight()); }
private contentScaling = () => {
const wscale = this.props.PanelWidth() / (this.nativeWidth ? this.nativeWidth : this.props.PanelWidth());
if (wscale * this.nativeHeight > this.props.PanelHeight()) {
@@ -73,21 +73,25 @@ export class ContentFittingDocumentView extends React.Component<ContentFittingDo
}
private PanelWidth = () => this.nativeWidth && (!this.props.Document || !this.props.Document.fitWidth) ? this.nativeWidth * this.contentScaling() : this.props.PanelWidth();
private PanelHeight = () => this.nativeHeight && (!this.props.Document || !this.props.Document.fitWidth) ? this.nativeHeight * this.contentScaling() : this.props.PanelHeight();
- private getTransform = () => this.props.getTransform().translate(-this.centeringOffset, 0).scale(1 / this.contentScaling());
+ private getTransform = () => this.props.getTransform().translate(-this.centeringOffset, -this.centeringYOffset).scale(1 / this.contentScaling());
private get centeringOffset() { return this.nativeWidth && (!this.props.Document || !this.props.Document.fitWidth) ? (this.props.PanelWidth() - this.nativeWidth * this.contentScaling()) / 2 : 0; }
+ private get centeringYOffset() { return Math.abs(this.centeringOffset) < 0.001 ? (this.props.PanelHeight() - this.nativeHeight * this.contentScaling()) / 2 : 0; }
- @computed get borderRounding() { return StrCast(this.props.Document!.borderRounding); }
+ @computed get borderRounding() { return StrCast(this.props.Document?.borderRounding); }
render() {
TraceMobx();
- return (<div className="contentFittingDocumentView" style={{ width: this.props.PanelWidth(), height: this.props.PanelHeight() }}>
+ return (<div className="contentFittingDocumentView" style={{
+ width: Math.abs(this.centeringYOffset) > 0.001 ? "auto" : this.props.PanelWidth(),
+ height: Math.abs(this.centeringOffset) > 0.0001 ? "auto" : this.props.PanelHeight()
+ }}>
{!this.props.Document || !this.props.PanelWidth ? (null) : (
<div className="contentFittingDocumentView-previewDoc"
style={{
transform: `translate(${this.centeringOffset}px, 0px)`,
borderRadius: this.borderRounding,
- height: this.props.PanelHeight(),
- width: `${100 * (this.props.PanelWidth() - this.centeringOffset * 2) / this.props.PanelWidth()}%`
+ height: Math.abs(this.centeringYOffset) > 0.001 ? `${100 * this.nativeHeight / this.nativeWidth * this.props.PanelWidth() / this.props.PanelHeight()}%` : this.props.PanelHeight(),
+ width: Math.abs(this.centeringOffset) > 0.001 ? `${100 * (this.props.PanelWidth() - this.centeringOffset * 2) / this.props.PanelWidth()}%` : this.props.PanelWidth()
}}>
<DocumentView {...this.props}
Document={this.props.Document}
diff --git a/src/client/views/nodes/DocumentBox.scss b/src/client/views/nodes/DocumentBox.scss
new file mode 100644
index 000000000..6a2f98166
--- /dev/null
+++ b/src/client/views/nodes/DocumentBox.scss
@@ -0,0 +1,6 @@
+.documentBox-container {
+ width: 100%;
+ height: 100%;
+ pointer-events: all;
+ background: gray;
+} \ No newline at end of file
diff --git a/src/client/views/nodes/DocumentBox.tsx b/src/client/views/nodes/DocumentBox.tsx
new file mode 100644
index 000000000..b9a97b42d
--- /dev/null
+++ b/src/client/views/nodes/DocumentBox.tsx
@@ -0,0 +1,55 @@
+import { observer } from "mobx-react";
+import { Doc } from "../../../new_fields/Doc";
+import { documentSchema } from "../../../new_fields/documentSchemas";
+import { makeInterface } from "../../../new_fields/Schema";
+import { ComputedField } from "../../../new_fields/ScriptField";
+import { emptyFunction, emptyPath } from "../../../Utils";
+import { ContextMenu } from "../ContextMenu";
+import { ContextMenuProps } from "../ContextMenuItem";
+import { DocComponent } from "../DocComponent";
+import { ContentFittingDocumentView } from "./ContentFittingDocumentView";
+import "./DocumentBox.scss";
+import { FieldView, FieldViewProps } from "./FieldView";
+import React = require("react");
+
+type DocBoxSchema = makeInterface<[typeof documentSchema]>;
+const DocBoxDocument = makeInterface(documentSchema);
+
+@observer
+export class DocumentBox extends DocComponent<FieldViewProps, DocBoxSchema>(DocBoxDocument) {
+ public static LayoutString(fieldKey: string) { return FieldView.LayoutString(DocumentBox, fieldKey); }
+
+
+ specificContextMenu = (e: React.MouseEvent): void => {
+ const funcs: ContextMenuProps[] = [];
+ funcs.push({ description: "Auto Show Selected", event: () => Doc.GetProto(this.props.Document).data = ComputedField.MakeFunction("selectedDocs(this,true,[_last_])?.[0]"), icon: "expand-arrows-alt" });
+
+ ContextMenu.Instance.addItem({ description: "DocumentBox Funcs...", subitems: funcs, icon: "asterisk" });
+ }
+ render() {
+ const containedDoc = this.props.Document[this.props.fieldKey] as Doc;
+ return <div className="documentBox-container" onContextMenu={this.specificContextMenu}>
+ {!containedDoc ? (null) : <ContentFittingDocumentView
+ Document={containedDoc}
+ DataDocument={undefined}
+ LibraryPath={emptyPath}
+ fitToBox={this.props.fitToBox}
+ addDocument={this.props.addDocument}
+ moveDocument={this.props.moveDocument}
+ removeDocument={this.props.removeDocument}
+ ruleProvider={this.props.ruleProvider}
+ addDocTab={this.props.addDocTab}
+ pinToPres={this.props.pinToPres}
+ getTransform={this.props.ScreenToLocalTransform}
+ renderDepth={this.props.renderDepth - 1}
+ PanelWidth={this.props.PanelWidth}
+ PanelHeight={this.props.PanelHeight}
+ focus={this.props.focus}
+ active={this.props.active}
+ whenActiveChanged={this.props.whenActiveChanged}
+ setPreviewScript={emptyFunction}
+ previewScript={undefined}
+ />}
+ </div>;
+ }
+}
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index 1bbc82119..8f6bfc8e1 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -14,6 +14,7 @@ import { LinkFollowBox } from "../linking/LinkFollowBox";
import { YoutubeBox } from "./../../apis/youtube/YoutubeBox";
import { AudioBox } from "./AudioBox";
import { ButtonBox } from "./ButtonBox";
+import { DocumentBox } from "./DocumentBox";
import { DocumentViewProps } from "./DocumentView";
import "./DocumentView.scss";
import { FontIconBox } from "./FontIconBox";
@@ -102,7 +103,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
FormattedTextBox, ImageBox, IconBox, DirectoryImportBox, FontIconBox: FontIconBox, ButtonBox, FieldView,
CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, WebBox, KeyValueBox,
PDFBox, VideoBox, AudioBox, HistogramBox, PresBox, YoutubeBox, LinkFollowBox, PresElementBox, QueryBox,
- ColorBox, DocuLinkBox, InkingStroke
+ ColorBox, DocuLinkBox, InkingStroke, DocumentBox
}}
bindings={this.CreateBindings()}
jsx={this.layout}
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index b096d68fc..18885f272 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -43,6 +43,8 @@ import { InteractionUtils } from '../../util/InteractionUtils';
import { InkingControl } from '../InkingControl';
import { InkTool } from '../../../new_fields/InkField';
import { TraceMobx } from '../../../new_fields/util';
+import { List } from '../../../new_fields/List';
+import { FormattedTextBoxComment } from './FormattedTextBoxComment';
library.add(fa.faEdit, fa.faTrash, fa.faShare, fa.faDownload, fa.faExpandArrowsAlt, fa.faCompressArrowsAlt, fa.faLayerGroup, fa.faExternalLinkAlt, fa.faAlignCenter, fa.faCaretSquareRight,
fa.faSquare, fa.faConciergeBell, fa.faWindowRestore, fa.faFolder, fa.faMapPin, fa.faLink, fa.faFingerprint, fa.faCrosshairs, fa.faDesktop, fa.faUnlock, fa.faLock, fa.faLaptopCode, fa.faMale,
@@ -170,6 +172,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
this.onClickHandler.script.run({ this: this.Document.isTemplateField && this.props.DataDoc ? this.props.DataDoc : this.props.Document }, console.log);
} else if (this.Document.type === DocumentType.BUTTON) {
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)]));
} 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);
@@ -316,6 +321,17 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
@undoBatch
+ makeSelBtnClicked = (): void => {
+ if (this.Document.isButton || this.Document.onClick || this.Document.ignoreClick) {
+ this.Document.isButton = false;
+ this.Document.ignoreClick = false;
+ this.Document.onClick = undefined;
+ } else {
+ this.props.Document.isButton = "Selector";
+ }
+ }
+
+ @undoBatch
@action
drop = async (e: Event, de: DragManager.DropEvent) => {
if (de.complete.annoDragData) {
@@ -433,13 +449,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
onClicks.push({ description: "Toggle Detail", event: () => this.Document.onClick = ScriptField.MakeScript("toggleDetail(this)"), icon: "window-restore" });
onClicks.push({ description: this.Document.ignoreClick ? "Select" : "Do Nothing", event: () => this.Document.ignoreClick = !this.Document.ignoreClick, icon: this.Document.ignoreClick ? "unlock" : "lock" });
onClicks.push({ description: this.Document.isButton || this.Document.onClick ? "Remove Click Behavior" : "Follow Link", event: this.makeBtnClicked, icon: "concierge-bell" });
+ onClicks.push({ description: this.props.Document.isButton ? "Remove Select Link Behavior" : "Select Link", event: this.makeSelBtnClicked, icon: "concierge-bell" });
onClicks.push({ description: "Edit onClick Script", icon: "edit", event: (obj: any) => ScriptBox.EditButtonScript("On Button Clicked ...", this.props.Document, "onClick", obj.x, obj.y) });
- onClicks.push({
- description: "Edit onClick Foreach Doc Script", icon: "edit", event: (obj: any) => {
- this.props.Document.collectionContext = this.props.ContainingCollectionDoc;
- ScriptBox.EditButtonScript("Foreach Collection Doc (d) => ", this.props.Document, "onClick", obj.x, obj.y, "docList(this.collectionContext.data).map(d => {", "});\n");
- }
- });
!existingOnClick && cm.addItem({ description: "OnClick...", subitems: onClicks, icon: "hand-point-right" });
const funcs: ContextMenuProps[] = [];
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index a298fd6af..3efbd434e 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -822,6 +822,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
clipboardTextSerializer: this.clipboardTextSerializer,
handlePaste: this.handlePaste,
});
+ this._editorView.state.schema.Document = this.props.Document;
if (startup && this._editorView) {
Doc.GetProto(doc).documentText = undefined;
this._editorView.dispatch(this._editorView.state.tr.insertText(startup));
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index f6aa47f15..09e627078 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -18,7 +18,6 @@ import { undoBatch } from '../../util/UndoManager';
import { ContextMenu } from "../../views/ContextMenu";
import { ContextMenuProps } from '../ContextMenuItem';
import { DocAnnotatableComponent } from '../DocComponent';
-import { InkingControl } from '../InkingControl';
import FaceRectangles from './FaceRectangles';
import { FieldView, FieldViewProps } from './FieldView';
import "./ImageBox.scss";
diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx
index 322d639dc..234a6a9d3 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -57,7 +57,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
value = eq ? value.substr(1) : value;
const dubEq = value.startsWith(":=") ? "computed" : value.startsWith(";=") ? "script" : false;
value = dubEq ? value.substr(2) : value;
- const options: ScriptOptions = { addReturn: true, params: { this: "Doc" } };
+ const options: ScriptOptions = { addReturn: true, params: { this: "Doc", _last_: "any" }, editable: false };
if (dubEq) options.typecheck = false;
const script = CompileScript(value, options);
if (!script.compiled) {
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 0f5cc0fa9..2f1e1832e 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -3,11 +3,11 @@ import { action, observable, runInAction, reaction, IReactionDisposer, trace, un
import { observer } from "mobx-react";
import * as Pdfjs from "pdfjs-dist";
import "pdfjs-dist/web/pdf_viewer.css";
-import { Opt, WidthSym, Doc } from "../../../new_fields/Doc";
+import { Opt, WidthSym, Doc, HeightSym } from "../../../new_fields/Doc";
import { makeInterface } from "../../../new_fields/Schema";
import { ScriptField } from '../../../new_fields/ScriptField';
-import { Cast, NumCast } from "../../../new_fields/Types";
-import { PdfField } from "../../../new_fields/URLField";
+import { Cast, NumCast, StrCast } from "../../../new_fields/Types";
+import { PdfField, URLField } from "../../../new_fields/URLField";
import { Utils } from '../../../Utils';
import { KeyCodes } from '../../northstar/utils/KeyCodes';
import { undoBatch } from '../../util/UndoManager';
@@ -49,16 +49,38 @@ export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument>
constructor(props: any) {
super(props);
this._initialScale = this.props.ScreenToLocalTransform().Scale;
+ const nw = this.Document.nativeWidth = NumCast(this.extensionDocSync.nativeWidth, NumCast(this.Document.nativeWidth, 927));
+ const nh = this.Document.nativeHeight = NumCast(this.extensionDocSync.nativeHeight, NumCast(this.Document.nativeHeight, 1200));
+ !this.Document.fitWidth && !this.Document.ignoreAspect && (this.Document.height = this.Document[WidthSym]() * (nh / nw));
+
+ const backup = "oldPath";
+ const { Document } = this.props;
+ const { url: { href } } = Cast(Document[this.props.fieldKey], PdfField)!;
+ const pathCorrectionTest = /upload\_[a-z0-9]{32}.(.*)/g;
+ const matches = pathCorrectionTest.exec(href);
+ console.log("\nHere's the { url } being fed into the outer regex:");
+ console.log(href);
+ console.log("And here's the 'properPath' build from the captured filename:\n");
+ if (matches !== null) {
+ const properPath = Utils.prepend(`/files/pdfs/${matches[0]}`);
+ console.log(properPath);
+ if (!properPath.includes(href)) {
+ console.log(`The two (url and proper path) were not equal`);
+ const proto = Doc.GetProto(Document);
+ proto[this.props.fieldKey] = new PdfField(properPath);
+ proto[backup] = href;
+ } else {
+ console.log(`The two (url and proper path) were equal`);
+ }
+ } else {
+ console.log("Outer matches was null!");
+ }
}
componentWillUnmount() {
this._selectReactionDisposer && this._selectReactionDisposer();
}
componentDidMount() {
- const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField);
- if (pdfUrl instanceof PdfField) {
- Pdfjs.getDocument(pdfUrl.url.pathname).promise.then(pdf => runInAction(() => this._pdf = pdf));
- }
this._selectReactionDisposer = reaction(() => this.props.isSelected(),
() => {
document.removeEventListener("keydown", this.onKeyDown);
@@ -67,9 +89,9 @@ export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument>
}
loaded = (nw: number, nh: number, np: number) => {
- this.dataDoc.numPages = np;
- this.Document.nativeWidth = nw * 96 / 72;
- this.Document.nativeHeight = nh * 96 / 72;
+ this.extensionDocSync.numPages = np;
+ this.extensionDocSync.nativeWidth = this.Document.nativeWidth = nw * 96 / 72;
+ this.extensionDocSync.nativeHeight = this.Document.nativeHeight = nh * 96 / 72;
!this.Document.fitWidth && !this.Document.ignoreAspect && (this.Document.height = this.Document[WidthSym]() * (nh / nw));
}
@@ -197,9 +219,9 @@ export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument>
@computed get renderTitleBox() {
const classname = "pdfBox" + (this.active() ? "-interactive" : "");
return <div className={classname} style={{
- width: !this.props.Document.fitWidth ? NumCast(this.props.Document.nativeWidth) : `${100 / this.contentScaling}%`,
- height: !this.props.Document.fitWidth ? NumCast(this.props.Document.nativeHeight) : `${100 / this.contentScaling}%`,
- transform: `scale(${this.props.ContentScaling()})`
+ width: !this.props.Document.fitWidth ? this.Document.nativeWidth || 0 : `${100 / this.contentScaling}%`,
+ height: !this.props.Document.fitWidth ? this.Document.nativeHeight || 0 : `${100 / this.contentScaling}%`,
+ transform: `scale(${this.contentScaling})`
}} >
<div className="pdfBox-title-outer">
<strong className="pdfBox-title" >{this.props.Document.title}</strong>
@@ -225,10 +247,19 @@ export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument>
</div>;
}
+ _pdfjsRequested = false;
render() {
const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField, null);
if (this.props.isSelected() || this.props.Document.scrollY !== undefined) this._everActive = true;
- return !pdfUrl || !this._pdf || !this.extensionDoc || (!this._everActive && this.props.ScreenToLocalTransform().Scale > 2.5) ?
- this.renderTitleBox : this.renderPdfView;
+ if (pdfUrl && this.extensionDoc && (this._everActive || (this.extensionDoc.nativeWidth && this.props.ScreenToLocalTransform().Scale < 2.5))) {
+ if (pdfUrl instanceof PdfField && this._pdf) {
+ return this.renderPdfView;
+ }
+ if (!this._pdfjsRequested) {
+ this._pdfjsRequested = true;
+ Pdfjs.getDocument(pdfUrl.url.href).promise.then(pdf => runInAction(() => this._pdf = pdf));
+ }
+ }
+ return this.renderTitleBox;
}
} \ No newline at end of file
diff --git a/src/client/views/pdf/PDFMenu.tsx b/src/client/views/pdf/PDFMenu.tsx
index 38e10e99a..503696ae9 100644
--- a/src/client/views/pdf/PDFMenu.tsx
+++ b/src/client/views/pdf/PDFMenu.tsx
@@ -12,19 +12,17 @@ export default class PDFMenu extends AntimodeMenu {
static Instance: PDFMenu;
private _commentCont = React.createRef<HTMLButtonElement>();
- private _snippetButton: React.RefObject<HTMLButtonElement> = React.createRef();
@observable private _keyValue: string = "";
@observable private _valueValue: string = "";
@observable private _added: boolean = false;
@observable public Highlighting: boolean = false;
- @observable public Status: "pdf" | "annotation" | "snippet" | "" = "";
+ @observable public Status: "pdf" | "annotation" | "" = "";
public StartDrag: (e: PointerEvent, ele: HTMLElement) => void = unimplementedFunction;
public Highlight: (color: string) => Opt<Doc> = (color: string) => undefined;
public Delete: () => void = unimplementedFunction;
- public Snippet: (marquee: { left: number, top: number, width: number, height: number }) => void = unimplementedFunction;
public AddTag: (key: string, value: string) => boolean = returnFalse;
public PinToPres: () => void = unimplementedFunction;
public Marquee: { left: number; top: number; width: number; height: number; } | undefined;
@@ -80,34 +78,6 @@ export default class PDFMenu extends AntimodeMenu {
this.Delete();
}
- snippetStart = (e: React.PointerEvent) => {
- document.removeEventListener("pointermove", this.snippetDrag);
- document.addEventListener("pointermove", this.snippetDrag);
- document.removeEventListener("pointerup", this.snippetEnd);
- document.addEventListener("pointerup", this.snippetEnd);
-
- e.stopPropagation();
- e.preventDefault();
- }
-
- snippetDrag = (e: PointerEvent) => {
- e.stopPropagation();
- e.preventDefault();
- if (!this._dragging) {
- this._dragging = true;
-
- this.Marquee && this.Snippet(this.Marquee);
- }
- }
-
- snippetEnd = (e: PointerEvent) => {
- this._dragging = false;
- document.removeEventListener("pointermove", this.snippetDrag);
- document.removeEventListener("pointerup", this.snippetEnd);
- e.stopPropagation();
- e.preventDefault();
- }
-
@action
keyChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
this._keyValue = e.currentTarget.value;
@@ -128,14 +98,12 @@ export default class PDFMenu extends AntimodeMenu {
}
render() {
- const buttons = this.Status === "pdf" || this.Status === "snippet" ?
+ const buttons = this.Status === "pdf" ?
[
<button key="1" className="antimodeMenu-button" title="Click to Highlight" onClick={this.highlightClicked} style={this.Highlighting ? { backgroundColor: "#121212" } : {}}>
<FontAwesomeIcon icon="highlighter" size="lg" style={{ transition: "transform 0.1s", transform: this.Highlighting ? "" : "rotate(-45deg)" }} /></button>,
<button key="2" className="antimodeMenu-button" title="Drag to Annotate" ref={this._commentCont} onPointerDown={this.pointerDown}>
<FontAwesomeIcon icon="comment-alt" size="lg" /></button>,
- <button key="3" className="antimodeMenu-button" title="Drag to Snippetize Selection" style={{ display: this.Status === "snippet" ? "" : "none" }} onPointerDown={this.snippetStart} ref={this._snippetButton}>
- <FontAwesomeIcon icon="cut" size="lg" /></button>,
<button key="4" className="antimodeMenu-button" title="Pin Menu" onClick={this.togglePin} style={this.Pinned ? { backgroundColor: "#121212" } : {}}>
<FontAwesomeIcon icon="thumbtack" size="lg" style={{ transition: "transform 0.1s", transform: this.Pinned ? "rotate(45deg)" : "" }} /> </button>
] : [
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index c85350264..1a09991d2 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -30,6 +30,7 @@ import { DocumentDecorations } from "../DocumentDecorations";
import { InkingControl } from "../InkingControl";
import { InkTool } from "../../../new_fields/InkField";
import { TraceMobx } from "../../../new_fields/util";
+import { PdfField } from "../../../new_fields/URLField";
const PDFJSViewer = require("pdfjs-dist/web/pdf_viewer");
const pdfjsLib = require("pdfjs-dist");
@@ -416,7 +417,6 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
// clear out old marquees and initialize menu for new selection
PDFMenu.Instance.StartDrag = this.startDrag;
PDFMenu.Instance.Highlight = this.highlight;
- PDFMenu.Instance.Snippet = this.createSnippet;
PDFMenu.Instance.Status = "pdf";
PDFMenu.Instance.fadeOut(true);
this._savedAnnotations.values().forEach(v => v.forEach(a => a.remove()));
@@ -514,7 +514,6 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
}
if (!e.ctrlKey) {
- PDFMenu.Instance.Status = "snippet";
PDFMenu.Instance.Marquee = { left: this._marqueeX, top: this._marqueeY, width: this._marqueeWidth, height: this._marqueeHeight };
}
PDFMenu.Instance.jumpTo(e.clientX, e.clientY);
@@ -598,12 +597,13 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
if (!this.props.Document[HeightSym]() || !this.props.Document.nativeHeight) {
setTimeout((() => {
this.Document.height = this.Document[WidthSym]() * this._coverPath.height / this._coverPath.width;
- this.Document.nativeHeight = nativeWidth * this._coverPath.height / this._coverPath.width;
+ this.Document.nativeHeight = (this.Document.nativeWidth || 0) * this._coverPath.height / this._coverPath.width;
}).bind(this), 0);
}
const nativeWidth = (this.Document.nativeWidth || 0);
const nativeHeight = (this.Document.nativeHeight || 0);
- return <img key={this._coverPath.path} src={this._coverPath.path} onError={action(() => this._coverPath.path = "http://www.cs.brown.edu/~bcz/face.gif")} onLoad={action(() => this._showWaiting = false)}
+ const resolved = Utils.prepend(this._coverPath.path);
+ return <img key={resolved} src={resolved} onError={action(() => this._coverPath.path = "http://www.cs.brown.edu/~bcz/face.gif")} onLoad={action(() => this._showWaiting = false)}
style={{ position: "absolute", display: "inline-block", top: 0, left: 0, width: `${nativeWidth}px`, height: `${nativeHeight}px` }} />;
}
diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx
index b80c3bb54..2f28ebf76 100644
--- a/src/client/views/search/SearchBox.tsx
+++ b/src/client/views/search/SearchBox.tsx
@@ -234,8 +234,7 @@ export class SearchBox extends React.Component {
y += 300;
}
}
- return Docs.Create.FreeformDocument(docs, { width: 400, height: 400, panX: 175, panY: 175, backgroundColor: "grey", title: `Search Docs: "${this._searchString}"` });
-
+ return Docs.Create.TreeDocument(docs, { width: 200, height: 400, backgroundColor: "grey", title: `Search Docs: "${this._searchString}"` });
}
@action.bound
@@ -341,7 +340,7 @@ export class SearchBox extends React.Component {
return (
<div className="searchBox-container" onPointerDown={e => { e.stopPropagation(); e.preventDefault(); }}>
<div className="searchBox-bar">
- <span className="searchBox-barChild searchBox-collection" onPointerDown={SetupDrag(this.collectionRef, this.startDragCollection)} ref={this.collectionRef} title="Drag Results as Collection">
+ <span className="searchBox-barChild searchBox-collection" onPointerDown={SetupDrag(this.collectionRef, () => this._searchString ? this.startDragCollection() : undefined)} ref={this.collectionRef} title="Drag Results as Collection">
<FontAwesomeIcon icon="object-group" size="lg" />
</span>
<input value={this._searchString} onChange={this.onChange} type="text" placeholder="Search..." id="search-input" ref={this.inputRef}
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index 17c1b7e16..b8f11c8c8 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -1,4 +1,4 @@
-import { observable, ObservableMap, runInAction, action } from "mobx";
+import { observable, ObservableMap, runInAction, action, untracked } from "mobx";
import { alias, map, serializable } from "serializr";
import { DocServer } from "../client/DocServer";
import { DocumentType } from "../client/documents/DocumentTypes";
@@ -467,9 +467,14 @@ export namespace Doc {
// to store annotations, ink, and other data.
//
export function fieldExtensionDoc(doc: Doc, fieldKey: string) {
- const extension = doc[fieldKey + "_ext"] as Doc;
- (extension === undefined) && setTimeout(() => CreateDocumentExtensionForField(doc, fieldKey), 0);
- return extension ? extension : undefined;
+ const extension = doc[fieldKey + "_ext"];
+ if (extension === undefined) {
+ setTimeout(() => CreateDocumentExtensionForField(doc, fieldKey), 0);
+ }
+ return extension ? extension as Doc : undefined;
+ }
+ export function fieldExtensionDocSync(doc: Doc, fieldKey: string) {
+ return (doc[fieldKey + "_ext"] as Doc) || CreateDocumentExtensionForField(doc, fieldKey);
}
export function CreateDocumentExtensionForField(doc: Doc, fieldKey: string) {
@@ -682,7 +687,9 @@ export namespace Doc {
}
+ export function LinkOtherAnchor(linkDoc: Doc, anchorDoc: Doc) { return Doc.AreProtosEqual(anchorDoc, Cast(linkDoc.anchor1, Doc) as Doc) ? Cast(linkDoc.anchor2, Doc) as Doc : Cast(linkDoc.anchor1, Doc) as Doc; }
export function LinkEndpoint(linkDoc: Doc, anchorDoc: Doc) { return Doc.AreProtosEqual(anchorDoc, Cast(linkDoc.anchor1, Doc) as Doc) ? "layoutKey1" : "layoutKey2"; }
+
export function linkFollowUnhighlight() {
Doc.UnhighlightAll();
document.removeEventListener("pointerdown", linkFollowUnhighlight);
@@ -748,4 +755,8 @@ Scripting.addGlobal(function aliasDocs(field: any) { return new List<Doc>(field.
Scripting.addGlobal(function docList(field: any) { return DocListCast(field); });
Scripting.addGlobal(function sameDocs(doc1: any, doc2: any) { return Doc.AreProtosEqual(doc1, doc2); });
Scripting.addGlobal(function undo() { return UndoManager.Undo(); });
-Scripting.addGlobal(function redo() { return UndoManager.Redo(); }); \ No newline at end of file
+Scripting.addGlobal(function redo() { return UndoManager.Redo(); });
+Scripting.addGlobal(function selectedDocs(container: Doc, excludeCollections: boolean, prevValue: any) {
+ const docs = DocListCast(Doc.UserDoc().SelectedDocs).filter(d => !Doc.AreProtosEqual(d, container) && (!excludeCollections || !Cast(d.data, listSpec(Doc), null)) && d.type !== DocumentType.KVP);
+ return docs.length ? new List(docs) : prevValue;
+}); \ No newline at end of file
diff --git a/src/new_fields/ScriptField.ts b/src/new_fields/ScriptField.ts
index 6b2723663..b5ad4a7f6 100644
--- a/src/new_fields/ScriptField.ts
+++ b/src/new_fields/ScriptField.ts
@@ -103,7 +103,7 @@ export class ScriptField extends ObjectField {
}
public static CompileScript(script: string, params: object = {}, addReturn = false) {
const compiled = CompileScript(script, {
- params: { this: Doc.name, ...params },
+ params: { this: Doc.name, _last_: "any", ...params },
typecheck: false,
editable: true,
addReturn: addReturn
@@ -124,8 +124,9 @@ export class ScriptField extends ObjectField {
@scriptingGlobal
@Deserializable("computed", deserializeScript)
export class ComputedField extends ScriptField {
+ _lastComputedResult: any;
//TODO maybe add an observable cache based on what is passed in for doc, considering there shouldn't really be that many possible values for doc
- value = computedFn((doc: Doc) => this.script.run({ this: doc }, console.log).result);
+ value = computedFn((doc: Doc) => this._lastComputedResult = this.script.run({ this: doc, _last_: this._lastComputedResult }, console.log).result);
public static MakeScript(script: string, params: object = {}, ) {
const compiled = ScriptField.CompileScript(script, params, false);
return compiled.compiled ? new ComputedField(compiled) : undefined;
diff --git a/src/new_fields/documentSchemas.ts b/src/new_fields/documentSchemas.ts
index 5c2ba6111..21e69fbed 100644
--- a/src/new_fields/documentSchemas.ts
+++ b/src/new_fields/documentSchemas.ts
@@ -48,7 +48,9 @@ export const documentSchema = createSchema({
animateToDimensions: listSpec("number"), // layout information about the target rectangle a document is animating towards
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"
+ fontSize: "string",
+ LODarea: "number", // area (width*height) where CollectionFreeFormViews switch from a label to rendering contents
+ LODdisable: "boolean", // whether to disbale LOD switching for CollectionFreeFormViews
});
export const positionSchema = createSchema({
diff --git a/src/server/ApiManagers/PDFManager.ts b/src/server/ApiManagers/PDFManager.ts
index a190ab0cb..7e862631d 100644
--- a/src/server/ApiManagers/PDFManager.ts
+++ b/src/server/ApiManagers/PDFManager.ts
@@ -1,7 +1,7 @@
import ApiManager, { Registration } from "./ApiManager";
import { Method } from "../RouteManager";
import RouteSubscriber from "../RouteSubscriber";
-import { exists, createReadStream, createWriteStream } from "fs";
+import { existsSync, createReadStream, createWriteStream } from "fs";
import * as Pdfjs from 'pdfjs-dist';
import { createCanvas } from "canvas";
const imageSize = require("probe-image-size");
@@ -24,35 +24,30 @@ export default class PDFManager extends ApiManager {
}
-function getOrCreateThumbnail(thumbnailName: string, res: express.Response) {
+async function getOrCreateThumbnail(thumbnailName: string, res: express.Response): Promise<void> {
const noExtension = thumbnailName.substring(0, thumbnailName.length - ".png".length);
const pageString = noExtension.split('-')[1];
const pageNumber = parseInt(pageString);
- return new Promise<void>(resolve => {
+ return new Promise<void>(async resolve => {
const path = serverPathToFile(Directory.pdf_thumbnails, thumbnailName);
- exists(path, (exists: boolean) => {
- if (exists) {
- const existingThumbnail = createReadStream(path);
- imageSize(existingThumbnail, (err: any, { width, height }: any) => {
- if (err) {
- console.log(red(`In PDF thumbnail response, unable to determine dimensions of ${thumbnailName}:`));
- console.log(err);
- return;
- }
- res.send({
- path: clientPathToFile(Directory.pdf_thumbnails, thumbnailName),
- width,
- height
- });
- });
- } else {
- const offset = thumbnailName.length - pageString.length - 5;
- const name = thumbnailName.substring(0, offset) + ".pdf";
- const path = serverPathToFile(Directory.pdfs, name);
- CreateThumbnail(path, pageNumber, res);
+ if (existsSync(path)) {
+ const existingThumbnail = createReadStream(path);
+ const { err, viewport } = await new Promise<any>(resolve => {
+ imageSize(existingThumbnail, (err: any, viewport: any) => resolve({ err, viewport }));
+ });
+ if (err) {
+ console.log(red(`In PDF thumbnail response, unable to determine dimensions of ${thumbnailName}:`));
+ console.log(err);
+ return;
}
- resolve();
- });
+ dispatchThumbnail(res, viewport, thumbnailName);
+ } else {
+ const offset = thumbnailName.length - pageString.length - 5;
+ const name = thumbnailName.substring(0, offset) + ".pdf";
+ const path = serverPathToFile(Directory.pdfs, name);
+ await CreateThumbnail(path, pageNumber, res);
+ }
+ resolve();
});
}
@@ -70,19 +65,28 @@ async function CreateThumbnail(file: string, pageNumber: number, res: express.Re
await page.render(renderContext).promise;
const pngStream = canvas.createPNGStream();
const filenames = path.basename(file).split(".");
- const pngFile = serverPathToFile(Directory.pdf_thumbnails, `${filenames[0]}-${pageNumber}.png`);
+ const thumbnailName = `${filenames[0]}-${pageNumber}.png`;
+ const pngFile = serverPathToFile(Directory.pdf_thumbnails, thumbnailName);
const out = createWriteStream(pngFile);
pngStream.pipe(out);
- out.on("finish", () => {
- res.send({
- path: pngFile,
- width: viewport.width,
- height: viewport.height
+ return new Promise<void>((resolve, reject) => {
+ out.on("finish", () => {
+ dispatchThumbnail(res, viewport, thumbnailName);
+ resolve();
+ });
+ out.on("error", error => {
+ console.log(red(`In PDF thumbnail creation, encountered the following error when piping ${pngFile}:`));
+ console.log(error);
+ reject();
});
});
- out.on("error", error => {
- console.log(red(`In PDF thumbnail creation, encountered the following error when piping ${pngFile}:`));
- console.log(error);
+}
+
+function dispatchThumbnail(res: express.Response, { width, height }: Pdfjs.PDFPageViewport, thumbnailName: string) {
+ res.send({
+ path: clientPathToFile(Directory.pdf_thumbnails, thumbnailName),
+ width,
+ height
});
}
diff --git a/src/server/ChildProcessUtilities/daemon/session.ts b/src/server/ChildProcessUtilities/daemon/session.ts
deleted file mode 100644
index fb2b3551e..000000000
--- a/src/server/ChildProcessUtilities/daemon/session.ts
+++ /dev/null
@@ -1,190 +0,0 @@
-import * as request from "request-promise";
-import { log_execution, pathFromRoot } from "../../ActionUtilities";
-import { red, yellow, cyan, green, Color } from "colors";
-import * as nodemailer from "nodemailer";
-import { MailOptions } from "nodemailer/lib/json-transport";
-import { writeFileSync, existsSync, mkdirSync } from "fs";
-import { resolve } from 'path';
-import { ChildProcess, exec, execSync } from "child_process";
-import { createInterface } from "readline";
-const killport = require("kill-port");
-
-process.on('SIGINT', endPrevious);
-const identifier = yellow("__session_manager__:");
-
-let manualRestartActive = false;
-createInterface(process.stdin, process.stdout).on('line', async line => {
- const prompt = line.trim().toLowerCase();
- switch (prompt) {
- case "restart":
- manualRestartActive = true;
- identifiedLog(cyan("Initializing manual restart..."));
- await endPrevious();
- break;
- case "exit":
- identifiedLog(cyan("Initializing session end"));
- await endPrevious();
- identifiedLog("Cleanup complete. Exiting session...\n");
- execSync(killAllCommand());
- break;
- default:
- identifiedLog(red("commands: { exit, restart }"));
- return;
- }
-});
-
-const logPath = resolve(__dirname, "./logs");
-const crashPath = resolve(logPath, "./crashes");
-if (!existsSync(logPath)) {
- mkdirSync(logPath);
-}
-if (!existsSync(crashPath)) {
- mkdirSync(crashPath);
-}
-
-const crashLogPath = resolve(crashPath, `./session_crashes_${new Date().toISOString()}.log`);
-function addLogEntry(message: string, color: Color) {
- const formatted = color(`${message} ${timestamp()}.`);
- identifiedLog(formatted);
- // appendFileSync(crashLogPath, `${formatted}\n`);
-}
-
-function identifiedLog(message?: any, ...optionalParams: any[]) {
- console.log(identifier, message, ...optionalParams);
-}
-
-if (!["win32", "darwin"].includes(process.platform)) {
- identifiedLog(red("Invalid operating system: this script is supported only on Mac and Windows."));
- process.exit(1);
-}
-
-const latency = 10;
-const ports = [1050, 4321];
-const onWindows = process.platform === "win32";
-const LOCATION = "http://localhost";
-const heartbeat = `${LOCATION}:1050/serverHeartbeat`;
-const recipient = "samuel_wilkins@brown.edu";
-const { pid } = process;
-let restarting = false;
-let count = 0;
-
-function startServerCommand() {
- if (onWindows) {
- return '"C:\\Program Files\\Git\\git-bash.exe" -c "npm run start-release"';
- }
- return `osascript -e 'tell app "Terminal"\ndo script "cd ${pathFromRoot()} && npm run start-release"\nend tell'`;
-}
-
-function killAllCommand() {
- if (onWindows) {
- return "taskkill /f /im node.exe";
- }
- return "killall -9 node";
-}
-
-identifiedLog("Initializing session...");
-
-writeLocalPidLog("session_manager", pid);
-
-function writeLocalPidLog(filename: string, contents: any) {
- const path = `./logs/current_${filename}_pid.log`;
- identifiedLog(cyan(`${contents} written to ${path}`));
- writeFileSync(resolve(__dirname, path), `${contents}\n`);
-}
-
-function timestamp() {
- return `@ ${new Date().toISOString()}`;
-}
-
-async function endPrevious() {
- identifiedLog(yellow("Cleaning up previous connections..."));
- current_backup?.kill("SIGTERM");
- await Promise.all(ports.map(port => {
- const task = killport(port, 'tcp');
- return task.catch((error: any) => identifiedLog(red(error)));
- }));
- identifiedLog(yellow("Done. Any failures will be printed in red immediately above."));
-}
-
-let current_backup: ChildProcess | undefined = undefined;
-
-async function checkHeartbeat() {
- let error: any;
- try {
- count && !restarting && process.stdout.write(`${identifier} 👂 `);
- await request.get(heartbeat);
- count && !restarting && console.log('⇠ 💚');
- if (restarting || manualRestartActive) {
- addLogEntry(count++ ? "Backup server successfully restarted" : "Server successfully started", green);
- restarting = false;
- }
- } catch (e) {
- count && !restarting && console.log("⇠ 💔");
- error = e;
- } finally {
- if (error) {
- if (!restarting || manualRestartActive) {
- restarting = true;
- if (count && !manualRestartActive) {
- console.log();
- addLogEntry("Detected a server crash", red);
- identifiedLog(red(error.message));
- await endPrevious();
- await log_execution({
- startMessage: identifier + " Sending crash notification email",
- endMessage: ({ error, result }) => {
- const success = error === null && result === true;
- return identifier + ` ${(success ? `Notification successfully sent to` : `Failed to notify`)} ${recipient} ${timestamp()}`;
- },
- action: async () => notify(error || "Hmm, no error to report..."),
- color: cyan
- });
- identifiedLog(green("Initiating server restart..."));
- }
- manualRestartActive = false;
- current_backup = exec(startServerCommand(), err => identifiedLog(err?.message || count ? "Previous server process exited." : "Spawned initial server."));
- writeLocalPidLog("server", `${(current_backup?.pid ?? -2) + 1} created ${timestamp()}`);
- }
- }
- setTimeout(checkHeartbeat, 1000 * latency);
- }
-}
-
-async function startListening() {
- identifiedLog(yellow(`After initialization, will poll server heartbeat repeatedly...\n`));
- if (!LOCATION) {
- identifiedLog(red("No location specified for session manager. Please include as a command line environment variable or in a .env file."));
- process.exit(0);
- }
- await checkHeartbeat();
-}
-
-function emailText(error: any) {
- return [
- `Hey ${recipient.split("@")[0]},`,
- "You, as a Dash Administrator, are being notified of a server crash event. Here's what we know:",
- `Location: ${LOCATION}\nError: ${error}`,
- "The server should already be restarting itself, but if you're concerned, use the Remote Desktop Connection to monitor progress."
- ].join("\n\n");
-}
-
-async function notify(error: any) {
- const smtpTransport = nodemailer.createTransport({
- service: 'Gmail',
- auth: {
- user: 'brownptcdash@gmail.com',
- pass: 'browngfx1'
- }
- });
- const mailOptions = {
- to: recipient,
- from: 'brownptcdash@gmail.com',
- subject: 'Dash Server Crash',
- text: emailText(error)
- } as MailOptions;
- return new Promise<boolean>(resolve => {
- smtpTransport.sendMail(mailOptions, (dispatchError: Error | null) => resolve(dispatchError === null));
- });
-}
-
-startListening(); \ No newline at end of file
diff --git a/src/server/ChildProcessUtilities/ProcessFactory.ts b/src/server/ProcessFactory.ts
index 745b1479a..acb8b3a99 100644
--- a/src/server/ChildProcessUtilities/ProcessFactory.ts
+++ b/src/server/ProcessFactory.ts
@@ -1,10 +1,8 @@
import { existsSync, mkdirSync } from "fs";
-import { pathFromRoot, log_execution, fileDescriptorFromStream } from '../ActionUtilities';
-import { red, green } from "colors";
+import { pathFromRoot, fileDescriptorFromStream } from './ActionUtilities';
import rimraf = require("rimraf");
import { ChildProcess, spawn, StdioOptions } from "child_process";
import { Stream } from "stream";
-import { resolve } from "path";
export namespace ProcessFactory {
@@ -20,27 +18,6 @@ export namespace ProcessFactory {
return child;
}
- export namespace NamedAgents {
-
- export async function persistenceDaemon() {
- await log_execution({
- startMessage: "\ninitializing persistence daemon",
- endMessage: ({ result, error }) => {
- const success = error === null && result !== undefined;
- if (!success) {
- console.log(red("failed to initialize the persistance daemon"));
- console.log(error);
- process.exit(0);
- }
- return "failsafe daemon process successfully spawned";
- },
- action: () => createWorker('npx', ['ts-node', resolve(__dirname, "./daemon/persistence_daemon.ts")], ["ignore", "inherit", "inherit"]),
- color: green
- });
- console.log();
- }
- }
-
}
export namespace Logger {
diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts
index 50bfb4832..fd01a291d 100644
--- a/src/server/authentication/models/current_user_utils.ts
+++ b/src/server/authentication/models/current_user_utils.ts
@@ -47,6 +47,7 @@ export class CurrentUserUtils {
doc.activePen = doc;
const docProtoData: { title: string, icon: string, drag?: string, ignoreClick?: boolean, click?: string, ischecked?: string, activePen?: Doc, backgroundColor?: string, dragFactory?: Doc }[] = [
{ title: "collection", icon: "folder", ignoreClick: true, drag: 'Docs.Create.FreeformDocument([], { nativeWidth: undefined, nativeHeight: undefined, width: 150, height: 100, title: "freeform" })' },
+ { title: "preview", icon: "expand", ignoreClick: true, drag: 'Docs.Create.DocumentDocument(undefined, { width: 250, height: 250, title: "container" })' },
{ title: "todo item", icon: "check", ignoreClick: true, drag: 'getCopy(this.dragFactory, true)', dragFactory: notes[notes.length - 1] },
{ title: "web page", icon: "globe-asia", ignoreClick: true, drag: 'Docs.Create.WebDocument("https://en.wikipedia.org/wiki/Hedgehog", { width: 300, height: 300, title: "New Webpage" })' },
{ title: "cat image", icon: "cat", ignoreClick: true, drag: 'Docs.Create.ImageDocument("https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg", { width: 200, title: "an image of a cat" })' },
diff --git a/src/server/index.ts b/src/server/index.ts
index bc481e579..2cc35ccec 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -23,7 +23,7 @@ import GeneralGoogleManager from "./ApiManagers/GeneralGoogleManager";
import GooglePhotosManager from "./ApiManagers/GooglePhotosManager";
import { yellow, red } from "colors";
import { disconnect } from "../server/Initialization";
-import { ProcessFactory, Logger } from "./ChildProcessUtilities/ProcessFactory";
+import { ProcessFactory, Logger } from "./ProcessFactory";
export const publicDirectory = path.resolve(__dirname, "public");
export const filesDirectory = path.resolve(publicDirectory, "files");
diff --git a/src/server/session_manager/config.ts b/src/server/session_manager/config.ts
new file mode 100644
index 000000000..ebbd999c6
--- /dev/null
+++ b/src/server/session_manager/config.ts
@@ -0,0 +1,33 @@
+import { resolve } from 'path';
+import { yellow } from "colors";
+
+export const latency = 10;
+export const ports = [1050, 4321];
+export const onWindows = process.platform === "win32";
+export const heartbeat = `http://localhost:1050/serverHeartbeat`;
+export const recipient = "samuel_wilkins@brown.edu";
+export const { pid, platform } = process;
+
+/**
+ * Logging
+ */
+export const identifier = yellow("__session_manager__:");
+
+/**
+ * Paths
+ */
+export const logPath = resolve(__dirname, "./logs");
+export const crashPath = resolve(logPath, "./crashes");
+
+/**
+ * State
+ */
+export enum SessionState {
+ STARTING = "STARTING",
+ INITIALIZED = "INITIALIZED",
+ LISTENING = "LISTENING",
+ AUTOMATICALLY_RESTARTING = "CRASH_RESTARTING",
+ MANUALLY_RESTARTING = "MANUALLY_RESTARTING",
+ EXITING = "EXITING",
+ UPDATING = "UPDATING"
+} \ No newline at end of file
diff --git a/src/server/session_manager/input_manager.ts b/src/server/session_manager/input_manager.ts
new file mode 100644
index 000000000..a95e6baae
--- /dev/null
+++ b/src/server/session_manager/input_manager.ts
@@ -0,0 +1,101 @@
+import { createInterface, Interface } from "readline";
+import { red } from "colors";
+
+export interface Configuration {
+ identifier: string;
+ onInvalid?: (culprit?: string) => string | string;
+ isCaseSensitive?: boolean;
+}
+
+export interface Registration {
+ argPattern: RegExp[];
+ action: (parsedArgs: IterableIterator<string>) => any | Promise<any>;
+}
+
+export default class InputManager {
+ private identifier: string;
+ private onInvalid: ((culprit?: 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, isCaseSensitive }: Configuration) {
+ this.identifier = prompt;
+ this.onInvalid = onInvalid || this.usage;
+ this.isCaseSensitive = isCaseSensitive ?? true;
+ this.interface = createInterface(process.stdin, process.stdout).on('line', this.considerInput);
+ }
+
+ private usage = () => {
+ 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.identifier} commands: { ${members.sort().join(", ")} }`;
+ }
+
+ public registerCommand = (basename: string, argPattern: RegExp[], action: any | Promise<any>) => {
+ const existing = this.commandMap.get(basename);
+ const registration = { argPattern, action };
+ if (existing) {
+ existing.push(registration);
+ } else {
+ this.commandMap.set(basename, [registration]);
+ }
+ }
+
+ private invalid = (culprit?: string) => {
+ console.log(red(typeof this.onInvalid === "string" ? this.onInvalid : this.onInvalid(culprit)));
+ 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();
+ }
+ const registered = this.commandMap.get(command);
+ if (registered) {
+ const { length } = args;
+ const candidates = registered.filter(({ argPattern: { length: count } }) => count === length);
+ for (const { argPattern, action } of candidates) {
+ const parsed: string[] = [];
+ let matched = false;
+ if (length) {
+ for (let i = 0; i < length; i++) {
+ let matches: RegExpExecArray | null;
+ if ((matches = argPattern[i].exec(args[i])) === null) {
+ break;
+ }
+ parsed.push(matches[0]);
+ }
+ matched = true;
+ }
+ if (!length || matched) {
+ await action(parsed[Symbol.iterator]());
+ this.busy = false;
+ return;
+ }
+ }
+ }
+ this.invalid(command);
+ }
+
+} \ No newline at end of file
diff --git a/src/server/session_manager/logs/current_daemon_pid.log b/src/server/session_manager/logs/current_daemon_pid.log
new file mode 100644
index 000000000..557e3d7c3
--- /dev/null
+++ b/src/server/session_manager/logs/current_daemon_pid.log
@@ -0,0 +1 @@
+26860
diff --git a/src/server/session_manager/logs/current_server_pid.log b/src/server/session_manager/logs/current_server_pid.log
new file mode 100644
index 000000000..85fdb7ae0
--- /dev/null
+++ b/src/server/session_manager/logs/current_server_pid.log
@@ -0,0 +1 @@
+54649 created @ 2019-12-14T08:04:42.391Z
diff --git a/src/server/session_manager/logs/current_session_manager_pid.log b/src/server/session_manager/logs/current_session_manager_pid.log
new file mode 100644
index 000000000..75c23b35a
--- /dev/null
+++ b/src/server/session_manager/logs/current_session_manager_pid.log
@@ -0,0 +1 @@
+54643
diff --git a/src/server/session_manager/session_manager.ts b/src/server/session_manager/session_manager.ts
new file mode 100644
index 000000000..d8b2f6e74
--- /dev/null
+++ b/src/server/session_manager/session_manager.ts
@@ -0,0 +1,199 @@
+import * as request from "request-promise";
+import { log_execution, pathFromRoot } from "../ActionUtilities";
+import { red, yellow, cyan, green, Color } from "colors";
+import * as nodemailer from "nodemailer";
+import { MailOptions } from "nodemailer/lib/json-transport";
+import { writeFileSync, existsSync, mkdirSync } from "fs";
+import { resolve } from 'path';
+import { ChildProcess, exec, execSync } from "child_process";
+import InputManager from "./input_manager";
+import { identifier, logPath, crashPath, onWindows, pid, ports, heartbeat, recipient, latency, SessionState } from "./config";
+const killport = require("kill-port");
+
+process.on('SIGINT', endPrevious);
+let state: SessionState = SessionState.STARTING;
+const is = (...reference: SessionState[]) => reference.includes(state);
+const set = (reference: SessionState) => state = reference;
+
+const { registerCommand } = new InputManager({ identifier });
+
+registerCommand("restart", [], async () => {
+ set(SessionState.MANUALLY_RESTARTING);
+ identifiedLog(cyan("Initializing manual restart..."));
+ await endPrevious();
+});
+
+registerCommand("exit", [], exit);
+
+async function exit() {
+ set(SessionState.EXITING);
+ identifiedLog(cyan("Initializing session end"));
+ await endPrevious();
+ identifiedLog("Cleanup complete. Exiting session...\n");
+ execSync(killAllCommand());
+}
+
+registerCommand("update", [], async () => {
+ set(SessionState.UPDATING);
+ identifiedLog(cyan("Initializing server update from version control..."));
+ await endPrevious();
+ await new Promise<void>(resolve => {
+ exec(updateCommand(), error => {
+ if (error) {
+ identifiedLog(red(error.message));
+ }
+ resolve();
+ });
+ });
+ await exit();
+});
+
+registerCommand("state", [], () => identifiedLog(state));
+
+if (!existsSync(logPath)) {
+ mkdirSync(logPath);
+}
+if (!existsSync(crashPath)) {
+ mkdirSync(crashPath);
+}
+
+function addLogEntry(message: string, color: Color) {
+ const formatted = color(`${message} ${timestamp()}.`);
+ identifiedLog(formatted);
+ // appendFileSync(resolve(crashPath, `./session_crashes_${new Date().toISOString()}.log`), `${formatted}\n`);
+}
+
+function identifiedLog(message?: any, ...optionalParams: any[]) {
+ console.log(identifier, message, ...optionalParams);
+}
+
+if (!["win32", "darwin"].includes(process.platform)) {
+ identifiedLog(red("Invalid operating system: this script is supported only on Mac and Windows."));
+ process.exit(1);
+}
+
+const windowsPrepend = (command: string) => `"C:\\Program Files\\Git\\git-bash.exe" -c "${command}"`;
+const macPrepend = (command: string) => `osascript -e 'tell app "Terminal"\ndo script "cd ${pathFromRoot()} && ${command}"\nend tell'`;
+
+function updateCommand() {
+ const command = "git pull && npm install";
+ if (onWindows) {
+ return windowsPrepend(command);
+ }
+ return macPrepend(command);
+}
+
+function startServerCommand() {
+ const command = "npm run start-release";
+ if (onWindows) {
+ return windowsPrepend(command);
+ }
+ return macPrepend(command);
+}
+
+function killAllCommand() {
+ if (onWindows) {
+ return "taskkill /f /im node.exe";
+ }
+ return "killall -9 node";
+}
+
+identifiedLog("Initializing session...");
+
+writeLocalPidLog("session_manager", pid);
+
+function writeLocalPidLog(filename: string, contents: any) {
+ const path = `./logs/current_${filename}_pid.log`;
+ identifiedLog(cyan(`${contents} written to ${path}`));
+ writeFileSync(resolve(__dirname, path), `${contents}\n`);
+}
+
+function timestamp() {
+ return `@ ${new Date().toISOString()}`;
+}
+
+async function endPrevious() {
+ identifiedLog(yellow("Cleaning up previous connections..."));
+ current_backup?.kill("SIGKILL");
+ await Promise.all(ports.map(port => {
+ const task = killport(port, 'tcp');
+ return task.catch((error: any) => identifiedLog(red(error)));
+ }));
+ identifiedLog(yellow("Done. Any failures will be printed in red immediately above."));
+}
+
+let current_backup: ChildProcess | undefined = undefined;
+
+async function checkHeartbeat() {
+ const listening = is(SessionState.LISTENING);
+ let error: any;
+ try {
+ listening && process.stdout.write(`${identifier} 👂 `);
+ await request.get(heartbeat);
+ listening && console.log('⇠ 💚');
+ if (!listening) {
+ addLogEntry(is(SessionState.INITIALIZED) ? "Server successfully started" : "Backup server successfully restarted", green);
+ set(SessionState.LISTENING);
+ }
+ } catch (e) {
+ listening && console.log("⇠ 💔\n");
+ error = e;
+ } finally {
+ if (error && !is(SessionState.AUTOMATICALLY_RESTARTING, SessionState.INITIALIZED, SessionState.UPDATING)) {
+ if (is(SessionState.STARTING)) {
+ set(SessionState.INITIALIZED);
+ } else if (is(SessionState.MANUALLY_RESTARTING)) {
+ set(SessionState.AUTOMATICALLY_RESTARTING);
+ } else {
+ set(SessionState.AUTOMATICALLY_RESTARTING);
+ addLogEntry("Detected a server crash", red);
+ identifiedLog(red(error.message));
+ await endPrevious();
+ await log_execution({
+ startMessage: identifier + " Sending crash notification email",
+ endMessage: ({ error, result }) => {
+ const success = error === null && result === true;
+ return identifier + ` ${(success ? `Notification successfully sent to` : `Failed to notify`)} ${recipient} ${timestamp()}`;
+ },
+ action: async () => notify(error || "Hmm, no error to report..."),
+ color: cyan
+ });
+ identifiedLog(green("Initiating server restart..."));
+ }
+ current_backup = exec(startServerCommand(), err => identifiedLog(err?.message || is(SessionState.INITIALIZED) ? "Spawned initial server." : "Previous server process exited."));
+ writeLocalPidLog("server", `${(current_backup?.pid ?? -2) + 1} created ${timestamp()}`);
+ }
+ setTimeout(checkHeartbeat, 1000 * latency);
+ }
+}
+
+function emailText(error: any) {
+ return [
+ `Hey ${recipient.split("@")[0]},`,
+ "You, as a Dash Administrator, are being notified of a server crash event. Here's what we know:",
+ `Location: ${heartbeat}\nError: ${error}`,
+ "The server should already be restarting itself, but if you're concerned, use the Remote Desktop Connection to monitor progress."
+ ].join("\n\n");
+}
+
+async function notify(error: any) {
+ const smtpTransport = nodemailer.createTransport({
+ service: 'Gmail',
+ auth: {
+ user: 'brownptcdash@gmail.com',
+ pass: 'browngfx1'
+ }
+ });
+ const mailOptions = {
+ to: recipient,
+ from: 'brownptcdash@gmail.com',
+ subject: 'Dash Server Crash',
+ text: emailText(error)
+ } as MailOptions;
+ return new Promise<boolean>(resolve => {
+ smtpTransport.sendMail(mailOptions, (dispatchError: Error | null) => resolve(dispatchError === null));
+ });
+}
+
+identifiedLog(yellow(`After initialization, will poll server heartbeat repeatedly...\n`));
+checkHeartbeat(); \ No newline at end of file