diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/client/apis/google_docs/GoogleApiClientUtils.ts | 27 | ||||
-rw-r--r-- | src/client/views/DocumentDecorations.tsx | 32 | ||||
-rw-r--r-- | src/client/views/collections/CollectionSubView.tsx | 14 | ||||
-rw-r--r-- | src/client/views/nodes/FormattedTextBox.tsx | 74 |
4 files changed, 98 insertions, 49 deletions
diff --git a/src/client/apis/google_docs/GoogleApiClientUtils.ts b/src/client/apis/google_docs/GoogleApiClientUtils.ts index 9d78b632d..55b4a76f8 100644 --- a/src/client/apis/google_docs/GoogleApiClientUtils.ts +++ b/src/client/apis/google_docs/GoogleApiClientUtils.ts @@ -27,8 +27,8 @@ export namespace GoogleApiClientUtils { 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<string[]>; - export type ReadResult = Opt<string>; + 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 @@ -148,18 +148,27 @@ export namespace GoogleApiClientUtils { }; export const read = async (options: ReadOptions): Promise<ReadResult> => { - return retrieve(options).then(schema => { - return schema ? Utils.extractText(schema, options.removeNewlines) : undefined; + 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(schema => { - if (!schema) { - return undefined; + 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 }; } - const lines = Utils.extractText(schema).split("\n"); - return options.removeNewlines ? lines.filter(line => line.length) : lines; + return result; }); }; diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 963722fe3..80d4ecb9b 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -1,6 +1,5 @@ import { library } from '@fortawesome/fontawesome-svg-core'; -import { faLink, faTag } from '@fortawesome/free-solid-svg-icons'; -import * as fa from '@fortawesome/free-brands-svg-icons'; +import { faLink, faTag, faArrowAltCircleDown, faArrowAltCircleUp } 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"; @@ -37,7 +36,8 @@ export const Flyout = higflyout.default; library.add(faLink); library.add(faTag); -library.add(fa.faGoogleDrive as any); +library.add(faArrowAltCircleDown); +library.add(faArrowAltCircleUp); @observer export class DocumentDecorations extends React.Component<{}, { value: string }> { @@ -620,13 +620,28 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> ); } - considerGoogleDoc = () => { + considerGoogleDocsPush = () => { let thisDoc = SelectionManager.SelectedDocuments()[0].props.Document; - let canEmbed = thisDoc.data && thisDoc.data instanceof RichTextField; - if (!canEmbed) return (null); + let canPush = thisDoc.data && thisDoc.data instanceof RichTextField; + if (!canPush) return (null); + return ( + <div className={"linkButtonWrapper"}> + <div title="Push to Google Docs" className="linkButton-linker" onClick={() => thisDoc.pushToGoogleDocsTrigger = !thisDoc.pushToGoogleDocsTrigger}> + <FontAwesomeIcon className="documentdecorations-icon" icon="arrow-alt-circle-up" size="sm" /> + </div> + </div> + ); + } + + considerGoogleDocsPull = () => { + let thisDoc = SelectionManager.SelectedDocuments()[0].props.Document; + let canPull = thisDoc.data && thisDoc.data instanceof RichTextField; + if (!canPull) return (null); return ( <div className={"linkButtonWrapper"}> - <FontAwesomeIcon className="documentdecorations-icon" icon="image" size="sm" /> + <div title="Pull From Google Docs" className="linkButton-linker" onClick={() => thisDoc.pullFromGoogleDocsTrigger = !thisDoc.pullFromGoogleDocsTrigger}> + <FontAwesomeIcon className="documentdecorations-icon" icon="arrow-alt-circle-down" size="sm" /> + </div> </div> ); } @@ -781,7 +796,8 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> </div> {this.metadataMenu} {this.considerEmbed()} - {this.considerGoogleDoc()} + {this.considerGoogleDocsPush()} + {this.considerGoogleDocsPull()} {/* {this.considerTooltip()} */} </div> </div > diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 077f3f941..70c0632d1 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -16,7 +16,7 @@ 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, Blank } from "../nodes/FormattedTextBox"; import { CollectionPDFView } from "./CollectionPDFView"; import { CollectionVideoView } from "./CollectionVideoView"; import { CollectionView } from "./CollectionView"; @@ -206,7 +206,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: 600, height: 400, title: "Fetching title from Google Docs..." }); + let proto = newBox.proto!; + proto.autoHeight = true; + proto.googleDocId = matches[2]; + proto.data = "Fetching contents 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/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index bc057bb5f..406c2c4a6 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, faUpload } from '@fortawesome/free-solid-svg-icons'; -import { action, IReactionDisposer, observable, reaction, runInAction, computed, Lambda, trace } from "mobx"; +import { action, IReactionDisposer, observable, reaction, runInAction, computed, Lambda, trace, untracked } from "mobx"; import { observer } from "mobx-react"; import { baseKeymap } from "prosemirror-commands"; import { history } from "prosemirror-history"; @@ -47,6 +47,8 @@ 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; @@ -84,7 +86,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe private _proxyReactionDisposer: Opt<IReactionDisposer>; private dropDisposer?: DragManager.DragDropDisposer; public get CurrentDiv(): HTMLDivElement { return this._ref.current!; } - private isOpening = false; + private isGoogleDocsUpdate = false; @observable _entered = false; @observable public static InputBoxOverlay?: FormattedTextBox = undefined; @@ -290,28 +292,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } } - componentWillMount() { - this.pollExportedCounterpart(); - } - - pollExportedCounterpart = async () => { - let dataDoc = Doc.GetProto(this.props.Document); - let documentId = StrCast(dataDoc[googleDocId]); - if (documentId) { - let exportState = await GoogleApiClientUtils.Docs.read({ documentId }); - if (exportState) { - let data = Cast(dataDoc.data, RichTextField); - if (data) { - this.isOpening = true; - dataDoc.data = new RichTextField(data[FromGoogleDocText](exportState)); - } - } else { - delete dataDoc[googleDocId]; - } - this.tryUpdateHeight(); - } - } - componentDidMount() { const config = { schema, @@ -347,23 +327,32 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe 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; }, 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.isOpening) { - this.isOpening = false; + 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(); } } ); + reaction(() => this.props.Document.pullFromGoogleDocsTrigger, () => { + this.pullFromGoogleDoc(); + }); + + reaction(() => this.props.Document.pushToGoogleDocsTrigger, () => { + this.pushToGoogleDoc(); + }); + this._textReactionDisposer = reaction( () => this.extensionDoc, () => { @@ -391,6 +380,13 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe this.unhighlightSearchTerms(); } }, { fireImmediately: true }); + + ["pushToGoogleDocsTrigger", "pullFromGoogleDocsTrigger"].map(key => { + let doc = this.props.Document; + if (doc[key] === undefined) { + untracked(() => doc[key] = false); + } + }); } clipboardTextSerializer = (slice: Slice): string => { @@ -644,7 +640,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe this._undoTyping.end(); this._undoTyping = undefined; } - this.updateGoogleDoc(); } public _undoTyping?: UndoManager.Batch; onKeyPress = (e: React.KeyboardEvent) => { @@ -701,13 +696,13 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe if (!(googleDocId in Doc.GetProto(this.props.Document))) { ContextMenu.Instance.addItem({ description: "Export to Google Doc...", - event: this.updateGoogleDoc, + event: this.pushToGoogleDoc, icon: "upload" }); } } - updateGoogleDoc = () => { + pushToGoogleDoc = () => { let modes = GoogleApiClientUtils.Docs.WriteMode; let mode = modes.Replace; let reference: Opt<GoogleApiClientUtils.Docs.Reference> = Cast(this.dataDoc[googleDocId], "string"); @@ -725,6 +720,25 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } } + pullFromGoogleDoc = async () => { + let dataDoc = Doc.GetProto(this.props.Document); + let documentId = StrCast(dataDoc[googleDocId]); + if (documentId) { + let exportState = await GoogleApiClientUtils.Docs.read({ documentId }); + if (exportState && exportState.body && exportState.title) { + let data = Cast(dataDoc.data, RichTextField); + if (data) { + this.isGoogleDocsUpdate = true; + dataDoc.data = new RichTextField(data[FromGoogleDocText](exportState.body)); + dataDoc.title = exportState.title; + } + } else { + delete dataDoc[googleDocId]; + } + } + } + + render() { let self = this; let style = this.props.isOverlay ? "scroll" : "hidden"; |