diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/client/DocServer.ts | 97 | ||||
-rw-r--r-- | src/client/util/History.ts | 2 | ||||
-rw-r--r-- | src/client/util/RichTextSchema.tsx | 21 | ||||
-rw-r--r-- | src/client/util/type_decls.d | 2 | ||||
-rw-r--r-- | src/client/views/DocumentDecorations.scss | 6 | ||||
-rw-r--r-- | src/client/views/DocumentDecorations.tsx | 2 | ||||
-rw-r--r-- | src/client/views/Main.tsx | 5 | ||||
-rw-r--r-- | src/client/views/MainOverlayTextBox.tsx | 9 | ||||
-rw-r--r-- | src/client/views/MetadataEntryMenu.scss | 64 | ||||
-rw-r--r-- | src/client/views/MetadataEntryMenu.tsx | 107 | ||||
-rw-r--r-- | src/client/views/collections/CollectionDockingView.tsx | 14 | ||||
-rw-r--r-- | src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx | 39 | ||||
-rw-r--r-- | src/debug/Repl.tsx | 6 | ||||
-rw-r--r-- | src/debug/Viewer.tsx | 15 | ||||
-rw-r--r-- | src/new_fields/DateField.ts | 6 | ||||
-rw-r--r-- | src/server/authentication/models/current_user_utils.ts | 17 | ||||
-rw-r--r-- | src/server/index.ts | 45 |
17 files changed, 357 insertions, 100 deletions
diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts index d05793ea2..6737657c8 100644 --- a/src/client/DocServer.ts +++ b/src/client/DocServer.ts @@ -5,6 +5,7 @@ import { Utils, emptyFunction } from '../Utils'; import { SerializationHelper } from './util/SerializationHelper'; import { RefField } from '../new_fields/RefField'; import { Id, HandleUpdate } from '../new_fields/FieldSymbols'; +import { CurrentUserUtils } from '../server/authentication/models/current_user_utils'; /** * This class encapsulates the transfer and cross-client synchronization of @@ -21,12 +22,31 @@ import { Id, HandleUpdate } from '../new_fields/FieldSymbols'; */ export namespace DocServer { let _cache: { [id: string]: RefField | Promise<Opt<RefField>> } = {}; - const _socket = OpenSocket(`${window.location.protocol}//${window.location.hostname}:4321`); + let _socket: SocketIOClient.Socket; // this client's distinct GUID created at initialization - const GUID: string = Utils.GenerateGuid(); + let GUID: string; // indicates whether or not a document is currently being udpated, and, if so, its id let updatingId: string | undefined; + export function init(protocol: string, hostname: string, port: number, identifier: string) { + _cache = {}; + GUID = identifier; + _socket = OpenSocket(`${protocol}//${hostname}:${port}`); + + _GetRefField = _GetRefFieldImpl; + _GetRefFields = _GetRefFieldsImpl; + _CreateField = _CreateFieldImpl; + _UpdateField = _UpdateFieldImpl; + + /** + * Whenever the server sends us its handshake message on our + * websocket, we use the above function to return the handshake. + */ + Utils.AddServerHandler(_socket, MessageStore.Foo, onConnection); + Utils.AddServerHandler(_socket, MessageStore.UpdateField, respondToUpdate); + Utils.AddServerHandler(_socket, MessageStore.DeleteField, respondToDelete); + Utils.AddServerHandler(_socket, MessageStore.DeleteFields, respondToDelete); + } /** * A convenience method. Prepends the full path (i.e. http://localhost:1050) to the * requested extension @@ -36,6 +56,10 @@ export namespace DocServer { return window.location.origin + extension; } + function errorFunc(): never { + throw new Error("Can't use DocServer without calling init first"); + } + export namespace Control { let _isReadOnly = false; @@ -63,22 +87,16 @@ export namespace DocServer { } - export namespace Util { - - /** - * Whenever the server sends us its handshake message on our - * websocket, we use the above function to return the handshake. - */ - Utils.AddServerHandler(_socket, MessageStore.Foo, onConnection); + /** + * This function emits a message (with this client's + * unique GUID) to the server + * indicating that this client has connected + */ + function onConnection() { + _socket.emit(MessageStore.Bar.Message, GUID); + } - /** - * This function emits a message (with this client's - * unique GUID) to the server - * indicating that this client has connected - */ - function onConnection() { - _socket.emit(MessageStore.Bar.Message, GUID); - } + export namespace Util { /** * Emits a message to the server that wipes @@ -98,7 +116,7 @@ export namespace DocServer { * the server if the document has not been cached. * @param id the id of the requested document */ - export async function GetRefField(id: string): Promise<Opt<RefField>> { + const _GetRefFieldImpl = (id: string): Promise<Opt<RefField>> => { // an initial pass through the cache to determine whether the document needs to be fetched, // is already in the process of being fetched or already exists in the // cache @@ -139,8 +157,14 @@ export namespace DocServer { return cached; } else { // CACHED => great, let's just return the cached field we have - return cached; + return Promise.resolve(cached); } + }; + + let _GetRefField: (id: string) => Promise<Opt<RefField>> = errorFunc; + + export function GetRefField(id: string): Promise<Opt<RefField>> { + return _GetRefField(id); } /** @@ -149,7 +173,7 @@ export namespace DocServer { * the server if the document has not been cached. * @param ids the ids that map to the reqested documents */ - export async function GetRefFields(ids: string[]): Promise<{ [id: string]: Opt<RefField> }> { + const _GetRefFieldsImpl = async (ids: string[]): Promise<{ [id: string]: Opt<RefField> }> => { const requestedIds: string[] = []; const waitingIds: string[] = []; const promises: Promise<Opt<RefField>>[] = []; @@ -245,16 +269,13 @@ export namespace DocServer { // argument to the caller's promise (i.e. GetRefFields(["_id1_", "_id2_", "_id3_"]).then(map => //do something with map...)) // or it is the direct return result if the promise is awaited (i.e. let fields = await GetRefFields(["_id1_", "_id2_", "_id3_"])). return map; - } + }; - function _UpdateFieldImpl(id: string, diff: any) { - if (id === updatingId) { - return; - } - Utils.Emit(_socket, MessageStore.UpdateField, { id, diff }); - } + let _GetRefFields: (ids: string[]) => Promise<{ [id: string]: Opt<RefField> }> = errorFunc; - let _UpdateField = _UpdateFieldImpl; + export function GetRefFields(ids: string[]) { + return _GetRefFields(ids); + } // WRITE A NEW DOCUMENT TO THE SERVER @@ -274,7 +295,7 @@ export namespace DocServer { Utils.Emit(_socket, MessageStore.CreateField, initialState); } - let _CreateField = _CreateFieldImpl; + let _CreateField: (field: RefField) => void = errorFunc; // NOTIFY THE SERVER OF AN UPDATE TO A DOC'S STATE @@ -290,6 +311,15 @@ export namespace DocServer { _UpdateField(id, updatedState); } + function _UpdateFieldImpl(id: string, diff: any) { + if (id === updatingId) { + return; + } + Utils.Emit(_socket, MessageStore.UpdateField, { id, diff }); + } + + let _UpdateField: (id: string, diff: any) => void = errorFunc; + function _respondToUpdateImpl(diff: any) { const id = diff.id; // to be valid, the Diff object must reference @@ -355,13 +385,4 @@ export namespace DocServer { function respondToDelete(ids: string | string[]) { _respondToDelete(ids); } - - function connected() { - _socket.emit(MessageStore.Bar.Message, GUID); - } - - Utils.AddServerHandler(_socket, MessageStore.Foo, connected); - Utils.AddServerHandler(_socket, MessageStore.UpdateField, respondToUpdate); - Utils.AddServerHandler(_socket, MessageStore.DeleteField, respondToDelete); - Utils.AddServerHandler(_socket, MessageStore.DeleteFields, respondToDelete); }
\ No newline at end of file diff --git a/src/client/util/History.ts b/src/client/util/History.ts index 1a807b581..cbf5b3fc8 100644 --- a/src/client/util/History.ts +++ b/src/client/util/History.ts @@ -135,7 +135,7 @@ export namespace HistoryUtil { } const queryObj = OmitKeys(state, keys).extract; const query: any = {}; - Object.keys(queryObj).forEach(key => query[key] = queryObj[key] === null ? null : queryObj[key]); + Object.keys(queryObj).forEach(key => query[key] = queryObj[key] === null ? null : JSON.stringify(queryObj[key])); const queryString = qs.stringify(query); return path + (queryString ? `?${queryString}` : ""); }; diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx index b6402da13..e0ff3074b 100644 --- a/src/client/util/RichTextSchema.tsx +++ b/src/client/util/RichTextSchema.tsx @@ -518,28 +518,39 @@ export class SummarizedView { _view: any; constructor(node: any, view: any, getPos: any) { this._collapsed = document.createElement("span"); - this._collapsed.textContent = "㊉"; + this._collapsed.textContent = node.attrs.visibility ? "㊀" : "㊉"; this._collapsed.style.opacity = "0.5"; this._collapsed.style.position = "relative"; this._collapsed.style.width = "40px"; this._collapsed.style.height = "20px"; let self = this; this._view = view; + const js = node.toJSON; + node.toJSON = function () { + + return js.apply(this, arguments); + }; this._collapsed.onpointerdown = function (e: any) { if (node.attrs.visibility) { - node.attrs.visibility = !node.attrs.visibility; + // node.attrs.visibility = !node.attrs.visibility; let y = getPos(); + const attrs = { ...node.attrs }; + attrs.visibility = !attrs.visibility; let { from, to } = self.updateSummarizedText(y + 1, view.state.schema.marks.highlight); let length = to - from; let newSelection = TextSelection.create(view.state.doc, y + 1, y + 1 + length); // update attrs of node - node.attrs.text = newSelection.content(); - node.attrs.textslice = newSelection.content().toJSON(); + attrs.text = newSelection.content(); + attrs.textslice = newSelection.content().toJSON(); + view.dispatch(view.state.tr.setNodeMarkup(y, undefined, attrs)); view.dispatch(view.state.tr.setSelection(newSelection).deleteSelection(view.state, () => { })); self._collapsed.textContent = "㊉"; } else { - node.attrs.visibility = !node.attrs.visibility; + // node.attrs.visibility = !node.attrs.visibility; let y = getPos(); + const attrs = { ...node.attrs }; + attrs.visibility = !attrs.visibility; + view.dispatch(view.state.tr.setNodeMarkup(y, undefined, attrs)); let mark = view.state.schema.mark(view.state.schema.marks.highlight); view.dispatch(view.state.tr.setSelection(TextSelection.create(view.state.doc, y + 1, y + 1))); const from = view.state.selection.from; diff --git a/src/client/util/type_decls.d b/src/client/util/type_decls.d index 2cbe1dd40..1f95af00c 100644 --- a/src/client/util/type_decls.d +++ b/src/client/util/type_decls.d @@ -204,3 +204,5 @@ declare const Docs: { TreeDocument(documents: Doc[], options?: DocumentOptions): Doc; StackingDocument(documents: Doc[], options?: DocumentOptions): Doc; }; + +declare function d(...args:any[]):any; diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss index 1afc5c147..0b7411fca 100644 --- a/src/client/views/DocumentDecorations.scss +++ b/src/client/views/DocumentDecorations.scss @@ -159,6 +159,7 @@ $linkGap : 3px; .linkButtonWrapper { pointer-events: auto; padding-right: 5px; + width: 25px; } .linkButton-linker { @@ -202,6 +203,7 @@ $linkGap : 3px; } .templating-menu { + position: absolute; pointer-events: auto; text-transform: uppercase; letter-spacing: 2px; @@ -237,8 +239,8 @@ $linkGap : 3px; #template-list { position: absolute; - top: 0; - left: 30px; + top: 25px; + left: 0px; width: max-content; font-family: $sans-serif; font-size: 12px; diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 56fbd75a0..2cb3de50f 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -639,7 +639,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> return ( <div className="linkButtonWrapper"> <Flyout anchorPoint={anchorPoints.TOP_LEFT} - content={<MetadataEntryMenu docs={() => SelectionManager.SelectedDocuments().map(dv => dv.props.Document)} />}>{/* tfs: @bcz This might need to be the data document? */} + content={<MetadataEntryMenu docs={() => SelectionManager.SelectedDocuments().map(dv => dv.props.Document)} suggestWithFunction />}>{/* tfs: @bcz This might need to be the data document? */} <div className="docDecs-tagButton" title="Add fields"><FontAwesomeIcon className="documentdecorations-icon" icon="tag" size="sm" /></div> </Flyout> </div> diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx index 589542806..80399e24b 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -6,6 +6,7 @@ import * as React from 'react'; import { Cast } from "../../new_fields/Types"; import { Doc, DocListCastAsync } from "../../new_fields/Doc"; import { List } from "../../new_fields/List"; +import { DocServer } from "../DocServer"; let swapDocs = async () => { let oldDoc = await Cast(CurrentUserUtils.UserDocument.linkManagerDoc, Doc); @@ -28,8 +29,10 @@ let swapDocs = async () => { } (async () => { + const info = await CurrentUserUtils.loadCurrentUser(); + DocServer.init(window.location.protocol, window.location.hostname, 4321, info.email); await Docs.Prototypes.initialize(); - await CurrentUserUtils.loadCurrentUser(); + await CurrentUserUtils.loadUserDocument(info); await swapDocs(); ReactDOM.render(<MainView />, document.getElementById('root')); })();
\ No newline at end of file diff --git a/src/client/views/MainOverlayTextBox.tsx b/src/client/views/MainOverlayTextBox.tsx index d8aaea259..126efd11c 100644 --- a/src/client/views/MainOverlayTextBox.tsx +++ b/src/client/views/MainOverlayTextBox.tsx @@ -1,4 +1,4 @@ -import { action, observable, reaction } from 'mobx'; +import { action, observable, reaction, trace } from 'mobx'; import { observer } from 'mobx-react'; import "normalize.css"; import * as React from 'react'; @@ -51,8 +51,11 @@ export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps> if (box) { this.TextDoc = box.props.Document; this.TextDataDoc = box.props.DataDoc; - let sxf = Utils.GetScreenTransform(box ? box.CurrentDiv : undefined); - let xf = () => { box.props.ScreenToLocalTransform(); return new Transform(-sxf.translateX, -sxf.translateY, 1 / sxf.scale); }; + let xf = () => { + box.props.ScreenToLocalTransform(); + let sxf = Utils.GetScreenTransform(box ? box.CurrentDiv : undefined); + return new Transform(-sxf.translateX, -sxf.translateY, 1 / sxf.scale); + }; this.setTextDoc(box.props.fieldKey, box.CurrentDiv, xf, BoolCast(box.props.Document.autoHeight, false) || box.props.height === "min-content"); } else { diff --git a/src/client/views/MetadataEntryMenu.scss b/src/client/views/MetadataEntryMenu.scss index 73e5b6a73..bcfc9a82d 100644 --- a/src/client/views/MetadataEntryMenu.scss +++ b/src/client/views/MetadataEntryMenu.scss @@ -1,10 +1,66 @@ +.metadataEntry-outerDiv { + display: flex; + width: 300px; +} + +.react-autosuggest__container { + position: relative; +} + +.react-autosuggest__container, .metadataEntry-input { - width: 40%; + width: 100%; margin-left: 5px; margin-right: 5px; } -.metadataEntry-outerDiv { - display: flex; - width: 300px; +.metadataEntry-input, +.react-autosuggest__input { + border: 1px solid #aaa; + border-radius: 4px; + width: 100%; +} + +.react-autosuggest__input--focused { + outline: none; +} + +.react-autosuggest__input--open { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; +} + +.react-autosuggest__suggestions-container { + display: none; +} + +.react-autosuggest__suggestions-container--open { + display: block; + position: fixed; + overflow-y: auto; + max-height: 400px; + width: 180px; + border: 1px solid #aaa; + background-color: #fff; + font-family: Helvetica, sans-serif; + font-weight: 300; + font-size: 16px; + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; + z-index: 2; +} + +.react-autosuggest__suggestions-list { + margin: 0; + padding: 0; + list-style-type: none; +} + +.react-autosuggest__suggestion { + cursor: pointer; + padding: 10px 20px; +} + +.react-autosuggest__suggestion--highlighted { + background-color: #ddd; }
\ No newline at end of file diff --git a/src/client/views/MetadataEntryMenu.tsx b/src/client/views/MetadataEntryMenu.tsx index 0dc7e0220..bd5a307b3 100644 --- a/src/client/views/MetadataEntryMenu.tsx +++ b/src/client/views/MetadataEntryMenu.tsx @@ -1,29 +1,77 @@ import * as React from 'react'; import "./MetadataEntryMenu.scss"; import { observer } from 'mobx-react'; -import { observable, action } from 'mobx'; +import { observable, action, runInAction, trace } from 'mobx'; import { KeyValueBox } from './nodes/KeyValueBox'; -import { Doc } from '../../new_fields/Doc'; +import { Doc, Field } from '../../new_fields/Doc'; +import * as Autosuggest from 'react-autosuggest'; export type DocLike = Doc | Doc[] | Promise<Doc> | Promise<Doc[]>; export interface MetadataEntryProps { docs: DocLike | (() => DocLike); onError?: () => boolean; + suggestWithFunction?: boolean; } @observer export class MetadataEntryMenu extends React.Component<MetadataEntryProps>{ @observable private _currentKey: string = ""; @observable private _currentValue: string = ""; + @observable private suggestions: string[] = []; + private userModified = false; + + private autosuggestRef = React.createRef<Autosuggest>(); @action - onKeyChange = (e: React.ChangeEvent<HTMLInputElement>) => { - this._currentKey = e.target.value; + onKeyChange = (e: React.ChangeEvent, { newValue }: { newValue: string }) => { + this._currentKey = newValue; + if (!this.userModified) { + this.previewValue(); + } + } + + previewValue = async () => { + let field: Field | undefined | null = null; + let onProto: boolean = false; + let value: string | undefined = undefined; + let docs = this.props.docs; + if (typeof docs === "function") { + if (this.props.suggestWithFunction) { + docs = docs(); + } else { + return; + } + } + docs = await docs; + if (docs instanceof Doc) { + await docs[this._currentKey]; + value = Field.toKeyValueString(docs, this._currentKey); + } else { + for (const doc of docs) { + const v = await doc[this._currentKey]; + onProto = onProto || !Object.keys(doc).includes(this._currentKey); + if (field === null) { + field = v; + } else if (v !== field) { + value = "multiple values"; + } + } + } + if (value === undefined) { + if (field !== null && field !== undefined) { + value = (onProto ? "" : "= ") + Field.toScriptString(field); + } else { + value = ""; + } + } + const s = value; + runInAction(() => this._currentValue = s); } @action onValueChange = (e: React.ChangeEvent<HTMLInputElement>) => { this._currentValue = e.target.value; + this.userModified = e.target.value.trim() !== ""; } onValueKeyDown = async (e: React.KeyboardEvent) => { @@ -59,13 +107,62 @@ export class MetadataEntryMenu extends React.Component<MetadataEntryProps>{ clearInputs = () => { this._currentKey = ""; this._currentValue = ""; + this.userModified = false; + if (this.autosuggestRef.current) { + const input: HTMLInputElement = (this.autosuggestRef.current as any).input; + input && input.focus(); + } + } + + getKeySuggestions = async (value: string): Promise<string[]> => { + value = value.toLowerCase(); + let docs = this.props.docs; + if (typeof docs === "function") { + if (this.props.suggestWithFunction) { + docs = docs(); + } else { + return []; + } + } + docs = await docs; + if (docs instanceof Doc) { + return Object.keys(docs).filter(key => key.toLowerCase().startsWith(value)); + } else { + const keys = new Set<string>(); + docs.forEach(doc => Doc.allKeys(doc).forEach(key => keys.add(key))); + return Array.from(keys).filter(key => key.toLowerCase().startsWith(value)); + } + } + getSuggestionValue = (suggestion: string) => suggestion; + + renderSuggestion = (suggestion: string) => { + return <p>{suggestion}</p>; + } + + onSuggestionFetch = async ({ value }: { value: string }) => { + const sugg = await this.getKeySuggestions(value); + runInAction(() => { + this.suggestions = sugg; + }); + } + + @action + onSuggestionClear = () => { + this.suggestions = []; } render() { return ( <div className="metadataEntry-outerDiv"> Key: - <input className="metadataEntry-input" value={this._currentKey} onChange={this.onKeyChange} /> + <Autosuggest inputProps={{ value: this._currentKey, onChange: this.onKeyChange }} + getSuggestionValue={this.getSuggestionValue} + suggestions={this.suggestions} + alwaysRenderSuggestions + renderSuggestion={this.renderSuggestion} + onSuggestionsFetchRequested={this.onSuggestionFetch} + onSuggestionsClearRequested={this.onSuggestionClear} + ref={this.autosuggestRef} /> Value: <input className="metadataEntry-input" value={this._currentValue} onChange={this.onValueChange} onKeyDown={this.onValueKeyDown} /> </div> diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index fe8288b28..781bafec0 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -412,8 +412,10 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp if (doc instanceof Doc) { let theDoc = doc; CollectionDockingView.Instance._removedDocs.push(theDoc); - if (CurrentUserUtils.UserDocument.recentlyClosed instanceof Doc) { - Doc.AddDocToList(CurrentUserUtils.UserDocument.recentlyClosed, "data", doc, undefined, true, true); + + const recent = await Cast(CurrentUserUtils.UserDocument.recentlyClosed, Doc); + if (recent) { + Doc.AddDocToList(recent, "data", doc, undefined, true, true); } SelectionManager.DeselectAll(); } @@ -442,12 +444,16 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp }); stack.header.controlsContainer.find('.lm_close') //get the close icon .off('click') //unbind the current click handler - .click(action(function () { + .click(action(async function () { //if (confirm('really close this?')) { + const recent = await Cast(CurrentUserUtils.UserDocument.recentlyClosed, Doc); stack.remove(); - stack.contentItems.map(async (contentItem: any) => { + stack.contentItems.forEach(async (contentItem: any) => { let doc = await DocServer.GetRefField(contentItem.config.props.documentId); if (doc instanceof Doc) { + if (recent) { + Doc.AddDocToList(recent, "data", doc, undefined, true, true); + } let theDoc = doc; CollectionDockingView.Instance._removedDocs.push(theDoc); } diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index e35546fec..19e280444 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -11,7 +11,7 @@ import { DragManager } from "../../../util/DragManager"; import { HistoryUtil } from "../../../util/History"; import { SelectionManager } from "../../../util/SelectionManager"; import { Transform } from "../../../util/Transform"; -import { undoBatch } from "../../../util/UndoManager"; +import { undoBatch, UndoManager } from "../../../util/UndoManager"; import { COLLECTION_BORDER_WIDTH } from "../../../views/globalCssVariables.scss"; import { ContextMenu } from "../../ContextMenu"; import { InkingCanvas } from "../../InkingCanvas"; @@ -455,24 +455,26 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { description: "Arrange contents in grid", event: async () => { const docs = await DocListCastAsync(this.Document[this.props.fieldKey]); - if (docs) { - let startX = this.Document.panX || 0; - let x = startX; - let y = this.Document.panY || 0; - let i = 0; - const width = Math.max(...docs.map(doc => NumCast(doc.width))); - const height = Math.max(...docs.map(doc => NumCast(doc.height))); - for (const doc of docs) { - doc.x = x; - doc.y = y; - x += width + 20; - if (++i === 6) { - i = 0; - x = startX; - y += height + 20; + UndoManager.RunInBatch(() => { + if (docs) { + let startX = this.Document.panX || 0; + let x = startX; + let y = this.Document.panY || 0; + let i = 0; + const width = Math.max(...docs.map(doc => NumCast(doc.width))); + const height = Math.max(...docs.map(doc => NumCast(doc.height))); + for (const doc of docs) { + doc.x = x; + doc.y = y; + x += width + 20; + if (++i === 6) { + i = 0; + x = startX; + y += height + 20; + } } } - } + }, "arrange contents"); } }); ContextMenu.Instance.addItem({ @@ -483,7 +485,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { const script = this.Document[key]; let originalText: string | undefined = undefined; if (script) originalText = script.script.originalScript; - let scriptingBox = <ScriptBox initialText={originalText} onCancel={overlayDisposer} onSave={(text, onError) => { + // tslint:disable-next-line: no-unnecessary-callback-wrapper + let scriptingBox = <ScriptBox initialText={originalText} onCancel={() => overlayDisposer()} onSave={(text, onError) => { const script = CompileScript(text, { params, requiredType, diff --git a/src/debug/Repl.tsx b/src/debug/Repl.tsx index 91b711c79..4f4db13d2 100644 --- a/src/debug/Repl.tsx +++ b/src/debug/Repl.tsx @@ -6,6 +6,7 @@ import { CompileScript } from '../client/util/Scripting'; import { makeInterface } from '../new_fields/Schema'; import { ObjectField } from '../new_fields/ObjectField'; import { RefField } from '../new_fields/RefField'; +import { DocServer } from '../client/DocServer'; @observer class Repl extends React.Component { @@ -63,4 +64,7 @@ class Repl extends React.Component { } } -ReactDOM.render(<Repl />, document.getElementById("root"));
\ No newline at end of file +(async function () { + DocServer.init(window.location.protocol, window.location.hostname, 4321, "repl"); + ReactDOM.render(<Repl />, document.getElementById("root")); +})();
\ No newline at end of file diff --git a/src/debug/Viewer.tsx b/src/debug/Viewer.tsx index f48eb696c..2b3eed154 100644 --- a/src/debug/Viewer.tsx +++ b/src/debug/Viewer.tsx @@ -178,9 +178,12 @@ class Viewer extends React.Component { } } -ReactDOM.render(( - <div style={{ position: "absolute", width: "100%", height: "100%" }}> - <Viewer /> - </div>), - document.getElementById('root') -);
\ No newline at end of file +(async function () { + await DocServer.init(window.location.protocol, window.location.hostname, 4321, "viewer"); + ReactDOM.render(( + <div style={{ position: "absolute", width: "100%", height: "100%" }}> + <Viewer /> + </div>), + document.getElementById('root') + ); +})();
\ No newline at end of file diff --git a/src/new_fields/DateField.ts b/src/new_fields/DateField.ts index fc8abb9d9..abec91e06 100644 --- a/src/new_fields/DateField.ts +++ b/src/new_fields/DateField.ts @@ -2,7 +2,9 @@ import { Deserializable } from "../client/util/SerializationHelper"; import { serializable, date } from "serializr"; import { ObjectField } from "./ObjectField"; import { Copy, ToScriptString } from "./FieldSymbols"; +import { scriptingGlobal, Scripting } from "../client/util/Scripting"; +@scriptingGlobal @Deserializable("date") export class DateField extends ObjectField { @serializable(date()) @@ -21,3 +23,7 @@ export class DateField extends ObjectField { return `new DateField(new Date(${this.date.toISOString()}))`; } } + +Scripting.addGlobal(function d(...dateArgs: any[]) { + return new DateField(new (Date as any)(...dateArgs)); +}); diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts index 384c579de..e796ccb43 100644 --- a/src/server/authentication/models/current_user_utils.ts +++ b/src/server/authentication/models/current_user_utils.ts @@ -73,17 +73,21 @@ export class CurrentUserUtils { } - public static async loadCurrentUser(): Promise<any> { - let userPromise = rp.get(DocServer.prepend(RouteStore.getCurrUser)).then(response => { + public static loadCurrentUser() { + return rp.get(DocServer.prepend(RouteStore.getCurrUser)).then(response => { if (response) { - let obj = JSON.parse(response); - CurrentUserUtils.curr_id = obj.id as string; - CurrentUserUtils.curr_email = obj.email as string; + const result: { id: string, email: string } = JSON.parse(response); + return result; } else { throw new Error("There should be a user! Why does Dash think there isn't one?"); } }); - let userDocPromise = await rp.get(DocServer.prepend(RouteStore.getUserDocumentId)).then(id => { + } + + public static async loadUserDocument({ id, email }: { id: string, email: string }) { + this.curr_id = id; + this.curr_email = email; + await rp.get(DocServer.prepend(RouteStore.getUserDocumentId)).then(id => { if (id) { return DocServer.GetRefField(id).then(async field => { if (field instanceof Doc) { @@ -108,7 +112,6 @@ export class CurrentUserUtils { } catch (e) { } - return Promise.all([userPromise, userDocPromise]); } /* Northstar catalog ... really just for testing so this should eventually go away */ diff --git a/src/server/index.ts b/src/server/index.ts index 21adff9e5..9cb43bf4e 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -149,6 +149,32 @@ app.get("/search", async (req, res) => { res.send(results); }); +function msToTime(duration: number) { + let milliseconds = Math.floor((duration % 1000) / 100), + seconds = Math.floor((duration / 1000) % 60), + minutes = Math.floor((duration / (1000 * 60)) % 60), + hours = Math.floor((duration / (1000 * 60 * 60)) % 24); + + let hoursS = (hours < 10) ? "0" + hours : hours; + let minutesS = (minutes < 10) ? "0" + minutes : minutes; + let secondsS = (seconds < 10) ? "0" + seconds : seconds; + + return hoursS + ":" + minutesS + ":" + secondsS + "." + milliseconds; +} + +app.get("/whosOnline", (req, res) => { + let users: any = { active: {}, inactive: {} }; + const now = Date.now(); + + for (const user in timeMap) { + const time = timeMap[user]; + const key = ((now - time) / 1000) < (60 * 5) ? "active" : "inactive"; + users[key][user] = `Last active ${msToTime(now - time)} ago`; + } + + res.send(users); +}); + app.get("/thumbnail/:filename", (req, res) => { let filename = req.params.filename; let noExt = filename.substring(0, filename.length - ".png".length); @@ -450,12 +476,21 @@ interface Map { } let clients: Map = {}; +let socketMap = new Map<SocketIO.Socket, string>(); +let timeMap: { [id: string]: number } = {}; + server.on("connection", function (socket: Socket) { - console.log("a user has connected"); + socket.use((packet, next) => { + let id = socketMap.get(socket); + if (id) { + timeMap[id] = Date.now(); + } + next(); + }); Utils.Emit(socket, MessageStore.Foo, "handshooken"); - Utils.AddServerHandler(socket, MessageStore.Bar, barReceived); + Utils.AddServerHandler(socket, MessageStore.Bar, guid => barReceived(socket, guid)); Utils.AddServerHandler(socket, MessageStore.SetField, (args) => setField(socket, args)); Utils.AddServerHandlerCallback(socket, MessageStore.GetField, getField); Utils.AddServerHandlerCallback(socket, MessageStore.GetFields, getFields); @@ -485,8 +520,10 @@ async function deleteAll() { await Search.Instance.clear(); } -function barReceived(guid: String) { - clients[guid.toString()] = new Client(guid.toString()); +function barReceived(socket: SocketIO.Socket, guid: string) { + clients[guid] = new Client(guid.toString()); + console.log(`User ${guid} has connected`); + socketMap.set(socket, guid); } function getField([id, callback]: [string, (result?: Transferable) => void]) { |