diff options
-rw-r--r-- | src/client/apis/google_docs/GoogleApiClientUtils.ts | 204 | ||||
-rw-r--r-- | src/client/views/nodes/FormattedTextBox.tsx | 35 | ||||
-rw-r--r-- | src/new_fields/RichTextUtils.ts | 56 |
3 files changed, 146 insertions, 149 deletions
diff --git a/src/client/apis/google_docs/GoogleApiClientUtils.ts b/src/client/apis/google_docs/GoogleApiClientUtils.ts index 689009254..ae7c2f997 100644 --- a/src/client/apis/google_docs/GoogleApiClientUtils.ts +++ b/src/client/apis/google_docs/GoogleApiClientUtils.ts @@ -9,108 +9,104 @@ export const Pushes = "googleDocsPushCount"; export namespace GoogleApiClientUtils { - export enum Service { - Documents = "Documents", - Slides = "Slides" - } - export enum Actions { Create = "create", Retrieve = "retrieve", Update = "update" } - export enum WriteMode { - Insert, - Replace - } - - export type Identifier = string; - export type Reference = Identifier | CreateOptions; - export interface Content { - text: string | string[]; - links: docs_v1.Schema$Request[]; - } - export type IdHandler = (id: Identifier) => any; - export type CreationResult = Opt<Identifier>; - export type ReadLinesResult = Opt<{ title?: string, bodyLines?: string[] }>; - export type ReadResult = { title: string, body: string }; + export namespace Docs { - export interface CreateOptions { - service: Service; - title?: string; // if excluded, will use a default title annotated with the current date - } + export type RetrievalResult = Opt<docs_v1.Schema$Document>; + export type UpdateResult = Opt<docs_v1.Schema$BatchUpdateDocumentResponse>; - export interface RetrieveOptions { - service: Service; - identifier: Identifier; - } + export interface UpdateOptions { + documentId: DocumentId; + requests: docs_v1.Schema$Request[]; + } - export interface ReadOptions { - identifier: Identifier; - removeNewlines?: boolean; - } + export enum WriteMode { + Insert, + Replace + } - export interface WriteOptions { - mode: WriteMode; - content: Content; - reference: Reference; - index?: number; // if excluded, will compute the last index of the document and append the content there - } + export type DocumentId = string; + export type Reference = DocumentId | CreateOptions; + export interface Content { + text: string | string[]; + requests: docs_v1.Schema$Request[]; + } + export type IdHandler = (id: DocumentId) => any; + export type CreationResult = Opt<DocumentId>; + export type ReadLinesResult = Opt<{ title?: string, bodyLines?: string[] }>; + export type ReadResult = { title: string, body: string }; - /** - * 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}/${options.service}/${Actions.Create}`; - const parameters = { - requestBody: { - title: options.title || `Dash Export (${new Date().toDateString()})` - } - }; - try { - const schema: any = await PostToServer(path, parameters); - let key = ["document", "presentation"].find(prefix => `${prefix}Id` in schema) + "Id"; - return schema[key]; - } catch { - return undefined; + export interface CreateOptions { + title?: string; // if excluded, will use a default title annotated with the current date } - }; - export namespace Docs { + export interface RetrieveOptions { + documentId: DocumentId; + } - export type RetrievalResult = Opt<docs_v1.Schema$Document | slides_v1.Schema$Presentation>; - export type UpdateResult = Opt<docs_v1.Schema$BatchUpdateDocumentResponse>; + export interface ReadOptions { + documentId: DocumentId; + removeNewlines?: boolean; + } - export interface UpdateOptions { - documentId: Identifier; - requests: docs_v1.Schema$Request[]; + export interface WriteOptions { + mode: WriteMode; + content: Content; + reference: Reference; + index?: number; // if excluded, will compute the last index of the document and append the content there } + /** + * 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}/Documents/${Actions.Create}`; + const parameters = { + requestBody: { + title: options.title || `Dash Export (${new Date().toDateString()})` + } + }; + try { + const schema: docs_v1.Schema$Document = await PostToServer(path, parameters); + return schema.documentId; + } catch { + return undefined; + } + }; + export namespace Utils { export const extractText = (document: docs_v1.Schema$Document, removeNewlines = false): string => { - const fragments: string[] = []; + let runs = extractTextRuns(document); + const text = runs.map(run => run.content).join(""); + return removeNewlines ? text.ReplaceAll("\n", "") : text; + }; + + export const extractTextRuns = (document: docs_v1.Schema$Document, filterEmpty = true) => { + const fragments: docs_v1.Schema$TextRun[] = []; 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); + fragments.push(inner.textRun); } } } } } - const text = fragments.join(""); - return removeNewlines ? text.ReplaceAll("\n", "") : text; + return filterEmpty ? fragments.filter(run => run.content) : fragments; }; export const endOf = (schema: docs_v1.Schema$Document): number | undefined => { @@ -133,27 +129,19 @@ export namespace GoogleApiClientUtils { } - const KeyMapping = new Map<Service, string>([ - [Service.Documents, "documentId"], - [Service.Slides, "presentationId"] - ]); - export const retrieve = async (options: RetrieveOptions): Promise<RetrievalResult> => { - const path = `${RouteStore.googleDocs}/${options.service}/${Actions.Retrieve}`; + const path = `${RouteStore.googleDocs}/Documents/${Actions.Retrieve}`; try { - let parameters: any = {}, key: string | undefined; - if ((key = KeyMapping.get(options.service))) { - parameters[key] = options.identifier; - const schema: RetrievalResult = await PostToServer(path, parameters); - return schema; - } + const parameters = { documentId: options.documentId }; + const schema: RetrievalResult = await PostToServer(path, parameters); + return schema; } catch { return undefined; } }; export const update = async (options: UpdateOptions): Promise<UpdateResult> => { - const path = `${RouteStore.googleDocs}/${Service.Documents}/${Actions.Update}`; + const path = `${RouteStore.googleDocs}/Documents/${Actions.Update}`; const parameters = { documentId: options.documentId, requestBody: { @@ -169,7 +157,7 @@ export namespace GoogleApiClientUtils { }; export const read = async (options: ReadOptions): Promise<Opt<ReadResult>> => { - return retrieve({ ...options, service: Service.Documents }).then(document => { + return retrieve({ documentId: options.documentId }).then(document => { if (document) { let title = document.title!; let body = Utils.extractText(document, options.removeNewlines); @@ -179,7 +167,7 @@ export namespace GoogleApiClientUtils { }; export const readLines = async (options: ReadOptions): Promise<Opt<ReadLinesResult>> => { - return retrieve({ ...options, service: Service.Documents }).then(document => { + return retrieve({ documentId: options.documentId }).then(document => { if (document) { let title = document.title; let bodyLines = Utils.extractText(document).split("\n"); @@ -203,14 +191,14 @@ export namespace GoogleApiClientUtils { export const write = async (options: WriteOptions): Promise<UpdateResult> => { const requests: docs_v1.Schema$Request[] = []; - const identifier = await Utils.initialize(options.reference); - if (!identifier) { + 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({ identifier, service: Service.Documents }); + let schema = await retrieve({ documentId }); if (!schema || !(index = Utils.endOf(schema))) { return undefined; } @@ -236,8 +224,8 @@ export namespace GoogleApiClientUtils { if (!requests.length) { return undefined; } - requests.push(...options.content.links); - let replies: any = await update({ documentId: identifier, requests }); + requests.push(...options.content.requests); + let replies: any = await update({ documentId: documentId, requests }); if ("errors" in replies) { console.log("Write operation failed:"); console.log(replies.errors.map((error: any) => error.message)); @@ -247,36 +235,4 @@ export namespace GoogleApiClientUtils { } - export namespace Slides { - - export namespace Utils { - - export const extractTextBoxes = (slides: slides_v1.Schema$Page[]) => { - slides.map(slide => { - let elements = slide.pageElements; - if (elements) { - let textboxes: slides_v1.Schema$TextContent[] = []; - for (let element of elements) { - if (element && element.shape && element.shape.shapeType === "TEXT_BOX" && element.shape.text) { - textboxes.push(element.shape.text); - } - } - textboxes.map(text => { - if (text.textElements) { - text.textElements.map(element => { - - }); - } - if (text.lists) { - - } - }); - } - }); - }; - - } - - } - }
\ No newline at end of file diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index 02bee2f82..eefac2285 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -60,14 +60,18 @@ export const GoogleRef = "googleDocId"; type RichTextDocument = makeInterface<[typeof richTextSchema]>; const RichTextDocument = makeInterface(richTextSchema); -type PullHandler = (exportState: Opt<GoogleApiClientUtils.ReadResult>, dataDoc: Doc) => void; +type PullHandler = (exportState: Opt<GoogleApiClientUtils.Docs.ReadResult>, dataDoc: Doc) => void; @observer export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTextBoxProps), RichTextDocument>(RichTextDocument) { public static LayoutString(fieldStr: string = "data") { return FieldView.LayoutString(FormattedTextBox, fieldStr); } + public static blankState = () => { + return EditorState.create(FormattedTextBox.Instance._configuration); + } public static Instance: FormattedTextBox; + private _configuration: any; private _ref: React.RefObject<HTMLDivElement>; private _proseRef?: HTMLDivElement; private _editorView: Opt<EditorView>; @@ -325,7 +329,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } componentDidMount() { - const config = { + this._configuration = { schema, inpRules, //these currently don't do anything, but could eventually be helpful plugins: this.props.isOverlay ? [ @@ -367,7 +371,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe incomingValue => { if (this._editorView && !this._applyingChange) { let updatedState = JSON.parse(incomingValue); - this._editorView.updateState(EditorState.fromJSON(config, updatedState)); + this._editorView.updateState(EditorState.fromJSON(this._configuration, updatedState)); this.tryUpdateHeight(); } } @@ -409,7 +413,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe this.dataDoc.lastModified = undefined; } }, { fireImmediately: true }); - this.setupEditor(config, this.dataDoc, this.props.fieldKey); + this.setupEditor(this._configuration, this.dataDoc, this.props.fieldKey); this._searchReactionDisposer = reaction(() => { return StrCast(this.props.Document.search_string); @@ -430,17 +434,17 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } pushToGoogleDoc = async () => { - this.pullFromGoogleDoc(async (exportState: Opt<GoogleApiClientUtils.ReadResult>, dataDoc: Doc) => { - let modes = GoogleApiClientUtils.WriteMode; + this.pullFromGoogleDoc(async (exportState: Opt<GoogleApiClientUtils.Docs.ReadResult>, dataDoc: Doc) => { + let modes = GoogleApiClientUtils.Docs.WriteMode; let mode = modes.Replace; - let reference: Opt<GoogleApiClientUtils.Reference> = Cast(this.dataDoc[GoogleRef], "string"); + let reference: Opt<GoogleApiClientUtils.Docs.Reference> = Cast(this.dataDoc[GoogleRef], "string"); if (!reference) { mode = modes.Insert; - reference = { service: GoogleApiClientUtils.Service.Documents, title: StrCast(this.dataDoc.title) }; + reference = { title: StrCast(this.dataDoc.title) }; } let redo = async () => { if (this._editorView && reference) { - let content = RichTextUtils.GoogleDocs.Convert(this._editorView.state); + let content = RichTextUtils.GoogleDocs.Export(this._editorView.state); let response = await GoogleApiClientUtils.Docs.write({ reference, content, mode }); response && (this.dataDoc[GoogleRef] = response.documentId); let pushSuccess = response !== undefined && !("errors" in response); @@ -452,9 +456,9 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe if (!exportState) { return; } - let content = { + let content: GoogleApiClientUtils.Docs.Content = { text: exportState.body, - links: [] + requests: [] }; if (reference && content) { GoogleApiClientUtils.Docs.write({ reference, content, mode }); @@ -468,14 +472,15 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe pullFromGoogleDoc = async (handler: PullHandler) => { let dataDoc = this.dataDoc; let documentId = StrCast(dataDoc[GoogleRef]); - let exportState: Opt<GoogleApiClientUtils.ReadResult>; + let test = await RichTextUtils.GoogleDocs.Import(documentId); + let exportState: Opt<GoogleApiClientUtils.Docs.ReadResult>; if (documentId) { - exportState = await GoogleApiClientUtils.Docs.read({ identifier: documentId }); + exportState = await GoogleApiClientUtils.Docs.read({ documentId }); } UndoManager.RunInBatch(() => handler(exportState, dataDoc), Pulls); } - updateState = (exportState: Opt<GoogleApiClientUtils.ReadResult>, dataDoc: Doc) => { + updateState = (exportState: Opt<GoogleApiClientUtils.Docs.ReadResult>, dataDoc: Doc) => { let pullSuccess = false; if (exportState !== undefined && exportState.body !== undefined && exportState.title !== undefined) { const data = Cast(dataDoc.data, RichTextField); @@ -499,7 +504,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe DocumentDecorations.Instance.startPullOutcome(pullSuccess); } - checkState = (exportState: Opt<GoogleApiClientUtils.ReadResult>, dataDoc: Doc) => { + checkState = (exportState: Opt<GoogleApiClientUtils.Docs.ReadResult>, dataDoc: Doc) => { if (exportState && this._editorView) { let storedPlainText = RichTextUtils.ToPlainText(this._editorView.state) + "\n"; let receivedPlainText = exportState.body; diff --git a/src/new_fields/RichTextUtils.ts b/src/new_fields/RichTextUtils.ts index b2b1dbaee..189819591 100644 --- a/src/new_fields/RichTextUtils.ts +++ b/src/new_fields/RichTextUtils.ts @@ -3,6 +3,8 @@ import { Node } from "prosemirror-model"; import { RichTextField } from "./RichTextField"; import { docs_v1 } from "googleapis"; import { GoogleApiClientUtils } from "../client/apis/google_docs/GoogleApiClientUtils"; +import { FormattedTextBox } from "../client/views/nodes/FormattedTextBox"; +import { Opt } from "./Doc"; export namespace RichTextUtils { @@ -81,34 +83,68 @@ export namespace RichTextUtils { export namespace GoogleDocs { - export const Convert = (state: EditorState): GoogleApiClientUtils.Content => { + export const Export = (state: EditorState): GoogleApiClientUtils.Docs.Content => { let textNodes: Node<any>[] = []; let text = ToPlainText(state); let content = state.doc.content; content.forEach(node => node.content.forEach(node => node.type.name === "text" && textNodes.push(node))); + let linkRequests = ExtractLinks(textNodes); + return { + text, + requests: [...linkRequests] + }; + }; + + export const Import = async (documentId: GoogleApiClientUtils.Docs.DocumentId) => { + let document = await GoogleApiClientUtils.Docs.retrieve({ documentId }); + if (!document) { + return; + } + // let title = document.title!; + let runs = GoogleApiClientUtils.Docs.Utils.extractTextRuns(document); + let state = FormattedTextBox.blankState(); + let from = 0; + runs.map(run => { + let text = run.content!; + state = state.apply(state.tr.insertText(text, from)); + let to = from + text.length + 1; + let href: Opt<string>; + if (run.textStyle && run.textStyle.link && (href = run.textStyle.link.url)) { + let mark = state.schema.mark(state.schema.marks.link, { href }); + state = state.apply(state.tr.addMark(from, to, mark)); + } + from = to; + }); + // return { title, body }; + }; + + interface LinkInformation { + startIndex: number; + endIndex: number; + bold: boolean; + url: string; + } + + const ExtractLinks = (nodes: Node<any>[]) => { let links: docs_v1.Schema$Request[] = []; let position = 1; - for (let node of textNodes) { + for (let node of nodes) { let link, length = node.nodeSize; let marks = node.marks; if (marks.length && (link = marks.find(mark => mark.type.name === "link"))) { - links.push(encode({ + links.push(Encode({ startIndex: position, endIndex: position + length, url: link.attrs.href, + bold: false })); } position += length; } - return { text, links }; + return links; }; - interface LinkInformation { - startIndex: number; - endIndex: number; - url: string; - } - const encode = (information: LinkInformation) => { + const Encode = (information: LinkInformation) => { return { updateTextStyle: { fields: "*", |