From d271205a94bcb2ebc524b70f8a3ff98398e69252 Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Mon, 12 Aug 2019 21:39:16 -0400 Subject: exporting text to google docs almost supported --- src/client/DocServer.ts | 1 + .../apis/google_docs/GoogleApiClientUtils.ts | 85 ++++++++++ src/client/views/MainView.tsx | 11 +- src/server/Message.ts | 1 + src/server/RouteStore.ts | 3 +- src/server/apis/google/GoogleApiServerUtils.ts | 109 +++++++++++++ src/server/apis/youtube/youtubeApiSample.d.ts | 2 + src/server/apis/youtube/youtubeApiSample.js | 179 +++++++++++++++++++++ .../credentials/google_docs_credentials.json | 1 + src/server/credentials/google_docs_token.json | 1 + src/server/index.ts | 33 +++- src/server/youtubeApi/youtubeApiSample.d.ts | 2 - src/server/youtubeApi/youtubeApiSample.js | 179 --------------------- 13 files changed, 417 insertions(+), 190 deletions(-) create mode 100644 src/client/apis/google_docs/GoogleApiClientUtils.ts create mode 100644 src/server/apis/google/GoogleApiServerUtils.ts create mode 100644 src/server/apis/youtube/youtubeApiSample.d.ts create mode 100644 src/server/apis/youtube/youtubeApiSample.js create mode 100644 src/server/credentials/google_docs_credentials.json create mode 100644 src/server/credentials/google_docs_token.json delete mode 100644 src/server/youtubeApi/youtubeApiSample.d.ts delete mode 100644 src/server/youtubeApi/youtubeApiSample.js (limited to 'src') diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts index bf5168c22..47aff2bb6 100644 --- a/src/client/DocServer.ts +++ b/src/client/DocServer.ts @@ -5,6 +5,7 @@ import { Utils, emptyFunction } from '../Utils'; import { SerializationHelper } from './util/SerializationHelper'; import { RefField } from '../new_fields/RefField'; import { Id, HandleUpdate } from '../new_fields/FieldSymbols'; +import { docs_v1 } from 'googleapis'; /** * This class encapsulates the transfer and cross-client synchronization of diff --git a/src/client/apis/google_docs/GoogleApiClientUtils.ts b/src/client/apis/google_docs/GoogleApiClientUtils.ts new file mode 100644 index 000000000..42e621f6b --- /dev/null +++ b/src/client/apis/google_docs/GoogleApiClientUtils.ts @@ -0,0 +1,85 @@ +import { docs_v1 } from "googleapis"; +import { PostToServer } from "../../../Utils"; +import { RouteStore } from "../../../server/RouteStore"; + +export namespace GoogleApiClientUtils { + + export namespace Docs { + + export enum Actions { + Create = "create", + Retrieve = "retrieve" + } + + export namespace Helpers { + + export const fromRgb = (red: number, green: number, blue: number) => { + return { color: { rgbColor: { red, green, blue } } }; + }; + + } + + export const ExampleDocumentSchema = { + title: "This is a Google Doc Created From Dash Web", + body: { + content: [ + { + paragraph: { + elements: [ + { + textRun: { + content: "And this is its bold, blue text!!!", + textStyle: { + bold: true, + backgroundColor: Helpers.fromRgb(0, 0, 1) + } + } + } + ] + } + } + ] as docs_v1.Schema$StructuralElement[] + } + } as docs_v1.Schema$Document; + + /** + * 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 schema whatever subset of a docs_v1.Schema$Document is required to properly initialize your + * Google Doc. This schema defines all aspects of a Google Doc, from the title to headers / footers to the + * actual document body and its styling! + * @returns the documentId of the newly generated document, or undefined if the creation process fails. + */ + export const Create = async (schema?: docs_v1.Schema$Document): Promise => { + let path = RouteStore.googleDocs + Actions.Create; + let parameters = { requestBody: schema || ExampleDocumentSchema }; + let generatedId: string | undefined; + try { + generatedId = await PostToServer(path, parameters); + } catch (e) { + console.error(e); + generatedId = undefined; + } finally { + return generatedId; + } + }; + + let path = RouteStore.googleDocs + Actions.Retrieve; + export const Retrieve = async (documentId: string): Promise => { + let parameters = { documentId }; + let documentContents: any; + try { + documentContents = await PostToServer(path, parameters); + } catch (e) { + console.error(e); + documentContents = undefined; + } finally { + return documentContents; + } + }; + + } + +} \ No newline at end of file diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 031478477..0a987de4a 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -15,7 +15,7 @@ import { listSpec } from '../../new_fields/Schema'; import { Cast, FieldValue, NumCast, BoolCast, StrCast } from '../../new_fields/Types'; import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils'; import { RouteStore } from '../../server/RouteStore'; -import { emptyFunction, returnOne, returnTrue, Utils, returnEmptyString } from '../../Utils'; +import { emptyFunction, returnOne, returnTrue, Utils, returnEmptyString, PostToServer } from '../../Utils'; import { DocServer } from '../DocServer'; import { Docs } from '../documents/Documents'; import { SetupDrag } from '../util/DragManager'; @@ -40,6 +40,8 @@ import { CollectionTreeView } from './collections/CollectionTreeView'; import { ClientUtils } from '../util/ClientUtils'; import { SchemaHeaderField, RandomPastel } from '../../new_fields/SchemaHeaderField'; import { DictationManager } from '../util/DictationManager'; +import { docs_v1 } from 'googleapis'; +import { GoogleApiClientUtils } from '../apis/google_docs/GoogleApiClientUtils'; @observer export class MainView extends React.Component { @@ -130,6 +132,8 @@ export class MainView extends React.Component { window.removeEventListener("keydown", KeyManager.Instance.handle); window.addEventListener("keydown", KeyManager.Instance.handle); + GoogleApiClientUtils.Docs.Create().then(id => console.log(id)); + reaction(() => { let workspaces = CurrentUserUtils.UserDocument.workspaces; let recent = CurrentUserUtils.UserDocument.recentlyClosed; @@ -569,6 +573,11 @@ export class MainView extends React.Component { render() { return (
+ { + if (e.which === 13) { + GoogleApiClientUtils.Docs.Retrieve(e.currentTarget.value.trim()).then((res: any) => console.log(res)); + } + }} /> {this.dictationOverlay} {this.mainContent} diff --git a/src/server/Message.ts b/src/server/Message.ts index aaee143e8..4ec390ade 100644 --- a/src/server/Message.ts +++ b/src/server/Message.ts @@ -1,4 +1,5 @@ import { Utils } from "../Utils"; +import { google, docs_v1 } from "googleapis"; export class Message { private _name: string; diff --git a/src/server/RouteStore.ts b/src/server/RouteStore.ts index e30015e39..5d977006a 100644 --- a/src/server/RouteStore.ts +++ b/src/server/RouteStore.ts @@ -30,6 +30,7 @@ export enum RouteStore { reset = "/reset/:token", // APIS - cognitiveServices = "/cognitiveservices" + cognitiveServices = "/cognitiveservices", + googleDocs = "/googleDocs/" } \ No newline at end of file diff --git a/src/server/apis/google/GoogleApiServerUtils.ts b/src/server/apis/google/GoogleApiServerUtils.ts new file mode 100644 index 000000000..817b2b696 --- /dev/null +++ b/src/server/apis/google/GoogleApiServerUtils.ts @@ -0,0 +1,109 @@ +import { google, docs_v1 } from "googleapis"; +import { createInterface } from "readline"; +import { readFile, writeFile } from "fs"; +import { OAuth2Client } from "google-auth-library"; + +/** + * Server side authentication for Google Api queries. + */ +export namespace GoogleApiServerUtils { + + // If modifying these scopes, delete token.json. + const prefix = 'https://www.googleapis.com/auth/'; + const SCOPES = [ + 'documents.readonly', + 'documents', + 'drive', + 'drive.file', + ]; + // The file token.json stores the user's access and refresh tokens, and is + // created automatically when the authorization flow completes for the first + // time. + export const parseBuffer = (data: Buffer) => JSON.parse(data.toString()); + + export namespace Docs { + + export interface CredentialPaths { + credentials: string; + token: string; + } + + export type Endpoint = docs_v1.Docs; + + export const GetEndpoint = async (paths: CredentialPaths) => { + return new Promise((resolve, reject) => { + readFile(paths.credentials, (err, credentials) => { + if (err) { + reject(err); + return console.log('Error loading client secret file:', err); + } + return authorize(parseBuffer(credentials), paths.token).then(auth => { + resolve(google.docs({ version: "v1", auth })); + }); + }); + }); + }; + + } + + /** + * Create an OAuth2 client with the given credentials, and returns the promise resolving to the authenticated client + * @param {Object} credentials The authorization client credentials. + */ + export function authorize(credentials: any, token_path: string): Promise { + const { client_secret, client_id, redirect_uris } = credentials.installed; + const oAuth2Client = new google.auth.OAuth2( + client_id, client_secret, redirect_uris[0]); + + return new Promise((resolve, reject) => { + readFile(token_path, (err, token) => { + // Check if we have previously stored a token. + if (err) { + return getNewToken(oAuth2Client, token_path).then(resolve, reject); + } + oAuth2Client.setCredentials(parseBuffer(token)); + resolve(oAuth2Client); + }); + }); + } + + /** + * Get and store new token after prompting for user authorization, and then + * execute the given callback with the authorized OAuth2 client. + * @param {google.auth.OAuth2} oAuth2Client The OAuth2 client to get token for. + * @param {getEventsCallback} callback The callback for the authorized client. + */ + function getNewToken(oAuth2Client: OAuth2Client, token_path: string) { + return new Promise((resolve, reject) => { + const authUrl = oAuth2Client.generateAuthUrl({ + access_type: 'offline', + scope: SCOPES.map(relative => prefix + relative), + }); + console.log('Authorize this app by visiting this url:', authUrl); + const rl = createInterface({ + input: process.stdin, + output: process.stdout, + }); + rl.question('Enter the code from that page here: ', (code) => { + rl.close(); + oAuth2Client.getToken(code, (err, token) => { + if (err || !token) { + reject(err); + return console.error('Error retrieving access token', err); + } + oAuth2Client.setCredentials(token); + // Store the token to disk for later program executions + writeFile(token_path, JSON.stringify(token), (err) => { + if (err) { + console.error(err); + reject(err); + } + console.log('Token stored to', token_path); + }); + resolve(oAuth2Client); + }); + }); + }); + } + +} \ No newline at end of file diff --git a/src/server/apis/youtube/youtubeApiSample.d.ts b/src/server/apis/youtube/youtubeApiSample.d.ts new file mode 100644 index 000000000..427f54608 --- /dev/null +++ b/src/server/apis/youtube/youtubeApiSample.d.ts @@ -0,0 +1,2 @@ +declare const YoutubeApi: any; +export = YoutubeApi; \ No newline at end of file diff --git a/src/server/apis/youtube/youtubeApiSample.js b/src/server/apis/youtube/youtubeApiSample.js new file mode 100644 index 000000000..50b3c7b38 --- /dev/null +++ b/src/server/apis/youtube/youtubeApiSample.js @@ -0,0 +1,179 @@ +const fs = require('fs'); +const readline = require('readline'); +const { google } = require('googleapis'); +const OAuth2 = google.auth.OAuth2; + + +// If modifying these scopes, delete your previously saved credentials +// at ~/.credentials/youtube-nodejs-quickstart.json +let SCOPES = ['https://www.googleapis.com/auth/youtube.readonly']; +let TOKEN_DIR = (process.env.HOME || process.env.HOMEPATH || + process.env.USERPROFILE) + '/.credentials/'; +let TOKEN_PATH = TOKEN_DIR + 'youtube-nodejs-quickstart.json'; + +module.exports.readApiKey = (callback) => { + fs.readFile('client_secret.json', function processClientSecrets(err, content) { + if (err) { + console.log('Error loading client secret file: ' + err); + return; + } + callback(content); + }); +} + +module.exports.authorizedGetChannel = (apiKey) => { + //this didnt get called + // Authorize a client with the loaded credentials, then call the YouTube API. + authorize(JSON.parse(apiKey), getChannel); +} + +module.exports.authorizedGetVideos = (apiKey, userInput, callBack) => { + authorize(JSON.parse(apiKey), getVideos, { userInput: userInput, callBack: callBack }); +} + +module.exports.authorizedGetVideoDetails = (apiKey, videoIds, callBack) => { + authorize(JSON.parse(apiKey), getVideoDetails, { videoIds: videoIds, callBack: callBack }); +} + + +/** + * Create an OAuth2 client with the given credentials, and then execute the + * given callback function. + * + * @param {Object} credentials The authorization client credentials. + * @param {function} callback The callback to call with the authorized client. + */ +function authorize(credentials, callback, args = {}) { + let clientSecret = credentials.installed.client_secret; + let clientId = credentials.installed.client_id; + let redirectUrl = credentials.installed.redirect_uris[0]; + let oauth2Client = new OAuth2(clientId, clientSecret, redirectUrl); + + // Check if we have previously stored a token. + fs.readFile(TOKEN_PATH, function (err, token) { + if (err) { + getNewToken(oauth2Client, callback); + } else { + oauth2Client.credentials = JSON.parse(token); + callback(oauth2Client, args); + } + }); +} + +/** + * Get and store new token after prompting for user authorization, and then + * execute the given callback with the authorized OAuth2 client. + * + * @param {google.auth.OAuth2} oauth2Client The OAuth2 client to get token for. + * @param {getEventsCallback} callback The callback to call with the authorized + * client. + */ +function getNewToken(oauth2Client, callback) { + var authUrl = oauth2Client.generateAuthUrl({ + access_type: 'offline', + scope: SCOPES + }); + console.log('Authorize this app by visiting this url: ', authUrl); + var rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); + rl.question('Enter the code from that page here: ', function (code) { + rl.close(); + oauth2Client.getToken(code, function (err, token) { + if (err) { + console.log('Error while trying to retrieve access token', err); + return; + } + oauth2Client.credentials = token; + storeToken(token); + callback(oauth2Client); + }); + }); +} + +/** + * Store token to disk be used in later program executions. + * + * @param {Object} token The token to store to disk. + */ +function storeToken(token) { + try { + fs.mkdirSync(TOKEN_DIR); + } catch (err) { + if (err.code != 'EEXIST') { + throw err; + } + } + fs.writeFile(TOKEN_PATH, JSON.stringify(token), (err) => { + if (err) throw err; + console.log('Token stored to ' + TOKEN_PATH); + }); + console.log('Token stored to ' + TOKEN_PATH); +} + +/** + * Lists the names and IDs of up to 10 files. + * + * @param {google.auth.OAuth2} auth An authorized OAuth2 client. + */ +function getChannel(auth) { + var service = google.youtube('v3'); + service.channels.list({ + auth: auth, + part: 'snippet,contentDetails,statistics', + forUsername: 'GoogleDevelopers' + }, function (err, response) { + if (err) { + console.log('The API returned an error: ' + err); + return; + } + var channels = response.data.items; + if (channels.length == 0) { + console.log('No channel found.'); + } else { + console.log('This channel\'s ID is %s. Its title is \'%s\', and ' + + 'it has %s views.', + channels[0].id, + channels[0].snippet.title, + channels[0].statistics.viewCount); + } + }); +} + +function getVideos(auth, args) { + let service = google.youtube('v3'); + service.search.list({ + auth: auth, + part: 'id, snippet', + type: 'video', + q: args.userInput, + maxResults: 10 + }, function (err, response) { + if (err) { + console.log('The API returned an error: ' + err); + return; + } + let videos = response.data.items; + args.callBack(videos); + }); +} + +function getVideoDetails(auth, args) { + if (args.videoIds === undefined) { + return; + } + let service = google.youtube('v3'); + service.videos.list({ + auth: auth, + part: 'contentDetails, statistics', + id: args.videoIds + }, function (err, response) { + if (err) { + console.log('The API returned an error from details: ' + err); + return; + } + let videoDetails = response.data.items; + args.callBack(videoDetails); + }); +} \ No newline at end of file diff --git a/src/server/credentials/google_docs_credentials.json b/src/server/credentials/google_docs_credentials.json new file mode 100644 index 000000000..8d097d363 --- /dev/null +++ b/src/server/credentials/google_docs_credentials.json @@ -0,0 +1 @@ +{"installed":{"client_id":"343179513178-ud6tvmh275r2fq93u9eesrnc66t6akh9.apps.googleusercontent.com","project_id":"quickstart-1565056383187","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://oauth2.googleapis.com/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","client_secret":"w8KIFSc0MQpmUYHed4qEzn8b","redirect_uris":["urn:ietf:wg:oauth:2.0:oob","http://localhost"]}} \ No newline at end of file diff --git a/src/server/credentials/google_docs_token.json b/src/server/credentials/google_docs_token.json new file mode 100644 index 000000000..07c02d56c --- /dev/null +++ b/src/server/credentials/google_docs_token.json @@ -0,0 +1 @@ +{"access_token":"ya29.GltjB4-x03xFpd2NY2555cxg1xlT_ajqRi78M9osOfdOF2jTIjlPkn_UZL8cUwVP0DPC8rH3vhhg8RpspFe8Vewx92shAO3RPos_uMH0CUqEiCiZlaaB5I3Jq3Mv","refresh_token":"1/teUKUqGKMLjVqs-eed0L8omI02pzSxMUYaxGc2QxBw0","scope":"https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents.readonly","token_type":"Bearer","expiry_date":1565654175862} \ No newline at end of file diff --git a/src/server/index.ts b/src/server/index.ts index eae018f13..9986e62d6 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -14,7 +14,6 @@ import * as mobileDetect from 'mobile-detect'; import * as passport from 'passport'; import * as path from 'path'; import * as request from 'request'; -import * as rp from 'request-promise'; import * as io from 'socket.io'; import { Socket } from 'socket.io'; import * as webpack from 'webpack'; @@ -36,19 +35,16 @@ const port = 1050; // default port to listen const serverPort = 4321; import expressFlash = require('express-flash'); import flash = require('connect-flash'); -import c = require("crypto"); import { Search } from './Search'; -import { debug } from 'util'; import _ = require('lodash'); import * as Archiver from 'archiver'; -import * as AdmZip from 'adm-zip'; -import * as YoutubeApi from './youtubeApi/youtubeApiSample.js'; +import AdmZip from 'adm-zip'; +import * as YoutubeApi from "./apis/youtube/youtubeApiSample"; import { Response } from 'express-serve-static-core'; +import { GoogleApiServerUtils } from "./apis/google/GoogleApiServerUtils"; const MongoStore = require('connect-mongo')(session); const mongoose = require('mongoose'); const probe = require("probe-image-size"); -var SolrNode = require('solr-node'); -var shell = require('shelljs'); const download = (url: string, dest: fs.PathLike) => request.get(url).pipe(fs.createWriteStream(dest)); let youtubeApiKey: string; @@ -790,6 +786,29 @@ 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"); + +app.post(RouteStore.googleDocs + ":action", (req, res) => { + let parameters = req.body; + console.log(parameters, req.params.action); + + GoogleApiServerUtils.Docs.GetEndpoint({ credentials, token }).then(endpoint => { + let results: Promise | undefined; + switch (req.params.action) { + case "create": + results = endpoint.documents.create(parameters).then(generated => generated.data.documentId); + break; + case "retrieve": + results = endpoint.documents.get(parameters).then(response => response.data); + break; + default: + results = undefined; + } + results && results.then(final => res.send(final)); + }); +}); + const suffixMap: { [type: string]: (string | [string, string | ((json: any) => any)]) } = { "number": "_n", "string": "_t", diff --git a/src/server/youtubeApi/youtubeApiSample.d.ts b/src/server/youtubeApi/youtubeApiSample.d.ts deleted file mode 100644 index 427f54608..000000000 --- a/src/server/youtubeApi/youtubeApiSample.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -declare const YoutubeApi: any; -export = YoutubeApi; \ No newline at end of file diff --git a/src/server/youtubeApi/youtubeApiSample.js b/src/server/youtubeApi/youtubeApiSample.js deleted file mode 100644 index 50b3c7b38..000000000 --- a/src/server/youtubeApi/youtubeApiSample.js +++ /dev/null @@ -1,179 +0,0 @@ -const fs = require('fs'); -const readline = require('readline'); -const { google } = require('googleapis'); -const OAuth2 = google.auth.OAuth2; - - -// If modifying these scopes, delete your previously saved credentials -// at ~/.credentials/youtube-nodejs-quickstart.json -let SCOPES = ['https://www.googleapis.com/auth/youtube.readonly']; -let TOKEN_DIR = (process.env.HOME || process.env.HOMEPATH || - process.env.USERPROFILE) + '/.credentials/'; -let TOKEN_PATH = TOKEN_DIR + 'youtube-nodejs-quickstart.json'; - -module.exports.readApiKey = (callback) => { - fs.readFile('client_secret.json', function processClientSecrets(err, content) { - if (err) { - console.log('Error loading client secret file: ' + err); - return; - } - callback(content); - }); -} - -module.exports.authorizedGetChannel = (apiKey) => { - //this didnt get called - // Authorize a client with the loaded credentials, then call the YouTube API. - authorize(JSON.parse(apiKey), getChannel); -} - -module.exports.authorizedGetVideos = (apiKey, userInput, callBack) => { - authorize(JSON.parse(apiKey), getVideos, { userInput: userInput, callBack: callBack }); -} - -module.exports.authorizedGetVideoDetails = (apiKey, videoIds, callBack) => { - authorize(JSON.parse(apiKey), getVideoDetails, { videoIds: videoIds, callBack: callBack }); -} - - -/** - * Create an OAuth2 client with the given credentials, and then execute the - * given callback function. - * - * @param {Object} credentials The authorization client credentials. - * @param {function} callback The callback to call with the authorized client. - */ -function authorize(credentials, callback, args = {}) { - let clientSecret = credentials.installed.client_secret; - let clientId = credentials.installed.client_id; - let redirectUrl = credentials.installed.redirect_uris[0]; - let oauth2Client = new OAuth2(clientId, clientSecret, redirectUrl); - - // Check if we have previously stored a token. - fs.readFile(TOKEN_PATH, function (err, token) { - if (err) { - getNewToken(oauth2Client, callback); - } else { - oauth2Client.credentials = JSON.parse(token); - callback(oauth2Client, args); - } - }); -} - -/** - * Get and store new token after prompting for user authorization, and then - * execute the given callback with the authorized OAuth2 client. - * - * @param {google.auth.OAuth2} oauth2Client The OAuth2 client to get token for. - * @param {getEventsCallback} callback The callback to call with the authorized - * client. - */ -function getNewToken(oauth2Client, callback) { - var authUrl = oauth2Client.generateAuthUrl({ - access_type: 'offline', - scope: SCOPES - }); - console.log('Authorize this app by visiting this url: ', authUrl); - var rl = readline.createInterface({ - input: process.stdin, - output: process.stdout - }); - rl.question('Enter the code from that page here: ', function (code) { - rl.close(); - oauth2Client.getToken(code, function (err, token) { - if (err) { - console.log('Error while trying to retrieve access token', err); - return; - } - oauth2Client.credentials = token; - storeToken(token); - callback(oauth2Client); - }); - }); -} - -/** - * Store token to disk be used in later program executions. - * - * @param {Object} token The token to store to disk. - */ -function storeToken(token) { - try { - fs.mkdirSync(TOKEN_DIR); - } catch (err) { - if (err.code != 'EEXIST') { - throw err; - } - } - fs.writeFile(TOKEN_PATH, JSON.stringify(token), (err) => { - if (err) throw err; - console.log('Token stored to ' + TOKEN_PATH); - }); - console.log('Token stored to ' + TOKEN_PATH); -} - -/** - * Lists the names and IDs of up to 10 files. - * - * @param {google.auth.OAuth2} auth An authorized OAuth2 client. - */ -function getChannel(auth) { - var service = google.youtube('v3'); - service.channels.list({ - auth: auth, - part: 'snippet,contentDetails,statistics', - forUsername: 'GoogleDevelopers' - }, function (err, response) { - if (err) { - console.log('The API returned an error: ' + err); - return; - } - var channels = response.data.items; - if (channels.length == 0) { - console.log('No channel found.'); - } else { - console.log('This channel\'s ID is %s. Its title is \'%s\', and ' + - 'it has %s views.', - channels[0].id, - channels[0].snippet.title, - channels[0].statistics.viewCount); - } - }); -} - -function getVideos(auth, args) { - let service = google.youtube('v3'); - service.search.list({ - auth: auth, - part: 'id, snippet', - type: 'video', - q: args.userInput, - maxResults: 10 - }, function (err, response) { - if (err) { - console.log('The API returned an error: ' + err); - return; - } - let videos = response.data.items; - args.callBack(videos); - }); -} - -function getVideoDetails(auth, args) { - if (args.videoIds === undefined) { - return; - } - let service = google.youtube('v3'); - service.videos.list({ - auth: auth, - part: 'contentDetails, statistics', - id: args.videoIds - }, function (err, response) { - if (err) { - console.log('The API returned an error from details: ' + err); - return; - } - let videoDetails = response.data.items; - args.callBack(videoDetails); - }); -} \ No newline at end of file -- cgit v1.2.3-70-g09d2 From 278cabf67a77edb3dd3bd9bb392550eeb08ab910 Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Tue, 13 Aug 2019 02:06:32 -0400 Subject: more util functions and clean up --- .../apis/google_docs/GoogleApiClientUtils.ts | 71 +++++++++++++++++++--- src/client/views/MainView.tsx | 24 +++++--- src/server/index.ts | 1 - 3 files changed, 78 insertions(+), 18 deletions(-) (limited to 'src') diff --git a/src/client/apis/google_docs/GoogleApiClientUtils.ts b/src/client/apis/google_docs/GoogleApiClientUtils.ts index 42e621f6b..71e5e1073 100644 --- a/src/client/apis/google_docs/GoogleApiClientUtils.ts +++ b/src/client/apis/google_docs/GoogleApiClientUtils.ts @@ -1,6 +1,7 @@ import { docs_v1 } from "googleapis"; import { PostToServer } from "../../../Utils"; import { RouteStore } from "../../../server/RouteStore"; +import { Opt } from "../../../new_fields/Doc"; export namespace GoogleApiClientUtils { @@ -11,33 +12,76 @@ export namespace GoogleApiClientUtils { Retrieve = "retrieve" } - export namespace Helpers { + export namespace Utils { export const fromRgb = (red: number, green: number, blue: number) => { return { color: { rgbColor: { red, green, blue } } }; }; + export const extractText = (document: docs_v1.Schema$Document, removeNewlines = false) => { + let fragments: string[] = []; + if (document.body && document.body.content) { + for (let element of document.body.content) { + if (element.paragraph && element.paragraph.elements) { + for (let inner of element.paragraph.elements) { + if (inner && inner.textRun) { + let fragment = inner.textRun.content; + fragment && fragments.push(fragment); + } + } + } + } + } + let text = fragments.join(""); + return removeNewlines ? text.ReplaceAll("\n", "") : text; + }; + } export const ExampleDocumentSchema = { title: "This is a Google Doc Created From Dash Web", body: { content: [ + { + endIndex: 1, + sectionBreak: { + sectionStyle: { + columnSeparatorStyle: "NONE", + contentDirection: "LEFT_TO_RIGHT" + } + } + }, { paragraph: { elements: [ { textRun: { - content: "And this is its bold, blue text!!!", + content: "And this is its bold, blue text!!!\n", textStyle: { bold: true, - backgroundColor: Helpers.fromRgb(0, 0, 1) + backgroundColor: Utils.fromRgb(0, 0, 1) } } } ] } - } + }, + { + paragraph: { + elements: [ + { + textRun: { + content: "And this is its bold, blue text!!!\n", + textStyle: { + bold: true, + backgroundColor: Utils.fromRgb(0, 0, 1) + } + } + } + ] + } + }, + ] as docs_v1.Schema$StructuralElement[] } } as docs_v1.Schema$Document; @@ -66,20 +110,27 @@ export namespace GoogleApiClientUtils { } }; - let path = RouteStore.googleDocs + Actions.Retrieve; - export const Retrieve = async (documentId: string): Promise => { + export const Read = async (documentId: string, removeNewlines = false): Promise> => { + return Retrieve(documentId).then(schema => { + return schema ? Utils.extractText(schema, removeNewlines) : undefined; + }); + }; + + export const Retrieve = async (documentId: string): Promise> => { + let path = RouteStore.googleDocs + Actions.Retrieve; let parameters = { documentId }; - let documentContents: any; + let schema: Opt; try { - documentContents = await PostToServer(path, parameters); + schema = await PostToServer(path, parameters); } catch (e) { console.error(e); - documentContents = undefined; + schema = undefined; } finally { - return documentContents; + return schema; } }; + } } \ No newline at end of file diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 0a987de4a..3a5285a66 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -87,6 +87,11 @@ export class MainView extends React.Component { if (!("presentationView" in doc)) { doc.presentationView = new List([Docs.Create.TreeDocument([], { title: "Presentation" })]); } + if (!("googleDocId" in doc)) { + GoogleApiClientUtils.Docs.Create().then(id => { + id && (doc.googleDocId = id); + }); + } CurrentUserUtils.UserDocument.activeWorkspace = doc; } } @@ -132,8 +137,6 @@ export class MainView extends React.Component { window.removeEventListener("keydown", KeyManager.Instance.handle); window.addEventListener("keydown", KeyManager.Instance.handle); - GoogleApiClientUtils.Docs.Create().then(id => console.log(id)); - reaction(() => { let workspaces = CurrentUserUtils.UserDocument.workspaces; let recent = CurrentUserUtils.UserDocument.recentlyClosed; @@ -151,6 +154,18 @@ export class MainView extends React.Component { }, { fireImmediately: true }); } + componentDidMount() { + reaction(() => this.mainContainer, () => { + let main = this.mainContainer, googleDocId; + if (main && (googleDocId = StrCast(main.googleDocId))) { + GoogleApiClientUtils.Docs.Read(googleDocId, true).then(text => { + text && Utils.CopyText(text); + console.log(text); + }); + } + }); + } + componentWillUnMount() { window.removeEventListener("keydown", KeyManager.Instance.handle); window.removeEventListener("pointerdown", this.globalPointerDown); @@ -573,11 +588,6 @@ export class MainView extends React.Component { render() { return (
- { - if (e.which === 13) { - GoogleApiClientUtils.Docs.Retrieve(e.currentTarget.value.trim()).then((res: any) => console.log(res)); - } - }} /> {this.dictationOverlay} {this.mainContent} diff --git a/src/server/index.ts b/src/server/index.ts index 9986e62d6..4ccb61681 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -791,7 +791,6 @@ const token = path.join(__dirname, "./credentials/google_docs_token.json"); app.post(RouteStore.googleDocs + ":action", (req, res) => { let parameters = req.body; - console.log(parameters, req.params.action); GoogleApiServerUtils.Docs.GetEndpoint({ credentials, token }).then(endpoint => { let results: Promise | undefined; -- cgit v1.2.3-70-g09d2 From 2ed0c3e1203794ddaa0b17ed908432582d0198d1 Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Tue, 13 Aug 2019 02:11:38 -0400 Subject: reading options --- src/client/apis/google_docs/GoogleApiClientUtils.ts | 11 ++++++++--- src/client/views/MainView.tsx | 6 +++--- 2 files changed, 11 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/client/apis/google_docs/GoogleApiClientUtils.ts b/src/client/apis/google_docs/GoogleApiClientUtils.ts index 71e5e1073..5e974b2e7 100644 --- a/src/client/apis/google_docs/GoogleApiClientUtils.ts +++ b/src/client/apis/google_docs/GoogleApiClientUtils.ts @@ -110,9 +110,14 @@ export namespace GoogleApiClientUtils { } }; - export const Read = async (documentId: string, removeNewlines = false): Promise> => { - return Retrieve(documentId).then(schema => { - return schema ? Utils.extractText(schema, removeNewlines) : undefined; + export interface ReadOptions { + documentId: string; + removeNewlines?: boolean; + } + + export const Read = async (options: ReadOptions): Promise> => { + return Retrieve(options.documentId).then(schema => { + return schema ? Utils.extractText(schema, options.removeNewlines) : undefined; }); }; diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 3a5285a66..705eacc0c 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -156,9 +156,9 @@ export class MainView extends React.Component { componentDidMount() { reaction(() => this.mainContainer, () => { - let main = this.mainContainer, googleDocId; - if (main && (googleDocId = StrCast(main.googleDocId))) { - GoogleApiClientUtils.Docs.Read(googleDocId, true).then(text => { + let main = this.mainContainer, documentId; + if (main && (documentId = StrCast(main.googleDocId))) { + GoogleApiClientUtils.Docs.Read({ documentId }).then(text => { text && Utils.CopyText(text); console.log(text); }); -- cgit v1.2.3-70-g09d2 From 2b829d1028a61869858ecd48e2f1801819e17488 Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Tue, 13 Aug 2019 22:15:47 -0400 Subject: robust google doc export api and cleanup --- .../apis/google_docs/GoogleApiClientUtils.ts | 185 ++++++++++++--------- src/client/views/MainView.tsx | 11 +- src/client/views/nodes/FormattedTextBox.tsx | 24 ++- src/new_fields/RichTextField.ts | 13 ++ src/server/index.ts | 25 ++- 5 files changed, 162 insertions(+), 96 deletions(-) (limited to 'src') diff --git a/src/client/apis/google_docs/GoogleApiClientUtils.ts b/src/client/apis/google_docs/GoogleApiClientUtils.ts index 5e974b2e7..dda36f05a 100644 --- a/src/client/apis/google_docs/GoogleApiClientUtils.ts +++ b/src/client/apis/google_docs/GoogleApiClientUtils.ts @@ -1,7 +1,8 @@ import { docs_v1 } from "googleapis"; import { PostToServer } from "../../../Utils"; import { RouteStore } from "../../../server/RouteStore"; -import { Opt } from "../../../new_fields/Doc"; +import { Opt, Doc } from "../../../new_fields/Doc"; +import { isArray } from "util"; export namespace GoogleApiClientUtils { @@ -9,15 +10,12 @@ export namespace GoogleApiClientUtils { export enum Actions { Create = "create", - Retrieve = "retrieve" + Retrieve = "retrieve", + Update = "update" } export namespace Utils { - export const fromRgb = (red: number, green: number, blue: number) => { - return { color: { rgbColor: { red, green, blue } } }; - }; - export const extractText = (document: docs_v1.Schema$Document, removeNewlines = false) => { let fragments: string[] = []; if (document.body && document.body.content) { @@ -36,55 +34,36 @@ export namespace GoogleApiClientUtils { return removeNewlines ? text.ReplaceAll("\n", "") : text; }; - } - - export const ExampleDocumentSchema = { - title: "This is a Google Doc Created From Dash Web", - body: { - content: [ - { - endIndex: 1, - sectionBreak: { - sectionStyle: { - columnSeparatorStyle: "NONE", - contentDirection: "LEFT_TO_RIGHT" + export const EndOf = (schema: docs_v1.Schema$Document): Opt => { + if (schema.body && schema.body.content) { + let paragraphs = schema.body.content.filter(el => el.paragraph); + if (paragraphs.length) { + let target = paragraphs[paragraphs.length - 1]; + if (target.paragraph && target.paragraph.elements) { + length = target.paragraph.elements.length; + if (length) { + let final = target.paragraph.elements[length - 1]; + return final.endIndex ? final.endIndex - 1 : undefined; } } - }, - { - paragraph: { - elements: [ - { - textRun: { - content: "And this is its bold, blue text!!!\n", - textStyle: { - bold: true, - backgroundColor: Utils.fromRgb(0, 0, 1) - } - } - } - ] - } - }, - { - paragraph: { - elements: [ - { - textRun: { - content: "And this is its bold, blue text!!!\n", - textStyle: { - bold: true, - backgroundColor: Utils.fromRgb(0, 0, 1) - } - } - } - ] - } - }, + } + } + }; - ] as docs_v1.Schema$StructuralElement[] - } - } as docs_v1.Schema$Document; + } + + export interface ReadOptions { + documentId: string; + removeNewlines?: boolean; + } + + export interface WriteOptions { + documentId?: string; + title?: string; + content: string | string[]; + index?: number; + store?: { receiver: Doc, key: string }; + } /** * After following the authentication routine, which connects this API call to the current signed in account @@ -96,45 +75,95 @@ export namespace GoogleApiClientUtils { * actual document body and its styling! * @returns the documentId of the newly generated document, or undefined if the creation process fails. */ - export const Create = async (schema?: docs_v1.Schema$Document): Promise => { + const Create = async (title?: string): Promise => { let path = RouteStore.googleDocs + Actions.Create; - let parameters = { requestBody: schema || ExampleDocumentSchema }; - let generatedId: string | undefined; + let parameters = { + requestBody: { + title: title || `Dash Export (${new Date().toDateString()})` + } + }; try { - generatedId = await PostToServer(path, parameters); - } catch (e) { - console.error(e); - generatedId = undefined; - } finally { - return generatedId; + let schema: docs_v1.Schema$Document = await PostToServer(path, parameters); + return schema.documentId; + } catch { + return undefined; } }; - export interface ReadOptions { - documentId: string; - removeNewlines?: boolean; - } + const Retrieve = async (documentId: string): Promise => { + let path = RouteStore.googleDocs + Actions.Retrieve; + let parameters = { + documentId + }; + try { + let schema: docs_v1.Schema$Document = await PostToServer(path, parameters); + return schema; + } catch { + return undefined; + } + }; + + const Update = async (documentId: string, requests: docs_v1.Schema$Request[]): Promise => { + let path = RouteStore.googleDocs + Actions.Update; + let parameters = { + documentId, + requestBody: { + requests + } + }; + try { + let replies: docs_v1.Schema$BatchUpdateDocumentResponse = await PostToServer(path, parameters); + console.log(replies); + return replies; + } catch { + return undefined; + } + }; - export const Read = async (options: ReadOptions): Promise> => { + export const Read = async (options: ReadOptions): Promise => { return Retrieve(options.documentId).then(schema => { return schema ? Utils.extractText(schema, options.removeNewlines) : undefined; }); }; - export const Retrieve = async (documentId: string): Promise> => { - let path = RouteStore.googleDocs + Actions.Retrieve; - let parameters = { documentId }; - let schema: Opt; - try { - schema = await PostToServer(path, parameters); - } catch (e) { - console.error(e); - schema = undefined; - } finally { - return schema; - } + export const ReadLines = async (options: ReadOptions) => { + return Retrieve(options.documentId).then(schema => { + if (!schema) { + return undefined; + } + let lines = Utils.extractText(schema).split("\n"); + return options.removeNewlines ? lines.filter(line => line.length) : lines; + }); }; + export const Write = async (options: WriteOptions): Promise => { + let target = options.documentId; + if (!target) { + if (!(target = await Create(options.title))) { + return undefined; + } + } + let index = options.index; + if (!index) { + let schema = await Retrieve(target); + if (!schema || !(index = Utils.EndOf(schema))) { + return undefined; + } + } + let text = options.content; + let request = { + insertText: { + text: isArray(text) ? text.join("\n") : text, + location: { index } + } + }; + return Update(target, [request]).then(res => { + if (res && options.store) { + options.store.receiver[options.store.key] = res.documentId; + } + return res; + }); + }; } diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 705eacc0c..7b15e9624 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -87,11 +87,6 @@ export class MainView extends React.Component { if (!("presentationView" in doc)) { doc.presentationView = new List([Docs.Create.TreeDocument([], { title: "Presentation" })]); } - if (!("googleDocId" in doc)) { - GoogleApiClientUtils.Docs.Create().then(id => { - id && (doc.googleDocId = id); - }); - } CurrentUserUtils.UserDocument.activeWorkspace = doc; } } @@ -158,9 +153,9 @@ export class MainView extends React.Component { reaction(() => this.mainContainer, () => { let main = this.mainContainer, documentId; if (main && (documentId = StrCast(main.googleDocId))) { - GoogleApiClientUtils.Docs.Read({ documentId }).then(text => { - text && Utils.CopyText(text); - console.log(text); + let options = { documentId, removeNewlines: true }; + GoogleApiClientUtils.Docs.ReadLines(options).then(lines => { + console.log(lines); }); } }); diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index 44b5d2c21..8c2af7c9e 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -1,5 +1,5 @@ import { library } from '@fortawesome/fontawesome-svg-core'; -import { faEdit, faSmile, faTextHeight } from '@fortawesome/free-solid-svg-icons'; +import { faEdit, faSmile, faTextHeight, faUpload } from '@fortawesome/free-solid-svg-icons'; import { action, IReactionDisposer, observable, reaction, runInAction, computed, Lambda, trace } from "mobx"; import { observer } from "mobx-react"; import { baseKeymap } from "prosemirror-commands"; @@ -38,9 +38,10 @@ import { For } from 'babel-types'; import { DateField } from '../../../new_fields/DateField'; import { Utils } from '../../../Utils'; import { MainOverlayTextBox } from '../MainOverlayTextBox'; +import { GoogleApiClientUtils } from '../../apis/google_docs/GoogleApiClientUtils'; library.add(faEdit); -library.add(faSmile, faTextHeight); +library.add(faSmile, faTextHeight, faUpload); // FormattedTextBox: Displays an editable plain text node that maps to a specified Key of a Document // @@ -58,6 +59,8 @@ const richTextSchema = createSchema({ documentText: "string" }); +const googleDocKey = "googleDocId"; + type RichTextDocument = makeInterface<[typeof richTextSchema]>; const RichTextDocument = makeInterface(richTextSchema); @@ -661,7 +664,24 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe event: action(() => Doc.GetProto(this.props.Document).autoHeight = !BoolCast(this.props.Document.autoHeight)), icon: "expand-arrows-alt" }); ContextMenu.Instance.addItem({ description: "Text Funcs...", subitems: subitems, icon: "text-height" }); + if (!(googleDocKey in Doc.GetProto(this.props.Document))) { + ContextMenu.Instance.addItem({ description: "Export to Google Doc...", event: this.exportToGoogleDoc, icon: "upload" }); + } } + + exportToGoogleDoc = () => { + let dataDoc = Doc.GetProto(this.props.Document); + let data = Cast(dataDoc.data, RichTextField); + let content: string | undefined; + if (data && (content = data.plainText())) { + GoogleApiClientUtils.Docs.Write({ + title: StrCast(dataDoc.title), + store: { receiver: dataDoc, key: googleDocKey }, + content + }); + } + } + render() { let self = this; let style = this.props.isOverlay ? "scroll" : "hidden"; diff --git a/src/new_fields/RichTextField.ts b/src/new_fields/RichTextField.ts index 89799b2af..dc66813e0 100644 --- a/src/new_fields/RichTextField.ts +++ b/src/new_fields/RichTextField.ts @@ -9,6 +9,7 @@ import { scriptingGlobal } from "../client/util/Scripting"; export class RichTextField extends ObjectField { @serializable(true) readonly Data: string; + private Extractor = /,\"text\":\"([^\"\}]*)\"\}/g; constructor(data: string) { super(); @@ -22,4 +23,16 @@ export class RichTextField extends ObjectField { [ToScriptString]() { return `new RichTextField("${this.Data}")`; } + + plainText = () => { + let contents = ""; + let matches: RegExpExecArray | null; + let considering = this.Data; + while ((matches = this.Extractor.exec(considering)) !== null) { + contents += matches[1]; + considering = considering.substring(matches.index + matches[0].length); + this.Extractor.lastIndex = 0; + } + return contents.length ? contents : undefined; + } } \ No newline at end of file diff --git a/src/server/index.ts b/src/server/index.ts index 4ccb61681..abaa29658 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -42,6 +42,7 @@ import AdmZip from 'adm-zip'; import * as YoutubeApi from "./apis/youtube/youtubeApiSample"; import { Response } from 'express-serve-static-core'; import { GoogleApiServerUtils } from "./apis/google/GoogleApiServerUtils"; +import { GaxiosResponse } from 'gaxios'; const MongoStore = require('connect-mongo')(session); const mongoose = require('mongoose'); const probe = require("probe-image-size"); @@ -170,6 +171,13 @@ const read_text_file = (relativePath: string) => { }); }; +const write_text_file = (relativePath: string, contents: any) => { + let target = path.join(__dirname, relativePath); + return new Promise((resolve, reject) => { + fs.writeFile(target, contents, (err) => err ? reject(err) : resolve()); + }); +}; + app.get("/version", (req, res) => { exec('"C:\\Program Files\\Git\\bin\\git.exe" rev-parse HEAD', (err, stdout, stderr) => { if (err) { @@ -790,21 +798,22 @@ const credentials = path.join(__dirname, "./credentials/google_docs_credentials. const token = path.join(__dirname, "./credentials/google_docs_token.json"); app.post(RouteStore.googleDocs + ":action", (req, res) => { - let parameters = req.body; - GoogleApiServerUtils.Docs.GetEndpoint({ credentials, token }).then(endpoint => { - let results: Promise | undefined; + let results: Promise | undefined; + let documents = endpoint.documents; + let parameters = req.body; switch (req.params.action) { case "create": - results = endpoint.documents.create(parameters).then(generated => generated.data.documentId); + results = documents.create(parameters); break; case "retrieve": - results = endpoint.documents.get(parameters).then(response => response.data); + results = documents.get(parameters); + break; + case "update": + results = documents.batchUpdate(parameters); break; - default: - results = undefined; } - results && results.then(final => res.send(final)); + !results ? res.send(undefined) : results.then(response => res.send(response.data)); }); }); -- cgit v1.2.3-70-g09d2 From dc766185075b5861686c68a704a8e49213eacbc6 Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Wed, 14 Aug 2019 01:00:29 -0400 Subject: cleanup and streamlined / robust type requirements --- .../apis/google_docs/GoogleApiClientUtils.ts | 88 +++++++++++----------- src/client/views/MainView.tsx | 12 --- src/client/views/nodes/FormattedTextBox.tsx | 20 ++--- src/new_fields/RichTextField.ts | 2 +- 4 files changed, 58 insertions(+), 64 deletions(-) (limited to 'src') diff --git a/src/client/apis/google_docs/GoogleApiClientUtils.ts b/src/client/apis/google_docs/GoogleApiClientUtils.ts index dda36f05a..f4fb87e0b 100644 --- a/src/client/apis/google_docs/GoogleApiClientUtils.ts +++ b/src/client/apis/google_docs/GoogleApiClientUtils.ts @@ -17,32 +17,32 @@ export namespace GoogleApiClientUtils { export namespace Utils { export const extractText = (document: docs_v1.Schema$Document, removeNewlines = false) => { - let fragments: string[] = []; + const fragments: string[] = []; if (document.body && document.body.content) { - for (let element of document.body.content) { + for (const element of document.body.content) { if (element.paragraph && element.paragraph.elements) { - for (let inner of element.paragraph.elements) { + for (const inner of element.paragraph.elements) { if (inner && inner.textRun) { - let fragment = inner.textRun.content; + const fragment = inner.textRun.content; fragment && fragments.push(fragment); } } } } } - let text = fragments.join(""); + const text = fragments.join(""); return removeNewlines ? text.ReplaceAll("\n", "") : text; }; export const EndOf = (schema: docs_v1.Schema$Document): Opt => { if (schema.body && schema.body.content) { - let paragraphs = schema.body.content.filter(el => el.paragraph); + const paragraphs = schema.body.content.filter(el => el.paragraph); if (paragraphs.length) { - let target = paragraphs[paragraphs.length - 1]; + const target = paragraphs[paragraphs.length - 1]; if (target.paragraph && target.paragraph.elements) { length = target.paragraph.elements.length; if (length) { - let final = target.paragraph.elements[length - 1]; + const final = target.paragraph.elements[length - 1]; return final.endIndex ? final.endIndex - 1 : undefined; } } @@ -52,17 +52,25 @@ export namespace GoogleApiClientUtils { } + export type IdHandler = (id: DocumentId) => any; + export interface CreateOptions { + handler: IdHandler; + // if excluded, will use a default title annotated with the current date + title?: string; + } + export interface ReadOptions { documentId: string; + // if exluded, will preserve newlines removeNewlines?: boolean; } + export type DocumentId = string; export interface WriteOptions { - documentId?: string; - title?: string; content: string | string[]; + reference: DocumentId | CreateOptions; + // if excluded, will compute the last index of the document and append the content there index?: number; - store?: { receiver: Doc, key: string }; } /** @@ -70,33 +78,36 @@ export namespace GoogleApiClientUtils { * 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 schema whatever subset of a docs_v1.Schema$Document is required to properly initialize your - * Google Doc. This schema defines all aspects of a Google Doc, from the title to headers / footers to the - * actual document body and its styling! + * @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. */ - const Create = async (title?: string): Promise => { - let path = RouteStore.googleDocs + Actions.Create; - let parameters = { + const Create = async (options: CreateOptions): Promise => { + const path = RouteStore.googleDocs + Actions.Create; + const parameters = { requestBody: { - title: title || `Dash Export (${new Date().toDateString()})` + title: options.title || `Dash Export (${new Date().toDateString()})` } }; try { - let schema: docs_v1.Schema$Document = await PostToServer(path, parameters); - return schema.documentId; + const schema: docs_v1.Schema$Document = await PostToServer(path, parameters); + const generatedId = schema.documentId; + if (generatedId) { + options.handler(generatedId); + return generatedId; + } } catch { return undefined; } }; const Retrieve = async (documentId: string): Promise => { - let path = RouteStore.googleDocs + Actions.Retrieve; - let parameters = { + const path = RouteStore.googleDocs + Actions.Retrieve; + const parameters = { documentId }; try { - let schema: docs_v1.Schema$Document = await PostToServer(path, parameters); + const schema: docs_v1.Schema$Document = await PostToServer(path, parameters); return schema; } catch { return undefined; @@ -104,16 +115,15 @@ export namespace GoogleApiClientUtils { }; const Update = async (documentId: string, requests: docs_v1.Schema$Request[]): Promise => { - let path = RouteStore.googleDocs + Actions.Update; - let parameters = { + const path = RouteStore.googleDocs + Actions.Update; + const parameters = { documentId, requestBody: { requests } }; try { - let replies: docs_v1.Schema$BatchUpdateDocumentResponse = await PostToServer(path, parameters); - console.log(replies); + const replies: docs_v1.Schema$BatchUpdateDocumentResponse = await PostToServer(path, parameters); return replies; } catch { return undefined; @@ -131,38 +141,32 @@ export namespace GoogleApiClientUtils { if (!schema) { return undefined; } - let lines = Utils.extractText(schema).split("\n"); + const lines = Utils.extractText(schema).split("\n"); return options.removeNewlines ? lines.filter(line => line.length) : lines; }); }; export const Write = async (options: WriteOptions): Promise => { - let target = options.documentId; - if (!target) { - if (!(target = await Create(options.title))) { - return undefined; - } + let documentId: string | undefined; + const ref = options.reference; + if (!(documentId = typeof ref === "string" ? ref : await Create(ref))) { + return undefined; } let index = options.index; if (!index) { - let schema = await Retrieve(target); + let schema = await Retrieve(documentId); if (!schema || !(index = Utils.EndOf(schema))) { return undefined; } } - let text = options.content; - let request = { + const text = options.content; + const request = { insertText: { text: isArray(text) ? text.join("\n") : text, location: { index } } }; - return Update(target, [request]).then(res => { - if (res && options.store) { - options.store.receiver[options.store.key] = res.documentId; - } - return res; - }); + return Update(documentId, [request]); }; } diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 7b15e9624..77f0e3d60 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -149,18 +149,6 @@ export class MainView extends React.Component { }, { fireImmediately: true }); } - componentDidMount() { - reaction(() => this.mainContainer, () => { - let main = this.mainContainer, documentId; - if (main && (documentId = StrCast(main.googleDocId))) { - let options = { documentId, removeNewlines: true }; - GoogleApiClientUtils.Docs.ReadLines(options).then(lines => { - console.log(lines); - }); - } - }); - } - componentWillUnMount() { window.removeEventListener("keydown", KeyManager.Instance.handle); window.removeEventListener("pointerdown", this.globalPointerDown); diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index 8c2af7c9e..50ec27259 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -670,16 +670,18 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } exportToGoogleDoc = () => { - let dataDoc = Doc.GetProto(this.props.Document); - let data = Cast(dataDoc.data, RichTextField); - let content: string | undefined; - if (data && (content = data.plainText())) { - GoogleApiClientUtils.Docs.Write({ - title: StrCast(dataDoc.title), - store: { receiver: dataDoc, key: googleDocKey }, - content - }); + const dataDoc = Doc.GetProto(this.props.Document); + const data = Cast(dataDoc.data, RichTextField); + if (!data) { + return; } + GoogleApiClientUtils.Docs.Write({ + reference: { + title: StrCast(dataDoc.title), + handler: id => dataDoc[googleDocKey] = id + }, + content: data.plainText() + }); } render() { diff --git a/src/new_fields/RichTextField.ts b/src/new_fields/RichTextField.ts index dc66813e0..3e8803a34 100644 --- a/src/new_fields/RichTextField.ts +++ b/src/new_fields/RichTextField.ts @@ -33,6 +33,6 @@ export class RichTextField extends ObjectField { considering = considering.substring(matches.index + matches[0].length); this.Extractor.lastIndex = 0; } - return contents.length ? contents : undefined; + return contents; } } \ No newline at end of file -- cgit v1.2.3-70-g09d2 From 48fcec82fa384ec260a02965f9f78c2e41256dd9 Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Wed, 14 Aug 2019 03:41:11 -0400 Subject: clean up and regex improvement --- .../apis/google_docs/GoogleApiClientUtils.ts | 111 ++++++++++++--------- src/client/views/nodes/FormattedTextBox.tsx | 15 ++- src/new_fields/RichTextField.ts | 4 +- src/server/index.ts | 35 ++++--- 4 files changed, 99 insertions(+), 66 deletions(-) (limited to 'src') diff --git a/src/client/apis/google_docs/GoogleApiClientUtils.ts b/src/client/apis/google_docs/GoogleApiClientUtils.ts index f4fb87e0b..c6c7d7bd4 100644 --- a/src/client/apis/google_docs/GoogleApiClientUtils.ts +++ b/src/client/apis/google_docs/GoogleApiClientUtils.ts @@ -1,7 +1,7 @@ import { docs_v1 } from "googleapis"; import { PostToServer } from "../../../Utils"; import { RouteStore } from "../../../server/RouteStore"; -import { Opt, Doc } from "../../../new_fields/Doc"; +import { Opt } from "../../../new_fields/Doc"; import { isArray } from "util"; export namespace GoogleApiClientUtils { @@ -14,9 +14,42 @@ export namespace GoogleApiClientUtils { Update = "update" } + export type DocumentId = string; + export type Reference = DocumentId | CreateOptions; + export type TextContent = string | string[]; + export type IdHandler = (id: DocumentId) => any; + + export type CreationResult = Opt; + export type RetrievalResult = Opt; + export type UpdateResult = Opt; + export type ReadLinesResult = Opt; + export type ReadResult = Opt; + + export interface CreateOptions { + handler: IdHandler; // callback to process the documentId of the newly created Google Doc + title?: string; // if excluded, will use a default title annotated with the current date + } + + export interface RetrieveOptions { + documentId: DocumentId; + } + + export type ReadOptions = RetrieveOptions & { removeNewlines?: boolean }; + + export interface WriteOptions { + content: TextContent; + reference: Reference; + index?: number; // if excluded, will compute the last index of the document and append the content there + } + + export interface UpdateOptions { + documentId: DocumentId; + requests: docs_v1.Schema$Request[]; + } + export namespace Utils { - export const extractText = (document: docs_v1.Schema$Document, removeNewlines = false) => { + export const extractText = (document: docs_v1.Schema$Document, removeNewlines = false): string => { const fragments: string[] = []; if (document.body && document.body.content) { for (const element of document.body.content) { @@ -34,7 +67,7 @@ export namespace GoogleApiClientUtils { return removeNewlines ? text.ReplaceAll("\n", "") : text; }; - export const EndOf = (schema: docs_v1.Schema$Document): Opt => { + export const endOf = (schema: docs_v1.Schema$Document): number | undefined => { if (schema.body && schema.body.content) { const paragraphs = schema.body.content.filter(el => el.paragraph); if (paragraphs.length) { @@ -50,27 +83,8 @@ export namespace GoogleApiClientUtils { } }; - } + export const initialize = async (reference: Reference) => typeof reference === "string" ? reference : create(reference); - export type IdHandler = (id: DocumentId) => any; - export interface CreateOptions { - handler: IdHandler; - // if excluded, will use a default title annotated with the current date - title?: string; - } - - export interface ReadOptions { - documentId: string; - // if exluded, will preserve newlines - removeNewlines?: boolean; - } - - export type DocumentId = string; - export interface WriteOptions { - content: string | string[]; - reference: DocumentId | CreateOptions; - // if excluded, will compute the last index of the document and append the content there - index?: number; } /** @@ -82,7 +96,7 @@ export namespace GoogleApiClientUtils { * 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. */ - const Create = async (options: CreateOptions): Promise => { + export const create = async (options: CreateOptions): Promise => { const path = RouteStore.googleDocs + Actions.Create; const parameters = { requestBody: { @@ -101,43 +115,40 @@ export namespace GoogleApiClientUtils { } }; - const Retrieve = async (documentId: string): Promise => { + export const retrieve = async (options: RetrieveOptions): Promise => { const path = RouteStore.googleDocs + Actions.Retrieve; - const parameters = { - documentId - }; try { - const schema: docs_v1.Schema$Document = await PostToServer(path, parameters); + const schema: RetrievalResult = await PostToServer(path, options); return schema; } catch { return undefined; } }; - const Update = async (documentId: string, requests: docs_v1.Schema$Request[]): Promise => { + export const update = async (options: UpdateOptions): Promise => { const path = RouteStore.googleDocs + Actions.Update; const parameters = { - documentId, + documentId: options.documentId, requestBody: { - requests + requests: options.requests } }; try { - const replies: docs_v1.Schema$BatchUpdateDocumentResponse = await PostToServer(path, parameters); + const replies: UpdateResult = await PostToServer(path, parameters); return replies; } catch { return undefined; } }; - export const Read = async (options: ReadOptions): Promise => { - return Retrieve(options.documentId).then(schema => { + export const read = async (options: ReadOptions): Promise => { + return retrieve(options).then(schema => { return schema ? Utils.extractText(schema, options.removeNewlines) : undefined; }); }; - export const ReadLines = async (options: ReadOptions) => { - return Retrieve(options.documentId).then(schema => { + export const readLines = async (options: ReadOptions): Promise => { + return retrieve(options).then(schema => { if (!schema) { return undefined; } @@ -146,27 +157,29 @@ export namespace GoogleApiClientUtils { }); }; - export const Write = async (options: WriteOptions): Promise => { - let documentId: string | undefined; - const ref = options.reference; - if (!(documentId = typeof ref === "string" ? ref : await Create(ref))) { + export const write = async (options: WriteOptions): Promise => { + const documentId = await Utils.initialize(options.reference); + if (!documentId) { return undefined; } let index = options.index; if (!index) { - let schema = await Retrieve(documentId); - if (!schema || !(index = Utils.EndOf(schema))) { + let schema = await retrieve({ documentId }); + if (!schema || !(index = Utils.endOf(schema))) { return undefined; } } const text = options.content; - const request = { - insertText: { - text: isArray(text) ? text.join("\n") : text, - location: { index } - } + const updateOptions = { + documentId, + requests: [{ + insertText: { + text: isArray(text) ? text.join("\n") : text, + location: { index } + } + }] }; - return Update(documentId, [request]); + return update(updateOptions); }; } diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index 50ec27259..46aed9b2d 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -288,6 +288,19 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } } + componentWillMount() { + this.pollExportedCounterpart(); + } + + pollExportedCounterpart = async () => { + let dataDoc = Doc.GetProto(this.props.Document); + let documentId = StrCast(dataDoc[googleDocKey]); + if (documentId) { + let contents = await GoogleApiClientUtils.Docs.read({ documentId }); + contents ? console.log(contents) : delete dataDoc[googleDocKey]; + } + } + componentDidMount() { const config = { schema, @@ -675,7 +688,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe if (!data) { return; } - GoogleApiClientUtils.Docs.Write({ + GoogleApiClientUtils.Docs.write({ reference: { title: StrCast(dataDoc.title), handler: id => dataDoc[googleDocKey] = id diff --git a/src/new_fields/RichTextField.ts b/src/new_fields/RichTextField.ts index 3e8803a34..4f782816c 100644 --- a/src/new_fields/RichTextField.ts +++ b/src/new_fields/RichTextField.ts @@ -9,7 +9,7 @@ import { scriptingGlobal } from "../client/util/Scripting"; export class RichTextField extends ObjectField { @serializable(true) readonly Data: string; - private Extractor = /,\"text\":\"([^\"\}]*)\"\}/g; + private Extractor = /,\"text\":\"([^\}]*)\"\}/g; constructor(data: string) { super(); @@ -33,6 +33,6 @@ export class RichTextField extends ObjectField { considering = considering.substring(matches.index + matches[0].length); this.Extractor.lastIndex = 0; } - return contents; + return contents.ReplaceAll("\\", ""); } } \ No newline at end of file diff --git a/src/server/index.ts b/src/server/index.ts index abaa29658..ef1829f30 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -43,6 +43,8 @@ import * as YoutubeApi from "./apis/youtube/youtubeApiSample"; import { Response } from 'express-serve-static-core'; import { GoogleApiServerUtils } from "./apis/google/GoogleApiServerUtils"; import { GaxiosResponse } from 'gaxios'; +import { Opt } from '../new_fields/Doc'; +import { docs_v1 } from 'googleapis'; const MongoStore = require('connect-mongo')(session); const mongoose = require('mongoose'); const probe = require("probe-image-size"); @@ -797,23 +799,28 @@ 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"); +type ApiResponse = Promise; +type ApiHandler = (endpoint: docs_v1.Resource$Documents, parameters: any) => ApiResponse; +type Action = "create" | "retrieve" | "update"; + +const EndpointHandlerMap = new Map([ + ["create", (api, params) => api.create(params)], + ["retrieve", (api, params) => api.get(params)], + ["update", (api, params) => api.batchUpdate(params)], +]); + app.post(RouteStore.googleDocs + ":action", (req, res) => { GoogleApiServerUtils.Docs.GetEndpoint({ credentials, token }).then(endpoint => { - let results: Promise | undefined; - let documents = endpoint.documents; - let parameters = req.body; - switch (req.params.action) { - case "create": - results = documents.create(parameters); - break; - case "retrieve": - results = documents.get(parameters); - break; - case "update": - results = documents.batchUpdate(parameters); - break; + let handler = EndpointHandlerMap.get(req.params.action); + if (handler) { + let execute = handler(endpoint.documents, req.body).then( + response => res.send(response.data), + rejection => res.send(rejection) + ); + execute.catch(exception => res.send(exception)); + return; } - !results ? res.send(undefined) : results.then(response => res.send(response.data)); + res.send(undefined); }); }); -- cgit v1.2.3-70-g09d2 From 9ea032b9ab14ba17511b1014044dba0236e93837 Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Wed, 14 Aug 2019 10:48:23 -0400 Subject: refactored plain text parsing and setting --- src/client/views/nodes/FormattedTextBox.tsx | 13 +++++--- src/new_fields/RichTextField.ts | 50 ++++++++++++++++++++++------- 2 files changed, 48 insertions(+), 15 deletions(-) (limited to 'src') diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index 46aed9b2d..5ba2aa0cf 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -12,7 +12,7 @@ import { EditorView } from "prosemirror-view"; import { Doc, Opt, DocListCast } from "../../../new_fields/Doc"; import { Id, Copy } from '../../../new_fields/FieldSymbols'; import { List } from '../../../new_fields/List'; -import { RichTextField } from "../../../new_fields/RichTextField"; +import { RichTextField, ToPlainText, FromPlainText } from "../../../new_fields/RichTextField"; import { createSchema, listSpec, makeInterface } from "../../../new_fields/Schema"; import { BoolCast, Cast, NumCast, StrCast, DateCast } from "../../../new_fields/Types"; import { DocServer } from "../../DocServer"; @@ -296,8 +296,13 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe let dataDoc = Doc.GetProto(this.props.Document); let documentId = StrCast(dataDoc[googleDocKey]); if (documentId) { - let contents = await GoogleApiClientUtils.Docs.read({ documentId }); - contents ? console.log(contents) : delete dataDoc[googleDocKey]; + let exportState = await GoogleApiClientUtils.Docs.read({ documentId }); + if (exportState) { + let data = Cast(dataDoc.data, RichTextField); + data && data[FromPlainText](exportState); + } else { + delete dataDoc[googleDocKey]; + } } } @@ -693,7 +698,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe title: StrCast(dataDoc.title), handler: id => dataDoc[googleDocKey] = id }, - content: data.plainText() + content: data[ToPlainText]() }); } diff --git a/src/new_fields/RichTextField.ts b/src/new_fields/RichTextField.ts index 4f782816c..9d8a1cecb 100644 --- a/src/new_fields/RichTextField.ts +++ b/src/new_fields/RichTextField.ts @@ -4,12 +4,14 @@ import { Deserializable } from "../client/util/SerializationHelper"; import { Copy, ToScriptString } from "./FieldSymbols"; import { scriptingGlobal } from "../client/util/Scripting"; +export const ToPlainText = Symbol("PlainText"); +export const FromPlainText = Symbol("PlainText"); + @scriptingGlobal @Deserializable("RichTextField") export class RichTextField extends ObjectField { @serializable(true) - readonly Data: string; - private Extractor = /,\"text\":\"([^\}]*)\"\}/g; + Data: string; constructor(data: string) { super(); @@ -24,15 +26,41 @@ export class RichTextField extends ObjectField { return `new RichTextField("${this.Data}")`; } - plainText = () => { - let contents = ""; - let matches: RegExpExecArray | null; - let considering = this.Data; - while ((matches = this.Extractor.exec(considering)) !== null) { - contents += matches[1]; - considering = considering.substring(matches.index + matches[0].length); - this.Extractor.lastIndex = 0; + [ToPlainText]() { + let content = JSON.parse(this.Data).doc.content; + let paragraphs = content.filter((item: any) => item.type === "paragraph"); + let output = ""; + for (let i = 0; i < paragraphs.length; i++) { + let paragraph = paragraphs[i]; + if (paragraph.content) { + output += paragraph.content.map((block: any) => block.text).join(""); + } else { + output += i > 0 && paragraphs[i - 1].content ? "\n\n" : "\n"; + } } - return contents.ReplaceAll("\\", ""); + return output; + } + + [FromPlainText](plainText: string) { + let elements = plainText.split("\n"); + let parsed = JSON.parse(this.Data); + parsed.doc.content = elements.map(text => { + let paragraph: any = { type: "paragraph" }; + if (text.length) { + paragraph.content = [{ + type: "text", + marks: [], + text + }]; + } + return paragraph; + }); + parsed.selection = { + type: "text", + anchor: 0, + head: 0 + }; + this.Data = JSON.stringify(parsed); } + } \ No newline at end of file -- cgit v1.2.3-70-g09d2 From 1b9ee607e0c34f82ef39da494329d24623debd18 Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Wed, 14 Aug 2019 10:58:26 -0400 Subject: changed selection --- src/new_fields/RichTextField.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/new_fields/RichTextField.ts b/src/new_fields/RichTextField.ts index 9d8a1cecb..f0284b1b8 100644 --- a/src/new_fields/RichTextField.ts +++ b/src/new_fields/RichTextField.ts @@ -57,8 +57,8 @@ export class RichTextField extends ObjectField { }); parsed.selection = { type: "text", - anchor: 0, - head: 0 + anchor: plainText.length, + head: plainText.length }; this.Data = JSON.stringify(parsed); } -- cgit v1.2.3-70-g09d2 From 13d2be7f66c01ee9f09cb65f567eb3da760df670 Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Wed, 14 Aug 2019 15:18:13 -0400 Subject: semi-synchronous editing between dash notes and google docs --- package.json | 2 + .../apis/google_docs/GoogleApiClientUtils.ts | 40 ++++++++++++++------ src/client/views/nodes/FormattedTextBox.tsx | 44 ++++++++++++++-------- src/new_fields/RichTextField.ts | 25 +++++++----- 4 files changed, 76 insertions(+), 35 deletions(-) (limited to 'src') diff --git a/package.json b/package.json index 326468ab9..7a629d813 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ "@types/cookie-parser": "^1.4.1", "@types/cookie-session": "^2.0.36", "@types/d3-format": "^1.3.1", + "@types/diff": "^4.0.2", "@types/dotenv": "^6.1.1", "@types/express": "^4.16.1", "@types/express-flash": "0.0.0", @@ -127,6 +128,7 @@ "cookie-session": "^2.0.0-beta.3", "crypto-browserify": "^3.11.0", "d3-format": "^1.3.2", + "diff": "^4.0.1", "dotenv": "^8.0.0", "express": "^4.16.4", "express-flash": "0.0.2", diff --git a/src/client/apis/google_docs/GoogleApiClientUtils.ts b/src/client/apis/google_docs/GoogleApiClientUtils.ts index c6c7d7bd4..c1cbba31f 100644 --- a/src/client/apis/google_docs/GoogleApiClientUtils.ts +++ b/src/client/apis/google_docs/GoogleApiClientUtils.ts @@ -14,6 +14,11 @@ export namespace GoogleApiClientUtils { Update = "update" } + export enum WriteMode { + Insert, + Replace + } + export type DocumentId = string; export type Reference = DocumentId | CreateOptions; export type TextContent = string | string[]; @@ -37,6 +42,7 @@ export namespace GoogleApiClientUtils { export type ReadOptions = RetrieveOptions & { removeNewlines?: boolean }; export interface WriteOptions { + mode: WriteMode; content: TextContent; reference: Reference; index?: number; // if excluded, will compute the last index of the document and append the content there @@ -158,28 +164,40 @@ export namespace GoogleApiClientUtils { }; export const write = async (options: WriteOptions): Promise => { + const requests: docs_v1.Schema$Request[] = []; const documentId = await Utils.initialize(options.reference); if (!documentId) { return undefined; } let index = options.index; - if (!index) { + const mode = options.mode; + if (!(index && mode === WriteMode.Insert)) { let schema = await retrieve({ documentId }); if (!schema || !(index = Utils.endOf(schema))) { return undefined; } } - const text = options.content; - const updateOptions = { - documentId, - requests: [{ - insertText: { - text: isArray(text) ? text.join("\n") : text, - location: { index } + if (mode === WriteMode.Replace) { + requests.push({ + deleteContentRange: { + range: { + startIndex: 1, + endIndex: index + } } - }] - }; - return update(updateOptions); + }); + index = 1; + } + const text = options.content; + requests.push({ + insertText: { + text: isArray(text) ? text.join("\n") : text, + location: { index } + } + }); + let replies = await update({ documentId, requests }); + console.log(replies); + return replies; }; } diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index 5ba2aa0cf..ad29ce775 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -12,7 +12,7 @@ import { EditorView } from "prosemirror-view"; import { Doc, Opt, DocListCast } from "../../../new_fields/Doc"; import { Id, Copy } from '../../../new_fields/FieldSymbols'; import { List } from '../../../new_fields/List'; -import { RichTextField, ToPlainText, FromPlainText } from "../../../new_fields/RichTextField"; +import { RichTextField, ToGoogleDocText, FromGoogleDocText } from "../../../new_fields/RichTextField"; import { createSchema, listSpec, makeInterface } from "../../../new_fields/Schema"; import { BoolCast, Cast, NumCast, StrCast, DateCast } from "../../../new_fields/Types"; import { DocServer } from "../../DocServer"; @@ -39,6 +39,7 @@ import { DateField } from '../../../new_fields/DateField'; import { Utils } from '../../../Utils'; import { MainOverlayTextBox } from '../MainOverlayTextBox'; import { GoogleApiClientUtils } from '../../apis/google_docs/GoogleApiClientUtils'; +import * as diff from "diff"; library.add(faEdit); library.add(faSmile, faTextHeight, faUpload); @@ -299,7 +300,9 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe let exportState = await GoogleApiClientUtils.Docs.read({ documentId }); if (exportState) { let data = Cast(dataDoc.data, RichTextField); - data && data[FromPlainText](exportState); + if (data) { + dataDoc.data = new RichTextField(data[FromGoogleDocText](exportState)); + } } else { delete dataDoc[googleDocKey]; } @@ -629,6 +632,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe this._undoTyping.end(); this._undoTyping = undefined; } + this.updateGoogleDoc(); } public _undoTyping?: UndoManager.Batch; onKeyPress = (e: React.KeyboardEvent) => { @@ -683,23 +687,33 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe }); ContextMenu.Instance.addItem({ description: "Text Funcs...", subitems: subitems, icon: "text-height" }); if (!(googleDocKey in Doc.GetProto(this.props.Document))) { - ContextMenu.Instance.addItem({ description: "Export to Google Doc...", event: this.exportToGoogleDoc, icon: "upload" }); + ContextMenu.Instance.addItem({ + description: "Export to Google Doc...", + event: this.updateGoogleDoc, + icon: "upload" + }); } } - exportToGoogleDoc = () => { - const dataDoc = Doc.GetProto(this.props.Document); - const data = Cast(dataDoc.data, RichTextField); - if (!data) { - return; + updateGoogleDoc = () => { + let modes = GoogleApiClientUtils.Docs.WriteMode; + let mode = modes.Replace; + let reference: Opt = Cast(this.dataDoc[googleDocKey], "string"); + if (!reference) { + mode = modes.Insert; + reference = { + title: StrCast(this.dataDoc.title), + handler: id => this.dataDoc[googleDocKey] = id + }; + } + const data = Cast(this.dataDoc.data, RichTextField); + if (data) { + GoogleApiClientUtils.Docs.write({ + mode, + content: data[ToGoogleDocText](), + reference + }); } - GoogleApiClientUtils.Docs.write({ - reference: { - title: StrCast(dataDoc.title), - handler: id => dataDoc[googleDocKey] = id - }, - content: data[ToPlainText]() - }); } render() { diff --git a/src/new_fields/RichTextField.ts b/src/new_fields/RichTextField.ts index f0284b1b8..92b19b921 100644 --- a/src/new_fields/RichTextField.ts +++ b/src/new_fields/RichTextField.ts @@ -4,14 +4,14 @@ import { Deserializable } from "../client/util/SerializationHelper"; import { Copy, ToScriptString } from "./FieldSymbols"; import { scriptingGlobal } from "../client/util/Scripting"; -export const ToPlainText = Symbol("PlainText"); -export const FromPlainText = Symbol("PlainText"); +export const ToGoogleDocText = Symbol("PlainText"); +export const FromGoogleDocText = Symbol("PlainText"); @scriptingGlobal @Deserializable("RichTextField") export class RichTextField extends ObjectField { @serializable(true) - Data: string; + readonly Data: string; constructor(data: string) { super(); @@ -26,24 +26,28 @@ export class RichTextField extends ObjectField { return `new RichTextField("${this.Data}")`; } - [ToPlainText]() { + [ToGoogleDocText]() { let content = JSON.parse(this.Data).doc.content; let paragraphs = content.filter((item: any) => item.type === "paragraph"); let output = ""; for (let i = 0; i < paragraphs.length; i++) { let paragraph = paragraphs[i]; + let addNewLine = i > 0 ? paragraphs[i - 1].content : false; if (paragraph.content) { output += paragraph.content.map((block: any) => block.text).join(""); } else { - output += i > 0 && paragraphs[i - 1].content ? "\n\n" : "\n"; + output += "\n"; } + addNewLine && (output += "\n"); } return output; } - [FromPlainText](plainText: string) { + [FromGoogleDocText](plainText: string) { let elements = plainText.split("\n"); + !elements[elements.length - 1].length && elements.pop(); let parsed = JSON.parse(this.Data); + let blankCount = 0; parsed.doc.content = elements.map(text => { let paragraph: any = { type: "paragraph" }; if (text.length) { @@ -52,15 +56,18 @@ export class RichTextField extends ObjectField { marks: [], text }]; + } else { + blankCount++; } return paragraph; }); + let selection = plainText.length + 2 * blankCount; parsed.selection = { type: "text", - anchor: plainText.length, - head: plainText.length + anchor: selection, + head: selection }; - this.Data = JSON.stringify(parsed); + return JSON.stringify(parsed); } } \ No newline at end of file -- cgit v1.2.3-70-g09d2 From 4f338a06345c6a002c9bebc9ad0c9c1fe910eebd Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Wed, 14 Aug 2019 16:15:56 -0400 Subject: clean up and edge cases --- src/client/apis/google_docs/GoogleApiClientUtils.ts | 15 +++++++++++---- src/new_fields/RichTextField.ts | 10 +++------- 2 files changed, 14 insertions(+), 11 deletions(-) (limited to 'src') diff --git a/src/client/apis/google_docs/GoogleApiClientUtils.ts b/src/client/apis/google_docs/GoogleApiClientUtils.ts index c1cbba31f..9d78b632d 100644 --- a/src/client/apis/google_docs/GoogleApiClientUtils.ts +++ b/src/client/apis/google_docs/GoogleApiClientUtils.ts @@ -178,7 +178,7 @@ export namespace GoogleApiClientUtils { } } if (mode === WriteMode.Replace) { - requests.push({ + index > 1 && requests.push({ deleteContentRange: { range: { startIndex: 1, @@ -189,14 +189,21 @@ export namespace GoogleApiClientUtils { index = 1; } const text = options.content; - requests.push({ + text.length && requests.push({ insertText: { text: isArray(text) ? text.join("\n") : text, location: { index } } }); - let replies = await update({ documentId, requests }); - console.log(replies); + if (!requests.length) { + return undefined; + } + let replies: any = await update({ documentId, requests }); + let errors = "errors"; + if (errors in replies) { + console.log("Write operation failed:"); + console.log(replies[errors].map((error: any) => error.message)); + } return replies; }; diff --git a/src/new_fields/RichTextField.ts b/src/new_fields/RichTextField.ts index 92b19b921..bd24bad1a 100644 --- a/src/new_fields/RichTextField.ts +++ b/src/new_fields/RichTextField.ts @@ -36,7 +36,7 @@ export class RichTextField extends ObjectField { if (paragraph.content) { output += paragraph.content.map((block: any) => block.text).join(""); } else { - output += "\n"; + output += i === 0 ? "" : "\n"; } addNewLine && (output += "\n"); } @@ -47,7 +47,6 @@ export class RichTextField extends ObjectField { let elements = plainText.split("\n"); !elements[elements.length - 1].length && elements.pop(); let parsed = JSON.parse(this.Data); - let blankCount = 0; parsed.doc.content = elements.map(text => { let paragraph: any = { type: "paragraph" }; if (text.length) { @@ -56,16 +55,13 @@ export class RichTextField extends ObjectField { marks: [], text }]; - } else { - blankCount++; } return paragraph; }); - let selection = plainText.length + 2 * blankCount; parsed.selection = { type: "text", - anchor: selection, - head: selection + anchor: 1, + head: 1 }; return JSON.stringify(parsed); } -- cgit v1.2.3-70-g09d2 From 6592660b944d745b7368f478cd9f5d9e778537ed Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Thu, 15 Aug 2019 23:52:39 -0400 Subject: simplified text testing --- src/new_fields/RichTextField.ts | 3 +++ 1 file changed, 3 insertions(+) (limited to 'src') diff --git a/src/new_fields/RichTextField.ts b/src/new_fields/RichTextField.ts index bd24bad1a..8963682c3 100644 --- a/src/new_fields/RichTextField.ts +++ b/src/new_fields/RichTextField.ts @@ -27,6 +27,9 @@ export class RichTextField extends ObjectField { } [ToGoogleDocText]() { + let state = JSON.parse(this.Data); + let text = state.doc.textBetween(0, state.doc.content.size, "\n\n"); + console.log(text); let content = JSON.parse(this.Data).doc.content; let paragraphs = content.filter((item: any) => item.type === "paragraph"); let output = ""; -- cgit v1.2.3-70-g09d2 From 7ea6b44b10e1bf23287ba33e0081cacbfc595780 Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Mon, 19 Aug 2019 14:58:02 -0400 Subject: seemed to fix syncing with remote google doc --- src/client/views/DocumentDecorations.tsx | 14 ++++++++ src/client/views/nodes/FormattedTextBox.tsx | 41 ++++++++++++++--------- src/new_fields/RichTextField.ts | 52 +++++++++++++---------------- 3 files changed, 63 insertions(+), 44 deletions(-) (limited to 'src') diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 6616d5d58..963722fe3 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -1,5 +1,6 @@ import { library } from '@fortawesome/fontawesome-svg-core'; import { faLink, faTag } from '@fortawesome/free-solid-svg-icons'; +import * as fa from '@fortawesome/free-brands-svg-icons'; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { action, computed, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; @@ -36,6 +37,7 @@ export const Flyout = higflyout.default; library.add(faLink); library.add(faTag); +library.add(fa.faGoogleDrive as any); @observer export class DocumentDecorations extends React.Component<{}, { value: string }> { @@ -618,6 +620,17 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> ); } + considerGoogleDoc = () => { + let thisDoc = SelectionManager.SelectedDocuments()[0].props.Document; + let canEmbed = thisDoc.data && thisDoc.data instanceof RichTextField; + if (!canEmbed) return (null); + return ( +
+ +
+ ); + } + considerTooltip = () => { let thisDoc = SelectionManager.SelectedDocuments()[0].props.Document; let isTextDoc = thisDoc.data && thisDoc.data instanceof RichTextField; @@ -768,6 +781,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
{this.metadataMenu} {this.considerEmbed()} + {this.considerGoogleDoc()} {/* {this.considerTooltip()} */}
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index ad29ce775..bc057bb5f 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -60,7 +60,7 @@ const richTextSchema = createSchema({ documentText: "string" }); -const googleDocKey = "googleDocId"; +const googleDocId = "googleDocId"; type RichTextDocument = makeInterface<[typeof richTextSchema]>; const RichTextDocument = makeInterface(richTextSchema); @@ -84,6 +84,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe private _proxyReactionDisposer: Opt; private dropDisposer?: DragManager.DragDropDisposer; public get CurrentDiv(): HTMLDivElement { return this._ref.current!; } + private isOpening = false; @observable _entered = false; @observable public static InputBoxOverlay?: FormattedTextBox = undefined; @@ -295,17 +296,19 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe pollExportedCounterpart = async () => { let dataDoc = Doc.GetProto(this.props.Document); - let documentId = StrCast(dataDoc[googleDocKey]); + let documentId = StrCast(dataDoc[googleDocId]); if (documentId) { let exportState = await GoogleApiClientUtils.Docs.read({ documentId }); if (exportState) { let data = Cast(dataDoc.data, RichTextField); if (data) { + this.isOpening = true; dataDoc.data = new RichTextField(data[FromGoogleDocText](exportState)); } } else { - delete dataDoc[googleDocKey]; + delete dataDoc[googleDocId]; } + this.tryUpdateHeight(); } } @@ -346,9 +349,18 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe const field = this.dataDoc ? Cast(this.dataDoc[this.props.fieldKey], RichTextField) : undefined; return field ? field.Data : `{"doc":{"type":"doc","content":[]},"selection":{"type":"text","anchor":0,"head":0}}`; }, - field2 => { - this._editorView && !this._applyingChange && - this._editorView.updateState(EditorState.fromJSON(config, JSON.parse(field2))); + incomingValue => { + if (this._editorView && !this._applyingChange) { + let updatedState = JSON.parse(incomingValue); + this._editorView.updateState(EditorState.fromJSON(config, updatedState)); + // manually sets cursor selection at the end of the text on focus + if (this.isOpening) { + this.isOpening = false; + let end = this._editorView.state.doc.content.size - 1; + updatedState.selection = { type: "text", anchor: end, head: end }; + this._editorView.updateState(EditorState.fromJSON(config, updatedState)); + } + } } ); @@ -686,7 +698,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe event: action(() => Doc.GetProto(this.props.Document).autoHeight = !BoolCast(this.props.Document.autoHeight)), icon: "expand-arrows-alt" }); ContextMenu.Instance.addItem({ description: "Text Funcs...", subitems: subitems, icon: "text-height" }); - if (!(googleDocKey in Doc.GetProto(this.props.Document))) { + if (!(googleDocId in Doc.GetProto(this.props.Document))) { ContextMenu.Instance.addItem({ description: "Export to Google Doc...", event: this.updateGoogleDoc, @@ -698,21 +710,18 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe updateGoogleDoc = () => { let modes = GoogleApiClientUtils.Docs.WriteMode; let mode = modes.Replace; - let reference: Opt = Cast(this.dataDoc[googleDocKey], "string"); + let reference: Opt = Cast(this.dataDoc[googleDocId], "string"); if (!reference) { mode = modes.Insert; reference = { title: StrCast(this.dataDoc.title), - handler: id => this.dataDoc[googleDocKey] = id + handler: id => this.dataDoc[googleDocId] = id }; } - const data = Cast(this.dataDoc.data, RichTextField); - if (data) { - GoogleApiClientUtils.Docs.write({ - mode, - content: data[ToGoogleDocText](), - reference - }); + if (this._editorView) { + let data = Cast(this.dataDoc.data, RichTextField); + let content = data ? data[ToGoogleDocText]() : this._editorView.state.doc.textContent; + GoogleApiClientUtils.Docs.write({ reference, content, mode }); } } diff --git a/src/new_fields/RichTextField.ts b/src/new_fields/RichTextField.ts index 8963682c3..ec08293e9 100644 --- a/src/new_fields/RichTextField.ts +++ b/src/new_fields/RichTextField.ts @@ -6,6 +6,8 @@ import { scriptingGlobal } from "../client/util/Scripting"; export const ToGoogleDocText = Symbol("PlainText"); export const FromGoogleDocText = Symbol("PlainText"); +const delimiter = "\n"; +const joiner = ""; @scriptingGlobal @Deserializable("RichTextField") @@ -27,45 +29,39 @@ export class RichTextField extends ObjectField { } [ToGoogleDocText]() { - let state = JSON.parse(this.Data); - let text = state.doc.textBetween(0, state.doc.content.size, "\n\n"); - console.log(text); + // Because we're working with plain text, just concatenate all paragraphs let content = JSON.parse(this.Data).doc.content; let paragraphs = content.filter((item: any) => item.type === "paragraph"); - let output = ""; - for (let i = 0; i < paragraphs.length; i++) { - let paragraph = paragraphs[i]; - let addNewLine = i > 0 ? paragraphs[i - 1].content : false; - if (paragraph.content) { - output += paragraph.content.map((block: any) => block.text).join(""); - } else { - output += i === 0 ? "" : "\n"; - } - addNewLine && (output += "\n"); - } - return output; + + // Functions to flatten ProseMirror paragraph objects (and their components) to plain text + // While this function already exists in state.doc.textBeteen(), it doesn't account for newlines + let blockText = (block: any) => block.text; + let concatenateParagraph = (p: any) => (p.content ? p.content.map(blockText).join(joiner) : "") + delimiter; + + // Concatentate paragraphs and string the result together. Trim the last newline, an artifact. + let textParagraphs = paragraphs.map(concatenateParagraph); + return textParagraphs.join(joiner).trimEnd(delimiter); } [FromGoogleDocText](plainText: string) { - let elements = plainText.split("\n"); + // Remap the text, creating blocks split on newlines + let elements = plainText.split(delimiter); + + // Google Docs adds in an extra carriage return automatically, so this counteracts it !elements[elements.length - 1].length && elements.pop(); + + // Preserve the current state, but re-write the content to be the blocks let parsed = JSON.parse(this.Data); parsed.doc.content = elements.map(text => { let paragraph: any = { type: "paragraph" }; - if (text.length) { - paragraph.content = [{ - type: "text", - marks: [], - text - }]; - } + text.length && (paragraph.content = [{ type: "text", marks: [], text }]); // An empty paragraph gets treated as a line break return paragraph; }); - parsed.selection = { - type: "text", - anchor: 1, - head: 1 - }; + + // If the new content is shorter than the previous content and selection is unchanged, may throw an out of bounds exception, so we reset it + parsed.selection = { type: "text", anchor: 1, head: 1 }; + + // Export the ProseMirror-compatible state object we've jsut built return JSON.stringify(parsed); } -- cgit v1.2.3-70-g09d2 From df11d1afdb271b37618ab9e1d362b2f4a1145982 Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Mon, 19 Aug 2019 17:48:57 -0400 Subject: cleanup, pull push --- .../apis/google_docs/GoogleApiClientUtils.ts | 27 +++++--- src/client/views/DocumentDecorations.tsx | 32 +++++++--- src/client/views/collections/CollectionSubView.tsx | 14 +++- src/client/views/nodes/FormattedTextBox.tsx | 74 +++++++++++++--------- 4 files changed, 98 insertions(+), 49 deletions(-) (limited to 'src') diff --git a/src/client/apis/google_docs/GoogleApiClientUtils.ts b/src/client/apis/google_docs/GoogleApiClientUtils.ts index 9d78b632d..55b4a76f8 100644 --- a/src/client/apis/google_docs/GoogleApiClientUtils.ts +++ b/src/client/apis/google_docs/GoogleApiClientUtils.ts @@ -27,8 +27,8 @@ export namespace GoogleApiClientUtils { export type CreationResult = Opt; export type RetrievalResult = Opt; export type UpdateResult = Opt; - export type ReadLinesResult = Opt; - export type ReadResult = Opt; + export type ReadLinesResult = Opt<{ title?: string, bodyLines?: string[] }>; + export type ReadResult = { title?: string, body?: string }; export interface CreateOptions { handler: IdHandler; // callback to process the documentId of the newly created Google Doc @@ -148,18 +148,27 @@ export namespace GoogleApiClientUtils { }; export const read = async (options: ReadOptions): Promise => { - return retrieve(options).then(schema => { - return schema ? Utils.extractText(schema, options.removeNewlines) : undefined; + return retrieve(options).then(document => { + let result: ReadResult = {}; + if (document) { + let title = document.title; + let body = Utils.extractText(document, options.removeNewlines); + result = { title, body }; + } + return result; }); }; export const readLines = async (options: ReadOptions): Promise => { - return retrieve(options).then(schema => { - if (!schema) { - return undefined; + return retrieve(options).then(document => { + let result: ReadLinesResult = {}; + if (document) { + let title = document.title; + let bodyLines = Utils.extractText(document).split("\n"); + options.removeNewlines && (bodyLines = bodyLines.filter(line => line.length)); + result = { title, bodyLines }; } - const lines = Utils.extractText(schema).split("\n"); - return options.removeNewlines ? lines.filter(line => line.length) : lines; + return result; }); }; diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 963722fe3..80d4ecb9b 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -1,6 +1,5 @@ import { library } from '@fortawesome/fontawesome-svg-core'; -import { faLink, faTag } from '@fortawesome/free-solid-svg-icons'; -import * as fa from '@fortawesome/free-brands-svg-icons'; +import { faLink, faTag, faArrowAltCircleDown, faArrowAltCircleUp } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { action, computed, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; @@ -37,7 +36,8 @@ export const Flyout = higflyout.default; library.add(faLink); library.add(faTag); -library.add(fa.faGoogleDrive as any); +library.add(faArrowAltCircleDown); +library.add(faArrowAltCircleUp); @observer export class DocumentDecorations extends React.Component<{}, { value: string }> { @@ -620,13 +620,28 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> ); } - considerGoogleDoc = () => { + considerGoogleDocsPush = () => { let thisDoc = SelectionManager.SelectedDocuments()[0].props.Document; - let canEmbed = thisDoc.data && thisDoc.data instanceof RichTextField; - if (!canEmbed) return (null); + let canPush = thisDoc.data && thisDoc.data instanceof RichTextField; + if (!canPush) return (null); + return ( +
+
thisDoc.pushToGoogleDocsTrigger = !thisDoc.pushToGoogleDocsTrigger}> + +
+
+ ); + } + + considerGoogleDocsPull = () => { + let thisDoc = SelectionManager.SelectedDocuments()[0].props.Document; + let canPull = thisDoc.data && thisDoc.data instanceof RichTextField; + if (!canPull) return (null); return (
- +
thisDoc.pullFromGoogleDocsTrigger = !thisDoc.pullFromGoogleDocsTrigger}> + +
); } @@ -781,7 +796,8 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> {this.metadataMenu} {this.considerEmbed()} - {this.considerGoogleDoc()} + {this.considerGoogleDocsPush()} + {this.considerGoogleDocsPull()} {/* {this.considerTooltip()} */} diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 077f3f941..70c0632d1 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -16,7 +16,7 @@ import { DragManager } from "../../util/DragManager"; import { undoBatch, UndoManager } from "../../util/UndoManager"; import { DocComponent } from "../DocComponent"; import { FieldViewProps } from "../nodes/FieldView"; -import { FormattedTextBox } from "../nodes/FormattedTextBox"; +import { FormattedTextBox, Blank } from "../nodes/FormattedTextBox"; import { CollectionPDFView } from "./CollectionPDFView"; import { CollectionVideoView } from "./CollectionVideoView"; import { CollectionView } from "./CollectionView"; @@ -206,7 +206,17 @@ export function CollectionSubView(schemaCtor: (doc: Doc) => T) { this.props.addDocument(Docs.Create.VideoDocument(url, { ...options, title: url, width: 400, height: 315, nativeWidth: 600, nativeHeight: 472.5 })); return; } - + let matches: RegExpExecArray | null; + if ((matches = /(https:\/\/)?docs\.google\.com\/document\/d\/([^\\]+)\/edit/g.exec(text)) !== null) { + let newBox = Docs.Create.TextDocument({ ...options, width: 600, height: 400, title: "Fetching title from Google Docs..." }); + let proto = newBox.proto!; + proto.autoHeight = true; + proto.googleDocId = matches[2]; + proto.data = "Fetching contents from Google Docs..."; + proto.backgroundColor = "#eeeeff"; + this.props.addDocument(newBox); + return; + } let batch = UndoManager.StartBatch("collection view drop"); let promises: Promise[] = []; // tslint:disable-next-line:prefer-for-of diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index bc057bb5f..406c2c4a6 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -1,6 +1,6 @@ import { library } from '@fortawesome/fontawesome-svg-core'; import { faEdit, faSmile, faTextHeight, faUpload } from '@fortawesome/free-solid-svg-icons'; -import { action, IReactionDisposer, observable, reaction, runInAction, computed, Lambda, trace } from "mobx"; +import { action, IReactionDisposer, observable, reaction, runInAction, computed, Lambda, trace, untracked } from "mobx"; import { observer } from "mobx-react"; import { baseKeymap } from "prosemirror-commands"; import { history } from "prosemirror-history"; @@ -47,6 +47,8 @@ library.add(faSmile, faTextHeight, faUpload); // FormattedTextBox: Displays an editable plain text node that maps to a specified Key of a Document // +export const Blank = `{"doc":{"type":"doc","content":[]},"selection":{"type":"text","anchor":0,"head":0}}`; + export interface FormattedTextBoxProps { isOverlay?: boolean; hideOnLeave?: boolean; @@ -84,7 +86,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe private _proxyReactionDisposer: Opt; private dropDisposer?: DragManager.DragDropDisposer; public get CurrentDiv(): HTMLDivElement { return this._ref.current!; } - private isOpening = false; + private isGoogleDocsUpdate = false; @observable _entered = false; @observable public static InputBoxOverlay?: FormattedTextBox = undefined; @@ -290,28 +292,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } } - componentWillMount() { - this.pollExportedCounterpart(); - } - - pollExportedCounterpart = async () => { - let dataDoc = Doc.GetProto(this.props.Document); - let documentId = StrCast(dataDoc[googleDocId]); - if (documentId) { - let exportState = await GoogleApiClientUtils.Docs.read({ documentId }); - if (exportState) { - let data = Cast(dataDoc.data, RichTextField); - if (data) { - this.isOpening = true; - dataDoc.data = new RichTextField(data[FromGoogleDocText](exportState)); - } - } else { - delete dataDoc[googleDocId]; - } - this.tryUpdateHeight(); - } - } - componentDidMount() { const config = { schema, @@ -347,23 +327,32 @@ 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 : `{"doc":{"type":"doc","content":[]},"selection":{"type":"text","anchor":0,"head":0}}`; + return field ? field.Data : Blank; }, incomingValue => { if (this._editorView && !this._applyingChange) { let updatedState = JSON.parse(incomingValue); this._editorView.updateState(EditorState.fromJSON(config, updatedState)); // manually sets cursor selection at the end of the text on focus - if (this.isOpening) { - this.isOpening = false; + if (this.isGoogleDocsUpdate) { + this.isGoogleDocsUpdate = false; let end = this._editorView.state.doc.content.size - 1; updatedState.selection = { type: "text", anchor: end, head: end }; this._editorView.updateState(EditorState.fromJSON(config, updatedState)); } + this.tryUpdateHeight(); } } ); + reaction(() => this.props.Document.pullFromGoogleDocsTrigger, () => { + this.pullFromGoogleDoc(); + }); + + reaction(() => this.props.Document.pushToGoogleDocsTrigger, () => { + this.pushToGoogleDoc(); + }); + this._textReactionDisposer = reaction( () => this.extensionDoc, () => { @@ -391,6 +380,13 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe this.unhighlightSearchTerms(); } }, { fireImmediately: true }); + + ["pushToGoogleDocsTrigger", "pullFromGoogleDocsTrigger"].map(key => { + let doc = this.props.Document; + if (doc[key] === undefined) { + untracked(() => doc[key] = false); + } + }); } clipboardTextSerializer = (slice: Slice): string => { @@ -644,7 +640,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe this._undoTyping.end(); this._undoTyping = undefined; } - this.updateGoogleDoc(); } public _undoTyping?: UndoManager.Batch; onKeyPress = (e: React.KeyboardEvent) => { @@ -701,13 +696,13 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe if (!(googleDocId in Doc.GetProto(this.props.Document))) { ContextMenu.Instance.addItem({ description: "Export to Google Doc...", - event: this.updateGoogleDoc, + event: this.pushToGoogleDoc, icon: "upload" }); } } - updateGoogleDoc = () => { + pushToGoogleDoc = () => { let modes = GoogleApiClientUtils.Docs.WriteMode; let mode = modes.Replace; let reference: Opt = Cast(this.dataDoc[googleDocId], "string"); @@ -725,6 +720,25 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } } + pullFromGoogleDoc = async () => { + let dataDoc = Doc.GetProto(this.props.Document); + let documentId = StrCast(dataDoc[googleDocId]); + if (documentId) { + let exportState = await GoogleApiClientUtils.Docs.read({ documentId }); + if (exportState && exportState.body && exportState.title) { + let data = Cast(dataDoc.data, RichTextField); + if (data) { + this.isGoogleDocsUpdate = true; + dataDoc.data = new RichTextField(data[FromGoogleDocText](exportState.body)); + dataDoc.title = exportState.title; + } + } else { + delete dataDoc[googleDocId]; + } + } + } + + render() { let self = this; let style = this.props.isOverlay ? "scroll" : "hidden"; -- cgit v1.2.3-70-g09d2 From 00cecb10d80bad8d717cd06ddc4bf156664b9f2d Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Mon, 19 Aug 2019 22:39:56 -0400 Subject: fixed push pull UI --- .../apis/google_docs/GoogleApiClientUtils.ts | 3 + src/client/views/DocumentDecorations.tsx | 14 ++- src/client/views/nodes/FormattedTextBox.tsx | 122 +++++++++++---------- src/new_fields/RichTextField.ts | 8 +- 4 files changed, 81 insertions(+), 66 deletions(-) (limited to 'src') diff --git a/src/client/apis/google_docs/GoogleApiClientUtils.ts b/src/client/apis/google_docs/GoogleApiClientUtils.ts index 55b4a76f8..821c52270 100644 --- a/src/client/apis/google_docs/GoogleApiClientUtils.ts +++ b/src/client/apis/google_docs/GoogleApiClientUtils.ts @@ -4,6 +4,9 @@ import { RouteStore } from "../../../server/RouteStore"; import { Opt } from "../../../new_fields/Doc"; import { isArray } from "util"; +export const Pulls = "googleDocsPullCount"; +export const Pushes = "googleDocsPushCount"; + export namespace GoogleApiClientUtils { export namespace Docs { diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 80d4ecb9b..797b43add 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -30,6 +30,7 @@ import { ObjectField } from '../../new_fields/ObjectField'; import { MetadataEntryMenu } from './MetadataEntryMenu'; import { ImageBox } from './nodes/ImageBox'; import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils'; +import { Pulls, Pushes } from '../apis/google_docs/GoogleApiClientUtils'; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @@ -626,7 +627,10 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> if (!canPush) return (null); return (
-
thisDoc.pushToGoogleDocsTrigger = !thisDoc.pushToGoogleDocsTrigger}> +
{ + DocumentDecorations.hasPushedHack = false; + thisDoc[Pushes] = NumCast(thisDoc[Pushes]) + 1; + }}>
@@ -639,13 +643,19 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> if (!canPull) return (null); return (
-
thisDoc.pullFromGoogleDocsTrigger = !thisDoc.pullFromGoogleDocsTrigger}> +
{ + DocumentDecorations.hasPulledHack = false; + thisDoc[Pulls] = NumCast(thisDoc[Pulls]) + 1; + }}>
); } + public static hasPushedHack = false; + public static hasPulledHack = false; + considerTooltip = () => { let thisDoc = SelectionManager.SelectedDocuments()[0].props.Document; let isTextDoc = thisDoc.data && thisDoc.data instanceof RichTextField; diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index 406c2c4a6..d2eb71350 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -12,7 +12,7 @@ import { EditorView } from "prosemirror-view"; import { Doc, Opt, DocListCast } from "../../../new_fields/Doc"; import { Id, Copy } from '../../../new_fields/FieldSymbols'; import { List } from '../../../new_fields/List'; -import { RichTextField, ToGoogleDocText, FromGoogleDocText } from "../../../new_fields/RichTextField"; +import { RichTextField, ToPlainText, FromPlainText } from "../../../new_fields/RichTextField"; import { createSchema, listSpec, makeInterface } from "../../../new_fields/Schema"; import { BoolCast, Cast, NumCast, StrCast, DateCast } from "../../../new_fields/Types"; import { DocServer } from "../../DocServer"; @@ -30,16 +30,14 @@ import { ContextMenu } from "../../views/ContextMenu"; import { ContextMenuProps } from '../ContextMenuItem'; import { DocComponent } from "../DocComponent"; import { InkingControl } from "../InkingControl"; -import { Templates } from '../Templates'; import { FieldView, FieldViewProps } from "./FieldView"; import "./FormattedTextBox.scss"; import React = require("react"); -import { For } from 'babel-types'; import { DateField } from '../../../new_fields/DateField'; import { Utils } from '../../../Utils'; import { MainOverlayTextBox } from '../MainOverlayTextBox'; -import { GoogleApiClientUtils } from '../../apis/google_docs/GoogleApiClientUtils'; -import * as diff from "diff"; +import { GoogleApiClientUtils, Pulls, Pushes } from '../../apis/google_docs/GoogleApiClientUtils'; +import { DocumentDecorations } from '../DocumentDecorations'; library.add(faEdit); library.add(faSmile, faTextHeight, faUpload); @@ -84,6 +82,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe private _searchReactionDisposer?: Lambda; private _textReactionDisposer: Opt; private _proxyReactionDisposer: Opt; + private pullReactionDisposer: Opt; + private pushReactionDisposer: Opt; private dropDisposer?: DragManager.DragDropDisposer; public get CurrentDiv(): HTMLDivElement { return this._ref.current!; } private isGoogleDocsUpdate = false; @@ -345,13 +345,25 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } ); - reaction(() => this.props.Document.pullFromGoogleDocsTrigger, () => { - this.pullFromGoogleDoc(); - }); + this.pullReactionDisposer = reaction( + () => this.props.Document[Pulls], + () => { + if (!DocumentDecorations.hasPulledHack) { + DocumentDecorations.hasPulledHack = true; + this.pullFromGoogleDoc(); + } + } + ); - reaction(() => this.props.Document.pushToGoogleDocsTrigger, () => { - this.pushToGoogleDoc(); - }); + this.pushReactionDisposer = reaction( + () => this.props.Document[Pushes], + () => { + if (!DocumentDecorations.hasPushedHack) { + DocumentDecorations.hasPushedHack = true; + this.pushToGoogleDoc(); + } + } + ); this._textReactionDisposer = reaction( () => this.extensionDoc, @@ -380,13 +392,44 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe this.unhighlightSearchTerms(); } }, { fireImmediately: true }); + } - ["pushToGoogleDocsTrigger", "pullFromGoogleDocsTrigger"].map(key => { - let doc = this.props.Document; - if (doc[key] === undefined) { - untracked(() => doc[key] = false); - } - }); + pushToGoogleDoc = () => { + let modes = GoogleApiClientUtils.Docs.WriteMode; + let mode = modes.Replace; + let reference: Opt = Cast(this.dataDoc[googleDocId], "string"); + if (!reference) { + mode = modes.Insert; + reference = { + title: StrCast(this.dataDoc.title), + handler: id => this.dataDoc[googleDocId] = id + }; + } + if (this._editorView) { + let data = Cast(this.dataDoc.data, RichTextField); + let content = data ? data[ToPlainText]() : this._editorView.state.doc.textContent; + GoogleApiClientUtils.Docs.write({ reference, content, mode }); + } + } + + pullFromGoogleDoc = async () => { + let dataDoc = Doc.GetProto(this.props.Document); + let documentId = StrCast(dataDoc[googleDocId]); + if (documentId) { + let exportState = await GoogleApiClientUtils.Docs.read({ documentId }); + UndoManager.RunInBatch(() => { + if (exportState && exportState.body && exportState.title) { + let data = Cast(dataDoc.data, RichTextField); + if (data) { + this.isGoogleDocsUpdate = true; + dataDoc.data = new RichTextField(data[FromPlainText](exportState.body)); + dataDoc.title = exportState.title; + } + } else { + delete dataDoc[googleDocId]; + } + }, Pulls); + } } clipboardTextSerializer = (slice: Slice): string => { @@ -516,6 +559,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe this._reactionDisposer && this._reactionDisposer(); this._proxyReactionDisposer && this._proxyReactionDisposer(); this._textReactionDisposer && this._textReactionDisposer(); + this.pushReactionDisposer && this.pushReactionDisposer(); + this.pullReactionDisposer && this.pullReactionDisposer(); } onPointerDown = (e: React.PointerEvent): void => { @@ -693,49 +738,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe event: action(() => Doc.GetProto(this.props.Document).autoHeight = !BoolCast(this.props.Document.autoHeight)), icon: "expand-arrows-alt" }); ContextMenu.Instance.addItem({ description: "Text Funcs...", subitems: subitems, icon: "text-height" }); - if (!(googleDocId in Doc.GetProto(this.props.Document))) { - ContextMenu.Instance.addItem({ - description: "Export to Google Doc...", - event: this.pushToGoogleDoc, - icon: "upload" - }); - } - } - - pushToGoogleDoc = () => { - let modes = GoogleApiClientUtils.Docs.WriteMode; - let mode = modes.Replace; - let reference: Opt = Cast(this.dataDoc[googleDocId], "string"); - if (!reference) { - mode = modes.Insert; - reference = { - title: StrCast(this.dataDoc.title), - handler: id => this.dataDoc[googleDocId] = id - }; - } - if (this._editorView) { - let data = Cast(this.dataDoc.data, RichTextField); - let content = data ? data[ToGoogleDocText]() : this._editorView.state.doc.textContent; - GoogleApiClientUtils.Docs.write({ reference, content, mode }); - } - } - - pullFromGoogleDoc = async () => { - let dataDoc = Doc.GetProto(this.props.Document); - let documentId = StrCast(dataDoc[googleDocId]); - if (documentId) { - let exportState = await GoogleApiClientUtils.Docs.read({ documentId }); - if (exportState && exportState.body && exportState.title) { - let data = Cast(dataDoc.data, RichTextField); - if (data) { - this.isGoogleDocsUpdate = true; - dataDoc.data = new RichTextField(data[FromGoogleDocText](exportState.body)); - dataDoc.title = exportState.title; - } - } else { - delete dataDoc[googleDocId]; - } - } } diff --git a/src/new_fields/RichTextField.ts b/src/new_fields/RichTextField.ts index ec08293e9..ab58329f9 100644 --- a/src/new_fields/RichTextField.ts +++ b/src/new_fields/RichTextField.ts @@ -4,8 +4,8 @@ import { Deserializable } from "../client/util/SerializationHelper"; import { Copy, ToScriptString } from "./FieldSymbols"; import { scriptingGlobal } from "../client/util/Scripting"; -export const ToGoogleDocText = Symbol("PlainText"); -export const FromGoogleDocText = Symbol("PlainText"); +export const ToPlainText = Symbol("PlainText"); +export const FromPlainText = Symbol("PlainText"); const delimiter = "\n"; const joiner = ""; @@ -28,7 +28,7 @@ export class RichTextField extends ObjectField { return `new RichTextField("${this.Data}")`; } - [ToGoogleDocText]() { + [ToPlainText]() { // Because we're working with plain text, just concatenate all paragraphs let content = JSON.parse(this.Data).doc.content; let paragraphs = content.filter((item: any) => item.type === "paragraph"); @@ -43,7 +43,7 @@ export class RichTextField extends ObjectField { return textParagraphs.join(joiner).trimEnd(delimiter); } - [FromGoogleDocText](plainText: string) { + [FromPlainText](plainText: string) { // Remap the text, creating blocks split on newlines let elements = plainText.split(delimiter); -- cgit v1.2.3-70-g09d2 From c92761206957aebdba1cc2e77f6b662ae42db847 Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Tue, 20 Aug 2019 01:44:18 -0400 Subject: interactive ui --- src/client/views/DocumentDecorations.tsx | 84 +++++++++++++++++++++++------ src/client/views/nodes/FormattedTextBox.tsx | 63 +++++++++++++++------- 2 files changed, 112 insertions(+), 35 deletions(-) (limited to 'src') diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 797b43add..1db452b45 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -1,5 +1,5 @@ -import { library } from '@fortawesome/fontawesome-svg-core'; -import { faLink, faTag, faArrowAltCircleDown, faArrowAltCircleUp } from '@fortawesome/free-solid-svg-icons'; +import { library, IconProp } from '@fortawesome/fontawesome-svg-core'; +import { faLink, faTag, faArrowAltCircleDown, faArrowAltCircleUp, faCheckCircle, faStopCircle, faCloudUploadAlt } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { action, computed, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; @@ -18,7 +18,7 @@ import { CollectionView } from "./collections/CollectionView"; import './DocumentDecorations.scss'; import { DocumentView, PositionDocument } from "./nodes/DocumentView"; import { FieldView } from "./nodes/FieldView"; -import { FormattedTextBox } from "./nodes/FormattedTextBox"; +import { FormattedTextBox, GoogleRef } from "./nodes/FormattedTextBox"; import { IconBox } from "./nodes/IconBox"; import { LinkMenu } from "./nodes/LinkMenu"; import { TemplateMenu } from "./TemplateMenu"; @@ -26,7 +26,6 @@ import { Template, Templates } from "./Templates"; import React = require("react"); import { RichTextField } from '../../new_fields/RichTextField'; import { LinkManager } from '../util/LinkManager'; -import { ObjectField } from '../../new_fields/ObjectField'; import { MetadataEntryMenu } from './MetadataEntryMenu'; import { ImageBox } from './nodes/ImageBox'; import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils'; @@ -39,6 +38,11 @@ library.add(faLink); library.add(faTag); library.add(faArrowAltCircleDown); library.add(faArrowAltCircleUp); +library.add(faStopCircle); +library.add(faCheckCircle); +library.add(faCloudUploadAlt); + +const cloud: IconProp = "cloud-upload-alt"; @observer export class DocumentDecorations extends React.Component<{}, { value: string }> { @@ -69,6 +73,52 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> @observable private _removeIcon = false; @observable public Interacting = false; + @observable public pushIcon: IconProp = "arrow-alt-circle-up"; + @observable public pullIcon: IconProp = "arrow-alt-circle-down"; + @observable public pullColor: string = "white"; + public pullColorAnimating = false; + + private pullAnimating = false; + private pushAnimating = false; + + public startPullOutcome = action((success: boolean) => { + if (this.pullAnimating) { + return; + } + this.pullAnimating = true; + this.pullIcon = success ? "check-circle" : "stop-circle"; + setTimeout(() => runInAction(() => { + this.pullIcon = "arrow-alt-circle-down"; + this.pullAnimating = false; + }), 1000); + }); + + public startPushOutcome = action((success: boolean) => { + if (this.pushAnimating) { + return; + } + this.pushAnimating = true; + this.pushIcon = success ? "check-circle" : "stop-circle"; + setTimeout(() => runInAction(() => { + this.pushIcon = "arrow-alt-circle-up"; + this.pushAnimating = false; + }), 1000); + }); + + public setPullState = (unchanged: boolean) => { + if (this.pullColorAnimating) { + return; + } + this.pullColorAnimating = true; + this.pullColor = unchanged ? "lawngreen" : "red"; + setTimeout(() => { + runInAction(() => { + this.pullColor = "white"; + this.pullColorAnimating = false; + }); + }, 2000); + } + constructor(props: Readonly<{}>) { super(props); DocumentDecorations.Instance = this; @@ -621,33 +671,37 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> ); } + private get targetDoc() { + return SelectionManager.SelectedDocuments()[0].props.Document; + } + considerGoogleDocsPush = () => { - let thisDoc = SelectionManager.SelectedDocuments()[0].props.Document; - let canPush = thisDoc.data && thisDoc.data instanceof RichTextField; + let canPush = this.targetDoc.data && this.targetDoc.data instanceof RichTextField; if (!canPush) return (null); + let published = Doc.GetProto(this.targetDoc)[GoogleRef] !== undefined; + let icon: IconProp = published ? (this.pushIcon as any) : (cloud as any); return (
-
{ +
{ DocumentDecorations.hasPushedHack = false; - thisDoc[Pushes] = NumCast(thisDoc[Pushes]) + 1; + this.targetDoc[Pushes] = NumCast(this.targetDoc[Pushes]) + 1; }}> - +
); } considerGoogleDocsPull = () => { - let thisDoc = SelectionManager.SelectedDocuments()[0].props.Document; - let canPull = thisDoc.data && thisDoc.data instanceof RichTextField; - if (!canPull) return (null); + let canPull = this.targetDoc.data && this.targetDoc.data instanceof RichTextField; + if (!canPull || !Doc.GetProto(this.targetDoc)[GoogleRef]) return (null); return (
-
{ +
{ DocumentDecorations.hasPulledHack = false; - thisDoc[Pulls] = NumCast(thisDoc[Pulls]) + 1; + this.targetDoc[Pulls] = NumCast(this.targetDoc[Pulls]) + 1; }}> - +
); diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index d2eb71350..33d222813 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -60,11 +60,13 @@ const richTextSchema = createSchema({ documentText: "string" }); -const googleDocId = "googleDocId"; +export const GoogleRef = "googleDocId"; type RichTextDocument = makeInterface<[typeof richTextSchema]>; const RichTextDocument = makeInterface(richTextSchema); +type PullHandler = (exportState: GoogleApiClientUtils.Docs.ReadResult, dataDoc: Doc) => void; + @observer export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTextBoxProps), RichTextDocument>(RichTextDocument) { public static LayoutString(fieldStr: string = "data") { @@ -324,6 +326,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe }, { fireImmediately: true }); } + this.pullFromGoogleDoc(this.checkState); + this._reactionDisposer = reaction( () => { const field = this.dataDoc ? Cast(this.dataDoc[this.props.fieldKey], RichTextField) : undefined; @@ -350,7 +354,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe () => { if (!DocumentDecorations.hasPulledHack) { DocumentDecorations.hasPulledHack = true; - this.pullFromGoogleDoc(); + this.pullFromGoogleDoc(this.updateState); } } ); @@ -394,44 +398,63 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe }, { fireImmediately: true }); } - pushToGoogleDoc = () => { + pushToGoogleDoc = async () => { let modes = GoogleApiClientUtils.Docs.WriteMode; let mode = modes.Replace; - let reference: Opt = Cast(this.dataDoc[googleDocId], "string"); + let reference: Opt = Cast(this.dataDoc[GoogleRef], "string"); if (!reference) { mode = modes.Insert; reference = { title: StrCast(this.dataDoc.title), - handler: id => this.dataDoc[googleDocId] = id + handler: id => this.dataDoc[GoogleRef] = id }; } if (this._editorView) { let data = Cast(this.dataDoc.data, RichTextField); let content = data ? data[ToPlainText]() : this._editorView.state.doc.textContent; - GoogleApiClientUtils.Docs.write({ reference, content, mode }); + let response = await GoogleApiClientUtils.Docs.write({ reference, content, mode }); + let pushSuccess = response !== undefined && !("errors" in response); + DocumentDecorations.Instance.startPushOutcome(pushSuccess); } } - pullFromGoogleDoc = async () => { + pullFromGoogleDoc = async (handler: PullHandler) => { let dataDoc = Doc.GetProto(this.props.Document); - let documentId = StrCast(dataDoc[googleDocId]); + let documentId = StrCast(dataDoc[GoogleRef]); if (documentId) { let exportState = await GoogleApiClientUtils.Docs.read({ documentId }); - UndoManager.RunInBatch(() => { - if (exportState && exportState.body && exportState.title) { - let data = Cast(dataDoc.data, RichTextField); - if (data) { - this.isGoogleDocsUpdate = true; - dataDoc.data = new RichTextField(data[FromPlainText](exportState.body)); - dataDoc.title = exportState.title; - } - } else { - delete dataDoc[googleDocId]; - } - }, Pulls); + UndoManager.RunInBatch(() => handler(exportState, dataDoc), Pulls); } } + updateState = (exportState: GoogleApiClientUtils.Docs.ReadResult, dataDoc: Doc) => { + let pullSuccess = false; + if (exportState !== undefined && exportState.body !== undefined && exportState.title !== undefined) { + let data = Cast(dataDoc.data, RichTextField); + if (data) { + pullSuccess = true; + this.isGoogleDocsUpdate = true; + dataDoc.data = new RichTextField(data[FromPlainText](exportState.body)); + dataDoc.title = exportState.title; + } + } else { + delete dataDoc[GoogleRef]; + } + DocumentDecorations.Instance.startPullOutcome(pullSuccess); + } + + checkState = (exportState: GoogleApiClientUtils.Docs.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; + DocumentDecorations.Instance.setPullState(storedPlainText === receivedPlainText); + } + } + } + + clipboardTextSerializer = (slice: Slice): string => { let text = "", separated = true; const from = 0, to = slice.content.size; -- cgit v1.2.3-70-g09d2 From aa0e6f5ffc30fdffc3be13a1948981b754544a01 Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Tue, 20 Aug 2019 12:03:50 -0400 Subject: automation of pull push button visibility --- src/client/views/DocumentDecorations.tsx | 3 +- src/client/views/nodes/FormattedTextBox.tsx | 61 +++++++++++++++++++---------- 2 files changed, 42 insertions(+), 22 deletions(-) (limited to 'src') diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 1db452b45..05239073b 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -694,7 +694,8 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> considerGoogleDocsPull = () => { let canPull = this.targetDoc.data && this.targetDoc.data instanceof RichTextField; - if (!canPull || !Doc.GetProto(this.targetDoc)[GoogleRef]) return (null); + let dataDoc = Doc.GetProto(this.targetDoc); + if (!canPull || !dataDoc[GoogleRef] || dataDoc.unchanged) return (null); return (
{ diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index 33d222813..8a355a425 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -1,6 +1,6 @@ import { library } from '@fortawesome/fontawesome-svg-core'; import { faEdit, faSmile, faTextHeight, faUpload } from '@fortawesome/free-solid-svg-icons'; -import { action, IReactionDisposer, observable, reaction, runInAction, computed, Lambda, trace, untracked } from "mobx"; +import { action, IReactionDisposer, observable, reaction, runInAction, computed, Lambda, trace, untracked, autorun } from "mobx"; import { observer } from "mobx-react"; import { baseKeymap } from "prosemirror-commands"; import { history } from "prosemirror-history"; @@ -399,32 +399,46 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } pushToGoogleDoc = async () => { - let modes = GoogleApiClientUtils.Docs.WriteMode; - let mode = modes.Replace; - let reference: Opt = Cast(this.dataDoc[GoogleRef], "string"); - if (!reference) { - mode = modes.Insert; - reference = { - title: StrCast(this.dataDoc.title), - handler: id => this.dataDoc[GoogleRef] = id + this.pullFromGoogleDoc(async (exportState: GoogleApiClientUtils.Docs.ReadResult, dataDoc: Doc) => { + let modes = GoogleApiClientUtils.Docs.WriteMode; + let mode = modes.Replace; + let reference: Opt = Cast(this.dataDoc[GoogleRef], "string"); + if (!reference) { + mode = modes.Insert; + reference = { + title: StrCast(this.dataDoc.title), + handler: id => this.dataDoc[GoogleRef] = id + }; + } + let redo = async () => { + if (this._editorView && reference) { + let data = Cast(this.dataDoc.data, RichTextField); + let content = data ? data[ToPlainText]() : this._editorView.state.doc.textContent; + let response = await GoogleApiClientUtils.Docs.write({ reference, content, mode }); + let pushSuccess = response !== undefined && !("errors" in response); + dataDoc.unchanged = pushSuccess; + DocumentDecorations.Instance.startPushOutcome(pushSuccess); + } }; - } - if (this._editorView) { - let data = Cast(this.dataDoc.data, RichTextField); - let content = data ? data[ToPlainText]() : this._editorView.state.doc.textContent; - let response = await GoogleApiClientUtils.Docs.write({ reference, content, mode }); - let pushSuccess = response !== undefined && !("errors" in response); - DocumentDecorations.Instance.startPushOutcome(pushSuccess); - } + let undo = () => { + let content = exportState.body; + if (reference && content) { + GoogleApiClientUtils.Docs.write({ reference, content, mode }); + } + }; + UndoManager.AddEvent({ undo, redo }); + redo(); + }); } pullFromGoogleDoc = async (handler: PullHandler) => { - let dataDoc = Doc.GetProto(this.props.Document); + let dataDoc = this.dataDoc; let documentId = StrCast(dataDoc[GoogleRef]); + let exportState: GoogleApiClientUtils.Docs.ReadResult = {}; if (documentId) { - let exportState = await GoogleApiClientUtils.Docs.read({ documentId }); - UndoManager.RunInBatch(() => handler(exportState, dataDoc), Pulls); + exportState = await GoogleApiClientUtils.Docs.read({ documentId }); } + UndoManager.RunInBatch(() => handler(exportState, dataDoc), Pulls); } updateState = (exportState: GoogleApiClientUtils.Docs.ReadResult, dataDoc: Doc) => { @@ -436,6 +450,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe this.isGoogleDocsUpdate = true; dataDoc.data = new RichTextField(data[FromPlainText](exportState.body)); dataDoc.title = exportState.title; + dataDoc.unchanged = true; } } else { delete dataDoc[GoogleRef]; @@ -449,7 +464,11 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe if (data) { let storedPlainText = data[ToPlainText]() + "\n"; let receivedPlainText = exportState.body; - DocumentDecorations.Instance.setPullState(storedPlainText === receivedPlainText); + 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 dc587b0fb493d869e6cd38fa94b81105da4fbaab Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Tue, 20 Aug 2019 20:03:08 -0400 Subject: finished UI --- src/client/views/DocumentDecorations.scss | 6 +- src/client/views/DocumentDecorations.tsx | 92 ++++++++++++++++++----------- src/client/views/nodes/FormattedTextBox.tsx | 3 +- src/new_fields/RichTextField.ts | 7 ++- 4 files changed, 67 insertions(+), 41 deletions(-) (limited to 'src') diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss index 0b7411fca..ef7159370 100644 --- a/src/client/views/DocumentDecorations.scss +++ b/src/client/views/DocumentDecorations.scss @@ -255,4 +255,8 @@ $linkGap : 3px; input { margin-right: 10px; } -} \ 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); } } \ No newline at end of file diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 05239073b..7645af054 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -1,5 +1,5 @@ import { library, IconProp } from '@fortawesome/fontawesome-svg-core'; -import { faLink, faTag, faArrowAltCircleDown, faArrowAltCircleUp, faCheckCircle, faStopCircle, faCloudUploadAlt } from '@fortawesome/free-solid-svg-icons'; +import { faLink, faTag, faArrowAltCircleDown, faArrowAltCircleUp, faCheckCircle, faStopCircle, faCloudUploadAlt, faSyncAlt } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { action, computed, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; @@ -41,8 +41,10 @@ library.add(faArrowAltCircleUp); library.add(faStopCircle); library.add(faCheckCircle); library.add(faCloudUploadAlt); +library.add(faSyncAlt); const cloud: IconProp = "cloud-upload-alt"; +const fetch: IconProp = "sync-alt"; @observer export class DocumentDecorations extends React.Component<{}, { value: string }> { @@ -76,48 +78,47 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> @observable public pushIcon: IconProp = "arrow-alt-circle-up"; @observable public pullIcon: IconProp = "arrow-alt-circle-down"; @observable public pullColor: string = "white"; + @observable public isAnimatingFetch = false; public pullColorAnimating = false; private pullAnimating = false; private pushAnimating = false; public startPullOutcome = action((success: boolean) => { - if (this.pullAnimating) { - return; + if (!this.pullAnimating) { + this.pullAnimating = true; + this.pullIcon = success ? "check-circle" : "stop-circle"; + setTimeout(() => runInAction(() => { + this.pullIcon = "arrow-alt-circle-down"; + this.pullAnimating = false; + }), 1000); } - this.pullAnimating = true; - this.pullIcon = success ? "check-circle" : "stop-circle"; - setTimeout(() => runInAction(() => { - this.pullIcon = "arrow-alt-circle-down"; - this.pullAnimating = false; - }), 1000); }); public startPushOutcome = action((success: boolean) => { - if (this.pushAnimating) { - return; + if (!this.pushAnimating) { + this.pushAnimating = true; + this.pushIcon = success ? "check-circle" : "stop-circle"; + setTimeout(() => runInAction(() => { + this.pushIcon = "arrow-alt-circle-up"; + this.pushAnimating = false; + }), 1000); } - this.pushAnimating = true; - this.pushIcon = success ? "check-circle" : "stop-circle"; - setTimeout(() => runInAction(() => { - this.pushIcon = "arrow-alt-circle-up"; - this.pushAnimating = false; - }), 1000); }); - public setPullState = (unchanged: boolean) => { - if (this.pullColorAnimating) { - return; + public setPullState = action((unchanged: boolean) => { + this.isAnimatingFetch = false; + if (!this.pullColorAnimating) { + this.pullColorAnimating = true; + this.pullColor = unchanged ? "lawngreen" : "red"; + setTimeout(this.clearPullColor, 1000); } - this.pullColorAnimating = true; - this.pullColor = unchanged ? "lawngreen" : "red"; - setTimeout(() => { - runInAction(() => { - this.pullColor = "white"; - this.pullColorAnimating = false; - }); - }, 2000); - } + }); + + private clearPullColor = action(() => { + this.pullColor = "white"; + this.pullColorAnimating = false; + }); constructor(props: Readonly<{}>) { super(props); @@ -679,7 +680,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> let canPush = this.targetDoc.data && this.targetDoc.data instanceof RichTextField; if (!canPush) return (null); let published = Doc.GetProto(this.targetDoc)[GoogleRef] !== undefined; - let icon: IconProp = published ? (this.pushIcon as any) : (cloud as any); + let icon: IconProp = published ? (this.pushIcon as any) : cloud; return (
{ @@ -695,14 +696,33 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> considerGoogleDocsPull = () => { let canPull = this.targetDoc.data && this.targetDoc.data instanceof RichTextField; let dataDoc = Doc.GetProto(this.targetDoc); - if (!canPull || !dataDoc[GoogleRef] || dataDoc.unchanged) return (null); + if (!canPull || !dataDoc[GoogleRef]) return (null); + let icon = !dataDoc.unchanged ? (this.pullIcon as any) : fetch; + let animation = this.isAnimatingFetch ? "spin 0.5s linear infinite" : "none"; return (
-
{ - DocumentDecorations.hasPulledHack = false; - this.targetDoc[Pulls] = NumCast(this.targetDoc[Pulls]) + 1; - }}> - +
{ + this.clearPullColor(); + DocumentDecorations.hasPulledHack = false; + this.targetDoc[Pulls] = NumCast(this.targetDoc[Pulls]) + 1; + dataDoc.unchanged && runInAction(() => this.isAnimatingFetch = true); + }}> +
); diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index 8a355a425..e06be7079 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -354,7 +354,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe () => { if (!DocumentDecorations.hasPulledHack) { DocumentDecorations.hasPulledHack = true; - this.pullFromGoogleDoc(this.updateState); + let unchanged = this.dataDoc.unchanged; + this.pullFromGoogleDoc(unchanged ? this.checkState : this.updateState); } } ); diff --git a/src/new_fields/RichTextField.ts b/src/new_fields/RichTextField.ts index ab58329f9..cae5623e6 100644 --- a/src/new_fields/RichTextField.ts +++ b/src/new_fields/RichTextField.ts @@ -38,9 +38,10 @@ export class RichTextField extends ObjectField { let blockText = (block: any) => block.text; let concatenateParagraph = (p: any) => (p.content ? p.content.map(blockText).join(joiner) : "") + delimiter; - // Concatentate paragraphs and string the result together. Trim the last newline, an artifact. - let textParagraphs = paragraphs.map(concatenateParagraph); - return textParagraphs.join(joiner).trimEnd(delimiter); + // Concatentate paragraphs and string the result together + let textParagraphs: string[] = paragraphs.map(concatenateParagraph); + let plainText = textParagraphs.join(joiner); + return plainText.substring(0, plainText.length - 1); } [FromPlainText](plainText: string) { -- cgit v1.2.3-70-g09d2 From bd734ec84fa8fae36fceebf74da44216a0746b23 Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Tue, 20 Aug 2019 21:02:05 -0400 Subject: open remote document on ctrl click --- src/client/views/DocumentDecorations.tsx | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) (limited to 'src') diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index d5d59a8ee..3482fd043 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -1,5 +1,5 @@ import { library, IconProp } from '@fortawesome/fontawesome-svg-core'; -import { faLink, faTag, faTimes, faArrowAltCircleDown, faArrowAltCircleUp, faCheckCircle, faStopCircle, faCloudUploadAlt, faSyncAlt } from '@fortawesome/free-solid-svg-icons'; +import { faLink, faTag, faTimes, faArrowAltCircleDown, faArrowAltCircleUp, faCheckCircle, faStopCircle, faCloudUploadAlt, faSyncAlt, faShare } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { action, computed, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; @@ -43,6 +43,7 @@ library.add(faStopCircle); library.add(faCheckCircle); library.add(faCloudUploadAlt); library.add(faSyncAlt); +library.add(faShare); const cloud: IconProp = "cloud-upload-alt"; const fetch: IconProp = "sync-alt"; @@ -81,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 openHover = false; public pullColorAnimating = false; private pullAnimating = false; @@ -710,21 +712,29 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> let dataDoc = Doc.GetProto(this.targetDoc); if (!canPull || !dataDoc[GoogleRef]) return (null); let icon = !dataDoc.unchanged ? (this.pullIcon as any) : fetch; + icon = this.openHover ? "share" : icon; let animation = this.isAnimatingFetch ? "spin 0.5s linear infinite" : "none"; + let title = `${!dataDoc.unchanged ? "Pull from" : "Fetch"} Google Docs`; return (
{ - this.clearPullColor(); - DocumentDecorations.hasPulledHack = false; - this.targetDoc[Pulls] = NumCast(this.targetDoc[Pulls]) + 1; - dataDoc.unchanged && runInAction(() => this.isAnimatingFetch = true); + onPointerEnter={e => e.ctrlKey && runInAction(() => this.openHover = true)} + onPointerLeave={() => runInAction(() => this.openHover = false)} + onClick={e => { + if (e.ctrlKey) { + window.open(`https://docs.google.com/document/d/${dataDoc[GoogleRef]}/edit`); + } else { + this.clearPullColor(); + DocumentDecorations.hasPulledHack = false; + this.targetDoc[Pulls] = NumCast(this.targetDoc[Pulls]) + 1; + dataDoc.unchanged && runInAction(() => this.isAnimatingFetch = true); + } }}> Date: Tue, 20 Aug 2019 21:05:25 -0400 Subject: auto height set on google docs publishing --- src/client/views/DocumentDecorations.tsx | 3 +++ 1 file changed, 3 insertions(+) (limited to 'src') diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 3482fd043..891fd7847 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -694,6 +694,9 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> let canPush = this.targetDoc.data && this.targetDoc.data instanceof RichTextField; if (!canPush) return (null); let published = Doc.GetProto(this.targetDoc)[GoogleRef] !== undefined; + if (!published) { + this.targetDoc.autoHeight = true; + } let icon: IconProp = published ? (this.pushIcon as any) : cloud; return (
-- cgit v1.2.3-70-g09d2 From 820ad7343816ee9d5881eefdbcbe8783fe903257 Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Tue, 20 Aug 2019 21:07:43 -0400 Subject: animation fix --- src/client/views/nodes/FormattedTextBox.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index 2705fac14..d4cbb5116 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -1,6 +1,6 @@ import { library } from '@fortawesome/fontawesome-svg-core'; import { faEdit, faSmile, faTextHeight, faUpload } from '@fortawesome/free-solid-svg-icons'; -import { action, computed, IReactionDisposer, Lambda, observable, reaction } from "mobx"; +import { action, computed, IReactionDisposer, Lambda, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; import { baseKeymap } from "prosemirror-commands"; import { history } from "prosemirror-history"; @@ -298,6 +298,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } this.pullFromGoogleDoc(this.checkState); + runInAction(() => DocumentDecorations.Instance.isAnimatingFetch = true); this._reactionDisposer = reaction( () => { -- cgit v1.2.3-70-g09d2 From c0439996ec8b4858c48a7871120e19bb7c06c67d Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Tue, 20 Aug 2019 21:37:32 -0400 Subject: drag and drop google docs tweaks --- package.json | 4 +--- src/client/DocServer.ts | 1 - src/client/views/collections/CollectionSubView.tsx | 8 ++++---- src/client/views/nodes/FormattedTextBox.tsx | 1 + 4 files changed, 6 insertions(+), 8 deletions(-) (limited to 'src') diff --git a/package.json b/package.json index e6a0a54b9..de1f3f6e6 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,6 @@ "@types/cookie-parser": "^1.4.1", "@types/cookie-session": "^2.0.36", "@types/d3-format": "^1.3.1", - "@types/diff": "^4.0.2", "@types/dotenv": "^6.1.1", "@types/express": "^4.16.1", "@types/express-flash": "0.0.0", @@ -128,7 +127,6 @@ "cookie-session": "^2.0.0-beta.3", "crypto-browserify": "^3.11.0", "d3-format": "^1.3.2", - "diff": "^4.0.1", "dotenv": "^8.0.0", "express": "^4.16.4", "express-flash": "0.0.2", @@ -221,4 +219,4 @@ "xoauth2": "^1.2.0", "youtube": "^0.1.0" } -} +} \ No newline at end of file diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts index 56d9147ae..2cec1046b 100644 --- a/src/client/DocServer.ts +++ b/src/client/DocServer.ts @@ -5,7 +5,6 @@ import { Utils, emptyFunction } from '../Utils'; import { SerializationHelper } from './util/SerializationHelper'; import { RefField } from '../new_fields/RefField'; import { Id, HandleUpdate } from '../new_fields/FieldSymbols'; -import { docs_v1 } from 'googleapis'; /** * This class encapsulates the transfer and cross-client synchronization of diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 3deec22b5..5b50c545c 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -17,7 +17,7 @@ import { DragManager } from "../../util/DragManager"; import { undoBatch, UndoManager } from "../../util/UndoManager"; import { DocComponent } from "../DocComponent"; import { FieldViewProps } from "../nodes/FieldView"; -import { FormattedTextBox, Blank } from "../nodes/FormattedTextBox"; +import { FormattedTextBox, GoogleRef } from "../nodes/FormattedTextBox"; import { CollectionPDFView } from "./CollectionPDFView"; import { CollectionVideoView } from "./CollectionVideoView"; import { CollectionView } from "./CollectionView"; @@ -209,11 +209,11 @@ export function CollectionSubView(schemaCtor: (doc: Doc) => T) { } let matches: RegExpExecArray | null; if ((matches = /(https:\/\/)?docs\.google\.com\/document\/d\/([^\\]+)\/edit/g.exec(text)) !== null) { - let newBox = Docs.Create.TextDocument({ ...options, width: 600, height: 400, title: "Fetching title from Google Docs..." }); + let newBox = Docs.Create.TextDocument({ ...options, width: 400, height: 200, title: "Awaiting title from Google Docs..." }); let proto = newBox.proto!; proto.autoHeight = true; - proto.googleDocId = matches[2]; - proto.data = "Fetching contents from Google Docs..."; + proto[GoogleRef] = matches[2]; + proto.data = "Please select and then click on this document's pull button to load its contents from from Google Docs..."; proto.backgroundColor = "#eeeeff"; this.props.addDocument(newBox); return; diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index d4cbb5116..2b01fb70a 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -434,6 +434,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe delete dataDoc[GoogleRef]; } DocumentDecorations.Instance.startPullOutcome(pullSuccess); + this.tryUpdateHeight(); } checkState = (exportState: GoogleApiClientUtils.Docs.ReadResult, dataDoc: Doc) => { -- cgit v1.2.3-70-g09d2 From 73bb0f572e261850583b698dd819d35a6fe768ec Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Tue, 20 Aug 2019 21:42:37 -0400 Subject: cleanup --- src/client/views/MainView.tsx | 2 +- src/client/views/collections/CollectionSubView.tsx | 2 +- src/client/views/nodes/FormattedTextBox.tsx | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index e85374f6a..f28844009 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -15,7 +15,7 @@ import { listSpec } from '../../new_fields/Schema'; import { BoolCast, Cast, FieldValue, StrCast } from '../../new_fields/Types'; import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils'; import { RouteStore } from '../../server/RouteStore'; -import { emptyFunction, returnOne, returnTrue, Utils, returnEmptyString, PostToServer } from '../../Utils'; +import { emptyFunction, returnOne, returnTrue, Utils, returnEmptyString } from '../../Utils'; import { DocServer } from '../DocServer'; import { Docs } from '../documents/Documents'; import { ClientUtils } from '../util/ClientUtils'; diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 5b50c545c..99e5ab7b3 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -213,7 +213,7 @@ export function CollectionSubView(schemaCtor: (doc: Doc) => T) { let proto = newBox.proto!; proto.autoHeight = true; proto[GoogleRef] = matches[2]; - proto.data = "Please select and then click on this document's pull button to load its contents from from Google Docs..."; + proto.data = "Please select this document and then click on its pull button to load its contents from from Google Docs..."; proto.backgroundColor = "#eeeeff"; this.props.addDocument(newBox); return; diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index 2b01fb70a..606e8edb0 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -389,9 +389,9 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe }; } let redo = async () => { - if (this._editorView && reference) { - let data = Cast(this.dataDoc.data, RichTextField); - let content = data ? data[ToPlainText]() : this._editorView.state.doc.textContent; + let data = Cast(this.dataDoc.data, RichTextField); + if (this._editorView && reference && data) { + let content = data[ToPlainText](); let response = await GoogleApiClientUtils.Docs.write({ reference, content, mode }); let pushSuccess = response !== undefined && !("errors" in response); dataDoc.unchanged = pushSuccess; -- cgit v1.2.3-70-g09d2