From 187a411024668a46e7a80022d3d549118b81abbc Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Tue, 27 Aug 2019 12:49:04 -0400 Subject: can push links to google docs --- src/client/views/nodes/FormattedTextBox.tsx | 51 +++++++++++++++-------------- 1 file changed, 26 insertions(+), 25 deletions(-) (limited to 'src/client/views/nodes/FormattedTextBox.tsx') diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index d6ba1700a..02bee2f82 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 } from '../../../Utils'; @@ -37,12 +37,11 @@ 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'; 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; @@ -61,7 +60,7 @@ 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, dataDoc: Doc) => void; @observer export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTextBoxProps), RichTextDocument>(RichTextDocument) { @@ -363,7 +362,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) { @@ -431,7 +430,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } pushToGoogleDoc = async () => { - this.pullFromGoogleDoc(async (exportState: GoogleApiClientUtils.ReadResult, dataDoc: Doc) => { + this.pullFromGoogleDoc(async (exportState: Opt, dataDoc: Doc) => { let modes = GoogleApiClientUtils.WriteMode; let mode = modes.Replace; let reference: Opt = Cast(this.dataDoc[GoogleRef], "string"); @@ -440,9 +439,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe reference = { service: GoogleApiClientUtils.Service.Documents, 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.Convert(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); @@ -451,7 +449,13 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } }; let undo = () => { - let content = exportState.body; + if (!exportState) { + return; + } + let content = { + text: exportState.body, + links: [] + }; if (reference && content) { GoogleApiClientUtils.Docs.write({ reference, content, mode }); } @@ -464,20 +468,20 @@ 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; if (documentId) { exportState = await GoogleApiClientUtils.Docs.read({ identifier: documentId }); } UndoManager.RunInBatch(() => handler(exportState, dataDoc), Pulls); } - updateState = (exportState: GoogleApiClientUtils.ReadResult, dataDoc: Doc) => { + updateState = (exportState: Opt, 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)); + dataDoc.data = RichTextUtils.Synthesize(exportState.body, data); setTimeout(() => { if (this._editorView) { let state = this._editorView.state; @@ -495,18 +499,15 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe 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, dataDoc: Doc) => { + if (exportState && this._editorView) { + let storedPlainText = RichTextUtils.ToPlainText(this._editorView.state) + "\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); } } -- cgit v1.2.3-70-g09d2 From 916aa5377a9f105cd128264f46c83b987861c713 Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Wed, 28 Aug 2019 17:45:44 -0400 Subject: separated docs and other apis, beginning less hacky import --- .../apis/google_docs/GoogleApiClientUtils.ts | 204 ++++++++------------- src/client/views/nodes/FormattedTextBox.tsx | 35 ++-- src/new_fields/RichTextUtils.ts | 56 +++++- 3 files changed, 146 insertions(+), 149 deletions(-) (limited to 'src/client/views/nodes/FormattedTextBox.tsx') 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; - 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; + export type UpdateResult = Opt; - 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; + 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 => { - 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; - export type UpdateResult = Opt; + 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 => { + 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.Documents, "documentId"], - [Service.Slides, "presentationId"] - ]); - export const retrieve = async (options: RetrieveOptions): Promise => { - 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 => { - 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> => { - 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> => { - 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 => { 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, dataDoc: Doc) => void; +type PullHandler = (exportState: Opt, 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; private _proseRef?: HTMLDivElement; private _editorView: Opt; @@ -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, dataDoc: Doc) => { - let modes = GoogleApiClientUtils.WriteMode; + this.pullFromGoogleDoc(async (exportState: Opt, dataDoc: Doc) => { + let modes = GoogleApiClientUtils.Docs.WriteMode; let mode = modes.Replace; - let reference: Opt = Cast(this.dataDoc[GoogleRef], "string"); + let reference: Opt = 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; + let test = await RichTextUtils.GoogleDocs.Import(documentId); + let exportState: Opt; if (documentId) { - exportState = await GoogleApiClientUtils.Docs.read({ identifier: documentId }); + exportState = await GoogleApiClientUtils.Docs.read({ documentId }); } UndoManager.RunInBatch(() => handler(exportState, dataDoc), Pulls); } - updateState = (exportState: Opt, dataDoc: Doc) => { + updateState = (exportState: Opt, 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, dataDoc: Doc) => { + checkState = (exportState: Opt, 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[] = []; 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; + 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[]) => { 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: "*", -- cgit v1.2.3-70-g09d2 From c53d721dd95bdb4ccddc7a89dbdda72a88d29058 Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Fri, 30 Aug 2019 13:43:04 -0400 Subject: model sharing --- .../apis/google_docs/GoogleApiClientUtils.ts | 22 ++++-- src/client/northstar/utils/Extensions.ts | 2 + src/client/views/DocumentDecorations.scss | 31 +++++++- src/client/views/DocumentDecorations.tsx | 7 +- src/client/views/Main.tsx | 20 ++++++ src/client/views/nodes/FormattedTextBox.tsx | 50 ++++++------- src/new_fields/RichTextUtils.ts | 82 +++++++++++++++++----- 7 files changed, 159 insertions(+), 55 deletions(-) (limited to 'src/client/views/nodes/FormattedTextBox.tsx') diff --git a/src/client/apis/google_docs/GoogleApiClientUtils.ts b/src/client/apis/google_docs/GoogleApiClientUtils.ts index ae7c2f997..9bf3cae38 100644 --- a/src/client/apis/google_docs/GoogleApiClientUtils.ts +++ b/src/client/apis/google_docs/GoogleApiClientUtils.ts @@ -3,6 +3,8 @@ 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"; @@ -40,6 +42,11 @@ export namespace GoogleApiClientUtils { export type CreationResult = Opt; export type ReadLinesResult = Opt<{ title?: string, bodyLines?: string[] }>; export type ReadResult = { title: string, body: string }; + export interface ImportResult { + title: string; + text: string; + data: RichTextField; + } export interface CreateOptions { title?: string; // if excluded, will use a default title annotated with the current date @@ -87,13 +94,16 @@ export namespace GoogleApiClientUtils { export namespace Utils { - export const extractText = (document: docs_v1.Schema$Document, removeNewlines = false): string => { + export type ExtractResult = { text: string, runs: docs_v1.Schema$TextRun[] }; + export const extractText = (document: docs_v1.Schema$Document, removeNewlines = false): ExtractResult => { let runs = extractTextRuns(document); - const text = runs.map(run => run.content).join(""); - return removeNewlines ? text.ReplaceAll("\n", "") : text; + let text = runs.map(run => run.content).join(""); + text = text.substring(0, text.length - 1); + removeNewlines && text.ReplaceAll("\n", ""); + return { text, runs }; }; - export const extractTextRuns = (document: docs_v1.Schema$Document, filterEmpty = true) => { + 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) { @@ -160,7 +170,7 @@ export namespace GoogleApiClientUtils { return retrieve({ documentId: options.documentId }).then(document => { if (document) { let title = document.title!; - let body = Utils.extractText(document, options.removeNewlines); + let body = Utils.extractText(document, options.removeNewlines).text; return { title, body }; } }); @@ -170,7 +180,7 @@ export namespace GoogleApiClientUtils { 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)); return { title, bodyLines }; } 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 579806a89..109228b54 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"; @@ -698,9 +700,12 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> return (
{ + 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" }}>
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(); diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index eefac2285..83011e590 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -60,16 +60,14 @@ export const GoogleRef = "googleDocId"; type RichTextDocument = makeInterface<[typeof richTextSchema]>; const RichTextDocument = makeInterface(richTextSchema); -type PullHandler = (exportState: Opt, dataDoc: Doc) => void; +type PullHandler = (exportState: Opt, 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 blankState = () => EditorState.create(FormattedTextBox.Instance._configuration); public static Instance: FormattedTextBox; private _configuration: any; private _ref: React.RefObject; @@ -434,7 +432,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } pushToGoogleDoc = async () => { - this.pullFromGoogleDoc(async (exportState: Opt, dataDoc: Doc) => { + this.pullFromGoogleDoc(async (exportState: Opt, dataDoc: Doc) => { let modes = GoogleApiClientUtils.Docs.WriteMode; let mode = modes.Replace; let reference: Opt = Cast(this.dataDoc[GoogleRef], "string"); @@ -457,7 +455,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe return; } let content: GoogleApiClientUtils.Docs.Content = { - text: exportState.body, + text: exportState.text, requests: [] }; if (reference && content) { @@ -472,42 +470,38 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe pullFromGoogleDoc = async (handler: PullHandler) => { let dataDoc = this.dataDoc; let documentId = StrCast(dataDoc[GoogleRef]); - let test = await RichTextUtils.GoogleDocs.Import(documentId); - let exportState: Opt; + let exportState: Opt; if (documentId) { - exportState = await GoogleApiClientUtils.Docs.read({ documentId }); + exportState = await RichTextUtils.GoogleDocs.Import(documentId); } UndoManager.RunInBatch(() => handler(exportState, dataDoc), Pulls); } - updateState = (exportState: Opt, dataDoc: Doc) => { + updateState = (exportState: Opt, 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 = RichTextUtils.Synthesize(exportState.body, data); - 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 = exportState.data; + 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: Opt, dataDoc: Doc) => { + checkState = (exportState: Opt, dataDoc: Doc) => { if (exportState && this._editorView) { let storedPlainText = RichTextUtils.ToPlainText(this._editorView.state) + "\n"; - let receivedPlainText = exportState.body; + let receivedPlainText = exportState.text; let storedTitle = dataDoc.title; let receivedTitle = exportState.title; let unchanged = storedPlainText === receivedPlainText && storedTitle === receivedTitle; diff --git a/src/new_fields/RichTextUtils.ts b/src/new_fields/RichTextUtils.ts index 189819591..4ca51d311 100644 --- a/src/new_fields/RichTextUtils.ts +++ b/src/new_fields/RichTextUtils.ts @@ -1,5 +1,5 @@ import { EditorState } from "prosemirror-state"; -import { Node } from "prosemirror-model"; +import { Node, Fragment, Mark } from "prosemirror-model"; import { RichTextField } from "./RichTextField"; import { docs_v1 } from "googleapis"; import { GoogleApiClientUtils } from "../client/apis/google_docs/GoogleApiClientUtils"; @@ -95,27 +95,75 @@ export namespace RichTextUtils { }; }; - export const Import = async (documentId: GoogleApiClientUtils.Docs.DocumentId) => { - let document = await GoogleApiClientUtils.Docs.retrieve({ documentId }); + export const Import = async (documentId: GoogleApiClientUtils.Docs.DocumentId): Promise> => { + let Docs = GoogleApiClientUtils.Docs; + let document = await Docs.retrieve({ documentId }); + if (!document) { - return; + return undefined; } - // let title = document.title!; - let runs = GoogleApiClientUtils.Docs.Utils.extractTextRuns(document); + + let title = document.title!; + + let { text, runs } = Docs.Utils.extractText(document); + let segments = runs[Symbol.iterator](); + let state = FormattedTextBox.blankState(); + let breaks: number[] = []; 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; - 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)); + let result = segments.next(); + while (!result.done) { + let run = result.value; + let fragment = run.content!; + if (fragment.hasNewline()) { + let trimmed = fragment.removeTrailingNewlines(); + if (fragment.length === 1) { + breaks.push(from); + } else { + let content = Fragment.from(state.schema.text(trimmed, styleToMarks(state.schema, run.textStyle))); + let node = state.schema.node("paragraph", null, content); + state = state.apply(state.tr.insert(from, node)); + from += node.nodeSize; + } + result = segments.next(); + } else { + let nodes: Node[] = []; + nodes.push(state.schema.text(fragment, styleToMarks(state.schema, run.textStyle))); + result = segments.next(); + while (!result.done) { + run = result.value; + fragment = run.content!; + let trimmed = fragment.removeTrailingNewlines(); + nodes.push(state.schema.text(trimmed, styleToMarks(state.schema, run.textStyle))); + if (fragment.hasNewline()) { + let node = state.schema.node("paragraph", null, Fragment.fromArray(nodes)); + state = state.apply(state.tr.insert(from, node)); + from += node.nodeSize; + result = segments.next(); + break; + } + result = segments.next(); + } + if (result.done) { + break; + } } - from = to; - }); - // return { title, body }; + } + breaks.forEach(position => state = state.apply(state.tr.insert(position, state.schema.node("paragraph")))); + let data = new RichTextField(JSON.stringify(state.toJSON())); + return { title, text, data }; + }; + + const styleToMarks = (schema: any, textStyle?: docs_v1.Schema$TextStyle) => { + if (!textStyle) { + return undefined; + } + let marks: Mark[] = []; + if (textStyle.link) { + let href = textStyle.link.url; + marks.push(schema.mark(schema.marks.link, { href })); + } + return marks; }; interface LinkInformation { -- cgit v1.2.3-70-g09d2 From c9a7d747916c31730f71479d8e516ba0ed2c658f Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Fri, 30 Aug 2019 15:51:13 -0400 Subject: complete transfer except for list items --- package.json | 2 + .../apis/google_docs/GoogleApiClientUtils.ts | 2 +- src/client/views/nodes/FormattedTextBox.tsx | 11 ++- src/new_fields/RichTextUtils.ts | 89 +++++++++++++--------- 4 files changed, 59 insertions(+), 45 deletions(-) (limited to 'src/client/views/nodes/FormattedTextBox.tsx') diff --git a/package.json b/package.json index cd60b7b55..f98224469 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "@types/bluebird": "^3.5.25", "@types/body-parser": "^1.17.0", "@types/classnames": "^2.2.8", + "@types/color": "^3.0.0", "@types/connect-flash": "0.0.34", "@types/cookie-parser": "^1.4.1", "@types/cookie-session": "^2.0.36", @@ -121,6 +122,7 @@ "canvas": "^2.5.0", "child_process": "^1.0.2", "class-transformer": "^0.2.0", + "color": "^3.1.2", "connect-flash": "^0.1.1", "connect-mongo": "^2.0.3", "cookie-parser": "^1.4.4", diff --git a/src/client/apis/google_docs/GoogleApiClientUtils.ts b/src/client/apis/google_docs/GoogleApiClientUtils.ts index 9bf3cae38..fdd708e31 100644 --- a/src/client/apis/google_docs/GoogleApiClientUtils.ts +++ b/src/client/apis/google_docs/GoogleApiClientUtils.ts @@ -45,7 +45,7 @@ export namespace GoogleApiClientUtils { export interface ImportResult { title: string; text: string; - data: RichTextField; + state: EditorState; } export interface CreateOptions { diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index 83011e590..6af5c8f0b 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -38,6 +38,7 @@ 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"; library.add(faEdit); library.add(faSmile, faTextHeight, faUpload); @@ -481,7 +482,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe let pullSuccess = false; if (exportState !== undefined) { pullSuccess = true; - dataDoc.data = exportState.data; + dataDoc.data = new RichTextField(JSON.stringify(exportState.state.toJSON())); setTimeout(() => { if (this._editorView) { let state = this._editorView.state; @@ -500,11 +501,9 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe checkState = (exportState: Opt, dataDoc: Doc) => { if (exportState && this._editorView) { - let storedPlainText = RichTextUtils.ToPlainText(this._editorView.state) + "\n"; - let receivedPlainText = exportState.text; - let storedTitle = dataDoc.title; - let receivedTitle = exportState.title; - let unchanged = storedPlainText === receivedPlainText && storedTitle === receivedTitle; + 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); } diff --git a/src/new_fields/RichTextUtils.ts b/src/new_fields/RichTextUtils.ts index 4ca51d311..bc338e45b 100644 --- a/src/new_fields/RichTextUtils.ts +++ b/src/new_fields/RichTextUtils.ts @@ -5,6 +5,7 @@ import { docs_v1 } from "googleapis"; import { GoogleApiClientUtils } from "../client/apis/google_docs/GoogleApiClientUtils"; import { FormattedTextBox } from "../client/views/nodes/FormattedTextBox"; import { Opt } from "./Doc"; +import * as Color from "color"; export namespace RichTextUtils { @@ -96,73 +97,85 @@ export namespace RichTextUtils { }; export const Import = async (documentId: GoogleApiClientUtils.Docs.DocumentId): Promise> => { - let Docs = GoogleApiClientUtils.Docs; - let document = await Docs.retrieve({ documentId }); - + const Docs = GoogleApiClientUtils.Docs; + const document = await Docs.retrieve({ documentId }); if (!document) { return undefined; } - let title = document.title!; - - let { text, runs } = Docs.Utils.extractText(document); - let segments = runs[Symbol.iterator](); + const title = document.title!; + const { text, runs } = Docs.Utils.extractText(document); + const segments = runs[Symbol.iterator](); let state = FormattedTextBox.blankState(); - let breaks: number[] = []; - let from = 0; + const schema = state.schema; + const nodes: Node[] = []; + let result = segments.next(); while (!result.done) { let run = result.value; - let fragment = run.content!; - if (fragment.hasNewline()) { - let trimmed = fragment.removeTrailingNewlines(); - if (fragment.length === 1) { - breaks.push(from); - } else { - let content = Fragment.from(state.schema.text(trimmed, styleToMarks(state.schema, run.textStyle))); - let node = state.schema.node("paragraph", null, content); - state = state.apply(state.tr.insert(from, node)); - from += node.nodeSize; - } + if (run.content!.hasNewline()) { + addParagraph(nodes, schema, textNode(schema, run)); result = segments.next(); } else { - let nodes: Node[] = []; - nodes.push(state.schema.text(fragment, styleToMarks(state.schema, run.textStyle))); + const inner: Node[] = []; + inner.push(textNode(schema, run)); result = segments.next(); while (!result.done) { run = result.value; - fragment = run.content!; - let trimmed = fragment.removeTrailingNewlines(); - nodes.push(state.schema.text(trimmed, styleToMarks(state.schema, run.textStyle))); - if (fragment.hasNewline()) { - let node = state.schema.node("paragraph", null, Fragment.fromArray(nodes)); - state = state.apply(state.tr.insert(from, node)); - from += node.nodeSize; - result = segments.next(); + inner.push(textNode(schema, run)); + result = segments.next(); + if (run.content!.hasNewline()) { + addParagraph(nodes, schema, inner); break; } - result = segments.next(); } if (result.done) { break; } } } - breaks.forEach(position => state = state.apply(state.tr.insert(position, state.schema.node("paragraph")))); - let data = new RichTextField(JSON.stringify(state.toJSON())); - return { title, text, data }; + state = state.apply(state.tr.replaceWith(0, 2, nodes)); + return { title, text, state }; + }; + + const addParagraph = (list: Node[], schema: any, content?: Node[] | Node) => { + list.push(schema.node("paragraph", null, content ? Fragment.from(content) : null)); }; + const textNode = (schema: any, run: docs_v1.Schema$TextRun) => { + let text = run.content!.removeTrailingNewlines(); + return text.length ? schema.text(text, styleToMarks(schema, run.textStyle)) : undefined; + }; + + const MarkMapping = new Map([ + ["bold", "strong"], + ["italic", "em"], + ["foregroundColor", "pFontColor"] + ]); + const styleToMarks = (schema: any, textStyle?: docs_v1.Schema$TextStyle) => { if (!textStyle) { return undefined; } let marks: Mark[] = []; - if (textStyle.link) { - let href = textStyle.link.url; - marks.push(schema.mark(schema.marks.link, { href })); - } + Object.keys(textStyle).forEach(key => { + let value: any; + let targeted = key as keyof docs_v1.Schema$TextStyle; + if (value = textStyle[targeted]) { + let attributes: any = {}; + let converted = MarkMapping.get(targeted) || targeted; + + value.url && (attributes.href = value.url); + if (value.color) { + let object: { [key: string]: number } = value.color.rgbColor; + attributes.color = Color.rgb(Object.values(object).map(value => value * 255)).hex(); + } + + let mark = schema.mark(schema.marks[converted], attributes); + mark && marks.push(mark); + } + }); return marks; }; -- cgit v1.2.3-70-g09d2 From 5d59e9a379417140e10778cd43e8f87ecb816c37 Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Fri, 6 Sep 2019 06:29:26 -0400 Subject: lightly tested functional export of any image doc (web url or stored in server public folder) to google photos, optionally stored in a given target album --- package.json | 2 +- .../apis/google_docs/GooglePhotosClientUtils.ts | 73 +++++----- src/client/views/MainView.tsx | 17 ++- src/client/views/nodes/FormattedTextBox.tsx | 1 + src/server/RouteStore.ts | 2 +- src/server/apis/google/GoogleApiServerUtils.ts | 28 ++-- src/server/apis/google/GooglePhotosServerUtils.ts | 68 ---------- src/server/apis/google/GooglePhotosUploadUtils.ts | 122 +++++++++++++++-- src/server/apis/google/typings/albums.ts | 150 --------------------- src/server/credentials/google_docs_token.json | 2 +- src/server/index.ts | 75 +++++------ 11 files changed, 206 insertions(+), 334 deletions(-) delete mode 100644 src/server/apis/google/GooglePhotosServerUtils.ts delete mode 100644 src/server/apis/google/typings/albums.ts (limited to 'src/client/views/nodes/FormattedTextBox.tsx') diff --git a/package.json b/package.json index f0f2b467e..f56e34ce0 100644 --- a/package.json +++ b/package.json @@ -224,4 +224,4 @@ "xoauth2": "^1.2.0", "youtube": "^0.1.0" } -} +} \ No newline at end of file diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts index b95cc98c9..2b72800a9 100644 --- a/src/client/apis/google_docs/GooglePhotosClientUtils.ts +++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts @@ -1,53 +1,42 @@ -import { Album } from "../../../server/apis/google/typings/albums"; -import { PostToServer } from "../../../Utils"; +import { PostToServer, Utils } from "../../../Utils"; import { RouteStore } from "../../../server/RouteStore"; import { ImageField } from "../../../new_fields/URLField"; +import { StrCast, Cast } from "../../../new_fields/Types"; +import { Doc, Opt } from "../../../new_fields/Doc"; +import { Id } from "../../../new_fields/FieldSymbols"; +import requestImageSize = require('../../util/request-image-size'); +import Photos = require('googlephotos'); 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 type AlbumReference = { id: string } | { title: string }; + export const endpoint = () => fetch(Utils.prepend(RouteStore.googlePhotosAccessToken)).then(async response => new Photos(await response.text())); - export const List = async (options?: Partial) => { - 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 interface MediaInput { + description: string; + source: string; + } - 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; + export const UploadMedia = async (sources: Doc[], album?: AlbumReference) => { + if (album && "title" in album) { + album = (await endpoint()).albums.create(album.title); + } + const media: MediaInput[] = []; + sources.forEach(document => { + const data = Cast(Doc.GetProto(document).data, ImageField); + const description = StrCast(document.caption); + if (!data) { + return undefined; + } + media.push({ + source: data.url.href, + description, + } as MediaInput); + }); + if (media.length) { + return PostToServer(RouteStore.googlePhotosMediaUpload, { media, album }); } - const image = document.createElement("img"); - image.src = field.url.href; - image.width = 200; - image.height = 200; - 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,/, ""); + return undefined; }; } \ No newline at end of file diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index c1c95fc88..6d366216e 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -40,11 +40,10 @@ 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 { @@ -131,10 +130,7 @@ 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); + this.executeGooglePhotosRoutine(); reaction(() => { let workspaces = CurrentUserUtils.UserDocument.workspaces; @@ -153,6 +149,15 @@ export class MainView extends React.Component { }, { fireImmediately: true }); } + executeGooglePhotosRoutine = async () => { + let imgurl = "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg"; + let doc = Docs.Create.ImageDocument(imgurl, { width: 200, title: "an image of a cat" }); + doc.caption = "Well isn't this a nice cat image!"; + let photos = await GooglePhotosClientUtils.endpoint(); + let albumId = (await photos.albums.list(50)).albums.filter((album: any) => album.title === "This is a generically created album!")[0].id; + console.log(await GooglePhotosClientUtils.UploadMedia([doc], { id: albumId })); + } + componentWillUnMount() { window.removeEventListener("keydown", KeyManager.Instance.handle); //close presentation diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index b671d06ea..fda9ea33f 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -133,6 +133,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe if (this.props.isOverlay) { DragManager.StartDragFunctions.push(() => FormattedTextBox.InputBoxOverlay = undefined); } + FormattedTextBox.Instance = this; } public get CurrentDiv(): HTMLDivElement { return this._ref.current!; } diff --git a/src/server/RouteStore.ts b/src/server/RouteStore.ts index 3b3fd9b4a..b221b71bc 100644 --- a/src/server/RouteStore.ts +++ b/src/server/RouteStore.ts @@ -32,7 +32,7 @@ export enum RouteStore { // APIS cognitiveServices = "/cognitiveservices", googleDocs = "/googleDocs", - googlePhotosQuery = "/googlePhotosQuery", + googlePhotosAccessToken = "/googlePhotosAccessToken", googlePhotosMediaUpload = "/googlePhotosMediaUpload" } \ No newline at end of file diff --git a/src/server/apis/google/GoogleApiServerUtils.ts b/src/server/apis/google/GoogleApiServerUtils.ts index b6330a609..ac8023ce1 100644 --- a/src/server/apis/google/GoogleApiServerUtils.ts +++ b/src/server/apis/google/GoogleApiServerUtils.ts @@ -7,6 +7,7 @@ import { GlobalOptions } from "googleapis-common"; import { GaxiosResponse } from "gaxios"; import request = require('request-promise'); import * as qs from 'query-string'; +import Photos = require('googlephotos'); /** * Server side authentication for Google Api queries. @@ -35,19 +36,19 @@ export namespace GoogleApiServerUtils { } export interface CredentialPaths { - credentials: string; - token: string; + credentialsPath: string; + tokenPath: string; } export type ApiResponse = Promise; - export type ApiRouter = (endpoint: Endpoint, paramters: any) => ApiResponse; + export type ApiRouter = (endpoint: Endpoint, parameters: any) => ApiResponse; export type ApiHandler = (parameters: any) => ApiResponse; export type Action = "create" | "retrieve" | "update"; export type Endpoint = { get: ApiHandler, create: ApiHandler, batchUpdate: ApiHandler }; export type EndpointParameters = GlobalOptions & { version: "v1" }; - export const GetEndpoint = async (sector: string, paths: CredentialPaths) => { + export const GetEndpoint = (sector: string, paths: CredentialPaths) => { return new Promise>(resolve => { RetrieveCredentials(paths).then(authentication => { let routed: Opt; @@ -65,19 +66,19 @@ export namespace GoogleApiServerUtils { }); }; - export const RetrieveCredentials = async (paths: CredentialPaths) => { + export const RetrieveCredentials = (paths: CredentialPaths) => { return new Promise((resolve, reject) => { - readFile(paths.credentials, async (err, credentials) => { + readFile(paths.credentialsPath, async (err, credentials) => { if (err) { reject(err); return console.log('Error loading client secret file:', err); } - authorize(parseBuffer(credentials), paths.token).then(resolve, reject); + authorize(parseBuffer(credentials), paths.tokenPath).then(resolve, reject); }); }); }; - export const RetrieveAccessToken = async (paths: CredentialPaths) => { + export const RetrieveAccessToken = (paths: CredentialPaths) => { return new Promise((resolve, reject) => { RetrieveCredentials(paths).then( credentials => resolve(credentials.token.access_token!), @@ -86,6 +87,15 @@ export namespace GoogleApiServerUtils { }); }; + export const RetrievePhotosEndpoint = (paths: CredentialPaths) => { + return new Promise((resolve, reject) => { + RetrieveAccessToken(paths).then( + token => resolve(new Photos(token)), + reject + ); + }); + }; + type TokenResult = { token: Credentials, client: OAuth2Client }; /** * Create an OAuth2 client with the given credentials, and returns the promise resolving to the authenticated client @@ -126,7 +136,7 @@ export namespace GoogleApiServerUtils { request.post(url, headerParameters).then(response => { let parsed = JSON.parse(response); credentials.access_token = parsed.access_token; - credentials.expiry_date = new Date().getTime() + parsed.expires_in * 1000; + credentials.expiry_date = new Date().getTime() + (parsed.expires_in * 1000); writeFile(token_path, JSON.stringify(credentials), (err) => { if (err) { console.error(err); diff --git a/src/server/apis/google/GooglePhotosServerUtils.ts b/src/server/apis/google/GooglePhotosServerUtils.ts deleted file mode 100644 index cb5464abc..000000000 --- a/src/server/apis/google/GooglePhotosServerUtils.ts +++ /dev/null @@ -1,68 +0,0 @@ -import request = require('request-promise'); -import { Album } from './typings/albums'; -import * as qs from 'query-string'; - -const apiEndpoint = "https://photoslibrary.googleapis.com/v1/"; - -export interface Authorization { - token: string; -} - -export namespace GooglePhotos { - - export type Query = Album.Query; - export type QueryParameters = { query: GooglePhotos.Query }; - interface DispatchParameters { - required: boolean; - method: "GET" | "POST"; - ignore?: boolean; - } - - export const ExecuteQuery = async (parameters: Authorization & QueryParameters): Promise => { - let action = parameters.query.action; - let dispatch = SuffixMap.get(action)!; - let suffix = Suffix(parameters, dispatch, action); - if (suffix) { - let query: any = parameters.query; - let options: any = { - headers: { 'Content-Type': 'application/json' }, - auth: { 'bearer': parameters.token }, - }; - if (query.body) { - options.body = query.body; - options.json = true; - } - let queryParameters = query.parameters; - if (queryParameters) { - suffix += `?${qs.stringify(queryParameters)}`; - } - let dispatcher = dispatch.method === "POST" ? request.post : request.get; - return dispatcher(apiEndpoint + suffix, options); - } - }; - - const Suffix = (parameters: QueryParameters, dispatch: DispatchParameters, action: Album.Action) => { - let query: any = parameters.query; - let id = query.albumId; - let suffix = 'albums'; - if (dispatch.required) { - if (!id) { - return undefined; - } - suffix += `/${id}${dispatch.ignore ? "" : `:${action}`}`; - } - return suffix; - }; - - const SuffixMap = new Map([ - [Album.Action.AddEnrichment, { required: true, method: "POST" }], - [Album.Action.BatchAddMediaItems, { required: true, method: "POST" }], - [Album.Action.BatchRemoveMediaItems, { required: true, method: "POST" }], - [Album.Action.Create, { required: false, method: "POST" }], - [Album.Action.Get, { required: true, ignore: true, method: "GET" }], - [Album.Action.List, { required: false, method: "GET" }], - [Album.Action.Share, { required: true, method: "POST" }], - [Album.Action.Unshare, { required: true, method: "POST" }] - ]); - -} diff --git a/src/server/apis/google/GooglePhotosUploadUtils.ts b/src/server/apis/google/GooglePhotosUploadUtils.ts index cd2a586eb..3b513aaf1 100644 --- a/src/server/apis/google/GooglePhotosUploadUtils.ts +++ b/src/server/apis/google/GooglePhotosUploadUtils.ts @@ -1,28 +1,122 @@ import request = require('request-promise'); -import { Authorization } from './GooglePhotosServerUtils'; +import { GoogleApiServerUtils } from './GoogleApiServerUtils'; +import * as fs from 'fs'; +import { Utils } from '../../../Utils'; +import * as path from 'path'; +import { Opt } from '../../../new_fields/Doc'; export namespace GooglePhotosUploadUtils { - interface UploadInformation { - title: string; - MEDIA_BINARY_DATA: string; + export interface Paths { + uploadDirectory: string; + credentialsPath: string; + tokenPath: string; } - const apiEndpoint = "https://photoslibrary.googleapis.com/v1/uploads"; + export interface MediaInput { + description: string; + source: string; + } + + export interface DownloadInformation { + mediaPath: string; + contentType?: string; + contentSize?: string; + } + + const prepend = (extension: string) => `https://photoslibrary.googleapis.com/v1/${extension}`; + const headers = (type: string) => ({ + 'Content-Type': `application/${type}`, + 'Authorization': Bearer, + }); + + let Bearer: string; + let Paths: Paths; - export const SubmitUpload = async (parameters: Authorization & UploadInformation) => { - let options = { + export const initialize = async (paths: Paths) => { + Paths = paths; + const { tokenPath, credentialsPath } = paths; + const token = await GoogleApiServerUtils.RetrieveAccessToken({ tokenPath, credentialsPath }); + Bearer = `Bearer ${token}`; + }; + + export const DispatchGooglePhotosUpload = async (filename: string) => { + let body: Buffer; + if (filename.includes('upload_')) { + const mediaPath = Paths.uploadDirectory + filename; + body = await new Promise((resolve, reject) => { + fs.readFile(mediaPath, (error, data) => error ? reject(error) : resolve(data)); + }); + } else { + body = await request(filename, { encoding: null }); + } + const parameters = { + method: 'POST', headers: { - 'Content-Type': 'application/octet-stream', - Authorization: `Bearer ${parameters.token}`, - 'X-Goog-Upload-File-Name': parameters.title, + ...headers('octet-stream'), + 'X-Goog-Upload-File-Name': filename, 'X-Goog-Upload-Protocol': 'raw' }, - body: { MEDIA_BINARY_DATA: parameters.MEDIA_BINARY_DATA }, - json: true + uri: prepend('uploads'), + body }; - const result = await request.post(apiEndpoint, options); - return result; + return new Promise(resolve => request(parameters, (error, _response, body) => resolve(error ? undefined : body))); + }; + + export const CreateMediaItems = (newMediaItems: any[], album?: { id: string }) => { + return new Promise((resolve, reject) => { + const parameters = { + method: 'POST', + headers: headers('json'), + uri: prepend('mediaItems:batchCreate'), + body: { newMediaItems } as any, + json: true + }; + album && (parameters.body.albumId = album.id); + request(parameters, (error, _response, body) => { + if (error) { + reject(error); + } else { + resolve(body); + } + }); + }); }; + export namespace IOUtils { + + export const Download = async (url: string): Promise> => { + const filename = `temporary_upload_${Utils.GenerateGuid()}${path.extname(url).toLowerCase()}`; + const temporaryDirectory = Paths.uploadDirectory + "temporary/"; + const mediaPath = temporaryDirectory + filename; + + if (!(await createIfNotExists(temporaryDirectory))) { + return undefined; + } + + return new Promise((resolve, reject) => { + request.head(url, (error, res) => { + if (error) { + return reject(error); + } + const information: DownloadInformation = { + mediaPath, + contentType: res.headers['content-type'], + contentSize: res.headers['content-length'], + }; + request(url).pipe(fs.createWriteStream(mediaPath)).on('close', () => resolve(information)); + }); + }); + }; + + export const createIfNotExists = async (path: string) => { + if (await new Promise(resolve => fs.exists(path, resolve))) { + return true; + } + return new Promise(resolve => fs.mkdir(path, error => resolve(error === null))); + }; + + export const Destroy = (mediaPath: string) => new Promise(resolve => fs.unlink(mediaPath, error => resolve(error === null))); + } + } \ No newline at end of file diff --git a/src/server/apis/google/typings/albums.ts b/src/server/apis/google/typings/albums.ts deleted file mode 100644 index f3025567d..000000000 --- a/src/server/apis/google/typings/albums.ts +++ /dev/null @@ -1,150 +0,0 @@ -export namespace Album { - - export type Query = (AddEnrichment | BatchAddMediaItems | BatchRemoveMediaItems | Create | Get | List | Share | Unshare); - - export enum Action { - AddEnrichment = "addEnrichment", - BatchAddMediaItems = "batchAddMediaItems", - BatchRemoveMediaItems = "batchRemoveMediaItems", - Create = "create", - Get = "get", - List = "list", - Share = "share", - Unshare = "unshare" - } - - export interface AddEnrichment { - action: Action.AddEnrichment; - albumId: string; - body: { - newEnrichmentItem: NewEnrichmentItem; - albumPosition: MediaRelativeAlbumPosition; - }; - } - - export interface BatchAddMediaItems { - action: Action.BatchAddMediaItems; - albumId: string; - body: { - mediaItemIds: string[]; - }; - } - - export interface BatchRemoveMediaItems { - action: Action.BatchRemoveMediaItems; - albumId: string; - body: { - mediaItemIds: string[]; - }; - } - - export interface Create { - action: Action.Create; - body: { - album: Template; - }; - } - - export interface Get { - action: Action.Get; - albumId: string; - } - - export interface List { - action: Action.List; - parameters: ListOptions; - } - - export interface ListOptions { - pageSize: number; - pageToken: string; - excludeNonAppCreatedData: boolean; - } - - export interface Share { - action: Action.Share; - albumId: string; - body: { - sharedAlbumOptions: SharedOptions; - }; - } - - export interface Unshare { - action: Action.Unshare; - albumId: string; - } - - export interface Template { - title: string; - } - - export interface Model { - id: string; - title: string; - productUrl: string; - isWriteable: boolean; - shareInfo: ShareInfo; - mediaItemsCount: string; - coverPhotoBaseUrl: string; - coverPhotoMediaItemId: string; - } - - export interface ShareInfo { - sharedAlbumOptions: SharedOptions; - shareableUrl: string; - shareToken: string; - isJoined: boolean; - isOwned: boolean; - } - - export interface SharedOptions { - isCollaborative: boolean; - isCommentable: boolean; - } - - export enum PositionType { - POSITION_TYPE_UNSPECIFIED, - FIRST_IN_ALBUM, - LAST_IN_ALBUM, - AFTER_MEDIA_ITEM, - AFTER_ENRICHMENT_ITEM - } - - export type Position = GeneralAlbumPosition | MediaRelativeAlbumPosition | EnrichmentRelativeAlbumPosition; - - interface GeneralAlbumPosition { - position: PositionType.FIRST_IN_ALBUM | PositionType.LAST_IN_ALBUM | PositionType.POSITION_TYPE_UNSPECIFIED; - } - - interface MediaRelativeAlbumPosition { - position: PositionType.AFTER_MEDIA_ITEM; - relativeMediaItemId: string; - } - - interface EnrichmentRelativeAlbumPosition { - position: PositionType.AFTER_ENRICHMENT_ITEM; - relativeEnrichmentItemId: string; - } - - export interface Location { - locationName: string; - latlng: { - latitude: number, - longitude: number - }; - } - - export interface NewEnrichmentItem { - textEnrichment: { - text: string; - }; - locationEnrichment: { - location: Location - }; - mapEnrichment: { - origin: { location: Location }, - destination: { location: Location } - }; - } - -} \ No newline at end of file diff --git a/src/server/credentials/google_docs_token.json b/src/server/credentials/google_docs_token.json index 2ac972ed8..f3c8cf82a 100644 --- a/src/server/credentials/google_docs_token.json +++ b/src/server/credentials/google_docs_token.json @@ -1 +1 @@ -{"access_token":"ya29.Glt5ByifP30HMz6a1fEG77qZlz9fBEOCz4PQ1VA8t_Ck2ZTPJKoyr6xc3-GFZISAqrrw2U8XpMyZv02_URfPwUX0Z_tMdlIFqsygowR-uClukbgQPNtgxp2LS1oW","refresh_token":"1/wK1cUVLnt581ba_pYGPdlTvAa-OS64nB5m5XOXEBJ8Q","scope":"https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/photoslibrary.sharing https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/photoslibrary.appendonly https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/photoslibrary","token_type":"Bearer","expiry_date":1567556163894} \ No newline at end of file +{"access_token":"ya29.Glx7B9S6zCKDE0EgYk9xX9-RhcN8j4IwG9ONopTl1NkPX9FUOw0GI_81mY9bhaouuyOTnrc6FrZD5SDHolWwp3ABNT6l7TmhTLDILgGXIixZkWFRBPpF-xHC8lUd8A","refresh_token":"1/HTv_xFHszu2Nf3iiFrUTaeKzC_Vp2-6bpIB06xW_WHI","scope":"https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/photoslibrary https://www.googleapis.com/auth/photoslibrary.appendonly https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/photoslibrary.sharing","token_type":"Bearer","expiry_date":1567765465073} \ No newline at end of file diff --git a/src/server/index.ts b/src/server/index.ts index 3940bbd58..fab00a02d 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -42,7 +42,6 @@ var AdmZip = require('adm-zip'); import * as YoutubeApi from "./apis/youtube/youtubeApiSample"; import { Response } from 'express-serve-static-core'; import { GoogleApiServerUtils } from "./apis/google/GoogleApiServerUtils"; -import { GooglePhotos } from './apis/google/GooglePhotosServerUtils'; import { GooglePhotosUploadUtils } from './apis/google/GooglePhotosUploadUtils'; const MongoStore = require('connect-mongo')(session); const mongoose = require('mongoose'); @@ -418,10 +417,10 @@ app.get("/thumbnail/:filename", (req, res) => { let filename = req.params.filename; let noExt = filename.substring(0, filename.length - ".png".length); let pagenumber = parseInt(noExt.split('-')[1]); - fs.exists(uploadDir + filename, (exists: boolean) => { - console.log(`${uploadDir + filename} ${exists ? "exists" : "does not exist"}`); + fs.exists(uploadDirectory + filename, (exists: boolean) => { + console.log(`${uploadDirectory + filename} ${exists ? "exists" : "does not exist"}`); if (exists) { - let input = fs.createReadStream(uploadDir + filename); + let input = fs.createReadStream(uploadDirectory + filename); probe(input, (err: any, result: any) => { if (err) { console.log(err); @@ -432,7 +431,7 @@ app.get("/thumbnail/:filename", (req, res) => { }); } else { - LoadPage(uploadDir + filename.substring(0, filename.length - noExt.split('-')[1].length - ".PNG".length - 1) + ".pdf", pagenumber, res); + LoadPage(uploadDirectory + filename.substring(0, filename.length - noExt.split('-')[1].length - ".PNG".length - 1) + ".pdf", pagenumber, res); } }); }); @@ -556,13 +555,13 @@ class NodeCanvasFactory { const pngTypes = [".png", ".PNG"]; const pdfTypes = [".pdf", ".PDF"]; const jpgTypes = [".jpg", ".JPG", ".jpeg", ".JPEG"]; -const uploadDir = __dirname + "/public/files/"; +const uploadDirectory = __dirname + "/public/files/"; // SETTERS app.post( RouteStore.upload, (req, res) => { let form = new formidable.IncomingForm(); - form.uploadDir = uploadDir; + form.uploadDir = uploadDirectory; form.keepExtensions = true; // let path = req.body.path; console.log("upload"); @@ -592,7 +591,7 @@ app.post( } if (isImage) { resizers.forEach(resizer => { - fs.createReadStream(uploadDir + file).pipe(resizer.resizer).pipe(fs.createWriteStream(uploadDir + file.substring(0, file.length - ext.length) + resizer.suffix + ext)); + fs.createReadStream(uploadDirectory + file).pipe(resizer.resizer).pipe(fs.createWriteStream(uploadDirectory + file.substring(0, file.length - ext.length) + resizer.suffix + ext)); }); } names.push(`/files/` + file); @@ -611,7 +610,7 @@ addSecureRoute( res.status(401).send("incorrect parameters specified"); return; } - imageDataUri.outputFile(uri, uploadDir + filename).then((savedName: string) => { + imageDataUri.outputFile(uri, uploadDirectory + filename).then((savedName: string) => { const ext = path.extname(savedName); let resizers = [ { resizer: sharp().resize(100, undefined, { withoutEnlargement: true }), suffix: "_s" }, @@ -632,7 +631,7 @@ addSecureRoute( } if (isImage) { resizers.forEach(resizer => { - fs.createReadStream(savedName).pipe(resizer.resizer).pipe(fs.createWriteStream(uploadDir + filename + resizer.suffix + ext)); + fs.createReadStream(savedName).pipe(resizer.resizer).pipe(fs.createWriteStream(uploadDirectory + filename + resizer.suffix + ext)); }); } res.send("/files/" + filename + ext); @@ -799,8 +798,8 @@ function HandleYoutubeQuery([query, callback]: [YoutubeQueryInput, (result?: any } } -const credentials = path.join(__dirname, "./credentials/google_docs_credentials.json"); -const token = path.join(__dirname, "./credentials/google_docs_token.json"); +const credentialsPath = path.join(__dirname, "./credentials/google_docs_credentials.json"); +const tokenPath = path.join(__dirname, "./credentials/google_docs_token.json"); const EndpointHandlerMap = new Map([ ["create", (api, params) => api.create(params)], @@ -811,7 +810,7 @@ const EndpointHandlerMap = new Map { let sector: any = req.params.sector; let action: any = req.params.action; - GoogleApiServerUtils.GetEndpoint(GoogleApiServerUtils.Service[sector], { credentials, token }).then(endpoint => { + GoogleApiServerUtils.GetEndpoint(GoogleApiServerUtils.Service[sector], { credentialsPath, tokenPath }).then(endpoint => { let handler = EndpointHandlerMap.get(action); if (endpoint && handler) { let execute = handler(endpoint, req.body).then( @@ -825,36 +824,28 @@ app.post(RouteStore.googleDocs + "/:sector/:action", (req, res) => { }); }); -app.post(RouteStore.googlePhotosQuery, (req, res) => { - GoogleApiServerUtils.RetrieveAccessToken({ credentials, token }).then( - token => { - GooglePhotos.ExecuteQuery({ token, query: req.body }) - .then(response => { - if (response === undefined) { - res.send("Error: unable to build suffix for Google Photos API request"); - return; - } - res.send(response); - }) - .catch(error => { - res.send(`Error: an exception occurred in the execution of this Google Photos API request\n${error}`); - }); - }, - error => res.send(error) - ); -}); +app.get(RouteStore.googlePhotosAccessToken, (req, res) => GoogleApiServerUtils.RetrieveAccessToken({ credentialsPath, tokenPath }).then(token => res.send(token))); -app.post(RouteStore.googlePhotosMediaUpload, (req, res) => { - GoogleApiServerUtils.RetrieveAccessToken({ credentials, token }).then( - token => { - GooglePhotosUploadUtils.SubmitUpload({ token, ...req.body }) - .then(response => { - res.send(response); - }).catch(error => { - res.send(`Error: an exception occurred in uploading the given media\n${error}`); - }); - }, - error => res.send(error)); +const tokenError = "Unable to successfully upload bytes for all images!"; +const mediaError = "Unable to convert all uploaded bytes to media items!"; + +app.post(RouteStore.googlePhotosMediaUpload, async (req, res) => { + const media: GooglePhotosUploadUtils.MediaInput[] = req.body.media; + await GooglePhotosUploadUtils.initialize({ uploadDirectory, credentialsPath, tokenPath }); + const newMediaItems = await Promise.all(media.map(async element => { + const uploadToken = await GooglePhotosUploadUtils.DispatchGooglePhotosUpload(element.source); + return !uploadToken ? undefined : { + description: element.description, + simpleMediaItem: { uploadToken } + }; + })); + if (!newMediaItems.every(item => item)) { + return res.send(tokenError); + } + GooglePhotosUploadUtils.CreateMediaItems(newMediaItems, req.body.album).then( + success => res.send(success), + () => res.send(mediaError) + ); }); const suffixMap: { [type: string]: (string | [string, string | ((json: any) => any)]) } = { -- cgit v1.2.3-70-g09d2 From 628eef55533118b1f2312b86b2ac5f7b64f7fc4a Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Tue, 10 Sep 2019 20:01:55 -0400 Subject: lots of refactoring, beginning autotagging --- src/Utils.ts | 5 + .../apis/google_docs/GooglePhotosClientUtils.ts | 339 ++++++++++++++------- .../util/Import & Export/DirectoryImportBox.tsx | 5 +- src/client/views/MainView.tsx | 18 +- src/client/views/nodes/DocumentView.tsx | 7 +- src/client/views/nodes/FormattedTextBox.tsx | 6 +- src/new_fields/RichTextUtils.ts | 13 +- src/server/apis/google/GooglePhotosUploadUtils.ts | 7 +- src/server/apis/google/SharedTypes.ts | 21 ++ src/server/credentials/google_docs_token.json | 2 +- src/server/index.ts | 21 +- 11 files changed, 284 insertions(+), 160 deletions(-) create mode 100644 src/server/apis/google/SharedTypes.ts (limited to 'src/client/views/nodes/FormattedTextBox.tsx') diff --git a/src/Utils.ts b/src/Utils.ts index 959b89fe5..71d88683a 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -37,6 +37,11 @@ export class Utils { public static prepend(extension: string): string { return window.location.origin + extension; } + + public static fileUrl(filename: string): string { + return this.prepend(`/file/${filename}`); + } + public static CorsProxy(url: string): string { return this.prepend(RouteStore.corsProxy + "/") + encodeURIComponent(url); } diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts index fddcf3aa5..b1de24d1a 100644 --- a/src/client/apis/google_docs/GooglePhotosClientUtils.ts +++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts @@ -1,8 +1,8 @@ import { PostToServer, Utils } from "../../../Utils"; import { RouteStore } from "../../../server/RouteStore"; import { ImageField } from "../../../new_fields/URLField"; -import { Cast } from "../../../new_fields/Types"; -import { Doc, Opt } from "../../../new_fields/Doc"; +import { Cast, StrCast } from "../../../new_fields/Types"; +import { Doc, Opt, DocListCastAsync } from "../../../new_fields/Doc"; import { Id } from "../../../new_fields/FieldSymbols"; import Photos = require('googlephotos'); import { RichTextField } from "../../../new_fields/RichTextField"; @@ -10,32 +10,15 @@ import { RichTextUtils } from "../../../new_fields/RichTextUtils"; import { EditorState } from "prosemirror-state"; import { FormattedTextBox } from "../../views/nodes/FormattedTextBox"; import { Docs, DocumentOptions } from "../../documents/Documents"; -import { type } from "os"; - -export namespace GooglePhotosClientUtils { - - export enum ContentCategories { - NONE = 'NONE', - LANDSCAPES = 'LANDSCAPES', - RECEIPTS = 'RECEIPTS', - CITYSCAPES = 'CITYSCAPES', - LANDMARKS = 'LANDMARKS', - SELFIES = 'SELFIES', - PEOPLE = 'PEOPLE', - PETS = 'PETS', - WEDDINGS = 'WEDDINGS', - BIRTHDAYS = 'BIRTHDAYS', - DOCUMENTS = 'DOCUMENTS', - TRAVEL = 'TRAVEL', - ANIMALS = 'ANIMALS', - FOOD = 'FOOD', - SPORT = 'SPORT', - NIGHT = 'NIGHT', - PERFORMANCES = 'PERFORMANCES', - WHITEBOARDS = 'WHITEBOARDS', - SCREENSHOTS = 'SCREENSHOTS', - UTILITY = 'UTILITY' - } +import { MediaItemCreationResult, NewMediaItemResult, MediaItem } from "../../../server/apis/google/SharedTypes"; + +export namespace GooglePhotos { + + const endpoint = async () => { + const getToken = Utils.prepend(RouteStore.googlePhotosAccessToken); + const token = await (await fetch(getToken)).text(); + return new Photos(token); + }; export enum MediaType { ALL_MEDIA = 'ALL_MEDIA', @@ -44,118 +27,238 @@ export namespace GooglePhotosClientUtils { } export type AlbumReference = { id: string } | { title: string }; - export const endpoint = () => fetch(Utils.prepend(RouteStore.googlePhotosAccessToken)).then(async response => new Photos(await response.text())); export interface MediaInput { url: string; description: string; } - export const UploadImages = async (sources: (Doc | string)[], album?: AlbumReference, descriptionKey = "caption") => { - if (album && "title" in album) { - album = await (await endpoint()).albums.create(album.title); + export const ContentCategories = { + NONE: 'NONE', + LANDSCAPES: 'LANDSCAPES', + RECEIPTS: 'RECEIPTS', + CITYSCAPES: 'CITYSCAPES', + LANDMARKS: 'LANDMARKS', + SELFIES: 'SELFIES', + PEOPLE: 'PEOPLE', + PETS: 'PETS', + WEDDINGS: 'WEDDINGS', + BIRTHDAYS: 'BIRTHDAYS', + DOCUMENTS: 'DOCUMENTS', + TRAVEL: 'TRAVEL', + ANIMALS: 'ANIMALS', + FOOD: 'FOOD', + SPORT: 'SPORT', + NIGHT: 'NIGHT', + PERFORMANCES: 'PERFORMANCES', + WHITEBOARDS: 'WHITEBOARDS', + SCREENSHOTS: 'SCREENSHOTS', + UTILITY: 'UTILITY' + }; + + export namespace Export { + + export interface AlbumCreationResult { + albumId: string; + mediaItems: MediaItem[]; } - const media: MediaInput[] = []; - sources.forEach(source => { - let url: string; - let description: string; - if (source instanceof Doc) { - const data = Cast(Doc.GetProto(source).data, ImageField); - if (!data) { - return; + + export const CollectionToAlbum = async (collection: Doc, title?: string, descriptionKey?: string): Promise> => { + const dataDocument = Doc.GetProto(collection); + const images = ((await DocListCastAsync(dataDocument.data)) || []).filter(doc => Cast(doc.data, ImageField)); + if (!images || !images.length) { + return undefined; + } + const resolved = title ? title : (StrCast(collection.title) || `Dash Collection (${collection[Id]}`); + const { id } = await Create.Album(resolved); + const result = await Transactions.UploadImages(images, { id }, descriptionKey); + if (result) { + const mediaItems = result.newMediaItemResults.map(item => item.mediaItem); + return { albumId: id, mediaItems }; + } + }; + + } + + export namespace Import { + + export type CollectionConstructor = (data: Array, options: DocumentOptions, ...args: any) => Doc; + + export const CollectionFromSearch = async (constructor: CollectionConstructor, requested: Opt>): Promise => { + let response = await Query.Search(requested); + let uploads = await Transactions.WriteMediaItemsToServer(response); + const children = uploads.map((upload: Transactions.UploadInformation) => { + let document = Docs.Create.ImageDocument(Utils.fileUrl(upload.fileNames.clean)); + document.fillColumn = true; + document.contentSize = upload.contentSize; + return document; + }); + const options = { width: 500, height: 500 }; + return constructor(children, options); + }; + + } + + export namespace Query { + + export const AppendImageMetadata = (sources: (Doc | string)[]) => { + let keys = Object.keys(ContentCategories); + let included: string[] = []; + let excluded: string[] = []; + for (let i = 0; i < keys.length; i++) { + for (let j = 0; j < keys.length; j++) { + let value = ContentCategories[keys[i] as keyof typeof ContentCategories]; + if (j === i) { + included.push(value); + } else { + excluded.push(value); + } } - url = data.url.href; - description = parseDescription(source, descriptionKey); - } else { - url = source; - description = Utils.GenerateGuid(); + //... + included = excluded = []; } - media.push({ url, description }); - }); - if (media.length) { - return PostToServer(RouteStore.googlePhotosMediaUpload, { media, album }); + }; + + interface DateRange { + after: Date; + before: Date; } - }; - const parseDescription = (document: Doc, descriptionKey: string) => { - let description: string = Utils.prepend("/doc/" + document[Id]); - const target = document[descriptionKey]; - if (typeof target === "string") { - description = target; - } else if (target instanceof RichTextField) { - description = RichTextUtils.ToPlainText(EditorState.fromJSON(FormattedTextBox.Instance.config, JSON.parse(target.Data))); + const DefaultSearchOptions: SearchOptions = { + pageSize: 20, + included: [], + excluded: [], + date: undefined, + includeArchivedMedia: true, + type: MediaType.ALL_MEDIA, + }; + + export interface SearchOptions { + pageSize: number; + included: ContentCategories[]; + excluded: ContentCategories[]; + date: Opt; + includeArchivedMedia: boolean; + type: MediaType; } - return description; - }; - export interface DateRange { - after: Date; - before: Date; - } - export interface SearchOptions { - pageSize: number; - included: ContentCategories[]; - excluded: ContentCategories[]; - date: Opt; - includeArchivedMedia: boolean; - type: MediaType; + export interface SearchResponse { + mediaItems: any[]; + nextPageToken: string; + } + + export const Search = async (requested: Opt>): Promise => { + const options = requested || DefaultSearchOptions; + const photos = await endpoint(); + const filters = new photos.Filters(options.includeArchivedMedia === undefined ? true : options.includeArchivedMedia); + + const included = options.included || []; + const excluded = options.excluded || []; + const contentFilter = new photos.ContentFilter(); + included.length && included.forEach(category => contentFilter.addIncludedContentCategories(category)); + excluded.length && excluded.forEach(category => contentFilter.addExcludedContentCategories(category)); + filters.setContentFilter(contentFilter); + + const date = options.date; + if (date) { + const dateFilter = new photos.DateFilter(); + if (date instanceof Date) { + dateFilter.addDate(date); + } else { + dateFilter.addRange(date.after, date.before); + } + filters.setDateFilter(dateFilter); + } + + filters.setMediaTypeFilter(new photos.MediaTypeFilter(options.type || MediaType.ALL_MEDIA)); + + return new Promise(resolve => { + photos.mediaItems.search(filters, options.pageSize || 20).then(resolve); + }); + }; + + export const GetImage = async (mediaItemId: string): Promise => { + return (await endpoint()).mediaItems.get(mediaItemId); + }; + } - const DefaultSearchOptions: SearchOptions = { - pageSize: 20, - included: [], - excluded: [], - date: undefined, - includeArchivedMedia: true, - type: MediaType.ALL_MEDIA, - }; + export namespace Create { + + export const Album = async (title: string) => { + return (await endpoint()).albums.create(title); + }; - export interface SearchResponse { - mediaItems: any[]; - nextPageToken: string; } - export type CollectionConstructor = (data: Array, options: DocumentOptions, ...args: any) => Doc; - export const CollectionFromSearch = async (provider: CollectionConstructor, requested: Opt>): Promise => { - let downloads = await Search(requested); - return provider(downloads.map((download: any) => { - let document = Docs.Create.ImageDocument(Utils.prepend(`/files/${download.fileNames.clean}`)); - document.fillColumn = true; - document.contentSize = download.contentSize; - return document; - }), { width: 500, height: 500 }); - }; + export namespace Transactions { - export const Search = async (requested: Opt>): Promise => { - const options = requested || DefaultSearchOptions; - const photos = await endpoint(); - const filters = new photos.Filters(options.includeArchivedMedia === undefined ? true : options.includeArchivedMedia); - - const included = options.included || []; - const excluded = options.excluded || []; - const contentFilter = new photos.ContentFilter(); - included.length && included.forEach(category => contentFilter.addIncludedContentCategories(category)); - excluded.length && excluded.forEach(category => contentFilter.addExcludedContentCategories(category)); - filters.setContentFilter(contentFilter); - - const date = options.date; - if (date) { - const dateFilter = new photos.DateFilter(); - if (date instanceof Date) { - dateFilter.addDate(date); - } else { - dateFilter.addRange(date.after, date.before); - } - filters.setDateFilter(dateFilter); + export interface UploadInformation { + mediaPaths: string[]; + fileNames: { [key: string]: string }; + contentSize?: number; + contentType?: string; + } + + export interface MediaItem { + id: string; + filename: string; + baseUrl: string; } - filters.setMediaTypeFilter(new photos.MediaTypeFilter(options.type || MediaType.ALL_MEDIA)); + export const WriteMediaItemsToServer = async (body: { mediaItems: any[] }): Promise => { + const uploads = await PostToServer(RouteStore.googlePhotosMediaDownload, body); + return uploads; + }; - return new Promise(resolve => { - photos.mediaItems.search(filters, options.pageSize || 20).then(async (response: SearchResponse) => { - response && resolve(await PostToServer(RouteStore.googlePhotosMediaDownload, response)); + export const UploadThenFetch = async (sources: (Doc | string)[], album?: AlbumReference, descriptionKey = "caption") => { + const result = await UploadImages(sources, album, descriptionKey); + if (!result) { + return undefined; + } + const baseUrls: string[] = await Promise.all(result.newMediaItemResults.map((result: any) => { + return new Promise(resolve => Query.GetImage(result.mediaItem.id).then(item => resolve(item.baseUrl))); + })); + return baseUrls; + }; + + export const UploadImages = async (sources: (Doc | string)[], album?: AlbumReference, descriptionKey = "caption"): Promise> => { + if (album && "title" in album) { + album = await Create.Album(album.title); + } + const media: MediaInput[] = []; + sources.forEach(source => { + let url: string; + let description: string; + if (source instanceof Doc) { + const data = Cast(Doc.GetProto(source).data, ImageField); + if (!data) { + return; + } + url = data.url.href; + description = parseDescription(source, descriptionKey); + } else { + url = source; + description = Utils.GenerateGuid(); + } + media.push({ url, description }); }); - }); - }; + if (media.length) { + return PostToServer(RouteStore.googlePhotosMediaUpload, { media, album }); + } + }; + + const parseDescription = (document: Doc, descriptionKey: string) => { + let description: string = Utils.prepend("/doc/" + document[Id]); + const target = document[descriptionKey]; + if (typeof target === "string") { + description = target; + } else if (target instanceof RichTextField) { + description = RichTextUtils.ToPlainText(EditorState.fromJSON(FormattedTextBox.Instance.config, JSON.parse(target.Data))); + } + return description; + }; + + } } \ No newline at end of file diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx index a19fd39b7..d58c02ce5 100644 --- a/src/client/util/Import & Export/DirectoryImportBox.tsx +++ b/src/client/util/Import & Export/DirectoryImportBox.tsx @@ -18,7 +18,7 @@ import { Id } from "../../../new_fields/FieldSymbols"; import { List } from "../../../new_fields/List"; import { Cast, BoolCast, NumCast } from "../../../new_fields/Types"; import { listSpec } from "../../../new_fields/Schema"; -import { GooglePhotosClientUtils } from "../../apis/google_docs/GooglePhotosClientUtils"; +import { GooglePhotos } from "../../apis/google_docs/GooglePhotosClientUtils"; const unsupported = ["text/html", "text/plain"]; interface FileResponse { @@ -117,8 +117,7 @@ export default class DirectoryImportBox extends React.Component console.log(`(${this.quota - this.remaining}/${this.quota}) ${upload.name}`); })); - await GooglePhotosClientUtils.UploadImages(docs, { title: directory }); - console.log("Finished upload!"); + await GooglePhotos.Transactions.UploadImages(docs, { title: directory }); for (let i = 0; i < docs.length; i++) { let doc = docs[i]; diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 0c0ed9072..8d10a91ce 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -40,7 +40,7 @@ import { PreviewCursor } from './PreviewCursor'; import { FilterBox } from './search/FilterBox'; import PresModeMenu from './presentationview/PresentationModeMenu'; import { PresBox } from './nodes/PresBox'; -import { GooglePhotosClientUtils } from '../apis/google_docs/GooglePhotosClientUtils'; +import { GooglePhotos } from '../apis/google_docs/GooglePhotosClientUtils'; import { ImageField } from '../../new_fields/URLField'; import { LinkFollowBox } from './linking/LinkFollowBox'; import { DocumentManager } from '../util/DocumentManager'; @@ -149,14 +149,14 @@ export class MainView extends React.Component { }, { fireImmediately: true }); } - executeGooglePhotosRoutine = async () => { - let imgurl = "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg"; - let doc = Docs.Create.ImageDocument(imgurl, { width: 200, title: "an image of a cat" }); - doc.caption = "Well isn't this a nice cat image!"; - let photos = await GooglePhotosClientUtils.endpoint(); - let albumId = (await photos.albums.list(50)).albums.filter((album: any) => album.title === "This is a generically created album!")[0].id; - console.log(await GooglePhotosClientUtils.UploadImages([doc], { id: albumId })); - } + // executeGooglePhotosRoutine = async () => { + // let imgurl = "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg"; + // let doc = Docs.Create.ImageDocument(imgurl, { width: 200, title: "an image of a cat" }); + // doc.caption = "Well isn't this a nice cat image!"; + // let photos = await GooglePhotos.endpoint(); + // let albumId = (await photos.albums.list(50)).albums.filter((album: any) => album.title === "This is a generically created album!")[0].id; + // console.log(await GooglePhotos.UploadImages([doc], { id: albumId })); + // } componentWillUnMount() { window.removeEventListener("keydown", KeyManager.Instance.handle); diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index cb9346a8b..a51f783ad 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -41,7 +41,7 @@ import "./DocumentView.scss"; import { FormattedTextBox } from './FormattedTextBox'; import React = require("react"); import { DocumentType } from '../../documents/DocumentTypes'; -import { GooglePhotosClientUtils } from '../../apis/google_docs/GooglePhotosClientUtils'; +import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils'; import { ImageField } from '../../../new_fields/URLField'; const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this? @@ -591,7 +591,10 @@ export class DocumentView extends DocComponent(Docu subitems.push({ description: "Open Fields", event: this.fieldsClicked, icon: "layer-group" }); cm.addItem({ description: "Open...", subitems: subitems, icon: "external-link-alt" }); if (Cast(this.props.Document.data, ImageField)) { - cm.addItem({ description: "Export to Google Photos", event: () => GooglePhotosClientUtils.UploadImages([this.props.Document]), icon: "caret-square-right" }); + cm.addItem({ description: "Export to Google Photos", event: () => GooglePhotos.Transactions.UploadImages([this.props.Document]), icon: "caret-square-right" }); + } + if (Cast(Doc.GetProto(this.props.Document).data, listSpec(Doc))) { + cm.addItem({ description: "Export to Google Photos Album", event: () => GooglePhotos.Export.CollectionToAlbum(this.props.Document).then(console.log), icon: "caret-square-right" }); } let existingMake = ContextMenu.Instance.findByDescription("Make..."); let makes: ContextMenuProps[] = existingMake && "subitems" in existingMake ? existingMake.subitems : []; diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index fda9ea33f..9d83fbd04 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -177,7 +177,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe syncNodeSelection(view: any, sel: any) { if (sel instanceof NodeSelection) { var desc = view.docView.descAt(sel.from); - if (desc != view.lastSelectedViewDesc) { + if (desc !== view.lastSelectedViewDesc) { if (view.lastSelectedViewDesc) { view.lastSelectedViewDesc.deselectNode(); view.lastSelectedViewDesc = null; @@ -463,7 +463,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } let redo = async () => { if (this._editorView && reference) { - let content = RichTextUtils.GoogleDocs.Export(this._editorView.state); + let content = await 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); @@ -636,7 +636,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe image(node, view, getPos) { return new ImageResizeView(node, view, getPos); }, star(node, view, getPos) { return new SummarizedView(node, view, getPos); }, ordered_list(node, view, getPos) { return new OrderedListView(node, view, getPos); }, - footnote(node, view, getPos) { return new FootnoteView(node, view, getPos) } + footnote(node, view, getPos) { return new FootnoteView(node, view, getPos); } }, clipboardTextSerializer: this.clipboardTextSerializer, handlePaste: this.handlePaste, diff --git a/src/new_fields/RichTextUtils.ts b/src/new_fields/RichTextUtils.ts index 500b93676..27737782b 100644 --- a/src/new_fields/RichTextUtils.ts +++ b/src/new_fields/RichTextUtils.ts @@ -11,7 +11,7 @@ import { Utils, PostToServer } from "../Utils"; import { RouteStore } from "../server/RouteStore"; import { Docs } from "../client/documents/Documents"; import { schema } from "../client/util/RichTextSchema"; -import { GooglePhotosClientUtils } from "../client/apis/google_docs/GooglePhotosClientUtils"; +import { GooglePhotos } from "../client/apis/google_docs/GooglePhotosClientUtils"; export namespace RichTextUtils { @@ -90,7 +90,7 @@ export namespace RichTextUtils { export namespace GoogleDocs { - export const Export = (state: EditorState): GoogleApiClientUtils.Docs.Content => { + export const Export = async (state: EditorState): Promise => { let nodes: { [type: string]: Node[] } = { text: [], image: [] @@ -107,7 +107,7 @@ export namespace RichTextUtils { } })); let linkRequests = ExtractLinks(nodes.text); - let imageRequests = ExtractImages(nodes.image); + let imageRequests = await ExtractImages(nodes.image); return { text, requests: [...linkRequests, ...imageRequests] @@ -298,10 +298,13 @@ export namespace RichTextUtils { const length = node.nodeSize; const attrs = node.attrs; const uri = attrs.src; - const result = (await GooglePhotosClientUtils.UploadImages([uri])).newMediaItemResults; + const baseUrls = await GooglePhotos.Transactions.UploadThenFetch([uri]); + if (!baseUrls) { + continue; + } images.push({ insertInlineImage: { - uri: result[0].mediaItem.productUrl, + uri: baseUrls[0], objectSize: { width: { magnitude: parseFloat(attrs.width.replace("px", "")), unit: "PT" } }, location: { index: position + length } } diff --git a/src/server/apis/google/GooglePhotosUploadUtils.ts b/src/server/apis/google/GooglePhotosUploadUtils.ts index d1f1f81bd..447ed23ac 100644 --- a/src/server/apis/google/GooglePhotosUploadUtils.ts +++ b/src/server/apis/google/GooglePhotosUploadUtils.ts @@ -5,6 +5,7 @@ import { Utils } from '../../../Utils'; import * as path from 'path'; import { Opt } from '../../../new_fields/Doc'; import * as sharp from 'sharp'; +import { MediaItemCreationResult } from './SharedTypes'; const uploadDirectory = path.join(__dirname, "../../public/files/"); @@ -52,8 +53,10 @@ export namespace GooglePhotosUploadUtils { return new Promise(resolve => request(parameters, (error, _response, body) => resolve(error ? undefined : body))); }; - export const CreateMediaItems = (newMediaItems: any[], album?: { id: string }) => { - return new Promise((resolve, reject) => { + + + export const CreateMediaItems = (newMediaItems: any[], album?: { id: string }): Promise => { + return new Promise((resolve, reject) => { const parameters = { method: 'POST', headers: headers('json'), diff --git a/src/server/apis/google/SharedTypes.ts b/src/server/apis/google/SharedTypes.ts new file mode 100644 index 000000000..9ad6130b6 --- /dev/null +++ b/src/server/apis/google/SharedTypes.ts @@ -0,0 +1,21 @@ +export interface NewMediaItemResult { + uploadToken: string; + status: { code: number, message: string }; + mediaItem: MediaItem; +} + +export interface MediaItem { + id: string; + description: string; + productUrl: string; + baseUrl: string; + mimeType: string; + mediaMetadata: { + creationTime: string; + width: string; + height: string; + }; + filename: string; +} + +export type MediaItemCreationResult = { newMediaItemResults: NewMediaItemResult[] }; \ No newline at end of file diff --git a/src/server/credentials/google_docs_token.json b/src/server/credentials/google_docs_token.json index 5c142fba1..22d57d744 100644 --- a/src/server/credentials/google_docs_token.json +++ b/src/server/credentials/google_docs_token.json @@ -1 +1 @@ -{"access_token":"ya29.Glx_B6G7Q_FYs1LK5VcyV6P6Zg9JkoHO2aC_TsnN7AVxPYWHEpsBSC0WyWX7Ztr8HWhOUYA5JXqnZDkLrK1V3Hb-0GgtyApLRNtEPOWf1dJ7lOm_iKVw2tRvPe7XDQ","refresh_token":"1/HTv_xFHszu2Nf3iiFrUTaeKzC_Vp2-6bpIB06xW_WHI","scope":"https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/photoslibrary https://www.googleapis.com/auth/photoslibrary.appendonly https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/photoslibrary.sharing","token_type":"Bearer","expiry_date":1568078116605} \ No newline at end of file +{"access_token":"ya29.GlyAB5T3dgJqWuYBcLaT94wQo7MZkmzJQZxDB2sSU95mdhW24E3diuFdLeNsUDVI57D3S765RweMnL98d-fdgu1dRxpzkV_J_3rLih99pZ8A4d6jVdm1354UT4py_w","refresh_token":"1/HTv_xFHszu2Nf3iiFrUTaeKzC_Vp2-6bpIB06xW_WHI","scope":"https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/photoslibrary https://www.googleapis.com/auth/photoslibrary.appendonly https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/photoslibrary.sharing","token_type":"Bearer","expiry_date":1568161931458} \ No newline at end of file diff --git a/src/server/index.ts b/src/server/index.ts index 8469770d5..2c3e76c55 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -795,19 +795,6 @@ const EndpointHandlerMap = new Map api.batchUpdate(params)], ]); -// app.post(RouteStore.googleDocsGet, async (req, res) => { -// const token = await GoogleApiServerUtils.RetrieveAccessToken({ credentialsPath, tokenPath }); -// request_promise.get({ -// uri: `https://docs.googleapis.com/v1/documents/${req.body.documentId}?fields=inlineObjects`, -// headers: { -// 'Authorization': `Bearer ${token}` -// } -// }).then(response => { -// console.log(response); -// res.send(response); -// }); -// }); - app.post(RouteStore.googleDocs + "/:sector/:action", (req, res) => { let sector: GoogleApiServerUtils.Service = req.params.sector; let action: GoogleApiServerUtils.Action = req.params.action; @@ -841,11 +828,11 @@ app.post(RouteStore.googlePhotosMediaUpload, async (req, res) => { }; })); if (!newMediaItems.every(item => item)) { - return res.status(STATUS.EXECUTION_ERROR).send(tokenError); + return _error(res, tokenError); } GooglePhotosUploadUtils.CreateMediaItems(newMediaItems, req.body.album).then( - success => res.status(STATUS.OK).send(success), - () => res.status(STATUS.EXECUTION_ERROR).send(mediaError) + mediaItems => _success(res, mediaItems), + error => _error(res, mediaError, error) ); }); @@ -871,7 +858,7 @@ app.post(RouteStore.googlePhotosMediaDownload, async (req, res) => { _invalid(res, requestError); }); -const _error = (res: Response, message: string, error: any) => { +const _error = (res: Response, message: string, error?: any) => { res.statusMessage = message; res.status(STATUS.EXECUTION_ERROR).send(error); }; -- cgit v1.2.3-70-g09d2 From 42500ed737e17c2f973a653d59491eb33d6ba66b Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Sat, 14 Sep 2019 13:49:34 -0400 Subject: beginnings of formatting conversion on push --- src/client/views/nodes/FormattedTextBox.tsx | 3 +- src/new_fields/RichTextUtils.ts | 153 ++++++++++++++------------ src/server/credentials/google_docs_token.json | 2 +- 3 files changed, 83 insertions(+), 75 deletions(-) (limited to 'src/client/views/nodes/FormattedTextBox.tsx') diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index 8a623e648..23f615f32 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -284,10 +284,11 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe let target = de.data.embeddableSourceDoc; // We're dealing with an internal document drop let url = de.data.urlField.url.href; - let model: NodeType = (url.includes(".mov") || url.includes(".mp4")) ? schema.nodes.video : schema.nodes.image; + let model: NodeType = [".mov", ".mp4"].includes(url) ? schema.nodes.video : schema.nodes.image; let pos = this._editorView!.posAtCoords({ left: de.x, top: de.y }); this._editorView!.dispatch(this._editorView!.state.tr.insert(pos!.pos, model.create({ src: url, docid: target[Id] }))); DocUtils.MakeLink(this.dataDoc, target, undefined, "ImgRef:" + target.title, undefined, undefined, target[Id]); + this.tryUpdateHeight(); e.stopPropagation(); } else if (de.data instanceof DragManager.DocumentDragData) { const draggedDoc = de.data.draggedDocuments.length && de.data.draggedDocuments[0]; diff --git a/src/new_fields/RichTextUtils.ts b/src/new_fields/RichTextUtils.ts index 6afe4ddfd..178cce839 100644 --- a/src/new_fields/RichTextUtils.ts +++ b/src/new_fields/RichTextUtils.ts @@ -5,7 +5,7 @@ import { docs_v1 } from "googleapis"; import { GoogleApiClientUtils } from "../client/apis/google_docs/GoogleApiClientUtils"; import { FormattedTextBox } from "../client/views/nodes/FormattedTextBox"; import { Opt } from "./Doc"; -import * as Color from "color"; +import Color from "color"; import { sinkListItem } from "prosemirror-schema-list"; import { Utils, PostToServer } from "../Utils"; import { RouteStore } from "../server/RouteStore"; @@ -91,27 +91,11 @@ export namespace RichTextUtils { export namespace GoogleDocs { export const Export = async (state: EditorState): Promise => { - let nodes: { [type: string]: Node[] } = { - text: [], - image: [] - }; + const nodes: Node[] = []; let text = ToPlainText(state); - let content = state.doc.content; - content.forEach(node => node.content.forEach(node => { - const type = node.type.name; - let existing = nodes[type]; - if (existing) { - existing.push(node); - } else { - nodes[type] = [node]; - } - })); - let linkRequests = ExtractLinks(nodes.text); - let imageRequests = await ExtractImages(nodes.image); - return { - text, - requests: [...linkRequests, ...imageRequests] - }; + state.doc.content.forEach(node => node.content.forEach(child => nodes.push(child))); + const requests = await marksToStyle(nodes); + return { text, requests }; }; type BulletPosition = { value: number, sinks: number }; @@ -186,8 +170,10 @@ export namespace RichTextUtils { const uploads = await PostToServer(RouteStore.googlePhotosMediaDownload, { mediaItems: inlineUrls }); for (let i = 0; i < uploads.length; i++) { - const src = Utils.prepend(`/files/${uploads[i].fileNames.clean}`); - state = state.apply(state.tr.insert(0, schema.nodes.image.create({ src, width: inlineUrls[i].width }))); + const src = Utils.fileUrl(uploads[i].fileNames.clean); + const width = inlineUrls[i].width; + const imageNode = schema.nodes.image.create({ src, width }); + state = state.apply(state.tr.insert(0, imageNode)); } return { title, text, state }; @@ -265,71 +251,92 @@ export namespace RichTextUtils { return marks; }; - interface LinkInformation { - startIndex: number; - endIndex: number; - bold: boolean; - url: string; - } - - const ExtractLinks = (nodes: Node[]) => { - let links: docs_v1.Schema$Request[] = []; + const marksToStyle = async (nodes: Node[]) => { + let requests: docs_v1.Schema$Request[] = []; let position = 1; 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({ - startIndex: position, - endIndex: position + length, - url: link.attrs.href, - bold: false + const length = node.nodeSize; + const marks = node.marks; + const attrs = node.attrs; + const textStyle: docs_v1.Schema$TextStyle = {}; + const information: LinkInformation = { + startIndex: position, + endIndex: position + length, + textStyle + }; + if (marks.length) { + + const link = marks.find(mark => mark.type.name === "link"); + if (link) { + textStyle.link = { url: link.attrs.href }; + textStyle.foregroundColor = fromRgb(0, 0, 1); + textStyle.bold = true; + } + const bold = marks.find(mark => mark.type.name === "strong"); + bold && (textStyle.bold = true); + const foregroundColor = marks.find(mark => mark.type.name === "pFontColor"); + foregroundColor && (textStyle.foregroundColor = fromHex(foregroundColor.attrs.color)); + } + requests.push(EncodeStyleUpdate(information)); + if (node.type.name === "image") { + requests.push(await EncodeImage({ + startIndex: position + length, + uri: attrs.src, + width: attrs.width })); } position += length; } - return links; + return requests; }; - const ExtractImages = async (nodes: Node[]) => { - const images: docs_v1.Schema$Request[] = []; - let position = 1; - for (let node of nodes) { - const length = node.nodeSize; - const attrs = node.attrs; - const uri = attrs.src; - const baseUrls = await GooglePhotos.Transactions.UploadThenFetch([Docs.Create.ImageDocument(uri)]); - if (!baseUrls) { - continue; - } - images.push({ - insertInlineImage: { - uri: baseUrls[0], - objectSize: { width: { magnitude: parseFloat(attrs.width.replace("px", "")), unit: "PT" } }, - location: { index: position + length } - } - }); - position += length; - } - return images; + interface LinkInformation { + startIndex: number; + endIndex: number; + textStyle: docs_v1.Schema$TextStyle; + } + + interface ImageInformation { + startIndex: number; + width: number; + uri: string; + } + + const fromRgb = (red: number, green: number, blue: number): docs_v1.Schema$OptionalColor => { + return { color: { rgbColor: { red, green, blue } } }; + }; + + const fromHex = (color: string): docs_v1.Schema$OptionalColor => { + const converted = new Color().hex(color).rgb(); + const { red, blue, green } = converted; + return fromRgb(red(), blue(), green()); }; - const Encode = (information: LinkInformation) => { + const EncodeStyleUpdate = (information: LinkInformation): docs_v1.Schema$Request => { + const { startIndex, endIndex, textStyle } = information; return { updateTextStyle: { fields: "*", - range: { - startIndex: information.startIndex, - endIndex: information.endIndex - }, - textStyle: { - bold: true, - link: { url: information.url }, - foregroundColor: { color: { rgbColor: { red: 0.0, green: 0.0, blue: 1.0 } } } - } - } + range: { startIndex, endIndex }, + textStyle + } as docs_v1.Schema$UpdateTextStyleRequest }; }; + + const EncodeImage = async (information: ImageInformation) => { + const source = [Docs.Create.ImageDocument(information.uri)]; + const baseUrls = await GooglePhotos.Transactions.UploadThenFetch(source); + if (baseUrls) { + return { + insertInlineImage: { + uri: baseUrls[0], + objectSize: { width: { magnitude: information.width, unit: "PT" } }, + location: { index: information.startIndex } + } + }; + } + return {}; + }; } } \ No newline at end of file diff --git a/src/server/credentials/google_docs_token.json b/src/server/credentials/google_docs_token.json index 293404853..c8fd3bbf5 100644 --- a/src/server/credentials/google_docs_token.json +++ b/src/server/credentials/google_docs_token.json @@ -1 +1 @@ -{"access_token":"ya29.ImCDB68jWP3W09KYIKQwzO_9eizRTpSx9UhP4tIPbSeMvjDsDyNOOl2hmffWHBrRFqz8kgaDxl_6lp-rlfbArI0x1Wm13J2h7wm5kBvOlCToqR-5qoINHetlWisCb11ig9A","refresh_token":"1/HTv_xFHszu2Nf3iiFrUTaeKzC_Vp2-6bpIB06xW_WHI","scope":"https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/photoslibrary https://www.googleapis.com/auth/photoslibrary.appendonly https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/photoslibrary.sharing","token_type":"Bearer","expiry_date":1568480323588} \ No newline at end of file +{"access_token":"ya29.GlyDBwCmpO9R1fAOMIzpdZiCWhEeaDHiJOPy7sNRAo-vAIqzIk7zy1DLdOhSFWaBQrbmewSOJZPvbBUAxqdDELc_aW_BsjwXFbxiTd4Us_N8IWkPDCtUeBmLAZjodA","refresh_token":"1/HTv_xFHszu2Nf3iiFrUTaeKzC_Vp2-6bpIB06xW_WHI","scope":"https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/photoslibrary https://www.googleapis.com/auth/photoslibrary.appendonly https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/photoslibrary.sharing","token_type":"Bearer","expiry_date":1568484185591} \ No newline at end of file -- cgit v1.2.3-70-g09d2 From c2f749238e9db63b81c3bec08a14dd6006ab876f Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Sat, 14 Sep 2019 21:00:37 -0400 Subject: sharing routine for docs and final formatting fixes --- src/Utils.ts | 4 + .../CollectionFreeFormLinkView.tsx | 2 +- src/client/views/nodes/FormattedTextBox.tsx | 9 +- src/new_fields/RichTextUtils.ts | 153 +++++++++++++-------- src/server/credentials/google_docs_token.json | 2 +- 5 files changed, 108 insertions(+), 62 deletions(-) (limited to 'src/client/views/nodes/FormattedTextBox.tsx') diff --git a/src/Utils.ts b/src/Utils.ts index 60f18eac2..a842f5a20 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -42,6 +42,10 @@ export class Utils { return this.prepend(`/files/${filename}`); } + public static shareUrl(documentId: string): string { + return this.prepend(`/doc/${documentId}?sharing=true`); + } + public static CorsProxy(url: string): string { return this.prepend(RouteStore.corsProxy + "/") + encodeURIComponent(url); } diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx index f19243bd6..12771d11e 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx @@ -50,7 +50,7 @@ export class CollectionFreeFormLinkView extends React.Component {/* this.props.addDocTab(document, undefined, location ? location : "onRight"), NumCast((jumpToDoc === linkDoc.anchor2 ? linkDoc.anchor2Page : linkDoc.anchor1Page))); + return; + // } } if (targetContext) { DocumentManager.Instance.jumpToDocument(targetContext, ctrlKey, false, document => this.props.addDocTab(document, undefined, location ? location : "inTab")); diff --git a/src/new_fields/RichTextUtils.ts b/src/new_fields/RichTextUtils.ts index 84744db2f..555c41b67 100644 --- a/src/new_fields/RichTextUtils.ts +++ b/src/new_fields/RichTextUtils.ts @@ -4,7 +4,7 @@ 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"; +import { Opt, Doc } from "./Doc"; import Color = require('color'); import { sinkListItem } from "prosemirror-schema-list"; import { Utils, PostToServer } from "../Utils"; @@ -12,6 +12,11 @@ import { RouteStore } from "../server/RouteStore"; import { Docs } from "../client/documents/Documents"; import { schema } from "../client/util/RichTextSchema"; import { GooglePhotos } from "../client/apis/google_docs/GooglePhotosClientUtils"; +import { SchemaHeaderField } from "./SchemaHeaderField"; +import { DocServer } from "../client/DocServer"; +import { Cast } from "./Types"; +import { Id } from "./FieldSymbols"; +import { DocumentView } from "../client/views/nodes/DocumentView"; export namespace RichTextUtils { @@ -91,9 +96,15 @@ export namespace RichTextUtils { export namespace GoogleDocs { export const Export = async (state: EditorState): Promise => { - const nodes: Node[] = []; + const nodes: (Node | null)[] = []; let text = ToPlainText(state); - state.doc.content.forEach(node => node.content.forEach(child => nodes.push(child))); + state.doc.content.forEach(node => { + if (!node.childCount) { + nodes.push(null); + } else { + node.content.forEach(child => nodes.push(child)); + } + }); const requests = await marksToStyle(nodes); return { text, requests }; }; @@ -223,30 +234,11 @@ export namespace RichTextUtils { const StyleToMark = new Map([ ["bold", "strong"], ["italic", "em"], - ["foregroundColor", "pFontColor"] - ]); - - const MarkToStyle = new Map([ - ["strong", "bold"], - ["em", "italic"], - ["pFontColor", "foregroundColor"], - ["timesNewRoman", "weightedFontFamily"], - ["georgia", "weightedFontFamily"], - ["comicSans", "weightedFontFamily"], - ["tahoma", "weightedFontFamily"], - ["impact", "weightedFontFamily"] + ["foregroundColor", "pFontColor"], + ["fontSize", "pFontSize"] ]); - const FontFamilyMapping = new Map([ - ["timesNewRoman", "Times New Roman"], - ["arial", "Arial"], - ["georgia", "Georgia"], - ["comicSans", "Comic Sans MS"], - ["tahoma", "Tahoma"], - ["impact", "Impact"] - ]); - - const styleToMarks = (schema: any, textStyle?: docs_v1.Schema$TextStyle): Opt => { + const styleToMarks = (schema: any, textStyle?: docs_v1.Schema$TextStyle) => { if (!textStyle) { return undefined; } @@ -263,20 +255,53 @@ export namespace RichTextUtils { let object = value.color.rgbColor; attributes.color = Color.rgb(["red", "green", "blue"].map(color => object[color] * 255 || 0)).hex(); } + if (value.magnitude) { + attributes.fontSize = value.magnitude; + } - let mark = schema.mark(schema.marks[converted], attributes); + let mapped = schema.marks[converted]; + if (!mapped) { + alert(`No mapping found for ${converted}!`); + return; + } + + let mark = schema.mark(mapped, attributes); mark && marks.push(mark); } }); return marks; }; + const MarkToStyle = new Map([ + ["strong", "bold"], + ["em", "italic"], + ["pFontColor", "foregroundColor"], + ["timesNewRoman", "weightedFontFamily"], + ["georgia", "weightedFontFamily"], + ["comicSans", "weightedFontFamily"], + ["tahoma", "weightedFontFamily"], + ["impact", "weightedFontFamily"] + ]); + + const FontFamilyMapping = new Map([ + ["timesNewRoman", "Times New Roman"], + ["arial", "Arial"], + ["georgia", "Georgia"], + ["comicSans", "Comic Sans MS"], + ["tahoma", "Tahoma"], + ["impact", "Impact"] + ]); + const ignored = ["user_mark"]; - const marksToStyle = async (nodes: Node[]): Promise => { + const marksToStyle = async (nodes: (Node | null)[]): Promise => { let requests: docs_v1.Schema$Request[] = []; let position = 1; for (let node of nodes) { + if (node === null) { + position += 2; + continue; + } const { marks, attrs, nodeSize } = node; const textStyle: docs_v1.Schema$TextStyle = {}; const information: LinkInformation = { @@ -284,37 +309,55 @@ export namespace RichTextUtils { endIndex: position + nodeSize, textStyle }; - if (marks.length) { - let mark: Mark; - const markMap = BuildMarkMap(marks); - Object.keys(schema.marks).map(markName => { - if (!ignored.includes(markName) && (mark = markMap[markName])) { - const converted = MarkToStyle.get(markName) || markName as keyof docs_v1.Schema$TextStyle; - let value: any = true; - if (converted) { - const { attrs } = mark; - switch (converted) { - case "link": - value = { url: attrs.href }; - textStyle.foregroundColor = fromRgb.blue; - textStyle.bold = true; - break; - case "fontSize": - value = attrs.fontSize; - break; - case "foregroundColor": - value = fromHex(attrs.color); - break; - case "weightedFontFamily": - value = { fontFamily: FontFamilyMapping.get(markName) }; + let mark: Mark; + const markMap = BuildMarkMap(marks); + for (let markName of Object.keys(schema.marks)) { + if (ignored.includes(markName) || !(mark = markMap[markName])) { + continue; + } + let converted = MarkToStyle.get(markName) || markName as keyof docs_v1.Schema$TextStyle; + let value: any = true; + if (!converted) { + continue; + } + const { attrs } = mark; + switch (converted) { + case "link": + let url = attrs.href; + const delimiter = "/doc/"; + const alreadyShared = "?sharing=true"; + if (new RegExp(window.location.origin + delimiter).test(url) && !url.endsWith(alreadyShared)) { + const linkDoc = await DocServer.GetRefField(url.split(delimiter)[1]); + if (linkDoc instanceof Doc) { + const target = (await Cast(linkDoc.anchor2, Doc))!; + const exported = Doc.MakeAlias(target); + DocumentView.makeCustomViewClicked(exported); + target && (url = Utils.shareUrl(exported[Id])); + linkDoc.anchor2 = exported; } - textStyle[converted] = value; } - } - }); - if (Object.keys(textStyle).length) { - requests.push(EncodeStyleUpdate(information)); + value = { url }; + textStyle.foregroundColor = fromRgb.blue; + textStyle.bold = true; + break; + case "fontSize": + value = attrs.fontSize; + break; + case "foregroundColor": + value = fromHex(attrs.color); + break; + case "weightedFontFamily": + value = { fontFamily: FontFamilyMapping.get(markName) }; + } + let matches: RegExpExecArray | null; + if ((matches = /p(\d+)/g.exec(markName)) !== null) { + converted = "fontSize"; + value = { magnitude: parseInt(matches[1]), unit: "PT" }; } + textStyle[converted] = value; + } + if (Object.keys(textStyle).length) { + requests.push(EncodeStyleUpdate(information)); } if (node.type.name === "image") { requests.push(await EncodeImage({ diff --git a/src/server/credentials/google_docs_token.json b/src/server/credentials/google_docs_token.json index 330d27141..265c07c69 100644 --- a/src/server/credentials/google_docs_token.json +++ b/src/server/credentials/google_docs_token.json @@ -1 +1 @@ -{"access_token":"ya29.GlyEBxgqsCRjX9SAJGGss3EtfrPgwSjeMsfsuwJqTk7o4GRrBpwU0eQXXBNgPdAPRSrJzuVgAqWxap9kKrtkpf2tuHxk7Ml9Jblj48tU0BN2X0lMh66S2EIRhLnQnw","refresh_token":"1/HTv_xFHszu2Nf3iiFrUTaeKzC_Vp2-6bpIB06xW_WHI","scope":"https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/photoslibrary https://www.googleapis.com/auth/photoslibrary.appendonly https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/photoslibrary.sharing","token_type":"Bearer","expiry_date":1568501067486} \ No newline at end of file +{"access_token":"ya29.ImCEByJgpv8e3CNTi-EwwqOtXUB1sNsOyOxM4WEyTybrQzCKc80SkjQZgb9gFCChbA7fFsdvewVAS_SiohfFziPOV4-YffeO417NS2CQf1cksmCQQBWxmL3i7qvQgz4VkdI","refresh_token":"1/HTv_xFHszu2Nf3iiFrUTaeKzC_Vp2-6bpIB06xW_WHI","scope":"https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/photoslibrary https://www.googleapis.com/auth/photoslibrary.appendonly https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/photoslibrary.sharing","token_type":"Bearer","expiry_date":1568509688650} \ No newline at end of file -- cgit v1.2.3-70-g09d2 From d7012323e87c21a25c29d89d66a1c54b99c8b458 Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Sun, 15 Sep 2019 19:28:10 -0400 Subject: syncing of images in imported google doc --- .../apis/google_docs/GoogleApiClientUtils.ts | 19 ++-- src/client/views/nodes/FormattedTextBox.tsx | 6 +- src/new_fields/RichTextUtils.ts | 115 +++++++++++++++------ src/server/apis/google/GooglePhotosUploadUtils.ts | 56 ++++++---- src/server/apis/google/existing_uploads.json | 1 + src/server/credentials/google_docs_token.json | 2 +- src/server/index.ts | 30 ++++-- 7 files changed, 162 insertions(+), 67 deletions(-) create mode 100644 src/server/apis/google/existing_uploads.json (limited to 'src/client/views/nodes/FormattedTextBox.tsx') diff --git a/src/client/apis/google_docs/GoogleApiClientUtils.ts b/src/client/apis/google_docs/GoogleApiClientUtils.ts index 828d4451a..cbc5da15b 100644 --- a/src/client/apis/google_docs/GoogleApiClientUtils.ts +++ b/src/client/apis/google_docs/GoogleApiClientUtils.ts @@ -97,25 +97,30 @@ export namespace GoogleApiClientUtils { 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(""); + let text = paragraphs.map(paragraph => paragraph.contents.filter(content => !("inlineObjectId" in content)).map(run => run as docs_v1.Schema$TextRun).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 }; + export type ContentArray = (docs_v1.Schema$TextRun | docs_v1.Schema$InlineObjectElement)[]; + export type DeconstructedParagraph = { contents: ContentArray, bullet: Opt }; 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) { - let runs: docs_v1.Schema$TextRun[] = []; + let runs: ContentArray = []; let bullet: Opt; 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 (inner) { + if (inner.textRun) { + let run = inner.textRun; + (run.content || !filterEmpty) && runs.push(inner.textRun); + } else if (inner.inlineObjectElement) { + runs.push(inner.inlineObjectElement); + } } } } @@ -123,7 +128,7 @@ export namespace GoogleApiClientUtils { bullet = element.paragraph.bullet.nestingLevel || 0; } } - (runs.length || !filterEmpty) && fragments.push({ runs, bullet }); + (runs.length || !filterEmpty) && fragments.push({ contents: runs, bullet }); } } return fragments; diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index fc5b27220..8f0f142c4 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -193,8 +193,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe else DocUtils.MakeLink(this.dataDoc, this.dataDoc[key] as Doc, undefined, "Ref:" + value, undefined, undefined, id); }); }); - const link = this._editorView!.state.schema.marks.link.create({ href: `http://localhost:1050/doc/${id}`, location: "onRight", title: value }); - const mval = this._editorView!.state.schema.marks.metadataVal.create(); + const link = this._editorView.state.schema.marks.link.create({ href: `http://localhost:1050/doc/${id}`, location: "onRight", title: value }); + const mval = this._editorView.state.schema.marks.metadataVal.create(); let offset = (tx.selection.to === range!.end - 1 ? -1 : 0); tx = tx.addMark(textEndSelection - value.length + offset, textEndSelection, link).addMark(textEndSelection - value.length + offset, textEndSelection, mval); this.dataDoc[key] = value; @@ -506,7 +506,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe let documentId = StrCast(dataDoc[GoogleRef]); let exportState: Opt; if (documentId) { - exportState = await RichTextUtils.GoogleDocs.Import(documentId); + exportState = await RichTextUtils.GoogleDocs.Import(documentId, dataDoc); } UndoManager.RunInBatch(() => handler(exportState, dataDoc), Pulls); } diff --git a/src/new_fields/RichTextUtils.ts b/src/new_fields/RichTextUtils.ts index 555c41b67..ab5e677c8 100644 --- a/src/new_fields/RichTextUtils.ts +++ b/src/new_fields/RichTextUtils.ts @@ -14,9 +14,10 @@ import { schema } from "../client/util/RichTextSchema"; import { GooglePhotos } from "../client/apis/google_docs/GooglePhotosClientUtils"; import { SchemaHeaderField } from "./SchemaHeaderField"; import { DocServer } from "../client/DocServer"; -import { Cast } from "./Types"; +import { Cast, StrCast } from "./Types"; import { Id } from "./FieldSymbols"; import { DocumentView } from "../client/views/nodes/DocumentView"; +import { AssertionError } from "assert"; export namespace RichTextUtils { @@ -109,45 +110,78 @@ export namespace RichTextUtils { return { text, requests }; }; + interface ImageTemplate { + width: number; + title: string; + url: string; + } + + const parseInlineObjects = async (document: docs_v1.Schema$Document): Promise> => { + const inlineObjectMap = new Map(); + const inlineObjects = document.inlineObjects; + + if (inlineObjects) { + const objects = Object.keys(inlineObjects).map(objectId => inlineObjects[objectId]); + const mediaItems: MediaItem[] = objects.map(object => { + const embeddedObject = object.inlineObjectProperties!.embeddedObject!; + const baseUrl = embeddedObject.imageProperties!.contentUri!; + const filename = `upload_${Utils.GenerateGuid()}.png`; + return { baseUrl, filename }; + }); + + const uploads = await PostToServer(RouteStore.googlePhotosMediaDownload, { mediaItems }); + + if (uploads.length !== mediaItems.length) { + throw new AssertionError({ expected: mediaItems.length, actual: uploads.length, message: "Error with internally uploading inlineObjects!" }); + } + + for (let i = 0; i < objects.length; i++) { + const object = objects[i]; + const { fileNames } = uploads[i]; + const embeddedObject = object.inlineObjectProperties!.embeddedObject!; + const size = embeddedObject.size!; + const width = size.width!.magnitude!; + const url = Utils.fileUrl(fileNames.clean); + + inlineObjectMap.set(object.objectId!, { + title: embeddedObject.title || `Imported Image from ${document.title}`, + width, + url + }); + } + } + return inlineObjectMap; + }; + type BulletPosition = { value: number, sinks: number }; interface MediaItem { baseUrl: string; filename: string; - width: number; } - export const Import = async (documentId: GoogleApiClientUtils.Docs.DocumentId): Promise> => { + + export const Import = async (documentId: GoogleApiClientUtils.Docs.DocumentId, textNote: Doc): Promise> => { const document = await GoogleApiClientUtils.Docs.retrieve({ documentId }); if (!document) { return undefined; } - + const inlineObjectMap = await parseInlineObjects(document); const title = document.title!; const { text, paragraphs } = GoogleApiClientUtils.Docs.Utils.extractText(document); let state = FormattedTextBox.blankState(); let structured = parseLists(paragraphs); - const inline = document.inlineObjects; - let inlineUrls: MediaItem[] = []; - if (inline) { - inlineUrls = Object.keys(inline).map(key => { - const embedded = inline[key].inlineObjectProperties!.embeddedObject!; - const baseUrl = embedded.imageProperties!.contentUri!; - const filename = `upload_${Utils.GenerateGuid()}.png`; - const width = embedded.size!.width!.magnitude!; - return { baseUrl, filename, width }; - }); - } let position = 3; let lists: ListGroup[] = []; const indentMap = new Map(); let globalOffset = 0; - const nodes = structured.map(element => { + const nodes: Node[] = []; + for (let element of structured) { if (Array.isArray(element)) { lists.push(element); let positions: BulletPosition[] = []; let items = element.map(paragraph => { - let item = listItem(state.schema, paragraph.runs); + let item = listItem(state.schema, paragraph.contents); let sinks = paragraph.bullet!; positions.push({ value: position + globalOffset, @@ -158,13 +192,26 @@ export namespace RichTextUtils { return item; }); indentMap.set(element, positions); - return list(state.schema, items); + nodes.push(list(state.schema, items)); } else { - let paragraph = paragraphNode(state.schema, element.runs); - position += paragraph.nodeSize; - return paragraph; + if (element.contents.some(child => "inlineObjectId" in child)) { + let node: Node; + for (const child of element.contents) { + if ("inlineObjectId" in child) { + node = imageNode(state.schema, inlineObjectMap.get(child.inlineObjectId!)!, textNote); + } else { + node = paragraphNode(state.schema, [child]); + } + nodes.push(node); + position += node.nodeSize; + } + } else { + let paragraph = paragraphNode(state.schema, element.contents); + nodes.push(paragraph); + position += paragraph.nodeSize; + } } - }); + } state = state.apply(state.tr.replaceWith(0, 2, nodes)); let sink = sinkListItem(state.schema.nodes.list_item); @@ -179,14 +226,6 @@ export namespace RichTextUtils { } } - const uploads = await PostToServer(RouteStore.googlePhotosMediaDownload, { mediaItems: inlineUrls }); - for (let i = 0; i < uploads.length; i++) { - const src = Utils.fileUrl(uploads[i].fileNames.clean); - const width = inlineUrls[i].width; - const imageNode = schema.nodes.image.create({ src, width }); - state = state.apply(state.tr.insert(0, imageNode)); - } - return { title, text, state }; }; @@ -226,6 +265,22 @@ export namespace RichTextUtils { return schema.node("paragraph", null, fragment); }; + const imageNode = (schema: any, image: ImageTemplate, textNote: Doc) => { + const { url: src, width } = image; + let docid: string; + const guid = Utils.GenerateDeterministicGuid(src); + const backingDocId = StrCast(textNote[guid]); + if (!backingDocId) { + const backingDoc = Docs.Create.ImageDocument(src, { width: 300, height: 300 }); + DocumentView.makeCustomViewClicked(backingDoc); + docid = backingDoc[Id]; + textNote[guid] = docid; + } else { + docid = backingDocId; + } + return schema.node("image", { src, width, docid }); + }; + const textNode = (schema: any, run: docs_v1.Schema$TextRun) => { let text = run.content!.removeTrailingNewlines(); return text.length ? schema.text(text, styleToMarks(schema, run.textStyle)) : undefined; diff --git a/src/server/apis/google/GooglePhotosUploadUtils.ts b/src/server/apis/google/GooglePhotosUploadUtils.ts index f582cebd2..3ab9ba90f 100644 --- a/src/server/apis/google/GooglePhotosUploadUtils.ts +++ b/src/server/apis/google/GooglePhotosUploadUtils.ts @@ -118,28 +118,44 @@ export namespace DownloadUtils { const generate = (prefix: string, url: string) => `${prefix}upload_${Utils.GenerateGuid()}${path.extname(url).toLowerCase()}`; const sanitize = (filename: string) => filename.replace(/\s+/g, "_"); - export const UploadImage = async (url: string, filename?: string, prefix = ""): Promise> => { - const resolved = filename ? sanitize(filename) : generate(prefix, url); - let extension = path.extname(url) || path.extname(resolved); + export interface InspectionResults { + isLocal: boolean; + stream: any; + normalizedUrl: string; + contentSize: number; + contentType: string; + } + + export const InspectImage = async (url: string) => { + const { isLocal, stream, normalized: normalizedUrl } = classify(url); + const metadata = (await new Promise((resolve, reject) => { + request.head(url, async (error, res) => { + if (error) { + return reject(error); + } + resolve(res); + }); + })).headers; + return { + contentSize: parseInt(metadata[size]), + contentType: metadata[type], + isLocal, + stream, + normalizedUrl + }; + }; + + export const UploadImage = async (metadata: InspectionResults, filename?: string, prefix = ""): Promise> => { + const { isLocal, stream, normalizedUrl, contentSize, contentType } = metadata; + const resolved = filename ? sanitize(filename) : generate(prefix, normalizedUrl); + let extension = path.extname(normalizedUrl) || path.extname(resolved); extension && (extension = extension.toLowerCase()); let information: UploadInformation = { mediaPaths: [], - fileNames: { clean: resolved } + fileNames: { clean: resolved }, + contentSize, + contentType, }; - const { isLocal, stream, normalized } = classify(url); - url = normalized; - if (!isLocal) { - const metadata = (await new Promise((resolve, reject) => { - request.head(url, async (error, res) => { - if (error) { - return reject(error); - } - resolve(res); - }); - })).headers; - information.contentSize = parseInt(metadata[size]); - information.contentType = metadata[type]; - } return new Promise(async (resolve, reject) => { const resizers = [ { resizer: sharp().rotate(), suffix: "_o" }, @@ -164,7 +180,7 @@ export namespace DownloadUtils { const filename = resolved.substring(0, resolved.length - extension.length) + suffix + extension; information.mediaPaths.push(mediaPath = uploadDirectory + filename); information.fileNames[suffix] = filename; - stream(url).pipe(resizer.resizer).pipe(fs.createWriteStream(mediaPath)) + stream(normalizedUrl).pipe(resizer.resizer).pipe(fs.createWriteStream(mediaPath)) .on('close', resolve) .on('error', reject); }); @@ -172,7 +188,7 @@ export namespace DownloadUtils { } if (!isLocal || nonVisual) { await new Promise(resolve => { - stream(url).pipe(fs.createWriteStream(uploadDirectory + resolved)).on('close', resolve); + stream(normalizedUrl).pipe(fs.createWriteStream(uploadDirectory + resolved)).on('close', resolve); }); } resolve(information); diff --git a/src/server/apis/google/existing_uploads.json b/src/server/apis/google/existing_uploads.json new file mode 100644 index 000000000..05c20c33b --- /dev/null +++ b/src/server/apis/google/existing_uploads.json @@ -0,0 +1 @@ +{"23625":{"mediaPaths":["C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_7e2d5fef-860a-49a8-b9ec-b91f28073180_o.png","C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_7e2d5fef-860a-49a8-b9ec-b91f28073180_s.png","C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_7e2d5fef-860a-49a8-b9ec-b91f28073180_m.png","C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_7e2d5fef-860a-49a8-b9ec-b91f28073180_l.png"],"fileNames":{"clean":"upload_7e2d5fef-860a-49a8-b9ec-b91f28073180.png","_o":"upload_7e2d5fef-860a-49a8-b9ec-b91f28073180_o.png","_s":"upload_7e2d5fef-860a-49a8-b9ec-b91f28073180_s.png","_m":"upload_7e2d5fef-860a-49a8-b9ec-b91f28073180_m.png","_l":"upload_7e2d5fef-860a-49a8-b9ec-b91f28073180_l.png"},"contentSize":23625,"contentType":"image/jpeg"}} \ No newline at end of file diff --git a/src/server/credentials/google_docs_token.json b/src/server/credentials/google_docs_token.json index 4f2fb0f9d..c10b0797f 100644 --- a/src/server/credentials/google_docs_token.json +++ b/src/server/credentials/google_docs_token.json @@ -1 +1 @@ -{"access_token":"ya29.GlyEB-6kaRm7dCD9x3j1b5AyujXvfpS5NWuJQwy6UKLO06KYXcF2e5XaCxvR7QJgH3Pn2iu3btjYrrJxNNaLffgEszcJHNsN_5IIWJBA4sdG6KLW63MmFwfV4U1hyQ","refresh_token":"1/HTv_xFHszu2Nf3iiFrUTaeKzC_Vp2-6bpIB06xW_WHI","scope":"https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/photoslibrary https://www.googleapis.com/auth/photoslibrary.appendonly https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/photoslibrary.sharing","token_type":"Bearer","expiry_date":1568573667294} \ No newline at end of file +{"access_token":"ya29.ImCFB_ghOybVB6A4HvIIwIlyGyZw6wOymdwJyWJJECIpCmFTHNEzOAfP98KFzm5OUV2zZNS5Wx1iUT1xYWW35PY7NoZc7PWwjzmOaGkMzDm7_fxpsgjT0StdvEwTJprFIv0","refresh_token":"1/HTv_xFHszu2Nf3iiFrUTaeKzC_Vp2-6bpIB06xW_WHI","scope":"https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/photoslibrary https://www.googleapis.com/auth/photoslibrary.appendonly https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/photoslibrary.sharing","token_type":"Bearer","expiry_date":1568590984976} \ No newline at end of file diff --git a/src/server/index.ts b/src/server/index.ts index 2e60d9be7..07ce4b6f0 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -46,6 +46,7 @@ const MongoStore = require('connect-mongo')(session); const mongoose = require('mongoose'); const probe = require("probe-image-size"); import * as qs from 'query-string'; +import { Opt } from '../new_fields/Doc'; const extensions = require("../client/util/UtilExtensions"); const download = (url: string, dest: fs.PathLike) => request.get(url).pipe(fs.createWriteStream(dest)); @@ -580,7 +581,8 @@ app.post( for (const key in files) { const { type, path: location, name } = files[key]; const filename = path.basename(location); - await UploadUtils.UploadImage(uploadDirectory + filename, filename).catch(() => console.log(`Unable to process ${filename}`)); + const metadata = await UploadUtils.InspectImage(uploadDirectory + filename); + await UploadUtils.UploadImage(metadata, filename).catch(() => console.log(`Unable to process ${filename}`)); results.push({ name, type, path: `/files/${filename}` }); } _success(res, results); @@ -884,14 +886,30 @@ const prefix = "google_photos_"; const downloadError = "Encountered an error while executing downloads."; const requestError = "Unable to execute download: the body's media items were malformed."; +app.get("/gapiCleanup", (req, res) => { + write_text_file(file, ""); + res.redirect(RouteStore.delete); +}); + +const file = "./apis/google/existing_uploads.json"; app.post(RouteStore.googlePhotosMediaDownload, async (req, res) => { const contents: { mediaItems: MediaItem[] } = req.body; if (contents) { - const pending = contents.mediaItems.map(item => - UploadUtils.UploadImage(item.baseUrl, item.filename, prefix) - ); - const completed = await Promise.all(pending).catch(error => _error(res, downloadError, error)); - Array.isArray(completed) && _success(res, completed); + const completed: Opt[] = []; + const content = await read_text_file(file); + let existing = content.length ? JSON.parse(content) : {}; + for (let item of contents.mediaItems) { + const { contentSize, ...attributes } = await UploadUtils.InspectImage(item.baseUrl); + const found: UploadUtils.UploadInformation = existing[contentSize]; + if (!found) { + const upload = await UploadUtils.UploadImage({ contentSize, ...attributes }, item.filename, prefix).catch(error => _error(res, downloadError, error)); + upload && completed.push(existing[contentSize] = upload); + } else { + completed.push(found); + } + } + await write_text_file(file, JSON.stringify(existing)); + _success(res, completed); return; } _invalid(res, requestError); -- cgit v1.2.3-70-g09d2 From f82458be8bc8beaab387cc2813b7b18c9b3caac2 Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Sun, 22 Sep 2019 17:16:18 -0400 Subject: initial commit post master merge --- src/Utils.ts | 21 +- .../apis/google_docs/GooglePhotosClientUtils.ts | 7 +- src/client/documents/Documents.ts | 51 +- src/client/util/DictationManager.ts | 2 +- src/client/util/DocumentManager.ts | 44 +- src/client/util/DragManager.ts | 44 +- src/client/util/History.ts | 4 +- .../util/Import & Export/DirectoryImportBox.tsx | 1 + src/client/util/ProsemirrorExampleTransfer.ts | 4 +- src/client/util/RichTextRules.ts | 24 +- src/client/util/RichTextSchema.tsx | 22 +- src/client/util/Scripting.ts | 8 +- src/client/util/SelectionManager.ts | 4 + src/client/util/SharingManager.tsx | 2 +- src/client/util/TooltipTextMenu.tsx | 20 +- src/client/util/UndoManager.ts | 2 +- src/client/views/ContextMenu.tsx | 2 +- src/client/views/DocumentButtonBar.scss | 129 ++++ src/client/views/DocumentButtonBar.tsx | 368 ++++++++++ src/client/views/DocumentDecorations.scss | 2 +- src/client/views/DocumentDecorations.tsx | 425 ++--------- src/client/views/GlobalKeyHandler.ts | 11 +- src/client/views/InkingCanvas.scss | 1 + src/client/views/InkingControl.tsx | 34 +- src/client/views/MainOverlayTextBox.tsx | 23 +- src/client/views/MainView.tsx | 80 +- src/client/views/OverlayView.tsx | 4 +- src/client/views/ScriptBox.tsx | 2 +- src/client/views/ScriptingRepl.tsx | 20 +- src/client/views/TemplateMenu.tsx | 57 +- .../views/collections/CollectionBaseView.tsx | 24 +- .../views/collections/CollectionDockingView.tsx | 130 ++-- .../views/collections/CollectionSchemaCells.tsx | 17 +- .../CollectionSchemaMovableTableHOC.tsx | 8 +- .../views/collections/CollectionSchemaView.tsx | 22 +- .../views/collections/CollectionStackingView.tsx | 9 +- .../CollectionStackingViewFieldColumn.tsx | 13 +- src/client/views/collections/CollectionSubView.tsx | 67 +- .../views/collections/CollectionTreeView.tsx | 36 +- src/client/views/collections/CollectionView.tsx | 6 + .../views/collections/CollectionViewChromes.tsx | 17 +- .../views/collections/ParentDocumentSelector.scss | 9 + .../views/collections/ParentDocumentSelector.tsx | 46 +- .../CollectionFreeFormLinkView.scss | 3 +- .../CollectionFreeFormLinkView.tsx | 1 - .../CollectionFreeFormLinksView.tsx | 6 +- .../collectionFreeForm/CollectionFreeFormView.tsx | 474 +++++------- .../collections/collectionFreeForm/MarqueeView.tsx | 78 +- .../document_templates/image_card/ImageCard.tsx | 3 - src/client/views/linking/LinkFollowBox.tsx | 48 +- src/client/views/linking/LinkMenu.tsx | 2 +- src/client/views/linking/LinkMenuGroup.tsx | 26 +- src/client/views/linking/LinkMenuItem.tsx | 2 +- src/client/views/nodes/ButtonBox.tsx | 6 +- .../nodes/CollectionFreeFormDocumentView.scss | 5 + .../views/nodes/CollectionFreeFormDocumentView.tsx | 95 ++- src/client/views/nodes/DocumentContentsView.tsx | 9 +- src/client/views/nodes/DocumentView.scss | 42 ++ src/client/views/nodes/DocumentView.tsx | 810 ++++++++------------- src/client/views/nodes/DragBox.tsx | 10 +- src/client/views/nodes/FieldView.tsx | 4 +- src/client/views/nodes/FormattedTextBox.scss | 1 - src/client/views/nodes/FormattedTextBox.tsx | 165 +++-- src/client/views/nodes/IconBox.tsx | 52 +- src/client/views/nodes/ImageBox.tsx | 55 +- src/client/views/nodes/KeyValueBox.tsx | 6 +- src/client/views/nodes/KeyValuePair.tsx | 9 +- src/client/views/nodes/PDFBox.tsx | 106 ++- src/client/views/nodes/VideoBox.tsx | 31 +- src/client/views/pdf/Annotation.tsx | 10 +- src/client/views/pdf/PDFAnnotationLayer.scss | 6 - src/client/views/pdf/PDFAnnotationLayer.tsx | 21 - src/client/views/pdf/PDFViewer.tsx | 89 +-- src/client/views/pdf/Page.scss | 5 + src/client/views/pdf/Page.tsx | 2 +- .../views/presentationview/PresentationElement.tsx | 2 + .../views/presentationview/PresentationList.tsx | 4 +- src/client/views/search/FilterBox.tsx | 6 +- src/client/views/search/SearchItem.tsx | 8 +- src/debug/Repl.tsx | 8 +- src/new_fields/Doc.ts | 176 +++-- src/new_fields/RichTextUtils.ts | 4 +- src/new_fields/ScriptField.ts | 36 +- src/server/authentication/config/passport.ts | 2 +- .../authentication/models/current_user_utils.ts | 40 +- 85 files changed, 2212 insertions(+), 2078 deletions(-) create mode 100644 src/client/views/DocumentButtonBar.scss create mode 100644 src/client/views/DocumentButtonBar.tsx create mode 100644 src/client/views/nodes/CollectionFreeFormDocumentView.scss delete mode 100644 src/client/views/pdf/PDFAnnotationLayer.scss delete mode 100644 src/client/views/pdf/PDFAnnotationLayer.tsx (limited to 'src/client/views/nodes/FormattedTextBox.tsx') diff --git a/src/Utils.ts b/src/Utils.ts index a842f5a20..6489eff77 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -123,28 +123,23 @@ export class Utils { // Calculate hue // No difference - if (delta == 0) - h = 0; + if (delta === 0) h = 0; // Red is max - else if (cmax == r) - h = ((g - b) / delta) % 6; + else if (cmax === r) h = ((g - b) / delta) % 6; // Green is max - else if (cmax == g) - h = (b - r) / delta + 2; + else if (cmax === g) h = (b - r) / delta + 2; // Blue is max - else - h = (r - g) / delta + 4; + else h = (r - g) / delta + 4; h = Math.round(h * 60); // Make negative hues positive behind 360° - if (h < 0) - h += 360; // Calculate lightness + if (h < 0) h += 360; // Calculate lightness l = (cmax + cmin) / 2; // Calculate saturation - s = delta == 0 ? 0 : delta / (1 - Math.abs(2 * l - 1)); + s = delta === 0 ? 0 : delta / (1 - Math.abs(2 * l - 1)); // Multiply l and s by 100 // s = +(s * 100).toFixed(1); @@ -248,6 +243,10 @@ export function timenow() { return now.toLocaleDateString() + ' ' + h + ':' + m + ' ' + ampm; } +export function percent2frac(percent: string) { + return Number(percent.substr(0, percent.length - 1)) / 100; +} + export function numberRange(num: number) { return Array.from(Array(num)).map((v, i) => i); } export function returnTrue() { return true; } diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts index 671d05421..559b8fd6a 100644 --- a/src/client/apis/google_docs/GooglePhotosClientUtils.ts +++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts @@ -15,6 +15,7 @@ import { AssertionError } from "assert"; import { List } from "../../../new_fields/List"; import { listSpec } from "../../../new_fields/Schema"; import { DocumentView } from "../../views/nodes/DocumentView"; +import { DocumentManager } from "../../util/DocumentManager"; export namespace GooglePhotos { @@ -140,6 +141,8 @@ export namespace GooglePhotos { export namespace Query { const delimiter = ", "; + const comparator = (a: string, b: string) => (a < b) ? -1 : (a > b ? 1 : 0); + export const TagChildImages = async (collection: Doc) => { const idMapping = await Cast(collection.googlePhotosIdMapping, Doc); if (!idMapping) { @@ -172,7 +175,7 @@ export namespace GooglePhotos { const tags = concatenated.split(delimiter); if (tags.length > 1) { const cleaned = concatenated.replace(ContentCategories.NONE + delimiter, ""); - image.googlePhotosTags = cleaned.split(delimiter).sort((a, b) => (a < b) ? -1 : (a > b ? 1 : 0)).join(delimiter); + image.googlePhotosTags = cleaned.split(delimiter).sort(comparator).join(delimiter); } else { image.googlePhotosTags = ContentCategories.NONE; } @@ -326,7 +329,7 @@ export namespace GooglePhotos { const url = data.url.href; const target = Doc.MakeAlias(source); const description = parseDescription(target, descriptionKey); - DocumentView.makeCustomViewClicked(target); + DocumentView.makeCustomViewClicked(target, undefined); media.push({ url, description }); }); if (media.length) { diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index cfed2bf14..a7a006f47 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -20,8 +20,8 @@ import { AttributeTransformationModel } from "../northstar/core/attribute/Attrib import { AggregateFunction } from "../northstar/model/idea/idea"; import { MINIMIZED_ICON_SIZE } from "../views/globalCssVariables.scss"; import { IconBox } from "../views/nodes/IconBox"; -import { Field, Doc, Opt } from "../../new_fields/Doc"; -import { OmitKeys, JSONUtils, Utils } from "../../Utils"; +import { OmitKeys, JSONUtils } from "../../Utils"; +import { Field, Doc, Opt, DocListCastAsync } from "../../new_fields/Doc"; import { ImageField, VideoField, AudioField, PdfField, WebField, YoutubeField } from "../../new_fields/URLField"; import { HtmlField } from "../../new_fields/HtmlField"; import { List } from "../../new_fields/List"; @@ -65,7 +65,7 @@ export interface DocumentOptions { panY?: number; page?: number; scale?: number; - layout?: string; + layout?: string | Doc; isTemplate?: boolean; templates?: List; viewType?: number; @@ -121,7 +121,7 @@ export namespace Docs { }], [DocumentType.IMG, { layout: { view: ImageBox, collectionView: [CollectionView, data, anno] as CollectionViewType }, - options: { nativeWidth: 600, curPage: 0 } + options: { curPage: 0 } }], [DocumentType.WEB, { layout: { view: WebBox, collectionView: [CollectionView, data, anno] as CollectionViewType }, @@ -137,7 +137,7 @@ export namespace Docs { }], [DocumentType.VID, { layout: { view: VideoBox, collectionView: [CollectionVideoView, data, anno] as CollectionViewType }, - options: { nativeWidth: 600, curPage: 0 }, + options: { curPage: 0 }, }], [DocumentType.AUDIO, { layout: { view: AudioBox }, @@ -614,10 +614,40 @@ export namespace Docs { export namespace DocUtils { - export function MakeLink(source: Doc, target: Doc, targetContext?: Doc, title: string = "", description: string = "", sourceContext?: Doc, id?: string) { + export function Publish(promoteDoc: Doc, targetID: string, addDoc: any, remDoc: any) { + targetID = targetID.replace(/^-/, "").replace(/\([0-9]*\)$/, ""); + DocServer.GetRefField(targetID).then(doc => { + if (promoteDoc !== doc) { + let copy = doc as Doc; + if (copy) { + Doc.Overwrite(promoteDoc, copy, true); + } else { + copy = Doc.MakeCopy(promoteDoc, true, targetID); + } + !doc && (copy.title = undefined) && (Doc.GetProto(copy).title = targetID); + addDoc && addDoc(copy); + remDoc && remDoc(promoteDoc); + if (!doc) { + DocListCastAsync(promoteDoc.links).then(links => { + links && links.map(async link => { + if (link) { + let a1 = await Cast(link.anchor1, Doc); + if (a1 && Doc.AreProtosEqual(a1, promoteDoc)) link.anchor1 = copy; + let a2 = await Cast(link.anchor2, Doc); + if (a2 && Doc.AreProtosEqual(a2, promoteDoc)) link.anchor2 = copy; + LinkManager.Instance.deleteLink(link); + LinkManager.Instance.addLink(link); + } + }); + }); + } + } + }); + } + export function MakeLink(source: Doc, target: Doc, targetContext?: Doc, title: string = "", description: string = "", sourceContext?: Doc, id?: string, anchored1?: boolean) { if (LinkManager.Instance.doesLinkExist(source, target)) return undefined; let sv = DocumentManager.Instance.getDocumentView(source); - if (sv && sv.props.ContainingCollectionView && sv.props.ContainingCollectionView.props.Document === target) return; + if (sv && sv.props.ContainingCollectionDoc === target) return; if (target === CurrentUserUtils.UserDocument) return undefined; let linkDocProto = new Doc(id, true); @@ -633,16 +663,15 @@ export namespace DocUtils { linkDocProto.anchor1 = source; linkDocProto.anchor1Page = source.curPage; linkDocProto.anchor1Groups = new List([]); + linkDocProto.anchor1anchored = anchored1; linkDocProto.anchor2 = target; linkDocProto.anchor2Page = target.curPage; linkDocProto.anchor2Groups = new List([]); LinkManager.Instance.addLink(linkDocProto); - let script = `return links(this);`; - let computed = CompileScript(script, { params: { this: "Doc" }, typecheck: false }); - computed.compiled && (Doc.GetProto(source).links = new ComputedField(computed)); - computed.compiled && (Doc.GetProto(target).links = new ComputedField(computed)); + Doc.GetProto(source).links = ComputedField.MakeFunction("links(this)"); + Doc.GetProto(target).links = ComputedField.MakeFunction("links(this)"); }, "make link"); return linkDocProto; } diff --git a/src/client/util/DictationManager.ts b/src/client/util/DictationManager.ts index 0711effe6..cebb56bbe 100644 --- a/src/client/util/DictationManager.ts +++ b/src/client/util/DictationManager.ts @@ -327,7 +327,7 @@ export namespace DictationManager { ["open fields", { action: (target: DocumentView) => { let kvp = Docs.Create.KVPDocument(target.props.Document, { width: 300, height: 300 }); - target.props.addDocTab(kvp, target.dataDoc, "onRight"); + target.props.addDocTab(kvp, target.props.DataDoc, "onRight"); } }], diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index ec731da84..a3c7429b9 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -10,6 +10,7 @@ import { DocumentView } from '../views/nodes/DocumentView'; import { LinkManager } from './LinkManager'; import { undoBatch, UndoManager } from './UndoManager'; import { Scripting } from './Scripting'; +import { List } from '../../new_fields/List'; export class DocumentManager { @@ -146,6 +147,7 @@ export class DocumentManager { if (!contextDoc) { let docs = docContext ? await DocListCastAsync(docContext.data) : undefined; let found = false; + // bcz: this just searches within the context for the target -- perhaps it should recursively search through all children? docs && docs.map(d => found = found || Doc.AreProtosEqual(d, docDelegate)); if (docContext && found) { let targetContextView: DocumentView | null; @@ -154,16 +156,19 @@ export class DocumentManager { docContext.panTransformType = "Ease"; targetContextView.props.focus(docDelegate, willZoom); } else { - (dockFunc || CollectionDockingView.Instance.AddRightSplit)(docContext, undefined); + (dockFunc || CollectionDockingView.AddRightSplit)(docContext, undefined); setTimeout(() => { - this.jumpToDocument(docDelegate, willZoom, forceDockFunc, dockFunc, linkPage); - }, 10); + let dv = DocumentManager.Instance.getDocumentView(docContext); + dv && this.jumpToDocument(docDelegate, willZoom, forceDockFunc, + doc => dv!.props.focus(dv!.props.Document, true, 1, () => dv!.props.addDocTab(doc, undefined, "inPlace")), + linkPage); + }, 1050); } } else { const actualDoc = Doc.MakeAlias(docDelegate); Doc.BrushDoc(actualDoc); if (linkPage !== undefined) actualDoc.curPage = linkPage; - (dockFunc || CollectionDockingView.Instance.AddRightSplit)(actualDoc, undefined); + (dockFunc || CollectionDockingView.AddRightSplit)(actualDoc, undefined); } } else { let contextView: DocumentView | null; @@ -172,7 +177,7 @@ export class DocumentManager { contextDoc.panTransformType = "Ease"; contextView.props.focus(docDelegate, willZoom); } else { - (dockFunc || CollectionDockingView.Instance.AddRightSplit)(contextDoc, undefined); + (dockFunc || CollectionDockingView.AddRightSplit)(contextDoc, undefined); setTimeout(() => { this.jumpToDocument(docDelegate, willZoom, forceDockFunc, dockFunc, linkPage); }, 10); @@ -203,5 +208,34 @@ export class DocumentManager { return 1; } } + + @action + animateBetweenPoint = (scrpt: number[], expandedDocs: Doc[] | undefined): void => { + expandedDocs && expandedDocs.map(expDoc => { + if (expDoc.isMinimized || expDoc.isAnimating === "min") { // MAXIMIZE DOC + if (expDoc.isMinimized) { // docs are never actaully at the minimized location. so when we unminimize one, we have to set our overrides to make it look like it was at the minimize location + expDoc.isMinimized = false; + expDoc.animateToPos = new List([...scrpt, 0]); + expDoc.animateToDimensions = new List([0, 0]); + } + setTimeout(() => { + expDoc.isAnimating = "max"; + expDoc.animateToPos = new List([0, 0, 1]); + expDoc.animateToDimensions = new List([NumCast(expDoc.width), NumCast(expDoc.height)]); + setTimeout(() => expDoc.isAnimating === "max" && (expDoc.isAnimating = expDoc.animateToPos = expDoc.animateToDimensions = undefined), 600); + }, 0); + } else { // MINIMIZE DOC + expDoc.isAnimating = "min"; + expDoc.animateToPos = new List([...scrpt, 0]); + expDoc.animateToDimensions = new List([0, 0]); + setTimeout(() => { + if (expDoc.isAnimating === "min") { + expDoc.isMinimized = true; + expDoc.isAnimating = expDoc.animateToPos = expDoc.animateToDimensions = undefined; + } + }, 600); + } + }); + } } Scripting.addGlobal(function focus(doc: any) { DocumentManager.Instance.getDocumentViews(Doc.GetProto(doc)).map(view => view.props.focus(doc, true)); }); \ No newline at end of file diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index 4c9c9c17c..56496c99b 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -32,7 +32,7 @@ export function SetupDrag( document.removeEventListener("pointermove", onRowMove); document.removeEventListener('pointerup', onRowUp); let doc = await docFunc(); - var dragData = new DragManager.DocumentDragData([doc], [undefined]); + var dragData = new DragManager.DocumentDragData([doc]); dragData.dropAction = dropAction; dragData.moveDocument = moveFunc; dragData.options = options; @@ -66,7 +66,7 @@ export function SetupDrag( function moveLinkedDocument(doc: Doc, targetCollection: Doc, addDocument: (doc: Doc) => boolean): boolean { const document = SelectionManager.SelectedDocuments()[0]; - document.props.removeDocument && document.props.removeDocument(doc); + document && document.props.removeDocument && document.props.removeDocument(doc); addDocument(doc); return true; } @@ -76,7 +76,7 @@ export async function DragLinkAsDocument(dragEle: HTMLElement, x: number, y: num if (draggeddoc) { let moddrag = await Cast(draggeddoc.annotationOn, Doc); let dragdocs = moddrag ? [moddrag] : [draggeddoc]; - let dragData = new DragManager.DocumentDragData(dragdocs, dragdocs); + let dragData = new DragManager.DocumentDragData(dragdocs); dragData.moveDocument = moveLinkedDocument; DragManager.StartLinkedDocumentDrag([dragEle], dragData, x, y, { handlers: { @@ -107,7 +107,7 @@ export async function DragLinksAsDocuments(dragEle: HTMLElement, x: number, y: n if (doc) moddrag.push(doc); } let dragdocs = moddrag.length ? moddrag : draggedDocs; - let dragData = new DragManager.DocumentDragData(dragdocs, dragdocs); + let dragData = new DragManager.DocumentDragData(dragdocs); dragData.moveDocument = moveLinkedDocument; DragManager.StartLinkedDocumentDrag([dragEle], dragData, x, y, { handlers: { @@ -201,18 +201,14 @@ export namespace DragManager { export type MoveFunction = (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean; export class DocumentDragData { - constructor(dragDoc: Doc[], dragDataDocs: (Doc | undefined)[]) { + constructor(dragDoc: Doc[]) { this.draggedDocuments = dragDoc; - this.draggedDataDocs = dragDataDocs; this.droppedDocuments = dragDoc; - this.xOffset = 0; - this.yOffset = 0; + this.offset = [0, 0]; } draggedDocuments: Doc[]; - draggedDataDocs: (Doc | undefined)[]; droppedDocuments: Doc[]; - xOffset: number; - yOffset: number; + offset: number[]; dropAction: dropActionType; userDropAction: dropActionType; moveDocument?: MoveFunction; @@ -225,14 +221,13 @@ export namespace DragManager { this.dragDocument = dragDoc; this.dropDocument = dropDoc; this.annotationDocument = annotationDoc; - this.xOffset = this.yOffset = 0; + this.offset = [0, 0]; } targetContext: Doc | undefined; dragDocument: Doc; annotationDocument: Doc; dropDocument: Doc; - xOffset: number; - yOffset: number; + offset: number[]; dropAction: dropActionType; userDropAction: dropActionType; } @@ -252,21 +247,13 @@ export namespace DragManager { }); } - export function StartButtonDrag(eles: HTMLElement[], script: string, title: string, vars: { [name: string]: Field }, params: string[], initialize?: (button: Doc) => void, downX: number, downY: number, options?: DragOptions) { - let dragData = new DragManager.DocumentDragData([], [undefined]); + export function StartButtonDrag(eles: HTMLElement[], script: string, title: string, vars: { [name: string]: Field }, params: string[], initialize: (button: Doc) => void, downX: number, downY: number, options?: DragOptions) { + let dragData = new DragManager.DocumentDragData([]); runInAction(() => StartDragFunctions.map(func => func())); StartDrag(eles, dragData, downX, downY, options, options && options.finishDrag ? options.finishDrag : (dropData: { [id: string]: any }) => { let bd = Docs.Create.ButtonDocument({ width: 150, height: 50, title: title }); - let compiled = CompileScript(script, { - params: { doc: Doc.name }, - typecheck: false, - editable: true - }); - if (compiled.compiled) { - let scriptField = new ScriptField(compiled); - bd.onClick = scriptField; - } + bd.onClick = ScriptField.MakeScript(script); params.map(p => Object.keys(vars).indexOf(p) !== -1 && (Doc.GetProto(bd)[p] = new PrefetchProxy(vars[p] as Doc))); initialize && initialize(bd); bd.buttonParams = new List(params); @@ -283,7 +270,8 @@ export namespace DragManager { let droppedDocuments: Doc[] = dragData.draggedDocuments.reduce((droppedDocs: Doc[], d) => { let dvs = DocumentManager.Instance.getDocumentViews(d); if (dvs.length) { - let inContext = dvs.filter(dv => dv.props.ContainingCollectionView === SelectionManager.SelectedDocuments()[0].props.ContainingCollectionView); + let containingView = SelectionManager.SelectedDocuments()[0] ? SelectionManager.SelectedDocuments()[0].props.ContainingCollectionView : undefined; + let inContext = dvs.filter(dv => dv.props.ContainingCollectionView === containingView); if (inContext.length) { inContext.forEach(dv => droppedDocs.push(dv.props.Document)); } else { @@ -363,8 +351,6 @@ export namespace DragManager { const docs: Doc[] = dragData instanceof DocumentDragData ? dragData.draggedDocuments : dragData instanceof AnnotationDragData ? [dragData.dragDocument] : []; - const datadocs: (Doc | undefined)[] = - dragData instanceof DocumentDragData ? dragData.draggedDataDocs : dragData instanceof AnnotationDragData ? [dragData.dragDocument] : []; let dragElements = eles.map(ele => { const w = ele.offsetWidth, h = ele.offsetHeight; @@ -449,7 +435,7 @@ export namespace DragManager { pageY: e.pageY, preventDefault: emptyFunction, button: 0 - }, docs, datadocs); + }, docs); } //TODO: Why can't we use e.movementX and e.movementY? let moveX = e.pageX - lastX; diff --git a/src/client/util/History.ts b/src/client/util/History.ts index c72ae05de..899abbe40 100644 --- a/src/client/util/History.ts +++ b/src/client/util/History.ts @@ -54,7 +54,9 @@ export namespace HistoryUtil { } export function getState(): ParsedUrl { - return copyState(history.state); + let state = copyState(history.state); + state.initializers = state.initializers || {}; + return state; } // export function addHandler(handler: (state: ParsedUrl | null) => void) { diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx index 8948b73f7..6670f685e 100644 --- a/src/client/util/Import & Export/DirectoryImportBox.tsx +++ b/src/client/util/Import & Export/DirectoryImportBox.tsx @@ -279,6 +279,7 @@ export default class DirectoryImportBox extends React.Component }} />