diff options
-rw-r--r-- | src/client/apis/google_docs/GoogleApiClientUtils.ts | 185 | ||||
-rw-r--r-- | src/client/views/MainView.tsx | 11 | ||||
-rw-r--r-- | src/client/views/nodes/FormattedTextBox.tsx | 24 | ||||
-rw-r--r-- | src/new_fields/RichTextField.ts | 13 | ||||
-rw-r--r-- | src/server/index.ts | 25 |
5 files changed, 162 insertions, 96 deletions
diff --git a/src/client/apis/google_docs/GoogleApiClientUtils.ts b/src/client/apis/google_docs/GoogleApiClientUtils.ts index 5e974b2e7..dda36f05a 100644 --- a/src/client/apis/google_docs/GoogleApiClientUtils.ts +++ b/src/client/apis/google_docs/GoogleApiClientUtils.ts @@ -1,7 +1,8 @@ import { docs_v1 } from "googleapis"; import { PostToServer } from "../../../Utils"; import { RouteStore } from "../../../server/RouteStore"; -import { Opt } from "../../../new_fields/Doc"; +import { Opt, Doc } from "../../../new_fields/Doc"; +import { isArray } from "util"; export namespace GoogleApiClientUtils { @@ -9,15 +10,12 @@ export namespace GoogleApiClientUtils { export enum Actions { Create = "create", - Retrieve = "retrieve" + Retrieve = "retrieve", + Update = "update" } export namespace Utils { - export const fromRgb = (red: number, green: number, blue: number) => { - return { color: { rgbColor: { red, green, blue } } }; - }; - export const extractText = (document: docs_v1.Schema$Document, removeNewlines = false) => { let fragments: string[] = []; if (document.body && document.body.content) { @@ -36,55 +34,36 @@ export namespace GoogleApiClientUtils { return removeNewlines ? text.ReplaceAll("\n", "") : text; }; - } - - export const ExampleDocumentSchema = { - title: "This is a Google Doc Created From Dash Web", - body: { - content: [ - { - endIndex: 1, - sectionBreak: { - sectionStyle: { - columnSeparatorStyle: "NONE", - contentDirection: "LEFT_TO_RIGHT" + export const EndOf = (schema: docs_v1.Schema$Document): Opt<number> => { + if (schema.body && schema.body.content) { + let paragraphs = schema.body.content.filter(el => el.paragraph); + if (paragraphs.length) { + let target = paragraphs[paragraphs.length - 1]; + if (target.paragraph && target.paragraph.elements) { + length = target.paragraph.elements.length; + if (length) { + let final = target.paragraph.elements[length - 1]; + return final.endIndex ? final.endIndex - 1 : undefined; } } - }, - { - paragraph: { - elements: [ - { - textRun: { - content: "And this is its bold, blue text!!!\n", - textStyle: { - bold: true, - backgroundColor: Utils.fromRgb(0, 0, 1) - } - } - } - ] - } - }, - { - paragraph: { - elements: [ - { - textRun: { - content: "And this is its bold, blue text!!!\n", - textStyle: { - bold: true, - backgroundColor: Utils.fromRgb(0, 0, 1) - } - } - } - ] - } - }, + } + } + }; - ] as docs_v1.Schema$StructuralElement[] - } - } as docs_v1.Schema$Document; + } + + export interface ReadOptions { + documentId: string; + removeNewlines?: boolean; + } + + export interface WriteOptions { + documentId?: string; + title?: string; + content: string | string[]; + index?: number; + store?: { receiver: Doc, key: string }; + } /** * After following the authentication routine, which connects this API call to the current signed in account @@ -96,45 +75,95 @@ export namespace GoogleApiClientUtils { * actual document body and its styling! * @returns the documentId of the newly generated document, or undefined if the creation process fails. */ - export const Create = async (schema?: docs_v1.Schema$Document): Promise<string | undefined> => { + const Create = async (title?: string): Promise<string | undefined> => { let path = RouteStore.googleDocs + Actions.Create; - let parameters = { requestBody: schema || ExampleDocumentSchema }; - let generatedId: string | undefined; + let parameters = { + requestBody: { + title: title || `Dash Export (${new Date().toDateString()})` + } + }; try { - generatedId = await PostToServer(path, parameters); - } catch (e) { - console.error(e); - generatedId = undefined; - } finally { - return generatedId; + let schema: docs_v1.Schema$Document = await PostToServer(path, parameters); + return schema.documentId; + } catch { + return undefined; } }; - export interface ReadOptions { - documentId: string; - removeNewlines?: boolean; - } + const Retrieve = async (documentId: string): Promise<docs_v1.Schema$Document | undefined> => { + let path = RouteStore.googleDocs + Actions.Retrieve; + let parameters = { + documentId + }; + try { + let schema: docs_v1.Schema$Document = await PostToServer(path, parameters); + return schema; + } catch { + return undefined; + } + }; + + const Update = async (documentId: string, requests: docs_v1.Schema$Request[]): Promise<docs_v1.Schema$BatchUpdateDocumentResponse | undefined> => { + let path = RouteStore.googleDocs + Actions.Update; + let parameters = { + documentId, + requestBody: { + requests + } + }; + try { + let replies: docs_v1.Schema$BatchUpdateDocumentResponse = await PostToServer(path, parameters); + console.log(replies); + return replies; + } catch { + return undefined; + } + }; - export const Read = async (options: ReadOptions): Promise<Opt<string>> => { + export const Read = async (options: ReadOptions): Promise<string | undefined> => { return Retrieve(options.documentId).then(schema => { return schema ? Utils.extractText(schema, options.removeNewlines) : undefined; }); }; - export const Retrieve = async (documentId: string): Promise<Opt<docs_v1.Schema$Document>> => { - let path = RouteStore.googleDocs + Actions.Retrieve; - let parameters = { documentId }; - let schema: Opt<docs_v1.Schema$Document>; - try { - schema = await PostToServer(path, parameters); - } catch (e) { - console.error(e); - schema = undefined; - } finally { - return schema; - } + export const ReadLines = async (options: ReadOptions) => { + return Retrieve(options.documentId).then(schema => { + if (!schema) { + return undefined; + } + let lines = Utils.extractText(schema).split("\n"); + return options.removeNewlines ? lines.filter(line => line.length) : lines; + }); }; + export const Write = async (options: WriteOptions): Promise<docs_v1.Schema$BatchUpdateDocumentResponse | undefined> => { + let target = options.documentId; + if (!target) { + if (!(target = await Create(options.title))) { + return undefined; + } + } + let index = options.index; + if (!index) { + let schema = await Retrieve(target); + if (!schema || !(index = Utils.EndOf(schema))) { + return undefined; + } + } + let text = options.content; + let request = { + insertText: { + text: isArray(text) ? text.join("\n") : text, + location: { index } + } + }; + return Update(target, [request]).then(res => { + if (res && options.store) { + options.store.receiver[options.store.key] = res.documentId; + } + return res; + }); + }; } diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 705eacc0c..7b15e9624 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -87,11 +87,6 @@ export class MainView extends React.Component { if (!("presentationView" in doc)) { doc.presentationView = new List<Doc>([Docs.Create.TreeDocument([], { title: "Presentation" })]); } - if (!("googleDocId" in doc)) { - GoogleApiClientUtils.Docs.Create().then(id => { - id && (doc.googleDocId = id); - }); - } CurrentUserUtils.UserDocument.activeWorkspace = doc; } } @@ -158,9 +153,9 @@ export class MainView extends React.Component { reaction(() => this.mainContainer, () => { let main = this.mainContainer, documentId; if (main && (documentId = StrCast(main.googleDocId))) { - GoogleApiClientUtils.Docs.Read({ documentId }).then(text => { - text && Utils.CopyText(text); - console.log(text); + let options = { documentId, removeNewlines: true }; + GoogleApiClientUtils.Docs.ReadLines(options).then(lines => { + console.log(lines); }); } }); diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index 44b5d2c21..8c2af7c9e 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -1,5 +1,5 @@ import { library } from '@fortawesome/fontawesome-svg-core'; -import { faEdit, faSmile, faTextHeight } from '@fortawesome/free-solid-svg-icons'; +import { faEdit, faSmile, faTextHeight, faUpload } from '@fortawesome/free-solid-svg-icons'; import { action, IReactionDisposer, observable, reaction, runInAction, computed, Lambda, trace } from "mobx"; import { observer } from "mobx-react"; import { baseKeymap } from "prosemirror-commands"; @@ -38,9 +38,10 @@ import { For } from 'babel-types'; import { DateField } from '../../../new_fields/DateField'; import { Utils } from '../../../Utils'; import { MainOverlayTextBox } from '../MainOverlayTextBox'; +import { GoogleApiClientUtils } from '../../apis/google_docs/GoogleApiClientUtils'; 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 // @@ -58,6 +59,8 @@ const richTextSchema = createSchema({ documentText: "string" }); +const googleDocKey = "googleDocId"; + type RichTextDocument = makeInterface<[typeof richTextSchema]>; const RichTextDocument = makeInterface(richTextSchema); @@ -661,7 +664,24 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe event: action(() => Doc.GetProto(this.props.Document).autoHeight = !BoolCast(this.props.Document.autoHeight)), icon: "expand-arrows-alt" }); ContextMenu.Instance.addItem({ description: "Text Funcs...", subitems: subitems, icon: "text-height" }); + if (!(googleDocKey in Doc.GetProto(this.props.Document))) { + ContextMenu.Instance.addItem({ description: "Export to Google Doc...", event: this.exportToGoogleDoc, icon: "upload" }); + } } + + exportToGoogleDoc = () => { + let dataDoc = Doc.GetProto(this.props.Document); + let data = Cast(dataDoc.data, RichTextField); + let content: string | undefined; + if (data && (content = data.plainText())) { + GoogleApiClientUtils.Docs.Write({ + title: StrCast(dataDoc.title), + store: { receiver: dataDoc, key: googleDocKey }, + content + }); + } + } + render() { let self = this; let style = this.props.isOverlay ? "scroll" : "hidden"; diff --git a/src/new_fields/RichTextField.ts b/src/new_fields/RichTextField.ts index 89799b2af..dc66813e0 100644 --- a/src/new_fields/RichTextField.ts +++ b/src/new_fields/RichTextField.ts @@ -9,6 +9,7 @@ import { scriptingGlobal } from "../client/util/Scripting"; export class RichTextField extends ObjectField { @serializable(true) readonly Data: string; + private Extractor = /,\"text\":\"([^\"\}]*)\"\}/g; constructor(data: string) { super(); @@ -22,4 +23,16 @@ export class RichTextField extends ObjectField { [ToScriptString]() { return `new RichTextField("${this.Data}")`; } + + plainText = () => { + let contents = ""; + let matches: RegExpExecArray | null; + let considering = this.Data; + while ((matches = this.Extractor.exec(considering)) !== null) { + contents += matches[1]; + considering = considering.substring(matches.index + matches[0].length); + this.Extractor.lastIndex = 0; + } + return contents.length ? contents : undefined; + } }
\ No newline at end of file diff --git a/src/server/index.ts b/src/server/index.ts index 4ccb61681..abaa29658 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -42,6 +42,7 @@ 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'; const MongoStore = require('connect-mongo')(session); const mongoose = require('mongoose'); const probe = require("probe-image-size"); @@ -170,6 +171,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,21 +798,22 @@ const credentials = path.join(__dirname, "./credentials/google_docs_credentials. const token = path.join(__dirname, "./credentials/google_docs_token.json"); app.post(RouteStore.googleDocs + ":action", (req, res) => { - let parameters = req.body; - GoogleApiServerUtils.Docs.GetEndpoint({ credentials, token }).then(endpoint => { - let results: Promise<any> | undefined; + let results: Promise<GaxiosResponse> | undefined; + let documents = endpoint.documents; + let parameters = req.body; switch (req.params.action) { case "create": - results = endpoint.documents.create(parameters).then(generated => generated.data.documentId); + results = documents.create(parameters); break; case "retrieve": - results = endpoint.documents.get(parameters).then(response => response.data); + results = documents.get(parameters); + break; + case "update": + results = documents.batchUpdate(parameters); break; - default: - results = undefined; } - results && results.then(final => res.send(final)); + !results ? res.send(undefined) : results.then(response => res.send(response.data)); }); }); |