diff options
Diffstat (limited to 'src/client')
| -rw-r--r-- | src/client/apis/google_docs/GoogleApiClientUtils.ts | 265 | ||||
| -rw-r--r-- | src/client/apis/google_docs/GooglePhotosClientUtils.ts | 51 | ||||
| -rw-r--r-- | src/client/documents/Documents.ts | 7 | ||||
| -rw-r--r-- | src/client/northstar/utils/Extensions.ts | 2 | ||||
| -rw-r--r-- | src/client/views/DocumentDecorations.scss | 31 | ||||
| -rw-r--r-- | src/client/views/DocumentDecorations.tsx | 7 | ||||
| -rw-r--r-- | src/client/views/Main.tsx | 20 | ||||
| -rw-r--r-- | src/client/views/MainView.tsx | 12 | ||||
| -rw-r--r-- | src/client/views/nodes/FormattedTextBox.tsx | 87 |
9 files changed, 291 insertions, 191 deletions
diff --git a/src/client/apis/google_docs/GoogleApiClientUtils.ts b/src/client/apis/google_docs/GoogleApiClientUtils.ts index 798886def..828d4451a 100644 --- a/src/client/apis/google_docs/GoogleApiClientUtils.ts +++ b/src/client/apis/google_docs/GoogleApiClientUtils.ts @@ -3,111 +3,130 @@ import { PostToServer } from "../../../Utils"; import { RouteStore } from "../../../server/RouteStore"; import { Opt } from "../../../new_fields/Doc"; import { isArray } from "util"; +import { EditorState } from "prosemirror-state"; +import { RichTextField } from "../../../new_fields/RichTextField"; export const Pulls = "googleDocsPullCount"; 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 type TextContent = string | string[]; - 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: TextContent; - 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 }; + export interface ImportResult { + title: string; + text: string; + state: EditorState; + } - /** - * 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[] = []; + export type ExtractResult = { text: string, paragraphs: DeconstructedParagraph[] }; + export const extractText = (document: docs_v1.Schema$Document, removeNewlines = false): ExtractResult => { + let paragraphs = extractParagraphs(document); + let text = paragraphs.map(paragraph => paragraph.runs.map(run => run.content).join("")).join(""); + text = text.substring(0, text.length - 1); + removeNewlines && text.ReplaceAll("\n", ""); + return { text, paragraphs }; + }; + + export type DeconstructedParagraph = { runs: docs_v1.Schema$TextRun[], bullet: Opt<number> }; + const extractParagraphs = (document: docs_v1.Schema$Document, filterEmpty = true): DeconstructedParagraph[] => { + const fragments: DeconstructedParagraph[] = []; 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); + let runs: docs_v1.Schema$TextRun[] = []; + let bullet: Opt<number>; + if (element.paragraph) { + if (element.paragraph.elements) { + for (const inner of element.paragraph.elements) { + if (inner && inner.textRun) { + let run = inner.textRun; + (run.content || !filterEmpty) && runs.push(inner.textRun); + } } } + if (element.paragraph.bullet) { + bullet = element.paragraph.bullet.nestingLevel || 0; + } } + (runs.length || !filterEmpty) && fragments.push({ runs, bullet }); } } - const text = fragments.join(""); - return removeNewlines ? text.ReplaceAll("\n", "") : text; + return fragments; }; export const endOf = (schema: docs_v1.Schema$Document): number | undefined => { @@ -130,27 +149,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: { @@ -165,41 +176,49 @@ export namespace GoogleApiClientUtils { } }; - export const read = async (options: ReadOptions): Promise<ReadResult> => { - return retrieve({ ...options, service: Service.Documents }).then(document => { - let result: ReadResult = {}; + export const read = async (options: ReadOptions): Promise<Opt<ReadResult>> => { + return retrieve({ documentId: options.documentId }).then(document => { if (document) { - let title = document.title; - let body = Utils.extractText(document, options.removeNewlines); - result = { title, body }; + let title = document.title!; + let body = Utils.extractText(document, options.removeNewlines).text; + return { title, body }; } - return result; }); }; - export const readLines = async (options: ReadOptions): Promise<ReadLinesResult> => { - return retrieve({ ...options, service: Service.Documents }).then(document => { - let result: ReadLinesResult = {}; + export const readLines = async (options: ReadOptions): Promise<Opt<ReadLinesResult>> => { + return retrieve({ documentId: options.documentId }).then(document => { if (document) { let title = document.title; - let bodyLines = Utils.extractText(document).split("\n"); + let bodyLines = Utils.extractText(document).text.split("\n"); options.removeNewlines && (bodyLines = bodyLines.filter(line => line.length)); - result = { title, bodyLines }; + return { title, bodyLines }; } - return result; }); }; + export const setStyle = async (options: UpdateOptions) => { + let replies: any = await update({ + documentId: options.documentId, + requests: options.requests + }); + if ("errors" in replies) { + console.log("Write operation failed:"); + console.log(replies.errors.map((error: any) => error.message)); + } + return replies; + }; + 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; } @@ -215,7 +234,7 @@ export namespace GoogleApiClientUtils { }); index = 1; } - const text = options.content; + const text = options.content.text; text.length && requests.push({ insertText: { text: isArray(text) ? text.join("\n") : text, @@ -225,47 +244,15 @@ export namespace GoogleApiClientUtils { if (!requests.length) { return undefined; } - let replies: any = await update({ documentId: identifier, requests }); - let errors = "errors"; - if (errors in replies) { + 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)); + console.log(replies.errors.map((error: any) => error.message)); } return replies; }; } - 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/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts new file mode 100644 index 000000000..0864ebdb1 --- /dev/null +++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts @@ -0,0 +1,51 @@ +import { Album } from "../../../server/apis/google/typings/albums"; +import { PostToServer } from "../../../Utils"; +import { RouteStore } from "../../../server/RouteStore"; +import { ImageField } from "../../../new_fields/URLField"; + +export namespace GooglePhotosClientUtils { + + export const Create = async (title: string) => { + let parameters = { + action: Album.Action.Create, + body: { album: { title } } + } as Album.Create; + return PostToServer(RouteStore.googlePhotosQuery, parameters); + }; + + export const List = async (options?: Partial<Album.ListOptions>) => { + let parameters = { + action: Album.Action.List, + parameters: { + pageSize: (options ? options.pageSize : 20) || 20, + pageToken: (options ? options.pageToken : undefined) || undefined, + excludeNonAppCreatedData: (options ? options.excludeNonAppCreatedData : false) || false, + } as Album.ListOptions + } as Album.List; + return PostToServer(RouteStore.googlePhotosQuery, parameters); + }; + + export const Get = async (albumId: string) => { + let parameters = { + action: Album.Action.Get, + albumId + } as Album.Get; + return PostToServer(RouteStore.googlePhotosQuery, parameters); + }; + + export const toDataURL = (field: ImageField | undefined) => { + if (!field) { + return undefined; + } + const image = document.createElement("img"); + image.src = field.url.href; + const canvas = document.createElement("canvas"); + canvas.width = image.width; + canvas.height = image.height; + const ctx = canvas.getContext("2d")!; + ctx.drawImage(image, 0, 0); + const dataUrl = canvas.toDataURL("image/png"); + return dataUrl.replace(/^data:image\/(png|jpg);base64,/, ""); + }; + +}
\ No newline at end of file diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index ef8b68c2f..4b7f1eeb6 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -506,10 +506,13 @@ export namespace Docs { * @param title an optional title to give to the highest parent document in the hierarchy */ export function DocumentHierarchyFromJson(input: any, title?: string): Opt<Doc> { - if (input === null || ![...primitives, "object"].includes(typeof input)) { + if (input === undefined || input === null || ![...primitives, "object"].includes(typeof input)) { return undefined; } - let parsed: any = typeof input === "string" ? JSONUtils.tryParse(input) : input; + let parsed = input; + if (typeof input === "string") { + parsed = JSONUtils.tryParse(input); + } let converted: Doc; if (typeof parsed === "object" && !(parsed instanceof Array)) { converted = convertObject(parsed, title); diff --git a/src/client/northstar/utils/Extensions.ts b/src/client/northstar/utils/Extensions.ts index df14d4da0..ab9384f1f 100644 --- a/src/client/northstar/utils/Extensions.ts +++ b/src/client/northstar/utils/Extensions.ts @@ -1,6 +1,8 @@ interface String { ReplaceAll(toReplace: string, replacement: string): string; Truncate(length: number, replacement: string): String; + removeTrailingNewlines(): string; + hasNewline(): boolean; } String.prototype.ReplaceAll = function (toReplace: string, replacement: string): string { diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss index ac8497bd0..470365627 100644 --- a/src/client/views/DocumentDecorations.scss +++ b/src/client/views/DocumentDecorations.scss @@ -266,6 +266,31 @@ $linkGap : 3px; } } -@-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 +@-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); + } +} + +@keyframes shadow-pulse { + 0% { + box-shadow: 0 0 0 0px rgba(0, 0, 0, 0.2); + } + + 100% { + box-shadow: 0 0 0 35px rgba(0, 0, 0, 0); + } +}
\ No newline at end of file diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 700a4b49d..35c45b03c 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -82,6 +82,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> @observable public pullIcon: IconProp = "arrow-alt-circle-down"; @observable public pullColor: string = "white"; @observable public isAnimatingFetch = false; + @observable public isAnimatingPulse = false; @observable public openHover = false; public pullColorAnimating = false; @@ -102,6 +103,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> public startPushOutcome = action((success: boolean) => { if (!this.pushAnimating) { this.pushAnimating = true; + this.isAnimatingPulse = false; this.pushIcon = success ? "check-circle" : "stop-circle"; setTimeout(() => runInAction(() => { this.pushIcon = "arrow-alt-circle-up"; @@ -707,9 +709,12 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> return ( <div className={"linkButtonWrapper"}> <div title={`${published ? "Push" : "Publish"} to Google Docs`} className="linkButton-linker" onClick={() => { + if (!published) { + runInAction(() => this.isAnimatingPulse = true); + } DocumentDecorations.hasPushedHack = false; this.targetDoc[Pushes] = NumCast(this.targetDoc[Pushes]) + 1; - }}> + }} style={{ animation: this.isAnimatingPulse ? "shadow-pulse 1s infinite" : "none" }}> <FontAwesomeIcon className="documentdecorations-icon" icon={icon} size={published ? "sm" : "xs"} /> </div> </div> diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx index 0e687737d..271326f70 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -8,6 +8,26 @@ import { Doc, DocListCastAsync } from "../../new_fields/Doc"; import { List } from "../../new_fields/List"; import { DocServer } from "../DocServer"; +String.prototype.removeTrailingNewlines = function () { + let sliced = this; + while (sliced.endsWith("\n")) { + sliced = sliced.substring(0, this.length - 1); + } + return sliced as string; +}; + +String.prototype.hasNewline = function () { + return this.endsWith("\n"); +}; + +(Array.prototype as any).lastElement = function (this: any[]) { + if (!this.length) { + return undefined; + } + return this[this.length - 1]; +}; + + let swapDocs = async () => { let oldDoc = await Cast(CurrentUserUtils.UserDocument.linkManagerDoc, Doc); // Docs.Prototypes.MainLinkDocument().allLinks = new List<Doc>(); diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index b64986084..f01083fbb 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -15,7 +15,7 @@ import { listSpec } from '../../new_fields/Schema'; import { BoolCast, Cast, FieldValue, StrCast, NumCast } from '../../new_fields/Types'; import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils'; import { RouteStore } from '../../server/RouteStore'; -import { emptyFunction, returnOne, returnTrue, Utils, returnEmptyString } from '../../Utils'; +import { emptyFunction, returnOne, returnTrue, Utils, returnEmptyString, PostToServer } from '../../Utils'; import { DocServer } from '../DocServer'; import { Docs } from '../documents/Documents'; import { ClientUtils } from '../util/ClientUtils'; @@ -40,8 +40,11 @@ import { PreviewCursor } from './PreviewCursor'; import { FilterBox } from './search/FilterBox'; import PresModeMenu from './presentationview/PresentationModeMenu'; import { PresBox } from './nodes/PresBox'; +import { docs_v1 } from 'googleapis'; +import { Album } from '../../server/apis/google/typings/albums'; +import { GooglePhotosClientUtils } from '../apis/google_docs/GooglePhotosClientUtils'; +import { ImageField } from '../../new_fields/URLField'; import { LinkFollowBox } from './linking/LinkFollowBox'; -import { DocumentManager } from '../util/DocumentManager'; @observer export class MainView extends React.Component { @@ -128,6 +131,11 @@ export class MainView extends React.Component { window.removeEventListener("keydown", KeyManager.Instance.handle); window.addEventListener("keydown", KeyManager.Instance.handle); + let imgurl = "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg"; + let image = Docs.Create.ImageDocument(imgurl, { width: 200, title: "an image of a cat" }); + let parameters = { title: StrCast(image.title), MEDIA_BINARY_DATA: GooglePhotosClientUtils.toDataURL(Cast(image.data, ImageField)) }; + PostToServer(RouteStore.googlePhotosMediaUpload, parameters).then(console.log); + reaction(() => { let workspaces = CurrentUserUtils.UserDocument.workspaces; let recent = CurrentUserUtils.UserDocument.recentlyClosed; diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index bb44c5ac6..b671d06ea 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -12,7 +12,7 @@ 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, ToPlainText, FromPlainText } from "../../../new_fields/RichTextField"; +import { RichTextField } from "../../../new_fields/RichTextField"; import { BoolCast, Cast, NumCast, StrCast, DateCast } from "../../../new_fields/Types"; import { createSchema, makeInterface } from "../../../new_fields/Schema"; import { Utils, numberRange } from '../../../Utils'; @@ -37,13 +37,13 @@ import { DocumentDecorations } from '../DocumentDecorations'; import { DictationManager } from '../../util/DictationManager'; import { ReplaceStep } from 'prosemirror-transform'; import { DocumentType } from '../../documents/DocumentTypes'; +import { RichTextUtils } from '../../../new_fields/RichTextUtils'; +import * as _ from "lodash"; import { formattedTextBoxCommentPlugin, FormattedTextBoxComment } from './FormattedTextBoxComment'; library.add(faEdit); library.add(faSmile, faTextHeight, faUpload); -export const Blank = `{"doc":{"type":"doc","content":[]},"selection":{"type":"text","anchor":0,"head":0}}`; - export interface FormattedTextBoxProps { isOverlay?: boolean; hideOnLeave?: boolean; @@ -62,13 +62,15 @@ export const GoogleRef = "googleDocId"; type RichTextDocument = makeInterface<[typeof richTextSchema]>; const RichTextDocument = makeInterface(richTextSchema); -type PullHandler = (exportState: GoogleApiClientUtils.ReadResult, dataDoc: Doc) => void; +type PullHandler = (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, 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 = () => EditorState.create(FormattedTextBox.Instance.config); + public static Instance: FormattedTextBox; private static _toolTipTextMenu: TooltipTextMenu | undefined = undefined; private _ref: React.RefObject<HTMLDivElement> = React.createRef(); private _proseRef?: HTMLDivElement; @@ -380,7 +382,7 @@ 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 : Blank; + return field ? field.Data : RichTextUtils.Initialize(); }, incomingValue => { if (this._editorView && !this._applyingChange) { @@ -450,18 +452,17 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } pushToGoogleDoc = async () => { - this.pullFromGoogleDoc(async (exportState: GoogleApiClientUtils.ReadResult, dataDoc: Doc) => { - let modes = GoogleApiClientUtils.WriteMode; + this.pullFromGoogleDoc(async (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, 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 () => { - let data = Cast(this.dataDoc.data, RichTextField); - if (this._editorView && reference && data) { - let content = data[ToPlainText](); + if (this._editorView && reference) { + 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); @@ -470,7 +471,13 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } }; let undo = () => { - let content = exportState.body; + if (!exportState) { + return; + } + let content: GoogleApiClientUtils.Docs.Content = { + text: exportState.text, + requests: [] + }; if (reference && content) { GoogleApiClientUtils.Docs.write({ reference, content, mode }); } @@ -483,49 +490,41 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe pullFromGoogleDoc = async (handler: PullHandler) => { let dataDoc = this.dataDoc; let documentId = StrCast(dataDoc[GoogleRef]); - let exportState: GoogleApiClientUtils.ReadResult = {}; + let exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>; if (documentId) { - exportState = await GoogleApiClientUtils.Docs.read({ identifier: documentId }); + exportState = await RichTextUtils.GoogleDocs.Import(documentId); } UndoManager.RunInBatch(() => handler(exportState, dataDoc), Pulls); } - updateState = (exportState: GoogleApiClientUtils.ReadResult, dataDoc: Doc) => { + updateState = (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, dataDoc: Doc) => { let pullSuccess = false; - if (exportState !== undefined && exportState.body !== undefined && exportState.title !== undefined) { - const data = Cast(dataDoc.data, RichTextField); - if (data instanceof RichTextField) { - pullSuccess = true; - dataDoc.data = new RichTextField(data[FromPlainText](exportState.body)); - setTimeout(() => { - if (this._editorView) { - let state = this._editorView.state; - let end = state.doc.content.size - 1; - this._editorView.dispatch(state.tr.setSelection(TextSelection.create(state.doc, end, end))); - } - }, 0); - dataDoc.title = exportState.title; - this.Document.customTitle = true; - dataDoc.unchanged = true; - } + if (exportState !== undefined) { + pullSuccess = true; + dataDoc.data = new RichTextField(JSON.stringify(exportState.state.toJSON())); + setTimeout(() => { + if (this._editorView) { + let state = this._editorView.state; + let end = state.doc.content.size - 1; + this._editorView.dispatch(state.tr.setSelection(TextSelection.create(state.doc, end, end))); + } + }, 0); + dataDoc.title = exportState.title; + this.Document.customTitle = true; + dataDoc.unchanged = true; } else { delete dataDoc[GoogleRef]; } DocumentDecorations.Instance.startPullOutcome(pullSuccess); } - checkState = (exportState: GoogleApiClientUtils.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); - } + checkState = (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, dataDoc: Doc) => { + if (exportState && this._editorView) { + let equalContent = _.isEqual(this._editorView.state.doc, exportState.state.doc); + let equalTitles = dataDoc.title === exportState.title; + let unchanged = equalContent && equalTitles; + dataDoc.unchanged = unchanged; + DocumentDecorations.Instance.setPullState(unchanged); } } |
