aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/client/apis/google_docs/GoogleApiClientUtils.ts204
-rw-r--r--src/client/views/nodes/FormattedTextBox.tsx35
-rw-r--r--src/new_fields/RichTextUtils.ts56
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: "*",