diff options
author | monikahedman <monika_hedman@brown.edu> | 2019-08-21 10:23:23 -0400 |
---|---|---|
committer | monikahedman <monika_hedman@brown.edu> | 2019-08-21 10:23:23 -0400 |
commit | 4a6231e1631cda4f3f09ce0202538108609a8d47 (patch) | |
tree | 41a355f31456e9181ec30ba85b746080add71282 | |
parent | 2423533d1044ed14b5b356709234bbaa27fd2561 (diff) | |
parent | 9984f2f19001c07e63738947172e12da25e4498e (diff) |
pulled from master
65 files changed, 1212 insertions, 692 deletions
diff --git a/package.json b/package.json index a4343982e..de1f3f6e6 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ "@types/express-session": "^1.15.12", "@types/express-validator": "^3.0.0", "@types/formidable": "^1.0.31", + "@types/gapi": "0.0.39", "@types/jquery": "^3.3.29", "@types/jquery-awesome-cursor": "^0.3.0", "@types/jsonwebtoken": "^8.3.2", @@ -218,4 +219,4 @@ "xoauth2": "^1.2.0", "youtube": "^0.1.0" } -} +}
\ No newline at end of file diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts index bf5168c22..2cec1046b 100644 --- a/src/client/DocServer.ts +++ b/src/client/DocServer.ts @@ -33,6 +33,8 @@ export namespace DocServer { LivePlayground = 3, } + export let AclsMode = WriteMode.Default; + const fieldWriteModes: { [field: string]: WriteMode } = {}; const docsWithUpdates: { [field: string]: Set<Doc> } = {}; diff --git a/src/client/apis/google_docs/GoogleApiClientUtils.ts b/src/client/apis/google_docs/GoogleApiClientUtils.ts new file mode 100644 index 000000000..821c52270 --- /dev/null +++ b/src/client/apis/google_docs/GoogleApiClientUtils.ts @@ -0,0 +1,224 @@ +import { docs_v1 } from "googleapis"; +import { PostToServer } from "../../../Utils"; +import { RouteStore } from "../../../server/RouteStore"; +import { Opt } from "../../../new_fields/Doc"; +import { isArray } from "util"; + +export const Pulls = "googleDocsPullCount"; +export const Pushes = "googleDocsPushCount"; + +export namespace GoogleApiClientUtils { + + export namespace Docs { + + export enum Actions { + Create = "create", + Retrieve = "retrieve", + Update = "update" + } + + export enum WriteMode { + Insert, + Replace + } + + export type DocumentId = string; + export type Reference = DocumentId | CreateOptions; + export type TextContent = string | string[]; + export type IdHandler = (id: DocumentId) => any; + + export type CreationResult = Opt<DocumentId>; + export type RetrievalResult = Opt<docs_v1.Schema$Document>; + export type UpdateResult = Opt<docs_v1.Schema$BatchUpdateDocumentResponse>; + export type ReadLinesResult = Opt<{ title?: string, bodyLines?: string[] }>; + export type ReadResult = { title?: string, body?: string }; + + export interface CreateOptions { + handler: IdHandler; // callback to process the documentId of the newly created Google Doc + title?: string; // if excluded, will use a default title annotated with the current date + } + + export interface RetrieveOptions { + documentId: DocumentId; + } + + export type ReadOptions = RetrieveOptions & { removeNewlines?: boolean }; + + export interface WriteOptions { + mode: WriteMode; + content: TextContent; + reference: Reference; + index?: number; // if excluded, will compute the last index of the document and append the content there + } + + export interface UpdateOptions { + documentId: DocumentId; + requests: docs_v1.Schema$Request[]; + } + + export namespace Utils { + + export const extractText = (document: docs_v1.Schema$Document, removeNewlines = false): string => { + const fragments: string[] = []; + if (document.body && document.body.content) { + for (const element of document.body.content) { + if (element.paragraph && element.paragraph.elements) { + for (const inner of element.paragraph.elements) { + if (inner && inner.textRun) { + const fragment = inner.textRun.content; + fragment && fragments.push(fragment); + } + } + } + } + } + const text = fragments.join(""); + return removeNewlines ? text.ReplaceAll("\n", "") : text; + }; + + export const endOf = (schema: docs_v1.Schema$Document): number | undefined => { + if (schema.body && schema.body.content) { + const paragraphs = schema.body.content.filter(el => el.paragraph); + if (paragraphs.length) { + const target = paragraphs[paragraphs.length - 1]; + if (target.paragraph && target.paragraph.elements) { + length = target.paragraph.elements.length; + if (length) { + const final = target.paragraph.elements[length - 1]; + return final.endIndex ? final.endIndex - 1 : undefined; + } + } + } + } + }; + + export const initialize = async (reference: Reference) => typeof reference === "string" ? reference : create(reference); + + } + + /** + * After following the authentication routine, which connects this API call to the current signed in account + * and grants the appropriate permissions, this function programmatically creates an arbitrary Google Doc which + * should appear in the user's Google Doc library instantaneously. + * + * @param options the title to assign to the new document, and the information necessary + * to store the new documentId returned from the creation process + * @returns the documentId of the newly generated document, or undefined if the creation process fails. + */ + export const create = async (options: CreateOptions): Promise<CreationResult> => { + const path = RouteStore.googleDocs + Actions.Create; + const parameters = { + requestBody: { + title: options.title || `Dash Export (${new Date().toDateString()})` + } + }; + try { + const schema: docs_v1.Schema$Document = await PostToServer(path, parameters); + const generatedId = schema.documentId; + if (generatedId) { + options.handler(generatedId); + return generatedId; + } + } catch { + return undefined; + } + }; + + export const retrieve = async (options: RetrieveOptions): Promise<RetrievalResult> => { + const path = RouteStore.googleDocs + Actions.Retrieve; + try { + const schema: RetrievalResult = await PostToServer(path, options); + return schema; + } catch { + return undefined; + } + }; + + export const update = async (options: UpdateOptions): Promise<UpdateResult> => { + const path = RouteStore.googleDocs + Actions.Update; + const parameters = { + documentId: options.documentId, + requestBody: { + requests: options.requests + } + }; + try { + const replies: UpdateResult = await PostToServer(path, parameters); + return replies; + } catch { + return undefined; + } + }; + + export const read = async (options: ReadOptions): Promise<ReadResult> => { + return retrieve(options).then(document => { + let result: ReadResult = {}; + if (document) { + let title = document.title; + let body = Utils.extractText(document, options.removeNewlines); + result = { title, body }; + } + return result; + }); + }; + + export const readLines = async (options: ReadOptions): Promise<ReadLinesResult> => { + return retrieve(options).then(document => { + let result: ReadLinesResult = {}; + if (document) { + let title = document.title; + let bodyLines = Utils.extractText(document).split("\n"); + options.removeNewlines && (bodyLines = bodyLines.filter(line => line.length)); + result = { title, bodyLines }; + } + return result; + }); + }; + + export const write = async (options: WriteOptions): Promise<UpdateResult> => { + const requests: docs_v1.Schema$Request[] = []; + const documentId = await Utils.initialize(options.reference); + if (!documentId) { + return undefined; + } + let index = options.index; + const mode = options.mode; + if (!(index && mode === WriteMode.Insert)) { + let schema = await retrieve({ documentId }); + if (!schema || !(index = Utils.endOf(schema))) { + return undefined; + } + } + if (mode === WriteMode.Replace) { + index > 1 && requests.push({ + deleteContentRange: { + range: { + startIndex: 1, + endIndex: index + } + } + }); + index = 1; + } + const text = options.content; + text.length && requests.push({ + insertText: { + text: isArray(text) ? text.join("\n") : text, + location: { index } + } + }); + if (!requests.length) { + return undefined; + } + let replies: any = await update({ documentId, requests }); + let errors = "errors"; + if (errors in replies) { + console.log("Write operation failed:"); + console.log(replies[errors].map((error: any) => error.message)); + } + return replies; + }; + + } + +}
\ No newline at end of file diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts new file mode 100644 index 000000000..1578e49fe --- /dev/null +++ b/src/client/documents/DocumentTypes.ts @@ -0,0 +1,22 @@ +export enum DocumentType { + NONE = "none", + TEXT = "text", + HIST = "histogram", + IMG = "image", + WEB = "web", + COL = "collection", + KVP = "kvp", + VID = "video", + AUDIO = "audio", + PDF = "pdf", + ICON = "icon", + IMPORT = "import", + LINK = "link", + LINKDOC = "linkdoc", + BUTTON = "button", + TEMPLATE = "template", + EXTENSION = "extension", + YOUTUBE = "youtube", + DRAGBOX = "dragbox", + PRES = "presentation", +}
\ No newline at end of file diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 9c8b6c129..47df17329 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -1,25 +1,3 @@ -export enum DocumentType { - NONE = "none", - TEXT = "text", - HIST = "histogram", - IMG = "image", - WEB = "web", - COL = "collection", - KVP = "kvp", - VID = "video", - AUDIO = "audio", - PDF = "pdf", - ICON = "icon", - IMPORT = "import", - LINK = "link", - LINKDOC = "linkdoc", - BUTTON = "button", - TEMPLATE = "template", - EXTENSION = "extension", - YOUTUBE = "youtube", - DRAGBOX = "dragbox", -} - import { HistogramField } from "../northstar/dash-fields/HistogramField"; import { HistogramBox } from "../northstar/dash-nodes/HistogramBox"; import { HistogramOperation } from "../northstar/operations/HistogramOperation"; @@ -63,8 +41,12 @@ import { Scripting, CompileScript } from "../util/Scripting"; import { ButtonBox } from "../views/nodes/ButtonBox"; import { DragBox } from "../views/nodes/DragBox"; import { SchemaHeaderField, RandomPastel } from "../../new_fields/SchemaHeaderField"; +import { PresBox } from "../views/nodes/PresBox"; import { ComputedField } from "../../new_fields/ScriptField"; import { ProxyField } from "../../new_fields/Proxy"; +import { DocumentType } from "./DocumentTypes"; +//import { PresBox } from "../views/nodes/PresBox"; +//import { PresField } from "../../new_fields/PresField"; var requestImageSize = require('../util/request-image-size'); var path = require('path'); @@ -180,6 +162,10 @@ export namespace Docs { [DocumentType.BUTTON, { layout: { view: ButtonBox }, }], + [DocumentType.PRES, { + layout: { view: PresBox }, + options: {} + }], [DocumentType.DRAGBOX, { layout: { view: DragBox }, options: { width: 40, height: 40 }, @@ -304,7 +290,7 @@ export namespace Docs { const { omit: protoProps, extract: delegateProps } = OmitKeys(options, delegateKeys); if (!("author" in protoProps)) { - protoProps.author = CurrentUserUtils.email; + protoProps.author = Doc.CurrentUserEmail; } if (!("creationDate" in protoProps)) { @@ -352,6 +338,9 @@ export namespace Docs { .catch((err: any) => console.log(err)); return inst; } + export function PresDocument(initial: List<Doc> = new List(), options: DocumentOptions = {}) { + return InstanceFromProto(Prototypes.get(DocumentType.PRES), initial, options); + } export function VideoDocument(url: string, options: DocumentOptions = {}) { return InstanceFromProto(Prototypes.get(DocumentType.VID), new VideoField(new URL(url)), options); diff --git a/src/client/util/DictationManager.ts b/src/client/util/DictationManager.ts index 9c61fe125..488a146bf 100644 --- a/src/client/util/DictationManager.ts +++ b/src/client/util/DictationManager.ts @@ -2,9 +2,10 @@ import { SelectionManager } from "./SelectionManager"; import { DocumentView } from "../views/nodes/DocumentView"; import { UndoManager } from "./UndoManager"; import * as interpreter from "words-to-numbers"; +import { DocumentType } from "../documents/DocumentTypes"; import { Doc } from "../../new_fields/Doc"; import { List } from "../../new_fields/List"; -import { Docs, DocumentType } from "../documents/Documents"; +import { Docs } from "../documents/Documents"; import { CollectionViewType } from "../views/collections/CollectionBaseView"; import { Cast, CastCtor } from "../../new_fields/Types"; import { listSpec } from "../../new_fields/Schema"; @@ -12,6 +13,7 @@ import { AudioField, ImageField } from "../../new_fields/URLField"; import { HistogramField } from "../northstar/dash-fields/HistogramField"; import { MainView } from "../views/MainView"; import { Utils } from "../../Utils"; +import { RichTextField } from "../../new_fields/RichTextField"; /** * This namespace provides a singleton instance of a manager that @@ -299,11 +301,15 @@ export namespace DictationManager { } }], - ["promote", { + ["create bulleted note", { action: (target: DocumentView) => { - console.log(target); - }, - restrictTo: [DocumentType.TEXT] + let newBox = Docs.Create.TextDocument({ width: 400, height: 200, title: "My Outline" }); + let proto = newBox.proto!; + let proseMirrorState = '"{"doc":{"type":"doc","content":[{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"type":"text","text":""}]}]}]}]},"selection":{"type":"text","anchor":1,"head":1}}"'; + proto.data = new RichTextField(proseMirrorState); + proto.backgroundColor = "#eeffff"; + target.props.addDocTab(newBox, proto, "onRight"); + } }] ]); diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index 0b6d9b5e5..894b366ef 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -9,8 +9,6 @@ import { DocumentManager } from "./DocumentManager"; import { LinkManager } from "./LinkManager"; import { SelectionManager } from "./SelectionManager"; import { SchemaHeaderField } from "../../new_fields/SchemaHeaderField"; -import { DocumentDecorations } from "../views/DocumentDecorations"; -import { NumberLiteralType } from "typescript"; export type dropActionType = "alias" | "copy" | undefined; export function SetupDrag( @@ -211,6 +209,7 @@ export namespace DragManager { dropAction: dropActionType; userDropAction: dropActionType; moveDocument?: MoveFunction; + applyAsTemplate?: boolean; [id: string]: any; } diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss index 3627edaae..ac8497bd0 100644 --- a/src/client/views/DocumentDecorations.scss +++ b/src/client/views/DocumentDecorations.scss @@ -264,4 +264,8 @@ $linkGap : 3px; input { margin-right: 10px; } -}
\ No newline at end of file +} + +@-moz-keyframes spin { 100% { -moz-transform: rotate(360deg); } } +@-webkit-keyframes spin { 100% { -webkit-transform: rotate(360deg); } } +@keyframes spin { 100% { -webkit-transform: rotate(360deg); transform: rotate(360deg); } }
\ No newline at end of file diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 3fdda00cf..b18a3c192 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -1,5 +1,5 @@ -import { library } from '@fortawesome/fontawesome-svg-core'; -import { faLink, faTag, faTimes } from '@fortawesome/free-solid-svg-icons'; +import { library, IconProp } from '@fortawesome/fontawesome-svg-core'; +import { faLink, faTag, faTimes, faArrowAltCircleDown, faArrowAltCircleUp, faCheckCircle, faStopCircle, faCloudUploadAlt, faSyncAlt, faShare } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { action, computed, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; @@ -18,7 +18,7 @@ import { CollectionView } from "./collections/CollectionView"; import './DocumentDecorations.scss'; import { DocumentView, PositionDocument } from "./nodes/DocumentView"; import { FieldView } from "./nodes/FieldView"; -import { FormattedTextBox } from "./nodes/FormattedTextBox"; +import { FormattedTextBox, GoogleRef } from "./nodes/FormattedTextBox"; import { IconBox } from "./nodes/IconBox"; import { LinkMenu } from "./nodes/LinkMenu"; import { TemplateMenu } from "./TemplateMenu"; @@ -26,10 +26,10 @@ import { Template, Templates } from "./Templates"; import React = require("react"); import { RichTextField } from '../../new_fields/RichTextField'; import { LinkManager } from '../util/LinkManager'; -import { ObjectField } from '../../new_fields/ObjectField'; import { MetadataEntryMenu } from './MetadataEntryMenu'; import { ImageBox } from './nodes/ImageBox'; import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils'; +import { Pulls, Pushes } from '../apis/google_docs/GoogleApiClientUtils'; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @@ -37,6 +37,16 @@ export const Flyout = higflyout.default; library.add(faLink); library.add(faTag); library.add(faTimes); +library.add(faArrowAltCircleDown); +library.add(faArrowAltCircleUp); +library.add(faStopCircle); +library.add(faCheckCircle); +library.add(faCloudUploadAlt); +library.add(faSyncAlt); +library.add(faShare); + +const cloud: IconProp = "cloud-upload-alt"; +const fetch: IconProp = "sync-alt"; @observer export class DocumentDecorations extends React.Component<{}, { value: string }> { @@ -68,6 +78,52 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> @observable public Interacting = false; @observable private _isMoving = false; + @observable public pushIcon: IconProp = "arrow-alt-circle-up"; + @observable public pullIcon: IconProp = "arrow-alt-circle-down"; + @observable public pullColor: string = "white"; + @observable public isAnimatingFetch = false; + @observable public openHover = false; + public pullColorAnimating = false; + + private pullAnimating = false; + private pushAnimating = false; + + public startPullOutcome = action((success: boolean) => { + if (!this.pullAnimating) { + this.pullAnimating = true; + this.pullIcon = success ? "check-circle" : "stop-circle"; + setTimeout(() => runInAction(() => { + this.pullIcon = "arrow-alt-circle-down"; + this.pullAnimating = false; + }), 1000); + } + }); + + public startPushOutcome = action((success: boolean) => { + if (!this.pushAnimating) { + this.pushAnimating = true; + this.pushIcon = success ? "check-circle" : "stop-circle"; + setTimeout(() => runInAction(() => { + this.pushIcon = "arrow-alt-circle-up"; + this.pushAnimating = false; + }), 1000); + } + }); + + public setPullState = action((unchanged: boolean) => { + this.isAnimatingFetch = false; + if (!this.pullColorAnimating) { + this.pullColorAnimating = true; + this.pullColor = unchanged ? "lawngreen" : "red"; + setTimeout(this.clearPullColor, 1000); + } + }); + + private clearPullColor = action(() => { + this.pullColor = "white"; + this.pullColorAnimating = false; + }); + constructor(props: Readonly<{}>) { super(props); DocumentDecorations.Instance = this; @@ -630,6 +686,76 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> ); } + private get targetDoc() { + return SelectionManager.SelectedDocuments()[0].props.Document; + } + + considerGoogleDocsPush = () => { + let canPush = this.targetDoc.data && this.targetDoc.data instanceof RichTextField; + if (!canPush) return (null); + let published = Doc.GetProto(this.targetDoc)[GoogleRef] !== undefined; + if (!published) { + this.targetDoc.autoHeight = true; + } + let icon: IconProp = published ? (this.pushIcon as any) : cloud; + return ( + <div className={"linkButtonWrapper"}> + <div title={`${published ? "Push" : "Publish"} to Google Docs`} className="linkButton-linker" onClick={() => { + DocumentDecorations.hasPushedHack = false; + this.targetDoc[Pushes] = NumCast(this.targetDoc[Pushes]) + 1; + }}> + <FontAwesomeIcon className="documentdecorations-icon" icon={icon} size={published ? "sm" : "xs"} /> + </div> + </div> + ); + } + + considerGoogleDocsPull = () => { + let canPull = this.targetDoc.data && this.targetDoc.data instanceof RichTextField; + let dataDoc = Doc.GetProto(this.targetDoc); + if (!canPull || !dataDoc[GoogleRef]) return (null); + let icon = !dataDoc.unchanged ? (this.pullIcon as any) : fetch; + icon = this.openHover ? "share" : icon; + let animation = this.isAnimatingFetch ? "spin 0.5s linear infinite" : "none"; + let title = `${!dataDoc.unchanged ? "Pull from" : "Fetch"} Google Docs`; + return ( + <div className={"linkButtonWrapper"}> + <div + title={title} + className="linkButton-linker" + style={{ + backgroundColor: this.pullColor, + transition: "0.2s ease all" + }} + onPointerEnter={e => e.ctrlKey && runInAction(() => this.openHover = true)} + onPointerLeave={() => runInAction(() => this.openHover = false)} + onClick={e => { + if (e.ctrlKey) { + window.open(`https://docs.google.com/document/d/${dataDoc[GoogleRef]}/edit`); + } else { + this.clearPullColor(); + DocumentDecorations.hasPulledHack = false; + this.targetDoc[Pulls] = NumCast(this.targetDoc[Pulls]) + 1; + dataDoc.unchanged && runInAction(() => this.isAnimatingFetch = true); + } + }}> + <FontAwesomeIcon + style={{ + WebkitAnimation: animation, + MozAnimation: animation + }} + className="documentdecorations-icon" + icon={icon} + size="sm" + /> + </div> + </div> + ); + } + + public static hasPushedHack = false; + public static hasPulledHack = false; + considerTooltip = () => { let thisDoc = SelectionManager.SelectedDocuments()[0].props.Document; let isTextDoc = thisDoc.data && thisDoc.data instanceof RichTextField; @@ -783,6 +909,8 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> </div> {this.metadataMenu} {this.considerEmbed()} + {this.considerGoogleDocsPush()} + {this.considerGoogleDocsPull()} {/* {this.considerTooltip()} */} </div> </div > diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts index 6238eb7b6..790784f46 100644 --- a/src/client/views/GlobalKeyHandler.ts +++ b/src/client/views/GlobalKeyHandler.ts @@ -199,8 +199,6 @@ export default class KeyManager { async printClipboard() { let text: string = await navigator.clipboard.readText(); - console.log(text) - console.log(document.activeElement) } private ctrl_shift = action((keyname: string) => { diff --git a/src/client/views/MainOverlayTextBox.tsx b/src/client/views/MainOverlayTextBox.tsx index b14a1e0ea..9fe435bc5 100644 --- a/src/client/views/MainOverlayTextBox.tsx +++ b/src/client/views/MainOverlayTextBox.tsx @@ -146,7 +146,8 @@ export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps> onClick={undefined} isSelected={returnTrue} select={emptyFunction} renderDepth={0} selectOnLoad={true} ContainingCollectionView={undefined} whenActiveChanged={emptyFunction} active={returnTrue} ContentScaling={returnOne} - ScreenToLocalTransform={this._textXf} PanelWidth={returnZero} PanelHeight={returnZero} focus={emptyFunction} addDocTab={this.addDocTab} outer_div={(tooltip: HTMLElement) => { this._tooltip = tooltip; this.updateTooltip(); }} /> + ScreenToLocalTransform={this._textXf} PanelWidth={returnZero} PanelHeight={returnZero} focus={emptyFunction} + pinToPres={returnZero} addDocTab={this.addDocTab} outer_div={(tooltip: HTMLElement) => { this._tooltip = tooltip; this.updateTooltip(); }} /> </div> </div> </div> diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 84fdf13a1..f28844009 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -1,5 +1,5 @@ import { IconName, library } from '@fortawesome/fontawesome-svg-core'; -import { faArrowDown, faArrowUp, faBolt, faCaretUp, faCat, faCheck, faClone, faCloudUploadAlt, faCommentAlt, faCut, faExclamation, faFilePdf, faFilm, faFont, faGlobeAsia, faLongArrowAltRight, faMusic, faObjectGroup, faPause, faPenNib, faPlay, faPortrait, faRedoAlt, faTable, faThumbtack, faTree, faUndoAlt } from '@fortawesome/free-solid-svg-icons'; +import { faArrowDown, faArrowUp, faBolt, faCaretUp, faCat, faCheck, faClone, faCloudUploadAlt, faCommentAlt, faCut, faExclamation, faFilePdf, faFilm, faFont, faGlobeAsia, faLongArrowAltRight, faMusic, faObjectGroup, faPause, faPenNib, faPlay, faPortrait, faRedoAlt, faThumbtack, faTree, faUndoAlt, faTv } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, configure, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; @@ -7,16 +7,15 @@ import "normalize.css"; import * as React from 'react'; import { SketchPicker } from 'react-color'; import Measure from 'react-measure'; -import { Doc, DocListCast, HeightSym, Opt } from '../../new_fields/Doc'; +import { Doc, DocListCast, Opt, HeightSym } from '../../new_fields/Doc'; +import { List } from '../../new_fields/List'; import { Id } from '../../new_fields/FieldSymbols'; import { InkTool } from '../../new_fields/InkField'; -import { List } from '../../new_fields/List'; import { listSpec } from '../../new_fields/Schema'; -import { SchemaHeaderField } from '../../new_fields/SchemaHeaderField'; import { BoolCast, Cast, FieldValue, StrCast } from '../../new_fields/Types'; import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils'; import { RouteStore } from '../../server/RouteStore'; -import { emptyFunction, returnEmptyString, returnOne, returnTrue, Utils } from '../../Utils'; +import { emptyFunction, returnOne, returnTrue, Utils, returnEmptyString } from '../../Utils'; import { DocServer } from '../DocServer'; import { Docs } from '../documents/Documents'; import { ClientUtils } from '../util/ClientUtils'; @@ -24,7 +23,7 @@ import { DictationManager } from '../util/DictationManager'; import { SetupDrag } from '../util/DragManager'; import { HistoryUtil } from '../util/History'; import { Transform } from '../util/Transform'; -import { UndoManager } from '../util/UndoManager'; +import { UndoManager, undoBatch } from '../util/UndoManager'; import { CollectionBaseView } from './collections/CollectionBaseView'; import { CollectionDockingView } from './collections/CollectionDockingView'; import { CollectionTreeView } from './collections/CollectionTreeView'; @@ -37,15 +36,15 @@ import { MainOverlayTextBox } from './MainOverlayTextBox'; import { DocumentView } from './nodes/DocumentView'; import { OverlayView } from './OverlayView'; import PDFMenu from './pdf/PDFMenu'; -import { PresentationView } from './presentationview/PresentationView'; import { PreviewCursor } from './PreviewCursor'; import { FilterBox } from './search/FilterBox'; +import PresModeMenu from './presentationview/PresentationModeMenu'; +import { PresBox } from './nodes/PresBox'; @observer export class MainView extends React.Component { public static Instance: MainView; @observable addMenuToggle = React.createRef<HTMLInputElement>(); - @observable private _workspacesShown: boolean = false; @observable public pwidth: number = 0; @observable public pheight: number = 0; @@ -82,9 +81,6 @@ export class MainView extends React.Component { public isPointerDown = false; private set mainContainer(doc: Opt<Doc>) { if (doc) { - if (!("presentationView" in doc)) { - doc.presentationView = new List<Doc>([Docs.Create.TreeDocument([], { title: "Presentation" })]); - } CurrentUserUtils.UserDocument.activeWorkspace = doc; } } @@ -149,6 +145,7 @@ export class MainView extends React.Component { componentWillUnMount() { window.removeEventListener("keydown", KeyManager.Instance.handle); + //close presentation window.removeEventListener("pointerdown", this.globalPointerDown); window.removeEventListener("pointerup", this.globalPointerUp); } @@ -174,7 +171,7 @@ export class MainView extends React.Component { library.add(faCat); library.add(faFilePdf); library.add(faObjectGroup); - library.add(faTable); + library.add(faTv); library.add(faGlobeAsia); library.add(faUndoAlt); library.add(faRedoAlt); @@ -312,7 +309,6 @@ export class MainView extends React.Component { @computed get dockingContent() { let flyoutWidth = this.flyoutWidth; let mainCont = this.mainContainer; - let castRes = mainCont ? FieldValue(Cast(mainCont.presentationView, listSpec(Doc))) : undefined; return <Measure offset onResize={this.onResize}> {({ measureRef }) => <div ref={measureRef} id="mainContent-div" style={{ width: `calc(100% - ${flyoutWidth}px`, transform: `translate(${flyoutWidth}px, 0px)` }} onDrop={this.onDrop}> @@ -321,6 +317,7 @@ export class MainView extends React.Component { DataDoc={undefined} addDocument={undefined} addDocTab={emptyFunction} + pinToPres={emptyFunction} onClick={undefined} removeDocument={undefined} ScreenToLocalTransform={Transform.Identity} @@ -338,7 +335,6 @@ export class MainView extends React.Component { zoomToScale={emptyFunction} getScale={returnOne} />} - {castRes ? <PresentationView Documents={castRes} key="presentation" /> : null} </div> } </Measure>; @@ -385,6 +381,7 @@ export class MainView extends React.Component { DataDoc={undefined} addDocument={undefined} addDocTab={this.addDocTabFunc} + pinToPres={emptyFunction} removeDocument={undefined} onClick={undefined} ScreenToLocalTransform={Transform.Identity} @@ -442,44 +439,27 @@ export class MainView extends React.Component { let imgurl = "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg"; - // let addDockingNode = action(() => Docs.Create.StandardCollectionDockingDocument([{ doc: addColNode(), initialWidth: 200 }], { width: 200, height: 200, title: "a nested docking freeform collection" })); - let addSchemaNode = action(() => Docs.Create.SchemaDocument([new SchemaHeaderField("title", "#f1efeb")], [], { width: 200, height: 200, title: "a schema collection" })); - //let addTreeNode = action(() => Docs.TreeDocument([CurrentUserUtils.UserDocument], { width: 250, height: 400, title: "Library:" + CurrentUserUtils.email, dropAction: "alias" })); - // let addTreeNode = action(() => Docs.TreeDocument(this._northstarSchemas, { width: 250, height: 400, title: "northstar schemas", dropAction: "copy" })); let addColNode = action(() => Docs.Create.FreeformDocument([], { width: this.pwidth * .7, height: this.pheight, title: "a freeform collection" })); + let addPresNode = action(() => Doc.UserDoc().curPresentation = Docs.Create.PresDocument(new List<Doc>(), { width: 200, height: 500, title: "a presentation trail" })); let addWebNode = action(() => Docs.Create.WebDocument("https://en.wikipedia.org/wiki/Hedgehog", { width: 300, height: 300, title: "New Webpage" })); let addDragboxNode = action(() => Docs.Create.DragboxDocument({ width: 40, height: 40, title: "drag collection" })); let addImageNode = action(() => Docs.Create.ImageDocument(imgurl, { width: 200, title: "an image of a cat" })); let addButtonDocument = action(() => Docs.Create.ButtonDocument({ width: 150, height: 50, title: "Button" })); let addImportCollectionNode = action(() => Docs.Create.DirectoryImportDocument({ title: "Directory Import", width: 400, height: 400 })); - let youtubeurl = "https://www.youtube.com/embed/TqcApsGRzWw"; - let addYoutubeSearcher = action(() => Docs.Create.YoutubeDocument(youtubeurl, { width: 600, height: 600, title: "youtube search" })); + // let youtubeurl = "https://www.youtube.com/embed/TqcApsGRzWw"; + // let addYoutubeSearcher = action(() => Docs.Create.YoutubeDocument(youtubeurl, { width: 600, height: 600, title: "youtube search" })); let btns: [React.RefObject<HTMLDivElement>, IconName, string, () => Doc][] = [ [React.createRef<HTMLDivElement>(), "object-group", "Add Collection", addColNode], + [React.createRef<HTMLDivElement>(), "tv", "Add Presentation Trail", addPresNode], [React.createRef<HTMLDivElement>(), "globe-asia", "Add Website", addWebNode], [React.createRef<HTMLDivElement>(), "bolt", "Add Button", addButtonDocument], - // [React.createRef<HTMLDivElement>(), "clone", "Add Docking Frame", addDockingNode], - [React.createRef<HTMLDivElement>(), "cloud-upload-alt", "Import Directory", addImportCollectionNode], - [React.createRef<HTMLDivElement>(), "play", "Add Youtube Searcher", addYoutubeSearcher], - [React.createRef<HTMLDivElement>(), "file", "Add Document Dragger", addDragboxNode] + [React.createRef<HTMLDivElement>(), "file", "Add Document Dragger", addDragboxNode], + [React.createRef<HTMLDivElement>(), "cloud-upload-alt", "Import Directory", addImportCollectionNode], //remove at some point in favor of addImportCollectionNode + //[React.createRef<HTMLDivElement>(), "play", "Add Youtube Searcher", addYoutubeSearcher], ]; if (!ClientUtils.RELEASE) btns.unshift([React.createRef<HTMLDivElement>(), "cat", "Add Cat Image", addImageNode]); - const setWriteMode = (mode: DocServer.WriteMode) => { - console.log(DocServer.WriteMode[mode]); - const mode1 = mode; - const mode2 = mode === DocServer.WriteMode.Default ? mode : DocServer.WriteMode.Playground; - DocServer.setFieldWriteMode("x", mode1); - DocServer.setFieldWriteMode("y", mode1); - DocServer.setFieldWriteMode("width", mode1); - DocServer.setFieldWriteMode("height", mode1); - - DocServer.setFieldWriteMode("panX", mode2); - DocServer.setFieldWriteMode("panY", mode2); - DocServer.setFieldWriteMode("scale", mode2); - DocServer.setFieldWriteMode("viewType", mode2); - }; return < div id="add-nodes-menu" style={{ left: this.flyoutWidth + 20, bottom: 20 }} > <input type="checkbox" id="add-menu-toggle" ref={this.addMenuToggle} /> @@ -488,7 +468,6 @@ export class MainView extends React.Component { <div id="add-options-content"> <ul id="add-options-list"> <li key="search"><button className="add-button round-button" title="Search" onClick={this.toggleSearch}><FontAwesomeIcon icon="search" size="sm" /></button></li> - <li key="presentation"><button className="add-button round-button" title="Open Presentation View" onClick={() => PresentationView.Instance.toggle(undefined)}><FontAwesomeIcon icon="table" size="sm" /></button></li> <li key="undo"><button className="add-button round-button" title="Undo" style={{ opacity: UndoManager.CanUndo() ? 1 : 0.5, transition: "0.4s ease all" }} onClick={() => UndoManager.Undo()}><FontAwesomeIcon icon="undo-alt" size="sm" /></button></li> <li key="redo"><button className="add-button round-button" title="Redo" style={{ opacity: UndoManager.CanRedo() ? 1 : 0.5, transition: "0.4s ease all" }} onClick={() => UndoManager.Redo()}><FontAwesomeIcon icon="redo-alt" size="sm" /></button></li> {btns.map(btn => @@ -497,13 +476,6 @@ export class MainView extends React.Component { <FontAwesomeIcon icon={btn[1]} size="sm" /> </button> </div></li>)} - <li key="undoTest"><button className="add-button round-button" title="Click if undo isn't working" onClick={() => UndoManager.TraceOpenBatches()}><FontAwesomeIcon icon="exclamation" size="sm" /></button></li> - {ClientUtils.RELEASE ? [] : [ - <li key="test"><button className="add-button round-button" title="Default" onClick={() => setWriteMode(DocServer.WriteMode.Default)}><FontAwesomeIcon icon="exclamation" size="sm" /></button></li>, - <li key="test1"><button className="add-button round-button" title="Playground" onClick={() => setWriteMode(DocServer.WriteMode.Playground)}><FontAwesomeIcon icon="exclamation" size="sm" /></button></li>, - <li key="test2"><button className="add-button round-button" title="Live Playground" onClick={() => setWriteMode(DocServer.WriteMode.LivePlayground)}><FontAwesomeIcon icon="exclamation" size="sm" /></button></li>, - <li key="test3"><button className="add-button round-button" title="Live Readonly" onClick={() => setWriteMode(DocServer.WriteMode.LiveReadonly)}><FontAwesomeIcon icon="exclamation" size="sm" /></button></li> - ]} <li key="color"><button className="add-button round-button" title="Select Color" style={{ zIndex: 1000 }} onClick={() => this.toggleColorPicker()}><div className="toolbar-color-button" style={{ backgroundColor: InkingControl.Instance.selectedColor }} > <div className="toolbar-color-picker" onClick={this.onColorClick} style={this._colorPickerDisplay ? { color: "black", display: "block" } : { color: "black", display: "none" }}> <SketchPicker color={InkingControl.Instance.selectedColor} onChange={InkingControl.Instance.switchColor} /> @@ -542,7 +514,6 @@ export class MainView extends React.Component { @observable isSearchVisible = false; @action.bound toggleSearch = () => { - // console.log("search toggling") this.isSearchVisible = !this.isSearchVisible; } @@ -571,12 +542,21 @@ export class MainView extends React.Component { ); } + @computed get miniPresentation() { + let next = () => PresBox.CurrentPresentation.next(); + let back = () => PresBox.CurrentPresentation.back(); + let startOrResetPres = () => PresBox.CurrentPresentation.startOrResetPres(); + let closePresMode = action(() => { PresBox.CurrentPresentation.presMode = false; this.addDocTabFunc(PresBox.CurrentPresentation.props.Document); }); + return !PresBox.CurrentPresentation || !PresBox.CurrentPresentation.presMode ? (null) : <PresModeMenu next={next} back={back} presStatus={PresBox.CurrentPresentation.presStatus} startOrResetPres={startOrResetPres} closePresMode={closePresMode} > </PresModeMenu>; + } + render() { return ( <div id="main-div"> {this.dictationOverlay} <DocumentDecorations /> {this.mainContent} + {this.miniPresentation} <PreviewCursor /> <ContextMenu /> {this.nodesMenu()} diff --git a/src/client/views/MetadataEntryMenu.scss b/src/client/views/MetadataEntryMenu.scss index bcfc9a82d..e28c4d0e0 100644 --- a/src/client/views/MetadataEntryMenu.scss +++ b/src/client/views/MetadataEntryMenu.scss @@ -1,8 +1,20 @@ .metadataEntry-outerDiv { display: flex; + flex-direction: column; width: 300px; } +.metadataEntry-keys { + max-height: 80; + overflow-y: auto; + display: flex; + flex-direction: column; +} +.metadataEntry-inputArea { + display:flex; + flex-direction: row; +} + .react-autosuggest__container { position: relative; } diff --git a/src/client/views/MetadataEntryMenu.tsx b/src/client/views/MetadataEntryMenu.tsx index 36c240dd8..4a45eede9 100644 --- a/src/client/views/MetadataEntryMenu.tsx +++ b/src/client/views/MetadataEntryMenu.tsx @@ -1,11 +1,12 @@ import * as React from 'react'; import "./MetadataEntryMenu.scss"; import { observer } from 'mobx-react'; -import { observable, action, runInAction, trace } from 'mobx'; +import { observable, action, runInAction, trace, computed, IReactionDisposer, reaction } from 'mobx'; import { KeyValueBox } from './nodes/KeyValueBox'; import { Doc, Field } from '../../new_fields/Doc'; import * as Autosuggest from 'react-autosuggest'; import { undoBatch } from '../util/UndoManager'; +import { emptyFunction } from '../../Utils'; export type DocLike = Doc | Doc[] | Promise<Doc> | Promise<Doc[]>; export interface MetadataEntryProps { @@ -18,7 +19,8 @@ export interface MetadataEntryProps { export class MetadataEntryMenu extends React.Component<MetadataEntryProps>{ @observable private _currentKey: string = ""; @observable private _currentValue: string = ""; - @observable private suggestions: string[] = []; + @observable _allSuggestions: string[] = []; + _suggestionDispser: IReactionDisposer | undefined; private userModified = false; private autosuggestRef = React.createRef<Autosuggest>(); @@ -140,35 +142,39 @@ export class MetadataEntryMenu extends React.Component<MetadataEntryProps>{ getSuggestionValue = (suggestion: string) => suggestion; renderSuggestion = (suggestion: string) => { - return <p>{suggestion}</p>; + return (null); } + componentDidMount() { - onSuggestionFetch = async ({ value }: { value: string }) => { - const sugg = await this.getKeySuggestions(value); - runInAction(() => { - this.suggestions = sugg; - }); + this._suggestionDispser = reaction(() => this._currentKey, + () => this.getKeySuggestions(this._currentKey).then(action((s: string[]) => this._allSuggestions = s)), + { fireImmediately: true }); } - - @action - onSuggestionClear = () => { - this.suggestions = []; + componentWillUnmount() { + this._suggestionDispser && this._suggestionDispser(); } render() { return ( <div className="metadataEntry-outerDiv"> - Key: - <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 className="metadataEntry-inputArea"> + Key: + <Autosuggest inputProps={{ value: this._currentKey, onChange: this.onKeyChange }} + getSuggestionValue={this.getSuggestionValue} + suggestions={[]} + alwaysRenderSuggestions={false} + renderSuggestion={this.renderSuggestion} + onSuggestionsFetchRequested={emptyFunction} + onSuggestionsClearRequested={emptyFunction} + ref={this.autosuggestRef} /> + Value: + <input className="metadataEntry-input" value={this._currentValue} onChange={this.onValueChange} onKeyDown={this.onValueKeyDown} /> + </div> + <div className="metadataEntry-keys" > + <ul> + {this._allSuggestions.map(s => <li key={s} onClick={action(() => { this._currentKey = s; this.previewValue(); })} >{s}</li>)} + </ul> + </div> </div> ); } diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index a8e723379..dc0cbda0b 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -29,6 +29,8 @@ import { faFile, faUnlockAlt } from '@fortawesome/free-solid-svg-icons'; import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils'; import { Docs } from '../../documents/Documents'; import { DateField } from '../../../new_fields/DateField'; +import { List } from '../../../new_fields/List'; +import { DocumentType } from '../../documents/DocumentTypes'; library.add(faFile); @observer @@ -162,6 +164,14 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp this.stateChanged(); } + public Has = (document: Doc) => { + let docs = Cast(this.props.Document.data, listSpec(Doc)); + if (!docs) { + return false; + } + return docs.includes(document); + } + // // Creates a vertical split on the right side of the docking view, and then adds the Document to that split // @@ -535,6 +545,27 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { })); } + /** + * Adds a document to the presentation view + **/ + @undoBatch + @action + public PinDoc(doc: Doc) { + //add this new doc to props.Document + let curPres = Cast(CurrentUserUtils.UserDocument.curPresentation, Doc) as Doc; + if (curPres) { + const data = Cast(curPres.data, listSpec(Doc)); + if (data) { + data.push(doc); + } else { + curPres.data = new List([doc]); + } + if (!DocumentManager.Instance.getDocumentView(curPres)) { + this.addDocTab(curPres, undefined, "onRight"); + } + } + } + componentDidMount() { this.props.glContainer.layoutManager.on("activeContentItemChanged", this.onActiveContentItemChanged); this.props.glContainer.on("tab", this.onActiveContentItemChanged); @@ -569,7 +600,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { } ScreenToLocalTransform = () => { - if (this._mainCont && this._mainCont!.children) { + if (this._mainCont && this._mainCont.children) { let { scale, translateX, translateY } = Utils.GetScreenTransform(this._mainCont.children[0].firstChild as HTMLElement); scale = Utils.GetScreenTransform(this._mainCont).scale; return CollectionDockingView.Instance.props.ScreenToLocalTransform().translate(-translateX, -translateY).scale(1 / this.contentScaling() / scale); @@ -583,6 +614,8 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { MainView.Instance.openWorkspace(doc); } else if (location === "onRight") { CollectionDockingView.Instance.AddRightSplit(doc, dataDoc); + } else if (location === "close") { + CollectionDockingView.Instance.CloseRightSplit(doc); } else { CollectionDockingView.Instance.AddTab(this._stack, doc, dataDoc); } @@ -609,6 +642,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { focus={emptyFunction} backgroundColor={returnEmptyString} addDocTab={this.addDocTab} + pinToPres={this.PinDoc} ContainingCollectionView={undefined} zoomToScale={emptyFunction} getScale={returnOne} />; diff --git a/src/client/views/collections/CollectionSchemaCells.tsx b/src/client/views/collections/CollectionSchemaCells.tsx index 7e3061354..551b485e7 100644 --- a/src/client/views/collections/CollectionSchemaCells.tsx +++ b/src/client/views/collections/CollectionSchemaCells.tsx @@ -40,6 +40,7 @@ export interface CellProps { fieldKey: string; renderDepth: number; addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void; + pinToPres: (document: Doc) => void; moveDocument: (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean; isFocused: boolean; changeFocusedCellByIndex: (row: number, col: number) => void; @@ -160,6 +161,7 @@ export class CollectionSchemaCell extends React.Component<CellProps> { PanelHeight: returnZero, PanelWidth: returnZero, addDocTab: this.props.addDocTab, + pinToPres: this.props.pinToPres, ContentScaling: returnOne }; diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index 4537dcc85..4008dea51 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -171,6 +171,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { active={this.props.active} whenActiveChanged={this.props.whenActiveChanged} addDocTab={this.props.addDocTab} + pinToPres={this.props.pinToPres} setPreviewScript={this.setPreviewScript} previewScript={this.previewScript} /> @@ -200,6 +201,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { active={this.props.active} onDrop={this.onDrop} addDocTab={this.props.addDocTab} + pinToPres={this.props.pinToPres} isSelected={this.props.isSelected} isFocused={this.isFocused} setFocused={this.setFocused} @@ -251,6 +253,7 @@ export interface SchemaTableProps { active: () => boolean; onDrop: (e: React.DragEvent<Element>, options: DocumentOptions, completed?: (() => void) | undefined) => void; addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void; + pinToPres: (document: Doc) => void; isSelected: () => boolean; isFocused: (document: Doc) => boolean; setFocused: (document: Doc) => void; @@ -377,6 +380,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> { fieldKey: this.props.fieldKey, renderDepth: this.props.renderDepth, addDocTab: this.props.addDocTab, + pinToPres: this.props.pinToPres, moveDocument: this.props.moveDocument, setIsEditing: this.setCellIsEditing, isEditable: isEditable, @@ -907,6 +911,7 @@ interface CollectionSchemaPreviewProps { active: () => boolean; whenActiveChanged: (isActive: boolean) => void; addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void; + pinToPres: (document: Doc) => void; setPreviewScript: (script: string) => void; previewScript?: string; } @@ -997,6 +1002,7 @@ export class CollectionSchemaPreview extends React.Component<CollectionSchemaPre whenActiveChanged={this.props.whenActiveChanged} ContainingCollectionView={this.props.CollectionView} addDocTab={this.props.addDocTab} + pinToPres={this.props.pinToPres} parentActive={this.props.active} ScreenToLocalTransform={this.getTransform} renderDepth={this.props.renderDepth + 1} diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index 2e4f6aff5..c74c60d8f 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -11,7 +11,7 @@ import { listSpec } from "../../../new_fields/Schema"; import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField"; import { BoolCast, Cast, NumCast, StrCast, ScriptCast } from "../../../new_fields/Types"; import { emptyFunction, Utils, numberRange } from "../../../Utils"; -import { DocumentType } from "../../documents/Documents"; +import { DocumentType } from "../../documents/DocumentTypes"; import { DragManager } from "../../util/DragManager"; import { Transform } from "../../util/Transform"; import { undoBatch } from "../../util/UndoManager"; @@ -155,11 +155,13 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { active={this.props.active} whenActiveChanged={this.props.whenActiveChanged} addDocTab={this.props.addDocTab} + pinToPres={this.props.pinToPres} setPreviewScript={emptyFunction} previewScript={undefined}> </CollectionSchemaPreview>; } - getDocHeight(d: Doc) { + getDocHeight(d?: Doc) { + if (!d) return 0; let nw = NumCast(d.nativeWidth); let nh = NumCast(d.nativeHeight); if (!d.ignoreAspect && nw && nh) { diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx index 74c7ef305..cc8476548 100644 --- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx +++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx @@ -78,11 +78,14 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC let parent = this.props.parent; parent._docXfs.length = 0; return docs.map((d, i) => { - let pair = Doc.GetLayoutDataDocPair(parent.props.Document, parent.props.DataDoc, parent.props.fieldKey, d); + const pair = Doc.GetLayoutDataDocPair(parent.props.Document, parent.props.DataDoc, parent.props.fieldKey, d); + if (!pair.layout || pair.data instanceof Promise) { + return (null); + } let width = () => Math.min(d.nativeWidth && !d.ignoreAspect && !parent.props.Document.fillColumn ? d[WidthSym]() : Number.MAX_VALUE, parent.columnWidth / parent.numGroupColumns); - let height = () => parent.getDocHeight(pair.layout); + let height = () => parent.getDocHeight(pair!.layout); let dref = React.createRef<HTMLDivElement>(); - let dxf = () => this.getDocTransform(pair.layout, dref.current!); + let dxf = () => this.getDocTransform(pair!.layout, dref.current!); this.props.parent._docXfs.push({ dxf: dxf, width: width, height: height }); let rowSpan = Math.ceil((height() + parent.gridGap) / parent.gridGap); let style = parent.isStackingView ? { width: width(), margin: "auto", marginTop: i === 0 ? 0 : parent.gridGap, height: height() } : { gridRowEnd: `span ${rowSpan}` }; diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 077f3f941..99e5ab7b3 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -11,12 +11,13 @@ import { CurrentUserUtils } from "../../../server/authentication/models/current_ import { RouteStore } from "../../../server/RouteStore"; import { Utils } from "../../../Utils"; import { DocServer } from "../../DocServer"; -import { Docs, DocumentOptions, DocumentType } from "../../documents/Documents"; +import { DocumentType } from "../../documents/DocumentTypes"; +import { Docs, DocumentOptions } from "../../documents/Documents"; import { DragManager } from "../../util/DragManager"; import { undoBatch, UndoManager } from "../../util/UndoManager"; import { DocComponent } from "../DocComponent"; import { FieldViewProps } from "../nodes/FieldView"; -import { FormattedTextBox } from "../nodes/FormattedTextBox"; +import { FormattedTextBox, GoogleRef } from "../nodes/FormattedTextBox"; import { CollectionPDFView } from "./CollectionPDFView"; import { CollectionVideoView } from "./CollectionVideoView"; import { CollectionView } from "./CollectionView"; @@ -82,7 +83,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { let ind; let doc = this.props.Document; let id = CurrentUserUtils.id; - let email = CurrentUserUtils.email; + let email = Doc.CurrentUserEmail; let pos = { x: position[0], y: position[1] }; if (id && email) { const proto = Doc.GetProto(doc); @@ -112,7 +113,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { @undoBatch @action protected drop(e: Event, de: DragManager.DropEvent): boolean { - if (de.data instanceof DragManager.DocumentDragData) { + if (de.data instanceof DragManager.DocumentDragData && !de.data.applyAsTemplate) { if (de.mods === "AltKey" && de.data.draggedDocuments.length) { this.childDocs.map(doc => Doc.ApplyTemplateTo(de.data.draggedDocuments[0], doc, undefined) @@ -206,7 +207,17 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { this.props.addDocument(Docs.Create.VideoDocument(url, { ...options, title: url, width: 400, height: 315, nativeWidth: 600, nativeHeight: 472.5 })); return; } - + let matches: RegExpExecArray | null; + if ((matches = /(https:\/\/)?docs\.google\.com\/document\/d\/([^\\]+)\/edit/g.exec(text)) !== null) { + let newBox = Docs.Create.TextDocument({ ...options, width: 400, height: 200, title: "Awaiting title from Google Docs..." }); + let proto = newBox.proto!; + proto.autoHeight = true; + proto[GoogleRef] = matches[2]; + proto.data = "Please select this document and then click on its pull button to load its contents from from Google Docs..."; + proto.backgroundColor = "#eeeeff"; + this.props.addDocument(newBox); + return; + } let batch = UndoManager.StartBatch("collection view drop"); let promises: Promise<void>[] = []; // tslint:disable-next-line:prefer-for-of diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 4b1fca18a..ebd385743 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -9,7 +9,8 @@ import { List } from '../../../new_fields/List'; import { Document, listSpec } from '../../../new_fields/Schema'; import { BoolCast, Cast, NumCast, StrCast } from '../../../new_fields/Types'; import { emptyFunction, Utils } from '../../../Utils'; -import { Docs, DocUtils, DocumentType } from '../../documents/Documents'; +import { Docs, DocUtils } from '../../documents/Documents'; +import { DocumentType } from "../../documents/DocumentTypes"; import { DocumentManager } from '../../util/DocumentManager'; import { DragManager, dropActionType, SetupDrag } from "../../util/DragManager"; import { SelectionManager } from '../../util/SelectionManager'; @@ -38,6 +39,7 @@ export interface TreeViewProps { moveDocument: DragManager.MoveFunction; dropAction: "alias" | "copy" | undefined; addDocTab: (doc: Doc, dataDoc: Doc | undefined, where: string) => void; + pinToPres: (document: Doc) => void; panelWidth: () => number; panelHeight: () => number; addDocument: (doc: Doc, relativeTo?: Doc, before?: boolean) => boolean; @@ -252,16 +254,16 @@ class TreeView extends React.Component<TreeViewProps> { doc && Object.keys(doc).forEach(key => !(key in ids) && doc[key] !== ComputedField.undefined && (ids[key] = key)); let rows: JSX.Element[] = []; - for (let key of Object.keys(ids).sort()) { + for (let key of Object.keys(ids).slice().sort()) { let contents = doc[key]; let contentElement: JSX.Element[] | JSX.Element = []; if (contents instanceof Doc || Cast(contents, listSpec(Doc))) { let remDoc = (doc: Doc) => this.remove(doc, key); - let addDoc = (doc: Doc, addBefore?: Doc, before?: boolean) => Doc.AddDocToList(this.dataDoc, key, doc, addBefore, before); + let addDoc = (doc: Doc, addBefore?: Doc, before?: boolean) => Doc.AddDocToList(this.dataDoc, key, doc, addBefore, before, !BoolCast(this.props.document.stackingHeadersSortDescending)); contentElement = TreeView.GetChildElements(contents instanceof Doc ? [contents] : DocListCast(contents), this.props.treeViewId, doc, undefined, key, addDoc, remDoc, this.move, - this.props.dropAction, this.props.addDocTab, this.props.ScreenToLocalTransform, this.props.outerXf, this.props.active, this.props.panelWidth, this.props.renderDepth); + this.props.dropAction, this.props.addDocTab, this.props.pinToPres, this.props.ScreenToLocalTransform, this.props.outerXf, this.props.active, this.props.panelWidth, this.props.renderDepth); } else { contentElement = <EditableView key="editableView" @@ -286,13 +288,13 @@ class TreeView extends React.Component<TreeViewProps> { const expandKey = this.treeViewExpandedView === this.fieldKey ? this.fieldKey : this.treeViewExpandedView === "links" ? "links" : undefined; if (expandKey !== undefined) { let remDoc = (doc: Doc) => this.remove(doc, expandKey); - let addDoc = (doc: Doc, addBefore?: Doc, before?: boolean) => Doc.AddDocToList(this.dataDoc, expandKey, doc, addBefore, before); + let addDoc = (doc: Doc, addBefore?: Doc, before?: boolean) => Doc.AddDocToList(this.dataDoc, expandKey, doc, addBefore, before, !BoolCast(this.props.document.stackingHeadersSortDescending)); let docs = expandKey === "links" ? this.childLinks : this.childDocs; return <ul key={expandKey + "more"}> {!docs ? (null) : TreeView.GetChildElements(docs as Doc[], this.props.treeViewId, this.props.document.layout as Doc, this.resolvedDataDoc, expandKey, addDoc, remDoc, this.move, - this.props.dropAction, this.props.addDocTab, this.props.ScreenToLocalTransform, + this.props.dropAction, this.props.addDocTab, this.props.pinToPres, this.props.ScreenToLocalTransform, this.props.outerXf, this.props.active, this.props.panelWidth, this.props.renderDepth)} </ul >; } else if (this.treeViewExpandedView === "fields") { @@ -318,6 +320,7 @@ class TreeView extends React.Component<TreeViewProps> { active={this.props.active} whenActiveChanged={emptyFunction as any} addDocTab={this.props.addDocTab} + pinToPres={this.props.pinToPres} setPreviewScript={emptyFunction}> </CollectionSchemaPreview> </div>; @@ -394,6 +397,7 @@ class TreeView extends React.Component<TreeViewProps> { move: DragManager.MoveFunction, dropAction: dropActionType, addDocTab: (doc: Doc, dataDoc: Doc | undefined, where: string) => void, + pinToPres: (document: Doc) => void, screenToLocalXf: () => Transform, outerXf: () => { translateX: number, translateY: number }, active: () => boolean, @@ -415,7 +419,7 @@ class TreeView extends React.Component<TreeViewProps> { } let descending = BoolCast(containingCollection.stackingHeadersSortDescending); - docs.sort(function (a, b): 1 | -1 { + docs.slice().sort(function (a, b): 1 | -1 { let descA = descending ? b : a; let descB = descending ? a : b; let first = descA[String(containingCollection.sectionFilter)]; @@ -439,6 +443,9 @@ class TreeView extends React.Component<TreeViewProps> { let rowWidth = () => panelWidth() - 20; return docs.map((child, i) => { let pair = Doc.GetLayoutDataDocPair(containingCollection, dataDoc, key, child); + if (!pair.layout || pair.data instanceof Promise) { + return (null); + } let indent = i === 0 ? undefined : () => { if (StrCast(docs[i - 1].layout).indexOf("CollectionView") !== -1) { @@ -470,6 +477,7 @@ class TreeView extends React.Component<TreeViewProps> { moveDocument={move} dropAction={dropAction} addDocTab={addDocTab} + pinToPres={pinToPres} ScreenToLocalTransform={screenToLocalXf} outerXf={outerXf} parentKey={key} @@ -553,7 +561,7 @@ export class CollectionTreeView extends CollectionSubView(Document) { render() { Doc.UpdateDocumentExtensionForField(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey); let dropAction = StrCast(this.props.Document.dropAction) as dropActionType; - let addDoc = (doc: Doc, relativeTo?: Doc, before?: boolean) => Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, relativeTo, before); + let addDoc = (doc: Doc, relativeTo?: Doc, before?: boolean) => Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, relativeTo, before, !BoolCast(this.props.Document.stackingHeadersSortDescending)); let moveDoc = (d: Doc, target: Doc, addDoc: (doc: Doc) => boolean) => this.props.moveDocument(d, target, addDoc); return !this.childDocs ? (null) : ( <div id="body" className="collectionTreeView-dropTarget" @@ -573,14 +581,14 @@ export class CollectionTreeView extends CollectionSubView(Document) { let doc = this.props.Document.detailedLayout instanceof Doc ? Doc.ApplyTemplate(Doc.GetProto(this.props.Document.detailedLayout)) : undefined; if (!doc) doc = Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, width: 100, height: 25, templates: new List<string>([Templates.Title.Layout]) }); TreeView.loadId = doc[Id]; - Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, this.childDocs.length ? this.childDocs[0] : undefined, true); + Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, this.childDocs.length ? this.childDocs[0] : undefined, true, !BoolCast(this.props.Document.stackingHeadersSortDescending)); }} /> {this.props.Document.workspaceLibrary ? this.renderNotifsButton : (null)} {this.props.Document.allowClear ? this.renderClearButton : (null)} <ul className="no-indent" style={{ width: "max-content" }} > { TreeView.GetChildElements(this.childDocs, this.props.Document[Id], this.props.Document, this.props.DataDoc, this.props.fieldKey, addDoc, this.remove, - moveDoc, dropAction, this.props.addDocTab, this.props.ScreenToLocalTransform, this.outerXf, this.props.active, this.props.PanelWidth, this.props.renderDepth) + moveDoc, dropAction, this.props.addDocTab, this.props.pinToPres, this.props.ScreenToLocalTransform, this.outerXf, this.props.active, this.props.PanelWidth, this.props.renderDepth) } </ul> </div > diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 7e1adaa19..3a8253720 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -108,9 +108,13 @@ export class CollectionView extends React.Component<FieldViewProps> { ContextMenu.Instance.addItem({ description: "View Modes...", subitems: subItems, icon: "eye" }); let existing = ContextMenu.Instance.findByDescription("Layout..."); let layoutItems: ContextMenuProps[] = existing && "subitems" in existing ? existing.subitems : []; - layoutItems.push({ description: "Create Layout Instance", event: () => this.props.addDocTab && this.props.addDocTab(Doc.ApplyTemplate(this.props.Document)!, undefined, "onRight"), icon: "project-diagram" }); layoutItems.push({ description: `${this.props.Document.forceActive ? "Select" : "Force"} Contents Active`, event: () => this.props.Document.forceActive = !this.props.Document.forceActive, icon: "project-diagram" }); !existing && ContextMenu.Instance.addItem({ description: "Layout...", subitems: layoutItems, icon: "hand-point-right" }); + + let makes = ContextMenu.Instance.findByDescription("Make..."); + let makeItems: ContextMenuProps[] = makes && "subitems" in makes ? makes.subitems : []; + makeItems.push({ description: "Template Layout Instance", event: () => this.props.addDocTab && this.props.addDocTab(Doc.ApplyTemplate(this.props.Document)!, undefined, "onRight"), icon: "project-diagram" }); + !makes && ContextMenu.Instance.addItem({ description: "Make...", subitems: makeItems, icon: "hand-point-right" }); } } diff --git a/src/client/views/collections/CollectionViewChromes.scss b/src/client/views/collections/CollectionViewChromes.scss index 595f079d1..f39bd877a 100644 --- a/src/client/views/collections/CollectionViewChromes.scss +++ b/src/client/views/collections/CollectionViewChromes.scss @@ -7,7 +7,6 @@ z-index: 9001; transition: top .5s; background: lightgrey; - padding: 10px; .collectionViewChrome { display: grid; diff --git a/src/client/views/collections/CollectionViewChromes.tsx b/src/client/views/collections/CollectionViewChromes.tsx index ccea6c1ea..9631243c0 100644 --- a/src/client/views/collections/CollectionViewChromes.tsx +++ b/src/client/views/collections/CollectionViewChromes.tsx @@ -1,30 +1,25 @@ -import * as React from "react"; -import { CollectionView } from "./CollectionView"; -import "./CollectionViewChromes.scss"; -import { CollectionViewType } from "./CollectionBaseView"; -import { undoBatch } from "../../util/UndoManager"; -import { action, observable, runInAction, computed, IObservable, IObservableValue, reaction, autorun } from "mobx"; -import { observer } from "mobx-react"; -import { Doc, DocListCast, FieldResult } from "../../../new_fields/Doc"; -import { DocLike } from "../MetadataEntryMenu"; -import * as Autosuggest from 'react-autosuggest'; -import { EditableView } from "../EditableView"; -import { StrCast, NumCast, BoolCast, Cast } from "../../../new_fields/Types"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { action, computed, observable, runInAction } from "mobx"; +import { observer } from "mobx-react"; +import * as React from "react"; +import { Doc, DocListCast } from "../../../new_fields/Doc"; +import { Id } from "../../../new_fields/FieldSymbols"; +import { List } from "../../../new_fields/List"; +import { listSpec } from "../../../new_fields/Schema"; +import { ScriptField } from "../../../new_fields/ScriptField"; +import { BoolCast, Cast, NumCast, StrCast } from "../../../new_fields/Types"; import { Utils } from "../../../Utils"; -import KeyRestrictionRow from "./KeyRestrictionRow"; +import { DragManager } from "../../util/DragManager"; import { CompileScript } from "../../util/Scripting"; -import { ScriptField } from "../../../new_fields/ScriptField"; -import { CollectionSchemaView } from "./CollectionSchemaView"; +import { undoBatch } from "../../util/UndoManager"; +import { EditableView } from "../EditableView"; import { COLLECTION_BORDER_WIDTH } from "../globalCssVariables.scss"; -import { listSpec } from "../../../new_fields/Schema"; -import { List } from "../../../new_fields/List"; -import { Id } from "../../../new_fields/FieldSymbols"; -import { threadId } from "worker_threads"; -import { DragManager } from "../../util/DragManager"; +import { DocLike } from "../MetadataEntryMenu"; +import { CollectionViewType } from "./CollectionBaseView"; +import { CollectionView } from "./CollectionView"; +import "./CollectionViewChromes.scss"; +import KeyRestrictionRow from "./KeyRestrictionRow"; const datepicker = require('js-datepicker'); -import * as $ from 'jquery'; -import { firebasedynamiclinks } from "googleapis/build/src/apis/firebasedynamiclinks"; interface CollectionViewChromeProps { CollectionView: CollectionView; @@ -51,7 +46,6 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro @computed private get filterValue() { return Cast(this.props.CollectionView.props.Document.viewSpecScript, ScriptField); } private _picker: any; - private _datePickerElGuid = Utils.GenerateGuid(); getFilters = (script: string) => { let re: any = /(!)?\(\(\(doc\.(\w+)\s+&&\s+\(doc\.\w+\s+as\s+\w+\)\.includes\(\"(\w+)\"\)/g; @@ -91,11 +85,6 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro } componentDidMount = () => { - setTimeout(() => this._picker = datepicker("#" + this._datePickerElGuid, { - disabler: (date: Date) => date > new Date(), - onSelect: (instance: any, date: Date) => runInAction(() => this._dateValue = date), - dateSelected: new Date() - }), 1000); let fields: Filter[] = []; if (this.filterValue) { @@ -173,8 +162,6 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro this.openViewSpecs(e); - console.log(this._keyRestrictions) - let keyRestrictionScript = "(" + this._keyRestrictions.map(i => i[1]).filter(i => i.length > 0).join(" && ") + ")"; let yearOffset = this._dateWithinValue[1] === 'y' ? 1 : 0; let monthOffset = this._dateWithinValue[1] === 'm' ? parseInt(this._dateWithinValue[0]) : 0; @@ -308,6 +295,15 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro this.addKeyRestrictions([]); } + datePickerRef = (node: HTMLInputElement) => { + if (node) { + this._picker = datepicker("#" + node.id, { + disabler: (date: Date) => date > new Date(), + onSelect: (instance: any, date: Date) => runInAction(() => this._dateValue = date), + dateSelected: new Date() + }); + } + } render() { let collapsed = this.props.CollectionView.props.Document.chromeStatus !== "enabled"; return ( @@ -368,7 +364,8 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro <option value="1y">1 year of</option> </select> <input className="collectionViewBaseChrome-viewSpecsMenu-rowRight" - id={this._datePickerElGuid} + id={Utils.GenerateGuid()} + ref={this.datePickerRef} value={this._dateValue instanceof Date ? this._dateValue.toLocaleDateString() : this._dateValue} onChange={(e) => runInAction(() => this._dateValue = e.target.value)} onPointerDown={this.openDatePicker} diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx index 3193f5624..b8148852d 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx @@ -55,7 +55,7 @@ export class CollectionFreeFormRemoteCursors extends React.Component<CollectionV ctx.stroke(); // ctx.font = "10px Arial"; - // ctx.fillText(CurrentUserUtils.email[0].toUpperCase(), 10, 10); + // ctx.fillText(Doc.CurrentUserEmail[0].toUpperCase(), 10, 10); } } } diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 903fd72f7..3be6aa3d3 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -36,8 +36,8 @@ import { CollectionFreeFormRemoteCursors } from "./CollectionFreeFormRemoteCurso import "./CollectionFreeFormView.scss"; import { MarqueeView } from "./MarqueeView"; import React = require("react"); -import { DocumentType, Docs } from "../../../documents/Documents"; -import { PreviewCursor } from "../../PreviewCursor"; +import { Docs } from "../../../documents/Documents"; +import { DocumentType } from "../../../documents/DocumentTypes"; library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload, faBraille, faChalkboard); @@ -622,20 +622,19 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { getScale = () => this.Document.scale ? this.Document.scale : 1; - getChildDocumentViewProps(childDocLayout: Doc): DocumentViewProps { - let pair = Doc.GetLayoutDataDocPair(this.props.Document, this.props.DataDoc, this.props.fieldKey, childDocLayout); + getChildDocumentViewProps(childLayout: Doc, childData?: Doc): DocumentViewProps { return { - DataDoc: pair.data, - Document: pair.layout, + DataDoc: childData, + Document: childLayout, addDocument: this.props.addDocument, removeDocument: this.props.removeDocument, moveDocument: this.props.moveDocument, onClick: this.props.onClick, - ScreenToLocalTransform: pair.layout.z ? this.getTransformOverlay : this.getTransform, + ScreenToLocalTransform: childLayout.z ? this.getTransformOverlay : this.getTransform, renderDepth: this.props.renderDepth + 1, - selectOnLoad: pair.layout[Id] === this._selectOnLoaded, - PanelWidth: pair.layout[WidthSym], - PanelHeight: pair.layout[HeightSym], + selectOnLoad: childLayout[Id] === this._selectOnLoaded, + PanelWidth: childLayout[WidthSym], + PanelHeight: childLayout[HeightSym], ContentScaling: returnOne, ContainingCollectionView: this.props.CollectionView, focus: this.focusDocument, @@ -644,6 +643,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { whenActiveChanged: this.props.whenActiveChanged, bringToFront: this.bringToFront, addDocTab: this.props.addDocTab, + pinToPres: this.props.pinToPres, zoomToScale: this.zoomToScale, getScale: this.getScale }; @@ -669,6 +669,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { whenActiveChanged: this.props.whenActiveChanged, bringToFront: this.bringToFront, addDocTab: this.props.addDocTab, + pinToPres: this.props.pinToPres, zoomToScale: this.zoomToScale, getScale: this.getScale }; @@ -743,12 +744,15 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { const pos = script ? this.getCalculatedPositions(script, { doc, index: prev.length, collection: this.Document, docs, state }) : { x: Cast(doc.x, "number"), y: Cast(doc.y, "number"), z: Cast(doc.z, "number"), width: Cast(doc.width, "number"), height: Cast(doc.height, "number") }; state = pos.state === undefined ? state : pos.state; - prev.push({ - ele: <CollectionFreeFormDocumentView key={doc[Id]} - x={script ? pos.x : undefined} y={script ? pos.y : undefined} - width={script ? pos.width : undefined} height={script ? pos.height : undefined} {...this.getChildDocumentViewProps(doc)} />, - bounds: (pos.x !== undefined && pos.y !== undefined) ? { x: pos.x, y: pos.y, z: pos.z, width: NumCast(pos.width), height: NumCast(pos.height) } : undefined - }); + let pair = Doc.GetLayoutDataDocPair(this.props.Document, this.props.DataDoc, this.props.fieldKey, doc); + if (pair.layout && !(pair.data instanceof Promise)) { + prev.push({ + ele: <CollectionFreeFormDocumentView key={doc[Id]} + x={script ? pos.x : undefined} y={script ? pos.y : undefined} + width={script ? pos.width : undefined} height={script ? pos.height : undefined} {...this.getChildDocumentViewProps(pair.layout, pair.data)} />, + bounds: (pos.x !== undefined && pos.y !== undefined) ? { x: pos.x, y: pos.y, z: pos.z, width: NumCast(pos.width), height: NumCast(pos.height) } : undefined + }); + } } } return prev; diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index c73e960d8..d77662355 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -13,6 +13,7 @@ import { FormattedTextBox } from "./FormattedTextBox"; import { ImageBox } from "./ImageBox"; import { DragBox } from "./DragBox"; import { ButtonBox } from "./ButtonBox"; +import { PresBox } from "./PresBox"; import { IconBox } from "./IconBox"; import { KeyValueBox } from "./KeyValueBox"; import { PDFBox } from "./PDFBox"; @@ -67,7 +68,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & { } get dataDoc() { - if (this.props.DataDoc === undefined && this.props.Document.layout instanceof Doc) { + if (this.props.DataDoc === undefined && (this.props.Document.layout instanceof Doc || this.props.Document instanceof Promise)) { // if there is no dataDoc (ie, we're not rendering a template layout), but this document // has a template layout document, then we will render the template layout but use // this document as the data document for the layout. @@ -107,7 +108,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & { if (!this.layout && (this.props.layoutKey !== "overlayLayout" || !this.templates.length)) return (null); return <ObserverJsxParser blacklistedAttrs={[]} - components={{ FormattedTextBox, ImageBox, IconBox, DirectoryImportBox, DragBox, ButtonBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox, YoutubeBox }} + components={{ FormattedTextBox, ImageBox, IconBox, DirectoryImportBox, DragBox, ButtonBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox, PresBox, YoutubeBox }} bindings={this.CreateBindings()} jsx={this.finalLayout} showWarnings={true} diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 900cd266e..9f1d98bb5 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -33,7 +33,6 @@ import { DocComponent } from "../DocComponent"; import { EditableView } from '../EditableView'; import { MainView } from '../MainView'; import { OverlayView } from '../OverlayView'; -import { PresentationView } from "../presentationview/PresentationView"; import { ScriptBox } from '../ScriptBox'; import { ScriptingRepl } from '../ScriptingRepl'; import { Template } from "./../Templates"; @@ -41,7 +40,7 @@ import { DocumentContentsView } from "./DocumentContentsView"; import "./DocumentView.scss"; import { FormattedTextBox } from './FormattedTextBox'; import React = require("react"); -import { IDisposable } from '../../northstar/utils/IDisposable'; +import { PresBox } from './PresBox'; const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this? library.add(fa.faTrash); @@ -98,6 +97,7 @@ export interface DocumentViewProps { whenActiveChanged: (isActive: boolean) => void; bringToFront: (doc: Doc, sendToBack?: boolean) => void; addDocTab: (doc: Doc, dataDoc: Doc | undefined, where: string) => void; + pinToPres: (document: Doc) => void; collapseToPoint?: (scrpt: number[], expandedDocs: Doc[] | undefined) => void; zoomToScale: (scale: number) => void; backgroundColor: (doc: Doc) => string | undefined; @@ -139,6 +139,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu private _lastTap: number = 0; private _doubleTap = false; private _hitExpander = false; + private _hitTemplateDrag = false; private _mainCont = React.createRef<HTMLDivElement>(); private _dropDisposer?: DragManager.DragDropDisposer; _animateToIconDisposer?: IReactionDisposer; @@ -228,7 +229,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } get dataDoc() { - if (this.props.DataDoc === undefined && this.props.Document.layout instanceof Doc) { + if (this.props.DataDoc === undefined && (this.props.Document.layout instanceof Doc || this.props.Document instanceof Promise)) { // if there is no dataDoc (ie, we're not rendering a temlplate layout), but this document // has a template layout document, then we will render the template layout but use // this document as the data document for the layout. @@ -236,7 +237,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } return this.props.DataDoc !== this.props.Document ? this.props.DataDoc : undefined; } - startDragging(x: number, y: number, dropAction: dropActionType, dragSubBullets: boolean) { + startDragging(x: number, y: number, dropAction: dropActionType, dragSubBullets: boolean, applyAsTemplate?: boolean) { if (this._mainCont.current) { let allConnected = [this.props.Document, ...(dragSubBullets ? DocListCast(this.props.Document.subBulletDocs) : [])]; let alldataConnected = [this.dataDoc, ...(dragSubBullets ? DocListCast(this.props.Document.subBulletDocs) : [])]; @@ -247,6 +248,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu dragData.xOffset = xoff; dragData.yOffset = yoff; dragData.moveDocument = this.props.moveDocument; + dragData.applyAsTemplate = applyAsTemplate; DragManager.StartDocumentDrag([this._mainCont.current], dragData, x, y, { handlers: { dragComplete: action(emptyFunction) @@ -377,15 +379,19 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } } } + + onPointerDown = (e: React.PointerEvent): void => { if (e.nativeEvent.cancelBubble) return; this._downX = e.clientX; this._downY = e.clientY; this._hitExpander = DocListCast(this.props.Document.subBulletDocs).length > 0; - // if (e.shiftKey && e.buttons === 1 && CollectionDockingView.Instance) { - // CollectionDockingView.Instance.StartOtherDrag(e, [Doc.MakeAlias(this.props.Document)], [this.dataDoc]); - // e.stopPropagation(); - // } else { + this._hitTemplateDrag = false; + for (let element = (e.target as any); element && !this._hitTemplateDrag; element = element.parentElement) { + if (element.className && element.className.toString() === "collectionViewBaseChrome-collapse") { + this._hitTemplateDrag = true; + } + } if (this.active) e.stopPropagation(); // events stop at the lowest document that is active. document.removeEventListener("pointermove", this.onPointerMove); document.addEventListener("pointermove", this.onPointerMove); @@ -399,7 +405,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu if (!e.altKey && !this.topMost && e.buttons === 1 && !BoolCast(this.props.Document.lockedPosition)) { document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); - this.startDragging(this._downX, this._downY, e.ctrlKey || e.altKey ? "alias" : undefined, this._hitExpander); + this.startDragging(this._downX, this._downY, e.ctrlKey || e.altKey ? "alias" : undefined, this._hitExpander, this._hitTemplateDrag); } } e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers @@ -457,6 +463,10 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu DocUtils.MakeLink(annotationDoc, targetDoc, this.props.ContainingCollectionView!.props.Document, `Link from ${StrCast(annotationDoc.title)}`); } + if (de.data instanceof DragManager.DocumentDragData && de.data.applyAsTemplate) { + Doc.ApplyTemplateTo(de.data.draggedDocuments[0], this.props.Document, this.props.DataDoc); + e.stopPropagation(); + } if (de.data instanceof DragManager.LinkDragData) { let sourceDoc = de.data.linkSourceDocument; let destDoc = this.props.Document; @@ -573,21 +583,14 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu cm.addItem({ description: "Open...", subitems: subitems, icon: "external-link-alt" }); let existingMake = ContextMenu.Instance.findByDescription("Make..."); let makes: ContextMenuProps[] = existingMake && "subitems" in existingMake ? existingMake.subitems : []; - makes.push({ description: this.props.Document.isBackground ? "Remove Background" : "Make Background", event: this.makeBackground, icon: BoolCast(this.props.Document.lockedPosition) ? "unlock" : "lock" }); - makes.push({ description: this.props.Document.isButton ? "Remove Button" : "Make Button", event: this.makeBtnClicked, icon: "concierge-bell" }); - makes.push({ description: "Edit OnClick script", icon: "edit", event: () => ScriptBox.EditClickScript(this.props.Document, "onClick") }); + makes.push({ description: this.props.Document.isBackground ? "Remove Background" : "Into Background", event: this.makeBackground, icon: BoolCast(this.props.Document.lockedPosition) ? "unlock" : "lock" }); + makes.push({ description: this.props.Document.isButton ? "Remove Button" : "Into Button", event: this.makeBtnClicked, icon: "concierge-bell" }); + makes.push({ description: "OnClick script", icon: "edit", event: () => ScriptBox.EditClickScript(this.props.Document, "onClick") }); makes.push({ - description: "Make Portal", event: () => { + description: "Into Portal", event: () => { let portal = Docs.Create.FreeformDocument([], { width: this.props.Document[WidthSym]() + 10, height: this.props.Document[HeightSym](), title: this.props.Document.title + ".portal" }); - //Doc.GetProto(this.props.Document).subBulletDocs = new List<Doc>([portal]); - //summary.proto!.maximizeLocation = "inTab"; // or "inPlace", or "onRight" - //Doc.GetProto(this.props.Document).templates = new List<string>([Templates.Bullet.Layout]); - //let coll = Docs.Create.StackingDocument([this.props.Document, portal], { x: NumCast(this.props.Document.x), y: NumCast(this.props.Document.y), width: this.props.Document[WidthSym]() + 10, height: this.props.Document[HeightSym](), title: this.props.Document.title + ".cont" }); - //this.props.addDocument && this.props.addDocument(coll); - //this.props.removeDocument && this.props.removeDocument(this.props.Document); DocUtils.MakeLink(this.props.Document, portal, undefined, this.props.Document.title + ".portal"); this.makeBtnClicked(); - }, icon: "window-restore" }); !existingMake && cm.addItem({ description: "Make...", subitems: makes, icon: "hand-point-right" }); @@ -614,7 +617,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu let analyzers: ContextMenuProps[] = existingAnalyze && "subitems" in existingAnalyze ? existingAnalyze.subitems : []; analyzers.push({ description: "Transcribe Speech", event: this.listen, icon: "microphone" }); !existingAnalyze && cm.addItem({ description: "Analyzers...", subitems: analyzers, icon: "hand-point-right" }); - cm.addItem({ description: "Pin to Presentation", event: () => PresentationView.Instance.PinDoc(this.props.Document), icon: "map-pin" }); + cm.addItem({ description: "Pin to Presentation", event: () => this.props.pinToPres(this.props.Document), icon: "map-pin" }); //I think this should work... and it does! A miracle! cm.addItem({ description: "Add Repl", icon: "laptop-code", event: () => OverlayView.Instance.addWindow(<ScriptingRepl />, { x: 300, y: 100, width: 200, height: 200, title: "Scripting REPL" }) }); cm.addItem({ description: "Download document", icon: "download", event: () => { @@ -661,7 +664,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu try { let stuff = await rp.get(Utils.prepend(RouteStore.getUsers)); const users: User[] = JSON.parse(stuff); - usersMenu = users.filter(({ email }) => email !== CurrentUserUtils.email).map(({ email, userDocumentId }) => ({ + usersMenu = users.filter(({ email }) => email !== Doc.CurrentUserEmail).map(({ email, userDocumentId }) => ({ description: email, event: async () => { const userDocument = await Cast(DocServer.GetRefField(userDocumentId), Doc); if (!userDocument) { @@ -684,6 +687,30 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } runInAction(() => { cm.addItem({ description: "Share...", subitems: usersMenu, icon: "share" }); + if (!ClientUtils.RELEASE) { + let setWriteMode = (mode: DocServer.WriteMode) => { + DocServer.AclsMode = mode; + const mode1 = mode; + const mode2 = mode === DocServer.WriteMode.Default ? mode : DocServer.WriteMode.Playground; + DocServer.setFieldWriteMode("x", mode1); + DocServer.setFieldWriteMode("y", mode1); + DocServer.setFieldWriteMode("width", mode1); + DocServer.setFieldWriteMode("height", mode1); + + DocServer.setFieldWriteMode("panX", mode2); + DocServer.setFieldWriteMode("panY", mode2); + DocServer.setFieldWriteMode("scale", mode2); + DocServer.setFieldWriteMode("viewType", mode2); + }; + let aclsMenu: ContextMenuProps[] = []; + aclsMenu.push({ description: "Default (write/read all)", event: () => setWriteMode(DocServer.WriteMode.Default), icon: DocServer.AclsMode === DocServer.WriteMode.Default ? "check" : "exclamation" }); + aclsMenu.push({ description: "Playground (write own/no read)", event: () => setWriteMode(DocServer.WriteMode.Playground), icon: DocServer.AclsMode === DocServer.WriteMode.Playground ? "check" : "exclamation" }); + aclsMenu.push({ description: "Live Playground (write own/read others)", event: () => setWriteMode(DocServer.WriteMode.LivePlayground), icon: DocServer.AclsMode === DocServer.WriteMode.LivePlayground ? "check" : "exclamation" }); + aclsMenu.push({ description: "Live Readonly (no write/read others)", event: () => setWriteMode(DocServer.WriteMode.LiveReadonly), icon: DocServer.AclsMode === DocServer.WriteMode.LiveReadonly ? "check" : "exclamation" }); + cm.addItem({ description: "Collaboration ACLs...", subitems: aclsMenu, icon: "share" }); + cm.addItem({ description: "Undo Debug Test", event: () => UndoManager.TraceOpenBatches(), icon: "exclamation" }); + } + if (!this.topMost) { // DocumentViews should stop propagation of this event e.stopPropagation(); diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index 3287949e2..f0f1b3b73 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -39,6 +39,7 @@ export interface FieldViewProps { selectOnLoad: boolean; addDocument?: (document: Doc, allowDuplicates?: boolean) => boolean; addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void; + pinToPres: (document: Doc) => void; removeDocument?: (document: Doc) => boolean; moveDocument?: (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean; ScreenToLocalTransform: () => Transform; @@ -57,6 +58,7 @@ export interface FieldViewProps { export class FieldView extends React.Component<FieldViewProps> { public static LayoutString(fieldType: { name: string }, fieldStr: string = "data", fieldExt: string = "") { return `<${fieldType.name} {...props} fieldKey={"${fieldStr}"} fieldExt={"${fieldExt}"} />`; + //"<ImageBox {...props} />" } @computed @@ -78,6 +80,9 @@ export class FieldView extends React.Component<FieldViewProps> { else if (field instanceof ImageField) { return <ImageBox {...this.props} leaveNativeSize={true} />; } + // else if (field instaceof PresBox) { + // return <PresBox {...this.props} />; + // } else if (field instanceof IconField) { return <IconBox {...this.props} />; } diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index c2015a147..9ae092bad 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -1,6 +1,6 @@ import { library } from '@fortawesome/fontawesome-svg-core'; -import { faEdit, faSmile, faTextHeight } from '@fortawesome/free-solid-svg-icons'; -import { action, computed, IReactionDisposer, Lambda, observable, reaction } from "mobx"; +import { faEdit, faSmile, faTextHeight, faUpload } from '@fortawesome/free-solid-svg-icons'; +import { action, computed, IReactionDisposer, Lambda, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; import { baseKeymap } from "prosemirror-commands"; import { history } from "prosemirror-history"; @@ -12,9 +12,9 @@ import { DateField } from '../../../new_fields/DateField'; import { Doc, DocListCast, Opt, WidthSym } from "../../../new_fields/Doc"; import { Copy, Id } from '../../../new_fields/FieldSymbols'; import { List } from '../../../new_fields/List'; -import { RichTextField } from "../../../new_fields/RichTextField"; +import { RichTextField, ToPlainText, FromPlainText } from "../../../new_fields/RichTextField"; +import { BoolCast, Cast, NumCast, StrCast, DateCast } from "../../../new_fields/Types"; import { createSchema, makeInterface } from "../../../new_fields/Schema"; -import { BoolCast, Cast, DateCast, NumCast, StrCast } from "../../../new_fields/Types"; import { Utils } from '../../../Utils'; import { DocServer } from "../../DocServer"; import { Docs, DocUtils } from '../../documents/Documents'; @@ -29,17 +29,21 @@ import { TooltipTextMenu } from "../../util/TooltipTextMenu"; import { undoBatch, UndoManager } from "../../util/UndoManager"; import { DocComponent } from "../DocComponent"; import { InkingControl } from "../InkingControl"; -import { MainOverlayTextBox } from '../MainOverlayTextBox'; import { FieldView, FieldViewProps } from "./FieldView"; import "./FormattedTextBox.scss"; import React = require("react"); +import { GoogleApiClientUtils, Pulls, Pushes } from '../../apis/google_docs/GoogleApiClientUtils'; +import { DocumentDecorations } from '../DocumentDecorations'; +import { MainOverlayTextBox } from '../MainOverlayTextBox'; library.add(faEdit); -library.add(faSmile, faTextHeight); +library.add(faSmile, faTextHeight, faUpload); // FormattedTextBox: Displays an editable plain text node that maps to a specified Key of a Document // +export const Blank = `{"doc":{"type":"doc","content":[]},"selection":{"type":"text","anchor":0,"head":0}}`; + export interface FormattedTextBoxProps { isOverlay?: boolean; hideOnLeave?: boolean; @@ -53,9 +57,13 @@ const richTextSchema = createSchema({ documentText: "string" }); +export const GoogleRef = "googleDocId"; + type RichTextDocument = makeInterface<[typeof richTextSchema]>; const RichTextDocument = makeInterface(richTextSchema); +type PullHandler = (exportState: GoogleApiClientUtils.Docs.ReadResult, dataDoc: Doc) => void; + @observer export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTextBoxProps), RichTextDocument>(RichTextDocument) { public static LayoutString(fieldStr: string = "data") { @@ -73,8 +81,11 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe private _textReactionDisposer: Opt<IReactionDisposer>; private _heightReactionDisposer: Opt<IReactionDisposer>; private _proxyReactionDisposer: Opt<IReactionDisposer>; + private pullReactionDisposer: Opt<IReactionDisposer>; + private pushReactionDisposer: Opt<IReactionDisposer>; private dropDisposer?: DragManager.DragDropDisposer; public get CurrentDiv(): HTMLDivElement { return this._ref.current!; } + private isGoogleDocsUpdate = false; @observable _entered = false; @observable public static InputBoxOverlay?: FormattedTextBox = undefined; @@ -289,19 +300,55 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe }, { fireImmediately: true }); } + this.pullFromGoogleDoc(this.checkState); + runInAction(() => DocumentDecorations.Instance.isAnimatingFetch = true); + this._reactionDisposer = reaction( () => { const field = this.dataDoc ? Cast(this.dataDoc[this.props.fieldKey], RichTextField) : undefined; - return field ? field.Data : `{"doc":{"type":"doc","content":[]},"selection":{"type":"text","anchor":0,"head":0}}`; + return field ? field.Data : Blank; }, - field2 => this._editorView && !this._applyingChange && - this._editorView.updateState(EditorState.fromJSON(config, JSON.parse(field2))) + incomingValue => { + if (this._editorView && !this._applyingChange) { + let updatedState = JSON.parse(incomingValue); + this._editorView.updateState(EditorState.fromJSON(config, updatedState)); + // manually sets cursor selection at the end of the text on focus + if (this.isGoogleDocsUpdate) { + this.isGoogleDocsUpdate = false; + let end = this._editorView.state.doc.content.size - 1; + updatedState.selection = { type: "text", anchor: end, head: end }; + this._editorView.updateState(EditorState.fromJSON(config, updatedState)); + } + this.tryUpdateHeight(); + } + } + ); + + this.pullReactionDisposer = reaction( + () => this.props.Document[Pulls], + () => { + if (!DocumentDecorations.hasPulledHack) { + DocumentDecorations.hasPulledHack = true; + let unchanged = this.dataDoc.unchanged; + this.pullFromGoogleDoc(unchanged ? this.checkState : this.updateState); + } + } ); - this.props.isOverlay && (this._heightReactionDisposer = reaction( + this.pushReactionDisposer = reaction( + () => this.props.Document[Pushes], + () => { + if (!DocumentDecorations.hasPushedHack) { + DocumentDecorations.hasPushedHack = true; + this.pushToGoogleDoc(); + } + } + ); + + this._heightReactionDisposer = reaction( () => this.props.Document[WidthSym](), () => this.tryUpdateHeight() - )); + ); this._textReactionDisposer = reaction( () => this.extensionDoc, @@ -332,6 +379,83 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe }, { fireImmediately: true }); } + pushToGoogleDoc = async () => { + this.pullFromGoogleDoc(async (exportState: GoogleApiClientUtils.Docs.ReadResult, dataDoc: Doc) => { + let modes = GoogleApiClientUtils.Docs.WriteMode; + let mode = modes.Replace; + let reference: Opt<GoogleApiClientUtils.Docs.Reference> = Cast(this.dataDoc[GoogleRef], "string"); + if (!reference) { + mode = modes.Insert; + reference = { + title: StrCast(this.dataDoc.title), + handler: id => this.dataDoc[GoogleRef] = id + }; + } + let redo = async () => { + let data = Cast(this.dataDoc.data, RichTextField); + if (this._editorView && reference && data) { + let content = data[ToPlainText](); + let response = await GoogleApiClientUtils.Docs.write({ reference, content, mode }); + let pushSuccess = response !== undefined && !("errors" in response); + dataDoc.unchanged = pushSuccess; + DocumentDecorations.Instance.startPushOutcome(pushSuccess); + } + }; + let undo = () => { + let content = exportState.body; + if (reference && content) { + GoogleApiClientUtils.Docs.write({ reference, content, mode }); + } + }; + UndoManager.AddEvent({ undo, redo }); + redo(); + }); + } + + pullFromGoogleDoc = async (handler: PullHandler) => { + let dataDoc = this.dataDoc; + let documentId = StrCast(dataDoc[GoogleRef]); + let exportState: GoogleApiClientUtils.Docs.ReadResult = {}; + if (documentId) { + exportState = await GoogleApiClientUtils.Docs.read({ documentId }); + } + UndoManager.RunInBatch(() => handler(exportState, dataDoc), Pulls); + } + + updateState = (exportState: GoogleApiClientUtils.Docs.ReadResult, dataDoc: Doc) => { + let pullSuccess = false; + if (exportState !== undefined && exportState.body !== undefined && exportState.title !== undefined) { + let data = Cast(dataDoc.data, RichTextField); + if (data) { + pullSuccess = true; + this.isGoogleDocsUpdate = true; + dataDoc.data = new RichTextField(data[FromPlainText](exportState.body)); + dataDoc.title = exportState.title; + dataDoc.unchanged = true; + } + } else { + delete dataDoc[GoogleRef]; + } + DocumentDecorations.Instance.startPullOutcome(pullSuccess); + this.tryUpdateHeight(); + } + + checkState = (exportState: GoogleApiClientUtils.Docs.ReadResult, dataDoc: Doc) => { + if (exportState !== undefined && exportState.body !== undefined && exportState.title !== undefined) { + let data = Cast(dataDoc.data, RichTextField); + if (data) { + let storedPlainText = data[ToPlainText]() + "\n"; + let receivedPlainText = exportState.body; + let storedTitle = dataDoc.title; + let receivedTitle = exportState.title; + let unchanged = storedPlainText === receivedPlainText && storedTitle === receivedTitle; + dataDoc.unchanged = unchanged; + DocumentDecorations.Instance.setPullState(unchanged); + } + } + } + + clipboardTextSerializer = (slice: Slice): string => { let text = "", separated = true; const from = 0, to = slice.content.size; @@ -450,8 +574,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe if (this.props.selectOnLoad) { if (!this.props.isOverlay) this.props.select(false); else this._editorView!.focus(); - this.tryUpdateHeight(); } + this.tryUpdateHeight(); } componentWillUnmount() { @@ -459,6 +583,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe this._reactionDisposer && this._reactionDisposer(); this._proxyReactionDisposer && this._proxyReactionDisposer(); this._textReactionDisposer && this._textReactionDisposer(); + this.pushReactionDisposer && this.pushReactionDisposer(); + this.pullReactionDisposer && this.pullReactionDisposer(); this._heightReactionDisposer && this._heightReactionDisposer(); this._searchReactionDisposer && this._searchReactionDisposer(); } @@ -612,9 +738,10 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe @action tryUpdateHeight() { - if (this.props.Document.autoHeight && this.props.isOverlay) { + if (this.props.Document.autoHeight && this._ref.current!.scrollHeight !== 0) { + console.log("DT = " + this.props.Document.title + " " + this._ref.current!.clientHeight + " " + this._ref.current!.scrollHeight + " " + this._ref.current!.textContent); let xf = this._ref.current!.getBoundingClientRect(); - let scrBounds = this.props.ScreenToLocalTransform().transformBounds(0, 0, xf.width, xf.height); + let scrBounds = this.props.ScreenToLocalTransform().transformBounds(0, 0, xf.width, this._ref.current!.textContent === "" ? 35 : this._ref.current!.scrollHeight); let nh = this.props.Document.isTemplate ? 0 : NumCast(this.dataDoc.nativeHeight, 0); let dh = NumCast(this.props.Document.height, 0); let sh = scrBounds.height; @@ -641,6 +768,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe // }); // ContextMenu.Instance.addItem({ description: "Text Funcs...", subitems: subitems, icon: "text-height" }); } + + render() { let self = this; let style = this.props.isOverlay ? "scroll" : "hidden"; diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 708e00576..6fc94a140 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -89,10 +89,7 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD drop = (e: Event, de: DragManager.DropEvent) => { if (de.data instanceof DragManager.DocumentDragData) { de.data.droppedDocuments.forEach(action((drop: Doc) => { - if (de.mods === "CtrlKey") { - Doc.ApplyTemplateTo(drop, this.props.Document, this.props.DataDoc); - e.stopPropagation(); - } else if (de.mods === "AltKey" && /*this.dataDoc !== this.props.Document &&*/ drop.data instanceof ImageField) { + if (de.mods === "AltKey" && /*this.dataDoc !== this.props.Document &&*/ drop.data instanceof ImageField) { Doc.GetProto(this.dataDoc)[this.props.fieldKey] = new ImageField(drop.data.url); e.stopPropagation(); } else if (de.mods === "MetaKey") { @@ -321,7 +318,7 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD let rotation = NumCast(this.dataDoc.rotation) % 180; let realsize = rotation === 90 || rotation === 270 ? { height: size.width, width: size.height } : size; let aspect = realsize.height / realsize.width; - if (Math.abs(NumCast(layoutdoc.height) - realsize.height) > 1 || Math.abs(NumCast(layoutdoc.width) - realsize.width) > 1) { + if (layoutdoc.nativeHeight !== 0 && layoutdoc.nativeWidth !== 0 && (Math.abs(NumCast(layoutdoc.height) - realsize.height) > 1 || Math.abs(NumCast(layoutdoc.width) - realsize.width) > 1)) { setTimeout(action(() => { layoutdoc.height = layoutdoc[WidthSym]() * aspect; layoutdoc.nativeHeight = realsize.height; diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx index 534a42efc..8001b24a7 100644 --- a/src/client/views/nodes/KeyValuePair.tsx +++ b/src/client/views/nodes/KeyValuePair.tsx @@ -68,6 +68,7 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> { PanelWidth: returnZero, PanelHeight: returnZero, addDocTab: returnZero, + pinToPres: returnZero, ContentScaling: returnOne }; let contents = <FieldView {...props} />; diff --git a/src/client/views/nodes/LinkMenu.tsx b/src/client/views/nodes/LinkMenu.tsx index fe7d88457..16e318f7a 100644 --- a/src/client/views/nodes/LinkMenu.tsx +++ b/src/client/views/nodes/LinkMenu.tsx @@ -12,9 +12,6 @@ import { library } from "@fortawesome/fontawesome-svg-core"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; library.add(faTrash); -import { Cast, FieldValue, StrCast } from "../../../new_fields/Types"; -import { Id } from "../../../new_fields/FieldSymbols"; -import { DocumentType } from "../../documents/Documents"; interface Props { docView: DocumentView; diff --git a/src/client/views/nodes/LinkMenuItem.tsx b/src/client/views/nodes/LinkMenuItem.tsx index 1e7ff07c8..9349dbbac 100644 --- a/src/client/views/nodes/LinkMenuItem.tsx +++ b/src/client/views/nodes/LinkMenuItem.tsx @@ -94,7 +94,7 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { let dockingFunc = (document: Doc) => { this.props.addDocTab(document, undefined, "inTab"); SelectionManager.DeselectAll(); }; if (this.props.destinationDoc === self.props.linkDoc.anchor2 && targetContext) { - DocumentManager.Instance.jumpToDocument(jumpToDoc, shouldZoom, false, async document => dockingFunc(document), undefined, targetContext!); + DocumentManager.Instance.jumpToDocument(jumpToDoc, shouldZoom, false, async document => dockingFunc(document), undefined, targetContext); } else if (this.props.destinationDoc === self.props.linkDoc.anchor1 && sourceContext) { DocumentManager.Instance.jumpToDocument(jumpToDoc, shouldZoom, false, document => dockingFunc(sourceContext!)); diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index 6450cb826..18f82ff47 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -198,7 +198,7 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen <PDFViewer pdf={this._pdf} url={pdfUrl.url.pathname} active={this.props.active} scrollTo={this.scrollTo} loaded={this.loaded} panY={NumCast(this.props.Document.panY)} Document={this.props.Document} DataDoc={this.props.DataDoc} addDocTab={this.props.addDocTab} setPanY={this.setPanY} - addDocument={this.props.addDocument} + pinToPres={this.props.pinToPres} addDocument={this.props.addDocument} fieldKey={this.props.fieldKey} fieldExtensionDoc={this.fieldExtensionDoc} /> {this.settingsPanel()} </div>); diff --git a/src/client/views/presentationview/PresentationView.tsx b/src/client/views/nodes/PresBox.tsx index bea70f00b..112d39c32 100644 --- a/src/client/views/presentationview/PresentationView.tsx +++ b/src/client/views/nodes/PresBox.tsx @@ -1,23 +1,22 @@ -import { observer } from "mobx-react"; import React = require("react"); -import { observable, action, runInAction, reaction, autorun } from "mobx"; -import "./PresentationView.scss"; -import { DocumentManager } from "../../util/DocumentManager"; -import { Utils } from "../../../Utils"; -import { Doc, DocListCast, DocListCastAsync, WidthSym } from "../../../new_fields/Doc"; -import { listSpec } from "../../../new_fields/Schema"; -import { Cast, NumCast, FieldValue, PromiseValue, StrCast, BoolCast } from "../../../new_fields/Types"; -import { Id } from "../../../new_fields/FieldSymbols"; -import { List } from "../../../new_fields/List"; -import PresentationElement, { buttonIndex } from "./PresentationElement"; import { library } from '@fortawesome/fontawesome-svg-core'; +import { faArrowLeft, faArrowRight, faEdit, faMinus, faPlay, faPlus, faStop, faTimes } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faArrowRight, faArrowLeft, faPlay, faStop, faPlus, faTimes, faMinus, faEdit, faEye } from '@fortawesome/free-solid-svg-icons'; -import { Docs } from "../../documents/Documents"; -import { undoBatch, UndoManager } from "../../util/UndoManager"; -import PresentationViewList from "./PresentationList"; -import PresModeMenu from "./PresentationModeMenu"; -import { CollectionDockingView } from "../collections/CollectionDockingView"; +import { action, computed, observable, runInAction } from "mobx"; +import { observer } from "mobx-react"; +import { Doc, DocListCast, DocListCastAsync } from "../../../new_fields/Doc"; +import { Id } from "../../../new_fields/FieldSymbols"; +import { List } from "../../../new_fields/List"; +import { listSpec } from "../../../new_fields/Schema"; +import { BoolCast, Cast, FieldValue, NumCast, StrCast } from "../../../new_fields/Types"; +import { Utils } from "../../../Utils"; +import { DocumentManager } from "../../util/DocumentManager"; +import { undoBatch } from "../../util/UndoManager"; +import PresentationElement, { buttonIndex } from "../presentationview/PresentationElement"; +import PresentationViewList from "../presentationview/PresentationList"; +import "../presentationview/PresentationView.scss"; +import { FieldView, FieldViewProps } from './FieldView'; +import { ContextMenu } from "../ContextMenu"; library.add(faArrowLeft); library.add(faArrowRight); @@ -27,19 +26,24 @@ library.add(faPlus); library.add(faTimes); library.add(faMinus); library.add(faEdit); -library.add(faEye); export interface PresViewProps { Documents: List<Doc>; } -const expandedWidth = 400; -const presMinWidth = 300; +const expandedWidth = 450; @observer -export class PresentationView extends React.Component<PresViewProps> { - public static Instance: PresentationView; +export class PresBox extends React.Component<FieldViewProps> { //FieldViewProps? + + + public static LayoutString(fieldKey?: string) { return FieldView.LayoutString(PresBox, fieldKey); } + + public static Instance: PresBox; + + //Keeping track of the doc for the current presentation -- bcz: keeping a list of current presentations shouldn't be needed. Let users create them, store them, as they see fit. + @computed get curPresentation() { return this.props.Document; } //Mapping from presentation ids to a list of doc that represent a group @observable groupMappings: Map<String, Doc[]> = new Map(); @@ -52,9 +56,6 @@ export class PresentationView extends React.Component<PresViewProps> { //back-up so that presentation stays the way it's when refreshed @observable presGroupBackUp: Doc = new Doc(); @observable presButtonBackUp: Doc = new Doc(); - - //Keeping track of the doc for the current presentation - @observable curPresentation: Doc = new Doc(); //Mapping of guids to presentations. @observable presentationsMapping: Map<String, Doc> = new Map(); //Mapping of presentations to guid, so that select option values can be given. @@ -66,17 +67,18 @@ export class PresentationView extends React.Component<PresViewProps> { //Variable that holds reference to title input, so that new presentations get titles assigned. @observable titleInputElement: HTMLInputElement | undefined; @observable PresTitleChangeOpen: boolean = false; - @observable presMode: boolean = false; - @observable opacity = 1; @observable persistOpacity = true; @observable labelOpacity = 0; + @observable presMode = false; + + @observable public static CurrentPresentation: PresBox; //initilize class variables - constructor(props: PresViewProps) { + constructor(props: FieldViewProps) { super(props); - PresentationView.Instance = this; + runInAction(() => PresBox.CurrentPresentation = this); } @action @@ -88,33 +90,10 @@ export class PresentationView extends React.Component<PresViewProps> { } } - //The first lifecycle function that gets called to set up the current presentation. - async componentWillMount() { - - this.props.Documents.forEach(async (doc, index: number) => { - - //For each presentation received from mainContainer, a mapping is created. - let curDoc: Doc = await doc; - let newGuid = Utils.GenerateGuid(); - this.presentationsKeyMapping.set(curDoc, newGuid); - this.presentationsMapping.set(newGuid, curDoc); - - //The Presentation at first index gets set as default start presentation - if (index === 0) { - runInAction(() => this.currentSelectedPresValue = newGuid); - runInAction(() => this.curPresentation = curDoc); - } - }); - } - - //Second lifecycle function that gets called when component mounts. It makes sure to + //Second lifecycle function that gets called when component mounts. It makes sure toS //get the back-up information from previous session for the current presentation. async componentDidMount() { - let docAtZero = await this.props.Documents[0]; - runInAction(() => this.curPresentation = docAtZero); - this.setPresentationBackUps(); - } @@ -207,7 +186,6 @@ export class PresentationView extends React.Component<PresViewProps> { //observable means render is re-called every time variable is changed @observable collapsed: boolean = false; - closePresentation = action(() => this.curPresentation.width = 0); next = async () => { const current = NumCast(this.curPresentation.selectedDoc); //asking to get document at current index @@ -368,16 +346,11 @@ export class PresentationView extends React.Component<PresViewProps> { //checking if curDoc has navigation open let curDocButtons = this.presElementsMappings.get(curDoc)!.selected; if (curDocButtons[buttonIndex.Navigate]) { - this.jumpToTabOrRight(curDocButtons, curDoc); + DocumentManager.Instance.jumpToDocument(curDoc, false); } else if (curDocButtons[buttonIndex.Show]) { let curScale = DocumentManager.Instance.getScaleOfDocView(this.childrenDocs[fromDoc]); - if (curDocButtons[buttonIndex.OpenRight]) { - //awaiting jump so that new scale can be found, since jumping is async - await DocumentManager.Instance.jumpToDocument(curDoc, true); - } else { - await DocumentManager.Instance.jumpToDocument(curDoc, true, undefined, doc => CollectionDockingView.Instance.AddTab(undefined, doc, undefined)); - } - + //awaiting jump so that new scale can be found, since jumping is async + await DocumentManager.Instance.jumpToDocument(curDoc, true); let newScale = DocumentManager.Instance.getScaleOfDocView(curDoc); curDoc.viewScale = newScale; @@ -390,15 +363,9 @@ export class PresentationView extends React.Component<PresViewProps> { return; } let curScale = DocumentManager.Instance.getScaleOfDocView(this.childrenDocs[fromDoc]); - let curDocButtons = this.presElementsMappings.get(docToJump)!.selected; - - if (curDocButtons[buttonIndex.OpenRight]) { - //awaiting jump so that new scale can be found, since jumping is async - await DocumentManager.Instance.jumpToDocument(docToJump, willZoom); - } else { - await DocumentManager.Instance.jumpToDocument(docToJump, willZoom, undefined, doc => CollectionDockingView.Instance.AddTab(undefined, doc, undefined)); - } + //awaiting jump so that new scale can be found, since jumping is async + await DocumentManager.Instance.jumpToDocument(docToJump, willZoom); let newScale = DocumentManager.Instance.getScaleOfDocView(curDoc); curDoc.viewScale = newScale; //saving the scale that user was on @@ -409,18 +376,6 @@ export class PresentationView extends React.Component<PresViewProps> { } /** - * This function checks if right option is clicked on a presentation element, if not it does open it as a tab - * with help of CollectionDockingView. - */ - jumpToTabOrRight = (curDocButtons: boolean[], curDoc: Doc) => { - if (curDocButtons[buttonIndex.OpenRight]) { - DocumentManager.Instance.jumpToDocument(curDoc, false); - } else { - DocumentManager.Instance.jumpToDocument(curDoc, false, undefined, doc => CollectionDockingView.Instance.AddTab(undefined, doc, undefined)); - } - } - - /** * Async function that supposedly return the doc that is located at given index. */ getDocAtIndex = async (index: number) => { @@ -464,6 +419,22 @@ export class PresentationView extends React.Component<PresViewProps> { } } + //removing it from the backUp of selected Buttons + // let castedList = Cast(this.presButtonBackUp.selectedButtonDocs, listSpec(Doc)); + // if (castedList) { + // castedList.forEach(async (doc, indexOfDoc) => { + // let curDoc = await doc; + // let curDocId = StrCast(curDoc.docId); + // if (curDocId === removedDoc[Id]) { + // if (castedList) { + // castedList.splice(indexOfDoc, 1); + // return; + // } + // } + // }); + + // } + //removing it from the backUp of selected Buttons let castedList = Cast(this.presButtonBackUp.selectedButtonDocs, listSpec(Doc)); if (castedList) { @@ -501,16 +472,13 @@ export class PresentationView extends React.Component<PresViewProps> { } } - /** - * An alternative remove method that removes a doc from presentation by its actual - * reference. - */ public removeDocByRef = (doc: Doc) => { let indexOfDoc = this.childrenDocs.indexOf(doc); const value = FieldValue(Cast(this.curPresentation.data, listSpec(Doc))); if (value) { value.splice(indexOfDoc, 1)[0]; } + //this.RemoveDoc(indexOfDoc, true); if (indexOfDoc !== - 1) { return true; } @@ -561,23 +529,6 @@ export class PresentationView extends React.Component<PresViewProps> { runInAction(() => this.groupMappings = new Map()); } - /** - * Adds a document to the presentation view - **/ - @undoBatch - @action - public PinDoc(doc: Doc) { - //add this new doc to props.Document - const data = Cast(this.curPresentation.data, listSpec(Doc)); - if (data) { - data.push(doc); - } else { - this.curPresentation.data = new List([doc]); - } - - this.toggle(true); - } - //Function that sets the store of the children docs. @action setChildrenDocs = (docList: Doc[]) => { @@ -596,40 +547,18 @@ export class PresentationView extends React.Component<PresViewProps> { //The function that starts or resets presentaton functionally, depending on status flag. @action - startOrResetPres = async () => { + startOrResetPres = () => { if (this.presStatus) { this.resetPresentation(); } else { this.presStatus = true; - let startIndex = await this.findStartDocument(); - this.startPresentation(startIndex); + this.startPresentation(0); const current = NumCast(this.curPresentation.selectedDoc); - this.gotoDocument(startIndex, current); + this.gotoDocument(0, current); } this.curPresentation.presStatus = this.presStatus; } - /** - * This method is called to find the start document of presentation. So - * that when user presses on play, the correct presentation element will be - * selected. - */ - findStartDocument = async () => { - let docAtZero = await this.getDocAtIndex(0); - if (docAtZero === undefined) { - return 0; - } - let docAtZeroPresId = StrCast(docAtZero.presentId); - - if (this.groupMappings.has(docAtZeroPresId)) { - let group = this.groupMappings.get(docAtZeroPresId)!; - let lastDoc = group[group.length - 1]; - return this.childrenDocs.indexOf(lastDoc); - } else { - return 0; - } - } - //The function that resets the presentation by removing every action done by it. It also //stops the presentaton. @action @@ -675,71 +604,15 @@ export class PresentationView extends React.Component<PresViewProps> { } - /** - * The function that is called to add a new presentation to the presentationView. - * It sets up te mappings and local copies of it. Resets the groupings and presentation. - * Makes the new presentation current selected, and retrieve the back-Ups if present. - */ - @action - addNewPresentation = (presTitle: string) => { - //creating a new presentation doc - let newPresentationDoc = Docs.Create.TreeDocument([], { title: presTitle }); - this.props.Documents.push(newPresentationDoc); - - //setting that new doc as current - this.curPresentation = newPresentationDoc; - - //storing the doc in local copies for easier access - let newGuid = Utils.GenerateGuid(); - this.presentationsMapping.set(newGuid, newPresentationDoc); - this.presentationsKeyMapping.set(newPresentationDoc, newGuid); - - //resetting the previous presentation's actions so that new presentation can be loaded. - this.resetGroupIds(); - this.resetPresentation(); - this.presElementsMappings = new Map(); - this.currentSelectedPresValue = newGuid; - this.setPresentationBackUps(); - - } - - /** - * The function that is called to change the current selected presentation. - * Changes the presentation, also resetting groupings and presentation in process. - * Plus retrieving the backUps for the newly selected presentation. - */ - @action - getSelectedPresentation = (e: React.ChangeEvent<HTMLSelectElement>) => { - //get the guid of the selected presentation - let selectedGuid = e.target.value; - //set that as current presentation - this.curPresentation = this.presentationsMapping.get(selectedGuid)!; - - //reset current Presentations local things so that new one can be loaded - this.resetGroupIds(); - this.resetPresentation(); - this.presElementsMappings = new Map(); - this.currentSelectedPresValue = selectedGuid; - this.setPresentationBackUps(); - - - } /** * The function that is called to render either select for presentations, or title inputting. */ renderSelectOrPresSelection = () => { - let presentationList = DocListCast(this.props.Documents); if (this.PresTitleInputOpen || this.PresTitleChangeOpen) { return <input ref={(e) => this.titleInputElement = e!} type="text" className="presentationView-title" placeholder="Enter Name!" onKeyDown={this.submitPresentationTitle} />; } else { - return <select value={this.currentSelectedPresValue} id="pres_selector" className="presentationView-title" onChange={this.getSelectedPresentation}> - {presentationList.map((doc: Doc, index: number) => { - let mappedGuid = this.presentationsKeyMapping.get(doc); - let docGuid: string = mappedGuid ? mappedGuid.toString() : ""; - return <option key={docGuid} value={docGuid}>{StrCast(doc.title)}</option>; - })} - </select>; + return (null); } } @@ -752,87 +625,12 @@ export class PresentationView extends React.Component<PresViewProps> { if (e.keyCode === 13) { let presTitle = this.titleInputElement!.value; this.titleInputElement!.value = ""; - if (this.PresTitleInputOpen) { - if (presTitle === "") { - presTitle = "Presentation"; - } - this.PresTitleInputOpen = false; - this.addNewPresentation(presTitle); - } else if (this.PresTitleChangeOpen) { + if (this.PresTitleChangeOpen) { this.PresTitleChangeOpen = false; this.changePresentationTitle(presTitle); } } } - - /** - * The function that is called to remove a presentation from all its copies, and the main Container's - * list. Sets up the next presentation as current. - */ - @action - removePresentation = async () => { - if (this.presentationsMapping.size !== 1) { - let presentationList = Cast(this.props.Documents, listSpec(Doc)); - let batch = UndoManager.StartBatch("presRemoval"); - - //getting the presentation that will be removed - let removedDoc = this.presentationsMapping.get(this.currentSelectedPresValue!); - //that presentation is removed - presentationList!.splice(presentationList!.indexOf(removedDoc!), 1); - - //its mappings are removed from local copies - this.presentationsKeyMapping.delete(removedDoc!); - this.presentationsMapping.delete(this.currentSelectedPresValue!); - - //the next presentation is set as current - let remainingPresentations = this.presentationsMapping.values(); - let nextDoc = remainingPresentations.next().value; - this.curPresentation = nextDoc; - - - //Storing these for being able to undo changes - let curGuid = this.currentSelectedPresValue!; - let curPresStatus = this.presStatus; - - //resetting the groups and presentation actions so that next presentation gets loaded - this.resetGroupIds(); - this.resetPresentation(); - this.currentSelectedPresValue = this.presentationsKeyMapping.get(nextDoc)!.toString(); - this.setPresentationBackUps(); - - //Storing for undo - let currentGroups = this.groupMappings; - let curPresElemMapping = this.presElementsMappings; - - //Event to undo actions that are not related to doc directly, aka. local things - UndoManager.AddEvent({ - undo: action(() => { - this.curPresentation = removedDoc!; - this.presentationsMapping.set(curGuid, removedDoc!); - this.presentationsKeyMapping.set(removedDoc!, curGuid); - this.currentSelectedPresValue = curGuid; - - this.presStatus = curPresStatus; - this.groupMappings = currentGroups; - this.presElementsMappings = curPresElemMapping; - this.setPresentationBackUps(); - - }), - redo: action(() => { - this.curPresentation = nextDoc; - this.presStatus = false; - this.presentationsKeyMapping.delete(removedDoc!); - this.presentationsMapping.delete(curGuid); - this.currentSelectedPresValue = this.presentationsKeyMapping.get(nextDoc)!.toString(); - this.setPresentationBackUps(); - - }), - }); - - batch.end(); - } - } - /** * The function that is called to change title of presentation to what user entered. */ @@ -844,151 +642,60 @@ export class PresentationView extends React.Component<PresViewProps> { this.curPresentation.title = newTitle; } - /** - * On pointer down element that is catched on resizer of te - * presentation view. Sets up the event listeners to change the size with - * mouse move. - */ - _downsize = 0; - onPointerDown = (e: React.PointerEvent) => { - this._downsize = e.clientX; - document.removeEventListener("pointermove", this.onPointerMove); - document.removeEventListener("pointerup", this.onPointerUp); - document.addEventListener("pointermove", this.onPointerMove); - document.addEventListener("pointerup", this.onPointerUp); - e.stopPropagation(); - e.preventDefault(); - } - /** - * Changes the size of the presentation view, with mouse move. - * Minimum size is set to 300, so that every button is visible. - */ - @action - onPointerMove = (e: PointerEvent) => { - - this.curPresentation.width = Math.max(window.innerWidth - e.clientX, presMinWidth); - } - - /** - * The method that is called on pointer up event. It checks if the button is just - * clicked so that presentation view will be closed. The way it's done is to check - * for minimal pixel change like 4, and accept it as it's just a click on top of the dragger. - */ - @action - onPointerUp = (e: PointerEvent) => { - if (Math.abs(e.clientX - this._downsize) < 4) { - let presWidth = NumCast(this.curPresentation.width); - if (presWidth - presMinWidth !== 0) { - this.curPresentation.width = 0; - } - if (presWidth === 0) { - this.curPresentation.width = presMinWidth; - } - } - document.removeEventListener("pointermove", this.onPointerMove); - document.removeEventListener("pointerup", this.onPointerUp); - } - - /** - * This function is a setter that opens up the - * presentation mode, by setting it's render flag - * to true. It also closes the presentation view. - */ - @action - openPresMode = () => { - if (!this.presMode) { - this.curPresentation.width = 0; - this.presMode = true; - } - } - - /** - * This function closes the presentation mode by setting its - * render flag to false. It also opens up the presentation view. - * By setting it to it's minimum size. - */ - @action - closePresMode = () => { - if (this.presMode) { - this.presMode = false; - this.curPresentation.width = presMinWidth; - } - + addPressElem = (keyDoc: Doc, elem: PresentationElement) => { + this.presElementsMappings.set(keyDoc, elem); } - /** - * Function that is called to render the presentation mode, depending on its flag. - */ - renderPresMode = () => { - if (this.presMode) { - return <PresModeMenu next={this.next} back={this.back} startOrResetPres={this.startOrResetPres} presStatus={this.presStatus} closePresMode={this.closePresMode} />; - } else { - return (null); - } + minimize = undoBatch(action(() => { + this.presMode = true; + this.props.addDocTab && this.props.addDocTab(this.props.Document, this.props.DataDoc, "close"); + })); + specificContextMenu = (e: React.MouseEvent): void => { + ContextMenu.Instance.addItem({ description: "Make Current Presentation", event: action(() => Doc.UserDoc().curPresentation = this.props.Document), icon: "asterisk" }); } render() { - let width = NumCast(this.curPresentation.width); - + let width = "100%"; //NumCast(this.curPresentation.width) return ( - <div> - <div className="presentationView-cont" onPointerEnter={action(() => !this.persistOpacity && (this.opacity = 1))} onPointerLeave={action(() => !this.persistOpacity && (this.opacity = 0.4))} style={{ width: width, overflowY: "scroll", overflowX: "hidden", opacity: this.opacity, transition: "0.7s opacity ease" }}> - <div className="presentationView-heading"> - {this.renderSelectOrPresSelection()} - <button title="Close Presentation" className='presentation-icon' onClick={this.closePresentation}><FontAwesomeIcon icon={"times"} /></button> - <button title="Open Presentation Mode" className="presentation-icon" style={{ marginRight: 10 }} onClick={this.openPresMode}><FontAwesomeIcon icon={"eye"} /></button> - <button title="Add Presentation" className="presentation-icon" style={{ marginRight: 10 }} onClick={() => { - runInAction(() => { if (this.PresTitleChangeOpen) { this.PresTitleChangeOpen = false; } }); - runInAction(() => this.PresTitleInputOpen ? this.PresTitleInputOpen = false : this.PresTitleInputOpen = true); - }}><FontAwesomeIcon icon={"plus"} /></button> - <button title="Remove Presentation" className='presentation-icon' style={{ marginRight: 10 }} onClick={this.removePresentation}><FontAwesomeIcon icon={"minus"} /></button> - <button title="Change Presentation Title" className="presentation-icon" style={{ marginRight: 10 }} onClick={() => { - runInAction(() => { if (this.PresTitleInputOpen) { this.PresTitleInputOpen = false; } }); - runInAction(() => this.PresTitleChangeOpen ? this.PresTitleChangeOpen = false : this.PresTitleChangeOpen = true); - }}><FontAwesomeIcon icon={"edit"} /></button> - </div> - <div className="presentation-buttons"> - <button title="Back" className="presentation-button" onClick={this.back}><FontAwesomeIcon icon={"arrow-left"} /></button> - {this.renderPlayPauseButton()} - <button title="Next" className="presentation-button" onClick={this.next}><FontAwesomeIcon icon={"arrow-right"} /></button> - </div> - - <PresentationViewList - mainDocument={this.curPresentation} - deleteDocument={this.RemoveDoc} - gotoDocument={this.gotoDocument} - groupMappings={this.groupMappings} - PresElementsMappings={this.presElementsMappings} - setChildrenDocs={this.setChildrenDocs} - presStatus={this.presStatus} - presButtonBackUp={this.presButtonBackUp} - presGroupBackUp={this.presGroupBackUp} - removeDocByRef={this.removeDocByRef} - clearElemMap={() => this.presElementsMappings.clear()} - /> - <input - type="checkbox" - onChange={action((e: React.ChangeEvent<HTMLInputElement>) => { - this.persistOpacity = e.target.checked; - this.opacity = this.persistOpacity ? 1 : 0.4; - })} - checked={this.persistOpacity} - style={{ position: "absolute", bottom: 5, left: 5 }} - onPointerEnter={action(() => this.labelOpacity = 1)} - onPointerLeave={action(() => this.labelOpacity = 0)} - /> - <p style={{ position: "absolute", bottom: 1, left: 22, opacity: this.labelOpacity, transition: "0.7s opacity ease" }}>opacity {this.persistOpacity ? "persistent" : "on focus"}</p> - </div> - <div className="mainView-libraryHandle" - style={{ cursor: "ew-resize", right: `${width - 10}px`, backgroundColor: "white", opacity: this.opacity, transition: "0.7s opacity ease" }} - onPointerDown={this.onPointerDown}> - <span title="library View Dragger" style={{ width: "100%", height: "100%", position: "absolute" }} /> + <div className="presentationView-cont" onPointerEnter={action(() => !this.persistOpacity && (this.opacity = 1))} onContextMenu={this.specificContextMenu} + onPointerLeave={action(() => !this.persistOpacity && (this.opacity = 0.4))} + style={{ width: width, opacity: this.opacity, }}> + <div className="presentation-buttons"> + <button title="Back" className="presentation-button" onClick={this.back}><FontAwesomeIcon icon={"arrow-left"} /></button> + {this.renderPlayPauseButton()} + <button title="Next" className="presentation-button" onClick={this.next}><FontAwesomeIcon icon={"arrow-right"} /></button> + <button title="Minimize" className="presentation-button" onClick={this.minimize}><FontAwesomeIcon icon={"eye"} /></button> </div> - {this.renderPresMode()} - + <input + type="checkbox" + onChange={action((e: React.ChangeEvent<HTMLInputElement>) => { + this.persistOpacity = e.target.checked; + this.opacity = this.persistOpacity ? 1 : 0.4; + })} + checked={this.persistOpacity} + style={{ position: "absolute", bottom: 5, left: 5 }} + onPointerEnter={action(() => this.labelOpacity = 1)} + onPointerLeave={action(() => this.labelOpacity = 0)} + /> + <p style={{ position: "absolute", bottom: 1, left: 22, opacity: this.labelOpacity, transition: "0.7s opacity ease" }}>opacity {this.persistOpacity ? "persistent" : "on focus"}</p> + <PresentationViewList + mainDocument={this.curPresentation} + deleteDocument={this.RemoveDoc} + gotoDocument={this.gotoDocument} + groupMappings={this.groupMappings} + PresElementsMappings={this.presElementsMappings} + setChildrenDocs={this.setChildrenDocs} + presStatus={this.presStatus} + presButtonBackUp={this.presButtonBackUp} + presGroupBackUp={this.presGroupBackUp} + removeDocByRef={this.removeDocByRef} + clearElemMap={() => this.presElementsMappings.clear()} + /> </div> ); } -} + + +}
\ No newline at end of file diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx index 7ba7b6d14..6f77a0a5b 100644 --- a/src/client/views/pdf/Annotation.tsx +++ b/src/client/views/pdf/Annotation.tsx @@ -6,10 +6,10 @@ import { Id } from "../../../new_fields/FieldSymbols"; import { List } from "../../../new_fields/List"; import { Cast, FieldValue, NumCast, StrCast } from "../../../new_fields/Types"; import { DocumentManager } from "../../util/DocumentManager"; -import { PresentationView } from "../presentationview/PresentationView"; import PDFMenu from "./PDFMenu"; import "./Annotation.scss"; import { scale } from "./PDFViewer"; +import { PresBox } from "../nodes/PresBox"; interface IAnnotationProps { anno: Doc; @@ -18,6 +18,7 @@ interface IAnnotationProps { fieldExtensionDoc: Doc; scrollTo?: (n: number) => void; addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void; + pinToPres: (document: Doc) => void; } export default class Annotation extends React.Component<IAnnotationProps> { @@ -37,6 +38,7 @@ interface IRegionAnnotationProps { fieldExtensionDoc: Doc; scrollTo?: (n: number) => void; addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void; + pinToPres: (document: Doc) => void; document: Doc; } @@ -81,7 +83,7 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> { pinToPres = () => { let group = FieldValue(Cast(this.props.document.group, Doc)); - group && PresentationView.Instance.PinDoc(group); + group && this.props.pinToPres(group); } @action diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index 08674720d..258e218f0 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -36,6 +36,7 @@ interface IViewerProps { active: () => boolean; setPanY?: (n: number) => void; addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void; + pinToPres: (document: Doc) => void; addDocument?: (doc: Doc, allowDuplicates?: boolean) => boolean; } diff --git a/src/client/views/pdf/Page.tsx b/src/client/views/pdf/Page.tsx index 7ca9d2d7d..0de1777e6 100644 --- a/src/client/views/pdf/Page.tsx +++ b/src/client/views/pdf/Page.tsx @@ -19,8 +19,8 @@ interface IPageProps { numPages: number; page: number; pageLoaded: (page: Pdfjs.PDFPageViewport) => void; - fieldExtensionDoc: Doc, - Document: Doc, + fieldExtensionDoc: Doc; + Document: Doc; renderAnnotations: (annotations: Doc[], removeOld: boolean) => void; sendAnnotations: (annotations: HTMLDivElement[], page: number) => void; createAnnotation: (div: HTMLDivElement, page: number) => void; @@ -112,7 +112,7 @@ export default class Page extends React.Component<IPageProps> { if (!BoolCast(annotationDoc.linkedToDoc)) { let annotations = await DocListCastAsync(annotationDoc.annotations); annotations && annotations.forEach(anno => anno.target = targetDoc); - DocUtils.MakeLink(annotationDoc, targetDoc, dragData.targetContext, `Annotation from ${StrCast(this.props.Document.title)}`) + DocUtils.MakeLink(annotationDoc, targetDoc, dragData.targetContext, `Annotation from ${StrCast(this.props.Document.title)}`); } } }, @@ -151,6 +151,9 @@ export default class Page extends React.Component<IPageProps> { PDFMenu.Instance.fadeOut(true); if (e.target && (e.target as any).parentElement === this._textLayer.current) { e.stopPropagation(); + if (!e.ctrlKey) { + this.props.sendAnnotations([], -1); + } } else { // set marquee x and y positions to the spatially transformed position @@ -161,14 +164,12 @@ export default class Page extends React.Component<IPageProps> { } this._marqueeing = true; this._marquee.current && (this._marquee.current.style.opacity = "0.2"); + this.props.sendAnnotations([], -1); } document.removeEventListener("pointermove", this.onSelectStart); document.addEventListener("pointermove", this.onSelectStart); document.removeEventListener("pointerup", this.onSelectEnd); document.addEventListener("pointerup", this.onSelectEnd); - if (!e.ctrlKey) { - this.props.sendAnnotations([], -1); - } } } diff --git a/src/client/views/presentationview/PresentationElement.tsx b/src/client/views/presentationview/PresentationElement.tsx index d98b66324..912970a50 100644 --- a/src/client/views/presentationview/PresentationElement.tsx +++ b/src/client/views/presentationview/PresentationElement.tsx @@ -15,7 +15,7 @@ import { SelectionManager } from "../../util/SelectionManager"; import { ContextMenu } from "../ContextMenu"; import { Transform } from "../../util/Transform"; import { DocumentView } from "../nodes/DocumentView"; -import { DocumentType } from "../../documents/Documents"; +import { DocumentType } from "../../documents/DocumentTypes"; import React = require("react"); @@ -839,6 +839,7 @@ export default class PresentationElement extends React.Component<PresentationEle removeDocument={returnFalse} ScreenToLocalTransform={Transform.Identity} addDocTab={returnFalse} + pinToPres={returnFalse} renderDepth={1} PanelWidth={() => 350} PanelHeight={() => 90} diff --git a/src/client/views/presentationview/PresentationList.tsx b/src/client/views/presentationview/PresentationList.tsx index e853c4070..288ade042 100644 --- a/src/client/views/presentationview/PresentationList.tsx +++ b/src/client/views/presentationview/PresentationList.tsx @@ -6,10 +6,7 @@ import { Utils } from "../../../Utils"; import { Doc, DocListCast, DocListCastAsync } from "../../../new_fields/Doc"; import { NumCast, StrCast } from "../../../new_fields/Types"; import { Id } from "../../../new_fields/FieldSymbols"; -import PresentationElement, { buttonIndex } from "./PresentationElement"; -import "../../../new_fields/Doc"; - - +import PresentationElement from "./PresentationElement"; interface PresListProps { diff --git a/src/client/views/presentationview/PresentationView.scss b/src/client/views/presentationview/PresentationView.scss index 65b09c833..b0968132b 100644 --- a/src/client/views/presentationview/PresentationView.scss +++ b/src/client/views/presentationview/PresentationView.scss @@ -7,7 +7,9 @@ top: 0; bottom: 0; letter-spacing: 2px; - + overflow: hidden; + transition: 0.7s opacity ease; + pointer-events: all; } .presentationView-item { @@ -93,7 +95,7 @@ .presentation-button { margin-right: 2.5%; margin-left: 2.5%; - width: 25%; + width: 20%; } .presentation-buttons { diff --git a/src/client/views/search/FilterBox.tsx b/src/client/views/search/FilterBox.tsx index 3e8582d61..c13d1d276 100644 --- a/src/client/views/search/FilterBox.tsx +++ b/src/client/views/search/FilterBox.tsx @@ -6,7 +6,7 @@ import { faTimes, faCheckCircle, faObjectGroup } from '@fortawesome/free-solid-s import { library } from '@fortawesome/fontawesome-svg-core'; import { Doc } from '../../../new_fields/Doc'; import { Id } from '../../../new_fields/FieldSymbols'; -import { DocumentType } from '../../documents/Documents'; +import { DocumentType } from "../../documents/DocumentTypes"; import { Cast, StrCast } from '../../../new_fields/Types'; import * as _ from "lodash"; import { IconBar } from './IconBar'; diff --git a/src/client/views/search/IconBar.tsx b/src/client/views/search/IconBar.tsx index 4712b0abc..c9924222f 100644 --- a/src/client/views/search/IconBar.tsx +++ b/src/client/views/search/IconBar.tsx @@ -4,7 +4,6 @@ import { observable, action } from 'mobx'; // import "./SearchBox.scss"; import "./IconBar.scss"; import "./IconButton.scss"; -import { DocumentType } from '../../documents/Documents'; import { faSearch, faFilePdf, faFilm, faImage, faObjectGroup, faStickyNote, faMusic, faLink, faChartBar, faGlobeAsia, faBan, faTimesCircle, faCheckCircle } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { library } from '@fortawesome/fontawesome-svg-core'; diff --git a/src/client/views/search/IconButton.tsx b/src/client/views/search/IconButton.tsx index 5d23f6eeb..d2cfe7fad 100644 --- a/src/client/views/search/IconButton.tsx +++ b/src/client/views/search/IconButton.tsx @@ -6,7 +6,7 @@ import "./IconButton.scss"; import { faSearch, faFilePdf, faFilm, faImage, faObjectGroup, faStickyNote, faMusic, faLink, faChartBar, faGlobeAsia, faBan, faVideo, faCaretDown } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { library, icon } from '@fortawesome/fontawesome-svg-core'; -import { DocumentType } from '../../documents/Documents'; +import { DocumentType } from "../../documents/DocumentTypes"; import '../globalCssVariables.scss'; import * as _ from "lodash"; import { IconBar } from './IconBar'; diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx index 4dc409e77..2ad69daca 100644 --- a/src/client/views/search/SearchBox.tsx +++ b/src/client/views/search/SearchBox.tsx @@ -245,7 +245,6 @@ export class SearchBox extends React.Component { @action.bound closeSearch = () => { - console.log("closing search") FilterBox.Instance.closeFilter(); this.closeResults(); this._searchbarOpen = false; diff --git a/src/client/views/search/SearchItem.tsx b/src/client/views/search/SearchItem.tsx index 8201aa374..41fc49c2e 100644 --- a/src/client/views/search/SearchItem.tsx +++ b/src/client/views/search/SearchItem.tsx @@ -11,7 +11,7 @@ import { RichTextField } from "../../../new_fields/RichTextField"; import { Cast, NumCast, StrCast } from "../../../new_fields/Types"; import { emptyFunction, returnEmptyString, returnFalse, returnOne, Utils } from "../../../Utils"; import { DocServer } from "../../DocServer"; -import { DocumentType } from "../../documents/Documents"; +import { DocumentType } from "../../documents/DocumentTypes"; import { DocumentManager } from "../../util/DocumentManager"; import { DragManager, SetupDrag } from "../../util/DragManager"; import { LinkManager } from "../../util/LinkManager"; @@ -203,6 +203,7 @@ export class SearchItem extends React.Component<SearchItemProps> { removeDocument={returnFalse} ScreenToLocalTransform={Transform.Identity} addDocTab={returnFalse} + pinToPres={returnFalse} renderDepth={1} PanelWidth={returnXDimension} PanelHeight={returnYDimension} diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts index b47811ac6..bd08edec8 100644 --- a/src/new_fields/Doc.ts +++ b/src/new_fields/Doc.ts @@ -1,19 +1,18 @@ -import { observable, action, runInAction, ObservableMap } from "mobx"; -import { serializable, primitive, map, alias, list, PropSchema, custom } from "serializr"; -import { autoObject, SerializationHelper, Deserializable, afterDocDeserialize } from "../client/util/SerializationHelper"; +import { observable, ObservableMap, runInAction } from "mobx"; +import { alias, map, serializable } from "serializr"; import { DocServer } from "../client/DocServer"; -import { setter, getter, getField, updateFunction, deleteProperty, makeEditable, makeReadOnly } from "./util"; -import { Cast, ToConstructor, PromiseValue, FieldValue, NumCast, BoolCast, StrCast } from "./Types"; -import { listSpec } from "./Schema"; -import { ObjectField } from "./ObjectField"; -import { RefField, FieldId } from "./RefField"; -import { ToScriptString, SelfProxy, Parent, OnUpdate, Self, HandleUpdate, Update, Id, Copy } from "./FieldSymbols"; -import { scriptingGlobal, CompileScript, Scripting } from "../client/util/Scripting"; +import { DocumentType } from "../client/documents/DocumentTypes"; +import { CompileScript, Scripting, scriptingGlobal } from "../client/util/Scripting"; +import { afterDocDeserialize, autoObject, Deserializable, SerializationHelper } from "../client/util/SerializationHelper"; +import { Copy, HandleUpdate, Id, OnUpdate, Parent, Self, SelfProxy, ToScriptString, Update } from "./FieldSymbols"; import { List } from "./List"; -import { DocumentType } from "../client/documents/Documents"; -import { ComputedField, ScriptField } from "./ScriptField"; +import { ObjectField } from "./ObjectField"; import { PrefetchProxy, ProxyField } from "./Proxy"; -import { CurrentUserUtils } from "../server/authentication/models/current_user_utils"; +import { FieldId, RefField } from "./RefField"; +import { listSpec } from "./Schema"; +import { ComputedField } from "./ScriptField"; +import { BoolCast, Cast, FieldValue, NumCast, PromiseValue, StrCast, ToConstructor } from "./Types"; +import { deleteProperty, getField, getter, makeEditable, makeReadOnly, setter, updateFunction } from "./util"; export namespace Field { export function toKeyValueString(doc: Doc, key: string): string { @@ -72,6 +71,7 @@ export const HeightSym = Symbol("Height"); export const UpdatingFromServer = Symbol("UpdatingFromServer"); const CachedUpdates = Symbol("Cached updates"); + function fetchProto(doc: Doc) { const proto = doc.proto; if (proto instanceof Promise) { @@ -151,10 +151,10 @@ export class Doc extends RefField { } private [CachedUpdates]: { [key: string]: () => void | Promise<any> } = {}; - + public static CurrentUserEmail: string = ""; public async [HandleUpdate](diff: any) { const set = diff.$set; - const sameAuthor = this.author === CurrentUserUtils.email; + const sameAuthor = this.author === Doc.CurrentUserEmail; if (set) { for (const key in set) { if (!key.startsWith("fields.")) { @@ -327,14 +327,12 @@ export namespace Doc { return Array.from(results); } - export function AddDocToList(target: Doc, key: string, doc: Doc, relativeTo?: Doc, before?: boolean, first?: boolean, allowDuplicates?: boolean) { + export function AddDocToList(target: Doc, key: string, doc: Doc, relativeTo?: Doc, before?: boolean, first?: boolean, allowDuplicates?: boolean, reversed?: boolean) { if (target[key] === undefined) { - // console.log("target key undefined"); Doc.GetProto(target)[key] = new List<Doc>(); } let list = Cast(target[key], listSpec(Doc)); if (list) { - // console.log("has list"); if (allowDuplicates !== true) { let pind = list.reduce((l, d, i) => d instanceof Doc && Doc.AreProtosEqual(d, doc) ? i : l, -1); if (pind !== -1) { @@ -342,15 +340,18 @@ export namespace Doc { } } if (first) { - // console.log("is first"); list.splice(0, 0, doc); } else { - // console.log("not first"); let ind = relativeTo ? list.indexOf(relativeTo) : -1; - if (ind === -1) list.push(doc); - else list.splice(before ? ind : ind + 1, 0, doc); - // console.log("index", ind); + if (ind === -1) { + if (reversed) list.splice(0, 0, doc); + else list.push(doc); + } + else { + if (reversed) list.splice(before ? (list.length - ind) + 1 : list.length - ind, 0, doc); + else list.splice(before ? ind : ind + 1, 0, doc); + } } } return true; @@ -445,20 +446,23 @@ export namespace Doc { if (expandedTemplateLayout instanceof Doc) { return expandedTemplateLayout; } + if (expandedTemplateLayout instanceof Promise) { + return undefined; + } let expandedLayoutFieldKey = "Layout[" + templateLayoutDoc[Id] + "]"; expandedTemplateLayout = dataDoc[expandedLayoutFieldKey]; if (expandedTemplateLayout instanceof Doc) { return expandedTemplateLayout; } if (expandedTemplateLayout === undefined) { - setTimeout(() => - dataDoc[expandedLayoutFieldKey] = Doc.MakeDelegate(templateLayoutDoc, undefined, "[" + templateLayoutDoc.title + "]"), 0); + setTimeout(() => dataDoc[expandedLayoutFieldKey] === undefined && + (dataDoc[expandedLayoutFieldKey] = Doc.MakeDelegate(templateLayoutDoc, undefined, "[" + templateLayoutDoc.title + "]")), 0); } - return templateLayoutDoc; // use the templateLayout when it's not a template or the expandedTemplate is pending. + return undefined; // use the templateLayout when it's not a template or the expandedTemplate is pending. } export function GetLayoutDataDocPair(doc: Doc, dataDoc: Doc | undefined, fieldKey: string, childDocLayout: Doc) { - let layoutDoc = childDocLayout; + let layoutDoc: Doc | undefined = childDocLayout; let resolvedDataDoc = !doc.isTemplate && dataDoc !== doc && dataDoc ? Doc.GetDataDoc(dataDoc) : undefined; if (resolvedDataDoc && Doc.WillExpandTemplateLayout(childDocLayout, resolvedDataDoc)) { Doc.UpdateDocumentExtensionForField(resolvedDataDoc, fieldKey); @@ -608,6 +612,14 @@ export namespace Doc { @observable BrushedDoc: ObservableMap<Doc, boolean> = new ObservableMap(); } const brushManager = new DocBrush(); + + export class DocData { + @observable _user_doc: Doc = undefined!; + @observable BrushedDoc: ObservableMap<Doc, boolean> = new ObservableMap(); + } + const manager = new DocData(); + export function UserDoc(): Doc { return manager._user_doc; } + export function SetUserDoc(doc: Doc) { manager._user_doc = doc; } export function IsBrushed(doc: Doc) { return brushManager.BrushedDoc.has(doc) || brushManager.BrushedDoc.has(Doc.GetDataDoc(doc)); } diff --git a/src/new_fields/ListSpec.ts b/src/new_fields/ListSpec.ts new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/src/new_fields/ListSpec.ts diff --git a/src/new_fields/PresField.ts b/src/new_fields/PresField.ts new file mode 100644 index 000000000..f236a04fd --- /dev/null +++ b/src/new_fields/PresField.ts @@ -0,0 +1,6 @@ +//insert code here +import { ObjectField } from "./ObjectField"; + +export abstract class PresField extends ObjectField { + +}
\ No newline at end of file diff --git a/src/new_fields/RichTextField.ts b/src/new_fields/RichTextField.ts index 89799b2af..cae5623e6 100644 --- a/src/new_fields/RichTextField.ts +++ b/src/new_fields/RichTextField.ts @@ -4,6 +4,11 @@ import { Deserializable } from "../client/util/SerializationHelper"; import { Copy, ToScriptString } from "./FieldSymbols"; import { scriptingGlobal } from "../client/util/Scripting"; +export const ToPlainText = Symbol("PlainText"); +export const FromPlainText = Symbol("PlainText"); +const delimiter = "\n"; +const joiner = ""; + @scriptingGlobal @Deserializable("RichTextField") export class RichTextField extends ObjectField { @@ -22,4 +27,43 @@ export class RichTextField extends ObjectField { [ToScriptString]() { return `new RichTextField("${this.Data}")`; } + + [ToPlainText]() { + // Because we're working with plain text, just concatenate all paragraphs + let content = JSON.parse(this.Data).doc.content; + let paragraphs = content.filter((item: any) => item.type === "paragraph"); + + // Functions to flatten ProseMirror paragraph objects (and their components) to plain text + // While this function already exists in state.doc.textBeteen(), it doesn't account for newlines + let blockText = (block: any) => block.text; + let concatenateParagraph = (p: any) => (p.content ? p.content.map(blockText).join(joiner) : "") + delimiter; + + // Concatentate paragraphs and string the result together + let textParagraphs: string[] = paragraphs.map(concatenateParagraph); + let plainText = textParagraphs.join(joiner); + return plainText.substring(0, plainText.length - 1); + } + + [FromPlainText](plainText: string) { + // Remap the text, creating blocks split on newlines + let elements = plainText.split(delimiter); + + // Google Docs adds in an extra carriage return automatically, so this counteracts it + !elements[elements.length - 1].length && elements.pop(); + + // Preserve the current state, but re-write the content to be the blocks + let parsed = JSON.parse(this.Data); + parsed.doc.content = elements.map(text => { + let paragraph: any = { type: "paragraph" }; + text.length && (paragraph.content = [{ type: "text", marks: [], text }]); // An empty paragraph gets treated as a line break + return paragraph; + }); + + // If the new content is shorter than the previous content and selection is unchanged, may throw an out of bounds exception, so we reset it + parsed.selection = { type: "text", anchor: 1, head: 1 }; + + // Export the ProseMirror-compatible state object we've jsut built + return JSON.stringify(parsed); + } + }
\ No newline at end of file diff --git a/src/new_fields/util.ts b/src/new_fields/util.ts index c546e2aac..04194509c 100644 --- a/src/new_fields/util.ts +++ b/src/new_fields/util.ts @@ -7,7 +7,6 @@ import { ObjectField } from "./ObjectField"; import { action } from "mobx"; import { Parent, OnUpdate, Update, Id, SelfProxy, Self } from "./FieldSymbols"; import { DocServer } from "../client/DocServer"; -import { CurrentUserUtils } from "../server/authentication/models/current_user_utils"; function _readOnlySetter(): never { throw new Error("Documents can't be modified in read-only mode"); @@ -61,7 +60,7 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number } const writeMode = DocServer.getFieldWriteMode(prop as string); const fromServer = target[UpdatingFromServer]; - const sameAuthor = fromServer || (receiver.author === CurrentUserUtils.email); + const sameAuthor = fromServer || (receiver.author === Doc.CurrentUserEmail); const writeToDoc = sameAuthor || (writeMode !== DocServer.WriteMode.LiveReadonly); const writeToServer = sameAuthor || (writeMode === DocServer.WriteMode.Default); if (writeToDoc) { diff --git a/src/scraping/buxton/source/.Bill_Notes_NewO.docx.icloud b/src/scraping/buxton/source/.Bill_Notes_NewO.docx.icloud Binary files differnew file mode 100644 index 000000000..f71886d8c --- /dev/null +++ b/src/scraping/buxton/source/.Bill_Notes_NewO.docx.icloud diff --git a/src/scraping/buxton/source/.Bill_Notes_OLPC.docx.icloud b/src/scraping/buxton/source/.Bill_Notes_OLPC.docx.icloud Binary files differnew file mode 100644 index 000000000..30ddb3091 --- /dev/null +++ b/src/scraping/buxton/source/.Bill_Notes_OLPC.docx.icloud diff --git a/src/scraping/buxton/source/Bill_Notes_NewO.docx b/src/scraping/buxton/source/Bill_Notes_NewO.docx Binary files differdeleted file mode 100644 index a514926d2..000000000 --- a/src/scraping/buxton/source/Bill_Notes_NewO.docx +++ /dev/null diff --git a/src/scraping/buxton/source/Bill_Notes_OLPC.docx b/src/scraping/buxton/source/Bill_Notes_OLPC.docx Binary files differdeleted file mode 100644 index 7a636e2d6..000000000 --- a/src/scraping/buxton/source/Bill_Notes_OLPC.docx +++ /dev/null diff --git a/src/server/Message.ts b/src/server/Message.ts index aaee143e8..4ec390ade 100644 --- a/src/server/Message.ts +++ b/src/server/Message.ts @@ -1,4 +1,5 @@ import { Utils } from "../Utils"; +import { google, docs_v1 } from "googleapis"; export class Message<T> { private _name: string; diff --git a/src/server/RouteStore.ts b/src/server/RouteStore.ts index e30015e39..5d977006a 100644 --- a/src/server/RouteStore.ts +++ b/src/server/RouteStore.ts @@ -30,6 +30,7 @@ export enum RouteStore { reset = "/reset/:token", // APIS - cognitiveServices = "/cognitiveservices" + cognitiveServices = "/cognitiveservices", + googleDocs = "/googleDocs/" }
\ No newline at end of file diff --git a/src/server/apis/google/GoogleApiServerUtils.ts b/src/server/apis/google/GoogleApiServerUtils.ts new file mode 100644 index 000000000..817b2b696 --- /dev/null +++ b/src/server/apis/google/GoogleApiServerUtils.ts @@ -0,0 +1,109 @@ +import { google, docs_v1 } from "googleapis"; +import { createInterface } from "readline"; +import { readFile, writeFile } from "fs"; +import { OAuth2Client } from "google-auth-library"; + +/** + * Server side authentication for Google Api queries. + */ +export namespace GoogleApiServerUtils { + + // If modifying these scopes, delete token.json. + const prefix = 'https://www.googleapis.com/auth/'; + const SCOPES = [ + 'documents.readonly', + 'documents', + 'drive', + 'drive.file', + ]; + // The file token.json stores the user's access and refresh tokens, and is + // created automatically when the authorization flow completes for the first + // time. + export const parseBuffer = (data: Buffer) => JSON.parse(data.toString()); + + export namespace Docs { + + export interface CredentialPaths { + credentials: string; + token: string; + } + + export type Endpoint = docs_v1.Docs; + + export const GetEndpoint = async (paths: CredentialPaths) => { + return new Promise<Endpoint>((resolve, reject) => { + readFile(paths.credentials, (err, credentials) => { + if (err) { + reject(err); + return console.log('Error loading client secret file:', err); + } + return authorize(parseBuffer(credentials), paths.token).then(auth => { + resolve(google.docs({ version: "v1", auth })); + }); + }); + }); + }; + + } + + /** + * Create an OAuth2 client with the given credentials, and returns the promise resolving to the authenticated client + * @param {Object} credentials The authorization client credentials. + */ + export function authorize(credentials: any, token_path: string): Promise<OAuth2Client> { + const { client_secret, client_id, redirect_uris } = credentials.installed; + const oAuth2Client = new google.auth.OAuth2( + client_id, client_secret, redirect_uris[0]); + + return new Promise<OAuth2Client>((resolve, reject) => { + readFile(token_path, (err, token) => { + // Check if we have previously stored a token. + if (err) { + return getNewToken(oAuth2Client, token_path).then(resolve, reject); + } + oAuth2Client.setCredentials(parseBuffer(token)); + resolve(oAuth2Client); + }); + }); + } + + /** + * Get and store new token after prompting for user authorization, and then + * execute the given callback with the authorized OAuth2 client. + * @param {google.auth.OAuth2} oAuth2Client The OAuth2 client to get token for. + * @param {getEventsCallback} callback The callback for the authorized client. + */ + function getNewToken(oAuth2Client: OAuth2Client, token_path: string) { + return new Promise<OAuth2Client>((resolve, reject) => { + const authUrl = oAuth2Client.generateAuthUrl({ + access_type: 'offline', + scope: SCOPES.map(relative => prefix + relative), + }); + console.log('Authorize this app by visiting this url:', authUrl); + const rl = createInterface({ + input: process.stdin, + output: process.stdout, + }); + rl.question('Enter the code from that page here: ', (code) => { + rl.close(); + oAuth2Client.getToken(code, (err, token) => { + if (err || !token) { + reject(err); + return console.error('Error retrieving access token', err); + } + oAuth2Client.setCredentials(token); + // Store the token to disk for later program executions + writeFile(token_path, JSON.stringify(token), (err) => { + if (err) { + console.error(err); + reject(err); + } + console.log('Token stored to', token_path); + }); + resolve(oAuth2Client); + }); + }); + }); + } + +}
\ No newline at end of file diff --git a/src/server/youtubeApi/youtubeApiSample.d.ts b/src/server/apis/youtube/youtubeApiSample.d.ts index 427f54608..427f54608 100644 --- a/src/server/youtubeApi/youtubeApiSample.d.ts +++ b/src/server/apis/youtube/youtubeApiSample.d.ts diff --git a/src/server/youtubeApi/youtubeApiSample.js b/src/server/apis/youtube/youtubeApiSample.js index 50b3c7b38..50b3c7b38 100644 --- a/src/server/youtubeApi/youtubeApiSample.js +++ b/src/server/apis/youtube/youtubeApiSample.js diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts index f36f5b73d..508655605 100644 --- a/src/server/authentication/models/current_user_utils.ts +++ b/src/server/authentication/models/current_user_utils.ts @@ -10,20 +10,17 @@ import { CollectionView } from "../../../client/views/collections/CollectionView import { Doc } from "../../../new_fields/Doc"; import { List } from "../../../new_fields/List"; import { listSpec } from "../../../new_fields/Schema"; -import { Cast, FieldValue, StrCast } from "../../../new_fields/Types"; -import { RouteStore } from "../../RouteStore"; +import { Cast, StrCast } from "../../../new_fields/Types"; import { Utils } from "../../../Utils"; +import { RouteStore } from "../../RouteStore"; export class CurrentUserUtils { - private static curr_email: string; private static curr_id: string; - @observable private static user_document: Doc; //TODO tfs: these should be temporary... private static mainDocId: string | undefined; - public static get email() { return this.curr_email; } public static get id() { return this.curr_id; } - @computed public static get UserDocument() { return this.user_document; } + @computed public static get UserDocument() { return Doc.UserDoc(); } public static get MainDocId() { return this.mainDocId; } public static set MainDocId(id: string | undefined) { this.mainDocId = id; } @@ -32,7 +29,7 @@ export class CurrentUserUtils { doc.viewType = CollectionViewType.Tree; doc.dropAction = "alias"; doc.layout = CollectionView.LayoutString(); - doc.title = this.email; + doc.title = Doc.CurrentUserEmail this.updateUserDocument(doc); doc.data = new List<Doc>(); doc.gridGap = 5; @@ -58,6 +55,12 @@ export class CurrentUserUtils { recentlyClosed.boxShadow = "0 0"; doc.recentlyClosed = recentlyClosed; } + if (doc.curPresentation === undefined) { + const curPresentation = Docs.Create.PresDocument(new List<Doc>(), { title: "Presentation" }); + curPresentation.excludeFromLibrary = true; + curPresentation.boxShadow = "0 0"; + doc.curPresentation = curPresentation; + } if (doc.sidebar === undefined) { const sidebar = Docs.Create.StackingDocument([doc.workspaces as Doc, doc, doc.recentlyClosed as Doc], { title: "Sidebar" }); sidebar.excludeFromLibrary = true; @@ -85,15 +88,15 @@ export class CurrentUserUtils { public static async loadUserDocument({ id, email }: { id: string, email: string }) { this.curr_id = id; - this.curr_email = email; + Doc.CurrentUserEmail = email; await rp.get(Utils.prepend(RouteStore.getUserDocumentId)).then(id => { if (id) { return DocServer.GetRefField(id).then(async field => { if (field instanceof Doc) { await this.updateUserDocument(field); - runInAction(() => this.user_document = field); + runInAction(() => Doc.SetUserDoc(field)); } else { - runInAction(() => this.user_document = this.createUserDocument(id)); + runInAction(() => Doc.SetUserDoc(this.createUserDocument(id))); } }); } else { diff --git a/src/server/credentials/google_docs_credentials.json b/src/server/credentials/google_docs_credentials.json new file mode 100644 index 000000000..8d097d363 --- /dev/null +++ b/src/server/credentials/google_docs_credentials.json @@ -0,0 +1 @@ +{"installed":{"client_id":"343179513178-ud6tvmh275r2fq93u9eesrnc66t6akh9.apps.googleusercontent.com","project_id":"quickstart-1565056383187","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://oauth2.googleapis.com/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","client_secret":"w8KIFSc0MQpmUYHed4qEzn8b","redirect_uris":["urn:ietf:wg:oauth:2.0:oob","http://localhost"]}}
\ No newline at end of file diff --git a/src/server/credentials/google_docs_token.json b/src/server/credentials/google_docs_token.json new file mode 100644 index 000000000..07c02d56c --- /dev/null +++ b/src/server/credentials/google_docs_token.json @@ -0,0 +1 @@ +{"access_token":"ya29.GltjB4-x03xFpd2NY2555cxg1xlT_ajqRi78M9osOfdOF2jTIjlPkn_UZL8cUwVP0DPC8rH3vhhg8RpspFe8Vewx92shAO3RPos_uMH0CUqEiCiZlaaB5I3Jq3Mv","refresh_token":"1/teUKUqGKMLjVqs-eed0L8omI02pzSxMUYaxGc2QxBw0","scope":"https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents.readonly","token_type":"Bearer","expiry_date":1565654175862}
\ No newline at end of file diff --git a/src/server/index.ts b/src/server/index.ts index eae018f13..ef1829f30 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -14,7 +14,6 @@ import * as mobileDetect from 'mobile-detect'; import * as passport from 'passport'; import * as path from 'path'; import * as request from 'request'; -import * as rp from 'request-promise'; import * as io from 'socket.io'; import { Socket } from 'socket.io'; import * as webpack from 'webpack'; @@ -36,19 +35,19 @@ const port = 1050; // default port to listen const serverPort = 4321; import expressFlash = require('express-flash'); import flash = require('connect-flash'); -import c = require("crypto"); import { Search } from './Search'; -import { debug } from 'util'; import _ = require('lodash'); import * as Archiver from 'archiver'; -import * as AdmZip from 'adm-zip'; -import * as YoutubeApi from './youtubeApi/youtubeApiSample.js'; +import AdmZip from 'adm-zip'; +import * as YoutubeApi from "./apis/youtube/youtubeApiSample"; import { Response } from 'express-serve-static-core'; +import { GoogleApiServerUtils } from "./apis/google/GoogleApiServerUtils"; +import { GaxiosResponse } from 'gaxios'; +import { Opt } from '../new_fields/Doc'; +import { docs_v1 } from 'googleapis'; const MongoStore = require('connect-mongo')(session); const mongoose = require('mongoose'); const probe = require("probe-image-size"); -var SolrNode = require('solr-node'); -var shell = require('shelljs'); const download = (url: string, dest: fs.PathLike) => request.get(url).pipe(fs.createWriteStream(dest)); let youtubeApiKey: string; @@ -174,6 +173,13 @@ const read_text_file = (relativePath: string) => { }); }; +const write_text_file = (relativePath: string, contents: any) => { + let target = path.join(__dirname, relativePath); + return new Promise<void>((resolve, reject) => { + fs.writeFile(target, contents, (err) => err ? reject(err) : resolve()); + }); +}; + app.get("/version", (req, res) => { exec('"C:\\Program Files\\Git\\bin\\git.exe" rev-parse HEAD', (err, stdout, stderr) => { if (err) { @@ -790,6 +796,34 @@ function HandleYoutubeQuery([query, callback]: [YoutubeQueryInput, (result?: any } } +const credentials = path.join(__dirname, "./credentials/google_docs_credentials.json"); +const token = path.join(__dirname, "./credentials/google_docs_token.json"); + +type ApiResponse = Promise<GaxiosResponse>; +type ApiHandler = (endpoint: docs_v1.Resource$Documents, parameters: any) => ApiResponse; +type Action = "create" | "retrieve" | "update"; + +const EndpointHandlerMap = new Map<Action, ApiHandler>([ + ["create", (api, params) => api.create(params)], + ["retrieve", (api, params) => api.get(params)], + ["update", (api, params) => api.batchUpdate(params)], +]); + +app.post(RouteStore.googleDocs + ":action", (req, res) => { + GoogleApiServerUtils.Docs.GetEndpoint({ credentials, token }).then(endpoint => { + let handler = EndpointHandlerMap.get(req.params.action); + if (handler) { + let execute = handler(endpoint.documents, req.body).then( + response => res.send(response.data), + rejection => res.send(rejection) + ); + execute.catch(exception => res.send(exception)); + return; + } + res.send(undefined); + }); +}); + const suffixMap: { [type: string]: (string | [string, string | ((json: any) => any)]) } = { "number": "_n", "string": "_t", |