aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/client/apis/google_docs/GoogleApiClientUtils.ts265
-rw-r--r--src/client/apis/google_docs/GooglePhotosClientUtils.ts51
-rw-r--r--src/client/documents/Documents.ts7
-rw-r--r--src/client/northstar/utils/Extensions.ts2
-rw-r--r--src/client/views/DocumentDecorations.scss31
-rw-r--r--src/client/views/DocumentDecorations.tsx7
-rw-r--r--src/client/views/Main.tsx20
-rw-r--r--src/client/views/MainView.tsx12
-rw-r--r--src/client/views/nodes/FormattedTextBox.tsx87
-rw-r--r--src/new_fields/RichTextField.ts49
-rw-r--r--src/new_fields/RichTextUtils.ts273
-rw-r--r--src/server/RouteStore.ts4
-rw-r--r--src/server/apis/google/GoogleApiServerUtils.ts98
-rw-r--r--src/server/apis/google/GooglePhotosServerUtils.ts68
-rw-r--r--src/server/apis/google/GooglePhotosUploadUtils.ts28
-rw-r--r--src/server/apis/google/typings/albums.ts150
-rw-r--r--src/server/authentication/config/passport.ts3
-rw-r--r--src/server/credentials/google_docs_credentials.json12
-rw-r--r--src/server/credentials/google_docs_token.json2
-rw-r--r--src/server/index.ts42
-rw-r--r--src/typings/index.d.ts632
21 files changed, 1253 insertions, 590 deletions
diff --git a/src/client/apis/google_docs/GoogleApiClientUtils.ts b/src/client/apis/google_docs/GoogleApiClientUtils.ts
index 798886def..828d4451a 100644
--- a/src/client/apis/google_docs/GoogleApiClientUtils.ts
+++ b/src/client/apis/google_docs/GoogleApiClientUtils.ts
@@ -3,111 +3,130 @@ import { PostToServer } from "../../../Utils";
import { RouteStore } from "../../../server/RouteStore";
import { Opt } from "../../../new_fields/Doc";
import { isArray } from "util";
+import { EditorState } from "prosemirror-state";
+import { RichTextField } from "../../../new_fields/RichTextField";
export const Pulls = "googleDocsPullCount";
export const Pushes = "googleDocsPushCount";
export namespace GoogleApiClientUtils {
- export enum Service {
- Documents = "Documents",
- Slides = "Slides"
- }
-
export enum Actions {
Create = "create",
Retrieve = "retrieve",
Update = "update"
}
- export enum WriteMode {
- Insert,
- Replace
- }
-
- export type Identifier = string;
- export type Reference = Identifier | CreateOptions;
- export type TextContent = string | string[];
- export type IdHandler = (id: Identifier) => any;
- export type CreationResult = Opt<Identifier>;
- export type ReadLinesResult = Opt<{ title?: string, bodyLines?: string[] }>;
- export type ReadResult = { title?: string, body?: string };
+ export namespace Docs {
- export interface CreateOptions {
- service: Service;
- title?: string; // if excluded, will use a default title annotated with the current date
- }
+ export type RetrievalResult = Opt<docs_v1.Schema$Document>;
+ export type UpdateResult = Opt<docs_v1.Schema$BatchUpdateDocumentResponse>;
- export interface RetrieveOptions {
- service: Service;
- identifier: Identifier;
- }
+ export interface UpdateOptions {
+ documentId: DocumentId;
+ requests: docs_v1.Schema$Request[];
+ }
- export interface ReadOptions {
- identifier: Identifier;
- removeNewlines?: boolean;
- }
+ export enum WriteMode {
+ Insert,
+ Replace
+ }
- export interface WriteOptions {
- mode: WriteMode;
- content: TextContent;
- reference: Reference;
- index?: number; // if excluded, will compute the last index of the document and append the content there
- }
+ export type DocumentId = string;
+ export type Reference = DocumentId | CreateOptions;
+ export interface Content {
+ text: string | string[];
+ requests: docs_v1.Schema$Request[];
+ }
+ export type IdHandler = (id: DocumentId) => any;
+ export type CreationResult = Opt<DocumentId>;
+ export type ReadLinesResult = Opt<{ title?: string, bodyLines?: string[] }>;
+ export type ReadResult = { title: string, body: string };
+ export interface ImportResult {
+ title: string;
+ text: string;
+ state: EditorState;
+ }
- /**
- * After following the authentication routine, which connects this API call to the current signed in account
- * and grants the appropriate permissions, this function programmatically creates an arbitrary Google Doc which
- * should appear in the user's Google Doc library instantaneously.
- *
- * @param options the title to assign to the new document, and the information necessary
- * to store the new documentId returned from the creation process
- * @returns the documentId of the newly generated document, or undefined if the creation process fails.
- */
- export const create = async (options: CreateOptions): Promise<CreationResult> => {
- const path = `${RouteStore.googleDocs}/${options.service}/${Actions.Create}`;
- const parameters = {
- requestBody: {
- title: options.title || `Dash Export (${new Date().toDateString()})`
- }
- };
- try {
- const schema: any = await PostToServer(path, parameters);
- let key = ["document", "presentation"].find(prefix => `${prefix}Id` in schema) + "Id";
- return schema[key];
- } catch {
- return undefined;
+ export interface CreateOptions {
+ title?: string; // if excluded, will use a default title annotated with the current date
}
- };
- export namespace Docs {
+ export interface RetrieveOptions {
+ documentId: DocumentId;
+ }
- export type RetrievalResult = Opt<docs_v1.Schema$Document | slides_v1.Schema$Presentation>;
- export type UpdateResult = Opt<docs_v1.Schema$BatchUpdateDocumentResponse>;
+ export interface ReadOptions {
+ documentId: DocumentId;
+ removeNewlines?: boolean;
+ }
- export interface UpdateOptions {
- documentId: Identifier;
- requests: docs_v1.Schema$Request[];
+ export interface WriteOptions {
+ mode: WriteMode;
+ content: Content;
+ reference: Reference;
+ index?: number; // if excluded, will compute the last index of the document and append the content there
}
+ /**
+ * After following the authentication routine, which connects this API call to the current signed in account
+ * and grants the appropriate permissions, this function programmatically creates an arbitrary Google Doc which
+ * should appear in the user's Google Doc library instantaneously.
+ *
+ * @param options the title to assign to the new document, and the information necessary
+ * to store the new documentId returned from the creation process
+ * @returns the documentId of the newly generated document, or undefined if the creation process fails.
+ */
+ export const create = async (options: CreateOptions): Promise<CreationResult> => {
+ const path = `${RouteStore.googleDocs}/Documents/${Actions.Create}`;
+ const parameters = {
+ requestBody: {
+ title: options.title || `Dash Export (${new Date().toDateString()})`
+ }
+ };
+ try {
+ const schema: docs_v1.Schema$Document = await PostToServer(path, parameters);
+ return schema.documentId;
+ } catch {
+ return undefined;
+ }
+ };
+
export namespace Utils {
- export const extractText = (document: docs_v1.Schema$Document, removeNewlines = false): string => {
- const fragments: string[] = [];
+ export type ExtractResult = { text: string, paragraphs: DeconstructedParagraph[] };
+ export const extractText = (document: docs_v1.Schema$Document, removeNewlines = false): ExtractResult => {
+ let paragraphs = extractParagraphs(document);
+ let text = paragraphs.map(paragraph => paragraph.runs.map(run => run.content).join("")).join("");
+ text = text.substring(0, text.length - 1);
+ removeNewlines && text.ReplaceAll("\n", "");
+ return { text, paragraphs };
+ };
+
+ export type DeconstructedParagraph = { runs: docs_v1.Schema$TextRun[], bullet: Opt<number> };
+ const extractParagraphs = (document: docs_v1.Schema$Document, filterEmpty = true): DeconstructedParagraph[] => {
+ const fragments: DeconstructedParagraph[] = [];
if (document.body && document.body.content) {
for (const element of document.body.content) {
- if (element.paragraph && element.paragraph.elements) {
- for (const inner of element.paragraph.elements) {
- if (inner && inner.textRun) {
- const fragment = inner.textRun.content;
- fragment && fragments.push(fragment);
+ let runs: docs_v1.Schema$TextRun[] = [];
+ let bullet: Opt<number>;
+ if (element.paragraph) {
+ if (element.paragraph.elements) {
+ for (const inner of element.paragraph.elements) {
+ if (inner && inner.textRun) {
+ let run = inner.textRun;
+ (run.content || !filterEmpty) && runs.push(inner.textRun);
+ }
}
}
+ if (element.paragraph.bullet) {
+ bullet = element.paragraph.bullet.nestingLevel || 0;
+ }
}
+ (runs.length || !filterEmpty) && fragments.push({ runs, bullet });
}
}
- const text = fragments.join("");
- return removeNewlines ? text.ReplaceAll("\n", "") : text;
+ return fragments;
};
export const endOf = (schema: docs_v1.Schema$Document): number | undefined => {
@@ -130,27 +149,19 @@ export namespace GoogleApiClientUtils {
}
- const KeyMapping = new Map<Service, string>([
- [Service.Documents, "documentId"],
- [Service.Slides, "presentationId"]
- ]);
-
export const retrieve = async (options: RetrieveOptions): Promise<RetrievalResult> => {
- const path = `${RouteStore.googleDocs}/${options.service}/${Actions.Retrieve}`;
+ const path = `${RouteStore.googleDocs}/Documents/${Actions.Retrieve}`;
try {
- let parameters: any = {}, key: string | undefined;
- if ((key = KeyMapping.get(options.service))) {
- parameters[key] = options.identifier;
- const schema: RetrievalResult = await PostToServer(path, parameters);
- return schema;
- }
+ const parameters = { documentId: options.documentId };
+ const schema: RetrievalResult = await PostToServer(path, parameters);
+ return schema;
} catch {
return undefined;
}
};
export const update = async (options: UpdateOptions): Promise<UpdateResult> => {
- const path = `${RouteStore.googleDocs}/${Service.Documents}/${Actions.Update}`;
+ const path = `${RouteStore.googleDocs}/Documents/${Actions.Update}`;
const parameters = {
documentId: options.documentId,
requestBody: {
@@ -165,41 +176,49 @@ export namespace GoogleApiClientUtils {
}
};
- export const read = async (options: ReadOptions): Promise<ReadResult> => {
- return retrieve({ ...options, service: Service.Documents }).then(document => {
- let result: ReadResult = {};
+ export const read = async (options: ReadOptions): Promise<Opt<ReadResult>> => {
+ return retrieve({ documentId: options.documentId }).then(document => {
if (document) {
- let title = document.title;
- let body = Utils.extractText(document, options.removeNewlines);
- result = { title, body };
+ let title = document.title!;
+ let body = Utils.extractText(document, options.removeNewlines).text;
+ return { title, body };
}
- return result;
});
};
- export const readLines = async (options: ReadOptions): Promise<ReadLinesResult> => {
- return retrieve({ ...options, service: Service.Documents }).then(document => {
- let result: ReadLinesResult = {};
+ export const readLines = async (options: ReadOptions): Promise<Opt<ReadLinesResult>> => {
+ return retrieve({ documentId: options.documentId }).then(document => {
if (document) {
let title = document.title;
- let bodyLines = Utils.extractText(document).split("\n");
+ let bodyLines = Utils.extractText(document).text.split("\n");
options.removeNewlines && (bodyLines = bodyLines.filter(line => line.length));
- result = { title, bodyLines };
+ return { title, bodyLines };
}
- return result;
});
};
+ export const setStyle = async (options: UpdateOptions) => {
+ let replies: any = await update({
+ documentId: options.documentId,
+ requests: options.requests
+ });
+ if ("errors" in replies) {
+ console.log("Write operation failed:");
+ console.log(replies.errors.map((error: any) => error.message));
+ }
+ return replies;
+ };
+
export const write = async (options: WriteOptions): Promise<UpdateResult> => {
const requests: docs_v1.Schema$Request[] = [];
- const identifier = await Utils.initialize(options.reference);
- if (!identifier) {
+ const documentId = await Utils.initialize(options.reference);
+ if (!documentId) {
return undefined;
}
let index = options.index;
const mode = options.mode;
if (!(index && mode === WriteMode.Insert)) {
- let schema = await retrieve({ identifier, service: Service.Documents });
+ let schema = await retrieve({ documentId });
if (!schema || !(index = Utils.endOf(schema))) {
return undefined;
}
@@ -215,7 +234,7 @@ export namespace GoogleApiClientUtils {
});
index = 1;
}
- const text = options.content;
+ const text = options.content.text;
text.length && requests.push({
insertText: {
text: isArray(text) ? text.join("\n") : text,
@@ -225,47 +244,15 @@ export namespace GoogleApiClientUtils {
if (!requests.length) {
return undefined;
}
- let replies: any = await update({ documentId: identifier, requests });
- let errors = "errors";
- if (errors in replies) {
+ requests.push(...options.content.requests);
+ let replies: any = await update({ documentId: documentId, requests });
+ if ("errors" in replies) {
console.log("Write operation failed:");
- console.log(replies[errors].map((error: any) => error.message));
+ console.log(replies.errors.map((error: any) => error.message));
}
return replies;
};
}
- export namespace Slides {
-
- export namespace Utils {
-
- export const extractTextBoxes = (slides: slides_v1.Schema$Page[]) => {
- slides.map(slide => {
- let elements = slide.pageElements;
- if (elements) {
- let textboxes: slides_v1.Schema$TextContent[] = [];
- for (let element of elements) {
- if (element && element.shape && element.shape.shapeType === "TEXT_BOX" && element.shape.text) {
- textboxes.push(element.shape.text);
- }
- }
- textboxes.map(text => {
- if (text.textElements) {
- text.textElements.map(element => {
-
- });
- }
- if (text.lists) {
-
- }
- });
- }
- });
- };
-
- }
-
- }
-
} \ No newline at end of file
diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
new file mode 100644
index 000000000..0864ebdb1
--- /dev/null
+++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
@@ -0,0 +1,51 @@
+import { Album } from "../../../server/apis/google/typings/albums";
+import { PostToServer } from "../../../Utils";
+import { RouteStore } from "../../../server/RouteStore";
+import { ImageField } from "../../../new_fields/URLField";
+
+export namespace GooglePhotosClientUtils {
+
+ export const Create = async (title: string) => {
+ let parameters = {
+ action: Album.Action.Create,
+ body: { album: { title } }
+ } as Album.Create;
+ return PostToServer(RouteStore.googlePhotosQuery, parameters);
+ };
+
+ export const List = async (options?: Partial<Album.ListOptions>) => {
+ let parameters = {
+ action: Album.Action.List,
+ parameters: {
+ pageSize: (options ? options.pageSize : 20) || 20,
+ pageToken: (options ? options.pageToken : undefined) || undefined,
+ excludeNonAppCreatedData: (options ? options.excludeNonAppCreatedData : false) || false,
+ } as Album.ListOptions
+ } as Album.List;
+ return PostToServer(RouteStore.googlePhotosQuery, parameters);
+ };
+
+ export const Get = async (albumId: string) => {
+ let parameters = {
+ action: Album.Action.Get,
+ albumId
+ } as Album.Get;
+ return PostToServer(RouteStore.googlePhotosQuery, parameters);
+ };
+
+ export const toDataURL = (field: ImageField | undefined) => {
+ if (!field) {
+ return undefined;
+ }
+ const image = document.createElement("img");
+ image.src = field.url.href;
+ const canvas = document.createElement("canvas");
+ canvas.width = image.width;
+ canvas.height = image.height;
+ const ctx = canvas.getContext("2d")!;
+ ctx.drawImage(image, 0, 0);
+ const dataUrl = canvas.toDataURL("image/png");
+ return dataUrl.replace(/^data:image\/(png|jpg);base64,/, "");
+ };
+
+} \ No newline at end of file
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index ef8b68c2f..4b7f1eeb6 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -506,10 +506,13 @@ export namespace Docs {
* @param title an optional title to give to the highest parent document in the hierarchy
*/
export function DocumentHierarchyFromJson(input: any, title?: string): Opt<Doc> {
- if (input === null || ![...primitives, "object"].includes(typeof input)) {
+ if (input === undefined || input === null || ![...primitives, "object"].includes(typeof input)) {
return undefined;
}
- let parsed: any = typeof input === "string" ? JSONUtils.tryParse(input) : input;
+ let parsed = input;
+ if (typeof input === "string") {
+ parsed = JSONUtils.tryParse(input);
+ }
let converted: Doc;
if (typeof parsed === "object" && !(parsed instanceof Array)) {
converted = convertObject(parsed, title);
diff --git a/src/client/northstar/utils/Extensions.ts b/src/client/northstar/utils/Extensions.ts
index df14d4da0..ab9384f1f 100644
--- a/src/client/northstar/utils/Extensions.ts
+++ b/src/client/northstar/utils/Extensions.ts
@@ -1,6 +1,8 @@
interface String {
ReplaceAll(toReplace: string, replacement: string): string;
Truncate(length: number, replacement: string): String;
+ removeTrailingNewlines(): string;
+ hasNewline(): boolean;
}
String.prototype.ReplaceAll = function (toReplace: string, replacement: string): string {
diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss
index ac8497bd0..470365627 100644
--- a/src/client/views/DocumentDecorations.scss
+++ b/src/client/views/DocumentDecorations.scss
@@ -266,6 +266,31 @@ $linkGap : 3px;
}
}
-@-moz-keyframes spin { 100% { -moz-transform: rotate(360deg); } }
-@-webkit-keyframes spin { 100% { -webkit-transform: rotate(360deg); } }
-@keyframes spin { 100% { -webkit-transform: rotate(360deg); transform: rotate(360deg); } } \ No newline at end of file
+@-moz-keyframes spin {
+ 100% {
+ -moz-transform: rotate(360deg);
+ }
+}
+
+@-webkit-keyframes spin {
+ 100% {
+ -webkit-transform: rotate(360deg);
+ }
+}
+
+@keyframes spin {
+ 100% {
+ -webkit-transform: rotate(360deg);
+ transform: rotate(360deg);
+ }
+}
+
+@keyframes shadow-pulse {
+ 0% {
+ box-shadow: 0 0 0 0px rgba(0, 0, 0, 0.2);
+ }
+
+ 100% {
+ box-shadow: 0 0 0 35px rgba(0, 0, 0, 0);
+ }
+} \ No newline at end of file
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 700a4b49d..35c45b03c 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -82,6 +82,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
@observable public pullIcon: IconProp = "arrow-alt-circle-down";
@observable public pullColor: string = "white";
@observable public isAnimatingFetch = false;
+ @observable public isAnimatingPulse = false;
@observable public openHover = false;
public pullColorAnimating = false;
@@ -102,6 +103,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
public startPushOutcome = action((success: boolean) => {
if (!this.pushAnimating) {
this.pushAnimating = true;
+ this.isAnimatingPulse = false;
this.pushIcon = success ? "check-circle" : "stop-circle";
setTimeout(() => runInAction(() => {
this.pushIcon = "arrow-alt-circle-up";
@@ -707,9 +709,12 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
return (
<div className={"linkButtonWrapper"}>
<div title={`${published ? "Push" : "Publish"} to Google Docs`} className="linkButton-linker" onClick={() => {
+ if (!published) {
+ runInAction(() => this.isAnimatingPulse = true);
+ }
DocumentDecorations.hasPushedHack = false;
this.targetDoc[Pushes] = NumCast(this.targetDoc[Pushes]) + 1;
- }}>
+ }} style={{ animation: this.isAnimatingPulse ? "shadow-pulse 1s infinite" : "none" }}>
<FontAwesomeIcon className="documentdecorations-icon" icon={icon} size={published ? "sm" : "xs"} />
</div>
</div>
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index 0e687737d..271326f70 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -8,6 +8,26 @@ import { Doc, DocListCastAsync } from "../../new_fields/Doc";
import { List } from "../../new_fields/List";
import { DocServer } from "../DocServer";
+String.prototype.removeTrailingNewlines = function () {
+ let sliced = this;
+ while (sliced.endsWith("\n")) {
+ sliced = sliced.substring(0, this.length - 1);
+ }
+ return sliced as string;
+};
+
+String.prototype.hasNewline = function () {
+ return this.endsWith("\n");
+};
+
+(Array.prototype as any).lastElement = function (this: any[]) {
+ if (!this.length) {
+ return undefined;
+ }
+ return this[this.length - 1];
+};
+
+
let swapDocs = async () => {
let oldDoc = await Cast(CurrentUserUtils.UserDocument.linkManagerDoc, Doc);
// Docs.Prototypes.MainLinkDocument().allLinks = new List<Doc>();
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index b64986084..f01083fbb 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -15,7 +15,7 @@ import { listSpec } from '../../new_fields/Schema';
import { BoolCast, Cast, FieldValue, StrCast, NumCast } from '../../new_fields/Types';
import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils';
import { RouteStore } from '../../server/RouteStore';
-import { emptyFunction, returnOne, returnTrue, Utils, returnEmptyString } from '../../Utils';
+import { emptyFunction, returnOne, returnTrue, Utils, returnEmptyString, PostToServer } from '../../Utils';
import { DocServer } from '../DocServer';
import { Docs } from '../documents/Documents';
import { ClientUtils } from '../util/ClientUtils';
@@ -40,8 +40,11 @@ import { PreviewCursor } from './PreviewCursor';
import { FilterBox } from './search/FilterBox';
import PresModeMenu from './presentationview/PresentationModeMenu';
import { PresBox } from './nodes/PresBox';
+import { docs_v1 } from 'googleapis';
+import { Album } from '../../server/apis/google/typings/albums';
+import { GooglePhotosClientUtils } from '../apis/google_docs/GooglePhotosClientUtils';
+import { ImageField } from '../../new_fields/URLField';
import { LinkFollowBox } from './linking/LinkFollowBox';
-import { DocumentManager } from '../util/DocumentManager';
@observer
export class MainView extends React.Component {
@@ -128,6 +131,11 @@ export class MainView extends React.Component {
window.removeEventListener("keydown", KeyManager.Instance.handle);
window.addEventListener("keydown", KeyManager.Instance.handle);
+ let imgurl = "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg";
+ let image = Docs.Create.ImageDocument(imgurl, { width: 200, title: "an image of a cat" });
+ let parameters = { title: StrCast(image.title), MEDIA_BINARY_DATA: GooglePhotosClientUtils.toDataURL(Cast(image.data, ImageField)) };
+ PostToServer(RouteStore.googlePhotosMediaUpload, parameters).then(console.log);
+
reaction(() => {
let workspaces = CurrentUserUtils.UserDocument.workspaces;
let recent = CurrentUserUtils.UserDocument.recentlyClosed;
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index bb44c5ac6..b671d06ea 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -12,7 +12,7 @@ import { DateField } from '../../../new_fields/DateField';
import { Doc, DocListCast, Opt, WidthSym } from "../../../new_fields/Doc";
import { Copy, Id } from '../../../new_fields/FieldSymbols';
import { List } from '../../../new_fields/List';
-import { RichTextField, ToPlainText, FromPlainText } from "../../../new_fields/RichTextField";
+import { RichTextField } from "../../../new_fields/RichTextField";
import { BoolCast, Cast, NumCast, StrCast, DateCast } from "../../../new_fields/Types";
import { createSchema, makeInterface } from "../../../new_fields/Schema";
import { Utils, numberRange } from '../../../Utils';
@@ -37,13 +37,13 @@ import { DocumentDecorations } from '../DocumentDecorations';
import { DictationManager } from '../../util/DictationManager';
import { ReplaceStep } from 'prosemirror-transform';
import { DocumentType } from '../../documents/DocumentTypes';
+import { RichTextUtils } from '../../../new_fields/RichTextUtils';
+import * as _ from "lodash";
import { formattedTextBoxCommentPlugin, FormattedTextBoxComment } from './FormattedTextBoxComment';
library.add(faEdit);
library.add(faSmile, faTextHeight, faUpload);
-export const Blank = `{"doc":{"type":"doc","content":[]},"selection":{"type":"text","anchor":0,"head":0}}`;
-
export interface FormattedTextBoxProps {
isOverlay?: boolean;
hideOnLeave?: boolean;
@@ -62,13 +62,15 @@ export const GoogleRef = "googleDocId";
type RichTextDocument = makeInterface<[typeof richTextSchema]>;
const RichTextDocument = makeInterface(richTextSchema);
-type PullHandler = (exportState: GoogleApiClientUtils.ReadResult, dataDoc: Doc) => void;
+type PullHandler = (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, dataDoc: Doc) => void;
@observer
export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTextBoxProps), RichTextDocument>(RichTextDocument) {
public static LayoutString(fieldStr: string = "data") {
return FieldView.LayoutString(FormattedTextBox, fieldStr);
}
+ public static blankState = () => EditorState.create(FormattedTextBox.Instance.config);
+ public static Instance: FormattedTextBox;
private static _toolTipTextMenu: TooltipTextMenu | undefined = undefined;
private _ref: React.RefObject<HTMLDivElement> = React.createRef();
private _proseRef?: HTMLDivElement;
@@ -380,7 +382,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
this._reactionDisposer = reaction(
() => {
const field = this.dataDoc ? Cast(this.dataDoc[this.props.fieldKey], RichTextField) : undefined;
- return field ? field.Data : Blank;
+ return field ? field.Data : RichTextUtils.Initialize();
},
incomingValue => {
if (this._editorView && !this._applyingChange) {
@@ -450,18 +452,17 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}
pushToGoogleDoc = async () => {
- this.pullFromGoogleDoc(async (exportState: GoogleApiClientUtils.ReadResult, dataDoc: Doc) => {
- let modes = GoogleApiClientUtils.WriteMode;
+ this.pullFromGoogleDoc(async (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, dataDoc: Doc) => {
+ let modes = GoogleApiClientUtils.Docs.WriteMode;
let mode = modes.Replace;
- let reference: Opt<GoogleApiClientUtils.Reference> = Cast(this.dataDoc[GoogleRef], "string");
+ let reference: Opt<GoogleApiClientUtils.Docs.Reference> = Cast(this.dataDoc[GoogleRef], "string");
if (!reference) {
mode = modes.Insert;
- reference = { service: GoogleApiClientUtils.Service.Documents, title: StrCast(this.dataDoc.title) };
+ reference = { title: StrCast(this.dataDoc.title) };
}
let redo = async () => {
- let data = Cast(this.dataDoc.data, RichTextField);
- if (this._editorView && reference && data) {
- let content = data[ToPlainText]();
+ if (this._editorView && reference) {
+ let content = RichTextUtils.GoogleDocs.Export(this._editorView.state);
let response = await GoogleApiClientUtils.Docs.write({ reference, content, mode });
response && (this.dataDoc[GoogleRef] = response.documentId);
let pushSuccess = response !== undefined && !("errors" in response);
@@ -470,7 +471,13 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}
};
let undo = () => {
- let content = exportState.body;
+ if (!exportState) {
+ return;
+ }
+ let content: GoogleApiClientUtils.Docs.Content = {
+ text: exportState.text,
+ requests: []
+ };
if (reference && content) {
GoogleApiClientUtils.Docs.write({ reference, content, mode });
}
@@ -483,49 +490,41 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
pullFromGoogleDoc = async (handler: PullHandler) => {
let dataDoc = this.dataDoc;
let documentId = StrCast(dataDoc[GoogleRef]);
- let exportState: GoogleApiClientUtils.ReadResult = {};
+ let exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>;
if (documentId) {
- exportState = await GoogleApiClientUtils.Docs.read({ identifier: documentId });
+ exportState = await RichTextUtils.GoogleDocs.Import(documentId);
}
UndoManager.RunInBatch(() => handler(exportState, dataDoc), Pulls);
}
- updateState = (exportState: GoogleApiClientUtils.ReadResult, dataDoc: Doc) => {
+ updateState = (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, dataDoc: Doc) => {
let pullSuccess = false;
- if (exportState !== undefined && exportState.body !== undefined && exportState.title !== undefined) {
- const data = Cast(dataDoc.data, RichTextField);
- if (data instanceof RichTextField) {
- pullSuccess = true;
- dataDoc.data = new RichTextField(data[FromPlainText](exportState.body));
- setTimeout(() => {
- if (this._editorView) {
- let state = this._editorView.state;
- let end = state.doc.content.size - 1;
- this._editorView.dispatch(state.tr.setSelection(TextSelection.create(state.doc, end, end)));
- }
- }, 0);
- dataDoc.title = exportState.title;
- this.Document.customTitle = true;
- dataDoc.unchanged = true;
- }
+ if (exportState !== undefined) {
+ pullSuccess = true;
+ dataDoc.data = new RichTextField(JSON.stringify(exportState.state.toJSON()));
+ setTimeout(() => {
+ if (this._editorView) {
+ let state = this._editorView.state;
+ let end = state.doc.content.size - 1;
+ this._editorView.dispatch(state.tr.setSelection(TextSelection.create(state.doc, end, end)));
+ }
+ }, 0);
+ dataDoc.title = exportState.title;
+ this.Document.customTitle = true;
+ dataDoc.unchanged = true;
} else {
delete dataDoc[GoogleRef];
}
DocumentDecorations.Instance.startPullOutcome(pullSuccess);
}
- checkState = (exportState: GoogleApiClientUtils.ReadResult, dataDoc: Doc) => {
- if (exportState !== undefined && exportState.body !== undefined && exportState.title !== undefined) {
- let data = Cast(dataDoc.data, RichTextField);
- if (data) {
- let storedPlainText = data[ToPlainText]() + "\n";
- let receivedPlainText = exportState.body;
- let storedTitle = dataDoc.title;
- let receivedTitle = exportState.title;
- let unchanged = storedPlainText === receivedPlainText && storedTitle === receivedTitle;
- dataDoc.unchanged = unchanged;
- DocumentDecorations.Instance.setPullState(unchanged);
- }
+ checkState = (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, dataDoc: Doc) => {
+ if (exportState && this._editorView) {
+ let equalContent = _.isEqual(this._editorView.state.doc, exportState.state.doc);
+ let equalTitles = dataDoc.title === exportState.title;
+ let unchanged = equalContent && equalTitles;
+ dataDoc.unchanged = unchanged;
+ DocumentDecorations.Instance.setPullState(unchanged);
}
}
diff --git a/src/new_fields/RichTextField.ts b/src/new_fields/RichTextField.ts
index 1b52e6f82..d2f76c969 100644
--- a/src/new_fields/RichTextField.ts
+++ b/src/new_fields/RichTextField.ts
@@ -4,11 +4,6 @@ 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");
-const delimiter = "\n";
-const joiner = "";
-
@scriptingGlobal
@Deserializable("RichTextField")
export class RichTextField extends ObjectField {
@@ -28,48 +23,4 @@ export class RichTextField extends ObjectField {
return `new RichTextField("${this.Data}")`;
}
- public static Initialize = (initial: string) => {
- !initial.length && (initial = " ");
- let pos = initial.length + 1;
- return `{"doc":{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"${initial}"}]}]},"selection":{"type":"text","anchor":${pos},"head":${pos}}}`;
- }
-
- [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");
-
- // 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
- let textParagraphs: string[] = paragraphs.map(concatenateParagraph);
- let plainText = textParagraphs.join(joiner);
- return plainText.substring(0, plainText.length - 1);
- }
-
- [FromPlainText](plainText: string) {
- // 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" };
- text.length && (paragraph.content = [{ type: "text", marks: [], text }]); // An empty paragraph gets treated as a line break
- return paragraph;
- });
-
- // 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);
- }
-
} \ No newline at end of file
diff --git a/src/new_fields/RichTextUtils.ts b/src/new_fields/RichTextUtils.ts
new file mode 100644
index 000000000..0aba50c0d
--- /dev/null
+++ b/src/new_fields/RichTextUtils.ts
@@ -0,0 +1,273 @@
+import { EditorState, Transaction, TextSelection } from "prosemirror-state";
+import { Node, Fragment, Mark } from "prosemirror-model";
+import { RichTextField } from "./RichTextField";
+import { docs_v1 } from "googleapis";
+import { GoogleApiClientUtils } from "../client/apis/google_docs/GoogleApiClientUtils";
+import { FormattedTextBox } from "../client/views/nodes/FormattedTextBox";
+import { Opt } from "./Doc";
+import * as Color from "color";
+import { sinkListItem } from "prosemirror-schema-list";
+
+export namespace RichTextUtils {
+
+ const delimiter = "\n";
+ const joiner = "";
+
+
+ export const Initialize = (initial?: string) => {
+ let content: any[] = [];
+ let state = {
+ doc: {
+ type: "doc",
+ content,
+ },
+ selection: {
+ type: "text",
+ anchor: 0,
+ head: 0
+ }
+ };
+ if (initial && initial.length) {
+ content.push({
+ type: "paragraph",
+ content: {
+ type: "text",
+ text: initial
+ }
+ });
+ state.selection.anchor = state.selection.head = initial.length + 1;
+ }
+ return JSON.stringify(state);
+ };
+
+ export const Synthesize = (plainText: string, oldState?: RichTextField) => {
+ return new RichTextField(ToProsemirrorState(plainText, oldState));
+ };
+
+ export const ToPlainText = (state: EditorState) => {
+ // Because we're working with plain text, just concatenate all paragraphs
+ let content = state.doc.content;
+ let paragraphs: Node<any>[] = [];
+ content.forEach(node => node.type.name === "paragraph" && paragraphs.push(node));
+
+ // Functions to flatten ProseMirror paragraph objects (and their components) to plain text
+ // Concatentate paragraphs and string the result together
+ let textParagraphs: string[] = paragraphs.map(paragraph => {
+ let text: string[] = [];
+ paragraph.content.forEach(node => node.text && text.push(node.text));
+ return text.join(joiner) + delimiter;
+ });
+ let plainText = textParagraphs.join(joiner);
+ return plainText.substring(0, plainText.length - 1);
+ };
+
+ export const ToProsemirrorState = (plainText: string, oldState?: RichTextField) => {
+ // 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(oldState ? oldState.Data : Initialize());
+ parsed.doc.content = elements.map(text => {
+ let paragraph: any = { type: "paragraph" };
+ text.length && (paragraph.content = [{ type: "text", marks: [], text }]); // An empty paragraph gets treated as a line break
+ return paragraph;
+ });
+
+ // 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 just built
+ return JSON.stringify(parsed);
+ };
+
+ export namespace GoogleDocs {
+
+ export const Export = (state: EditorState): GoogleApiClientUtils.Docs.Content => {
+ let textNodes: Node<any>[] = [];
+ let text = ToPlainText(state);
+ let content = state.doc.content;
+ content.forEach(node => node.content.forEach(node => node.type.name === "text" && textNodes.push(node)));
+ let linkRequests = ExtractLinks(textNodes);
+ return {
+ text,
+ requests: [...linkRequests]
+ };
+ };
+
+ type BulletPosition = { value: number, sinks: number };
+
+ export const Import = async (documentId: GoogleApiClientUtils.Docs.DocumentId): Promise<Opt<GoogleApiClientUtils.Docs.ImportResult>> => {
+ const document = await GoogleApiClientUtils.Docs.retrieve({ documentId });
+ if (!document) {
+ return undefined;
+ }
+
+ const title = document.title!;
+ const { text, paragraphs } = GoogleApiClientUtils.Docs.Utils.extractText(document);
+ let state = FormattedTextBox.blankState();
+ let structured = parseLists(paragraphs);
+
+ let position = 3;
+ let lists: ListGroup[] = [];
+ const indentMap = new Map<ListGroup, BulletPosition[]>();
+ let globalOffset = 0;
+ const nodes = structured.map(element => {
+ if (Array.isArray(element)) {
+ lists.push(element);
+ let positions: BulletPosition[] = [];
+ let items = element.map(paragraph => {
+ let item = listItem(state.schema, paragraph.runs);
+ let sinks = paragraph.bullet!;
+ positions.push({
+ value: position + globalOffset,
+ sinks
+ });
+ position += item.nodeSize;
+ globalOffset += 2 * sinks;
+ return item;
+ });
+ indentMap.set(element, positions);
+ return list(state.schema, items);
+ } else {
+ let paragraph = paragraphNode(state.schema, element.runs);
+ position += paragraph.nodeSize;
+ return paragraph;
+ }
+ });
+ state = state.apply(state.tr.replaceWith(0, 2, nodes));
+
+ let sink = sinkListItem(state.schema.nodes.list_item);
+ let dispatcher = (tr: Transaction) => state = state.apply(tr);
+ for (let list of lists) {
+ for (let pos of indentMap.get(list)!) {
+ let resolved = state.doc.resolve(pos.value);
+ state = state.apply(state.tr.setSelection(new TextSelection(resolved)));
+ for (let i = 0; i < pos.sinks; i++) {
+ sink(state, dispatcher);
+ }
+ }
+ }
+
+ return { title, text, state };
+ };
+
+ type Paragraph = GoogleApiClientUtils.Docs.Utils.DeconstructedParagraph;
+ type ListGroup = Paragraph[];
+ type PreparedParagraphs = (ListGroup | Paragraph)[];
+
+ const parseLists = (paragraphs: ListGroup) => {
+ let groups: PreparedParagraphs = [];
+ let group: ListGroup = [];
+ for (let paragraph of paragraphs) {
+ if (paragraph.bullet !== undefined) {
+ group.push(paragraph);
+ } else {
+ if (group.length) {
+ groups.push(group);
+ group = [];
+ }
+ groups.push(paragraph);
+ }
+ }
+ group.length && groups.push(group);
+ return groups;
+ };
+
+ const listItem = (schema: any, runs: docs_v1.Schema$TextRun[]): Node => {
+ return schema.node("list_item", null, paragraphNode(schema, runs));
+ };
+
+ const list = (schema: any, items: Node[]): Node => {
+ return schema.node("bullet_list", null, items);
+ };
+
+ const paragraphNode = (schema: any, runs: docs_v1.Schema$TextRun[]): Node => {
+ let children = runs.map(run => textNode(schema, run)).filter(child => child !== undefined);
+ let fragment = children.length ? Fragment.from(children) : undefined;
+ return schema.node("paragraph", null, fragment);
+ };
+
+ const textNode = (schema: any, run: docs_v1.Schema$TextRun) => {
+ let text = run.content!.removeTrailingNewlines();
+ return text.length ? schema.text(text, styleToMarks(schema, run.textStyle)) : undefined;
+ };
+
+ const MarkMapping = new Map<keyof docs_v1.Schema$TextStyle, string>([
+ ["bold", "strong"],
+ ["italic", "em"],
+ ["foregroundColor", "pFontColor"]
+ ]);
+
+ const styleToMarks = (schema: any, textStyle?: docs_v1.Schema$TextStyle) => {
+ if (!textStyle) {
+ return undefined;
+ }
+ let marks: Mark[] = [];
+ Object.keys(textStyle).forEach(key => {
+ let value: any;
+ let targeted = key as keyof docs_v1.Schema$TextStyle;
+ if (value = textStyle[targeted]) {
+ let attributes: any = {};
+ let converted = MarkMapping.get(targeted) || targeted;
+
+ value.url && (attributes.href = value.url);
+ if (value.color) {
+ let object: { [key: string]: number } = value.color.rgbColor;
+ attributes.color = Color.rgb(Object.values(object).map(value => value * 255)).hex();
+ }
+
+ let mark = schema.mark(schema.marks[converted], attributes);
+ mark && marks.push(mark);
+ }
+ });
+ return marks;
+ };
+
+ interface LinkInformation {
+ startIndex: number;
+ endIndex: number;
+ bold: boolean;
+ url: string;
+ }
+
+ const ExtractLinks = (nodes: Node<any>[]) => {
+ let links: docs_v1.Schema$Request[] = [];
+ let position = 1;
+ for (let node of nodes) {
+ let link, length = node.nodeSize;
+ let marks = node.marks;
+ if (marks.length && (link = marks.find(mark => mark.type.name === "link"))) {
+ links.push(Encode({
+ startIndex: position,
+ endIndex: position + length,
+ url: link.attrs.href,
+ bold: false
+ }));
+ }
+ position += length;
+ }
+ return links;
+ };
+
+ const Encode = (information: LinkInformation) => {
+ return {
+ updateTextStyle: {
+ fields: "*",
+ range: {
+ startIndex: information.startIndex,
+ endIndex: information.endIndex
+ },
+ textStyle: {
+ bold: true,
+ link: { url: information.url },
+ foregroundColor: { color: { rgbColor: { red: 0.0, green: 0.0, blue: 1.0 } } }
+ }
+ }
+ };
+ };
+ }
+
+} \ No newline at end of file
diff --git a/src/server/RouteStore.ts b/src/server/RouteStore.ts
index 014906054..3b3fd9b4a 100644
--- a/src/server/RouteStore.ts
+++ b/src/server/RouteStore.ts
@@ -31,6 +31,8 @@ export enum RouteStore {
// APIS
cognitiveServices = "/cognitiveservices",
- googleDocs = "/googleDocs"
+ googleDocs = "/googleDocs",
+ googlePhotosQuery = "/googlePhotosQuery",
+ googlePhotosMediaUpload = "/googlePhotosMediaUpload"
} \ No newline at end of file
diff --git a/src/server/apis/google/GoogleApiServerUtils.ts b/src/server/apis/google/GoogleApiServerUtils.ts
index 8785cd974..2c9085ebb 100644
--- a/src/server/apis/google/GoogleApiServerUtils.ts
+++ b/src/server/apis/google/GoogleApiServerUtils.ts
@@ -1,10 +1,12 @@
-import { google, docs_v1, slides_v1 } from "googleapis";
+import { google } from "googleapis";
import { createInterface } from "readline";
import { readFile, writeFile } from "fs";
-import { OAuth2Client } from "google-auth-library";
+import { OAuth2Client, Credentials } from "google-auth-library";
import { Opt } from "../../../new_fields/Doc";
import { GlobalOptions } from "googleapis-common";
import { GaxiosResponse } from "gaxios";
+import request = require('request-promise');
+import * as qs from 'query-string';
/**
* Server side authentication for Google Api queries.
@@ -20,6 +22,8 @@ export namespace GoogleApiServerUtils {
'presentations.readonly',
'drive',
'drive.file',
+ 'photoslibrary',
+ 'photoslibrary.sharing'
];
export const parseBuffer = (data: Buffer) => JSON.parse(data.toString());
@@ -29,7 +33,6 @@ export namespace GoogleApiServerUtils {
Slides = "Slides"
}
-
export interface CredentialPaths {
credentials: string;
token: string;
@@ -44,51 +47,98 @@ export namespace GoogleApiServerUtils {
export type EndpointParameters = GlobalOptions & { version: "v1" };
export const GetEndpoint = async (sector: string, paths: CredentialPaths) => {
- return new Promise<Opt<Endpoint>>((resolve, reject) => {
- readFile(paths.credentials, (err, credentials) => {
+ return new Promise<Opt<Endpoint>>(resolve => {
+ RetrieveCredentials(paths).then(authentication => {
+ let routed: Opt<Endpoint>;
+ let parameters: EndpointParameters = { auth: authentication.client, version: "v1" };
+ switch (sector) {
+ case Service.Documents:
+ routed = google.docs(parameters).documents;
+ break;
+ case Service.Slides:
+ routed = google.slides(parameters).presentations;
+ break;
+ }
+ resolve(routed);
+ });
+ });
+ };
+
+ export const RetrieveCredentials = async (paths: CredentialPaths) => {
+ return new Promise<TokenResult>((resolve, reject) => {
+ readFile(paths.credentials, async (err, credentials) => {
if (err) {
reject(err);
return console.log('Error loading client secret file:', err);
}
- return authorize(parseBuffer(credentials), paths.token).then(auth => {
- let routed: Opt<Endpoint>;
- let parameters: EndpointParameters = { auth, version: "v1" };
- switch (sector) {
- case Service.Documents:
- routed = google.docs(parameters).documents;
- break;
- case Service.Slides:
- routed = google.slides(parameters).presentations;
- break;
- }
- resolve(routed);
- });
+ authorize(parseBuffer(credentials), paths.token).then(resolve, reject);
});
});
};
+ export const RetrieveAccessToken = async (paths: CredentialPaths) => {
+ return new Promise<string>((resolve, reject) => {
+ RetrieveCredentials(paths).then(
+ credentials => resolve(credentials.token.access_token!),
+ error => reject(`Error: unable to authenticate Google Photos API request.\n${error}`)
+ );
+ });
+ };
+ type TokenResult = { token: Credentials, client: OAuth2Client };
/**
* 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<OAuth2Client> {
+ export function authorize(credentials: any, token_path: string): Promise<TokenResult> {
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<OAuth2Client>((resolve, reject) => {
+ return new Promise<TokenResult>((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);
+ let parsed: Credentials = parseBuffer(token);
+ if (parsed.expiry_date! < new Date().getTime()) {
+ return refreshToken(parsed, client_id, client_secret, oAuth2Client, token_path).then(resolve, reject);
+ }
+ oAuth2Client.setCredentials(parsed);
+ resolve({ token: parsed, client: oAuth2Client });
});
});
}
+ const refreshEndpoint = "https://oauth2.googleapis.com/token";
+ const refreshToken = (credentials: Credentials, client_id: string, client_secret: string, oAuth2Client: OAuth2Client, token_path: string) => {
+ return new Promise<TokenResult>((resolve, reject) => {
+ let headerParameters = { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } };
+ let queryParameters = {
+ refreshToken: credentials.refresh_token,
+ client_id,
+ client_secret,
+ grant_type: "refresh_token"
+ };
+ let url = `${refreshEndpoint}?${qs.stringify(queryParameters)}`;
+ request.post(url, headerParameters).then(response => {
+ let parsed = JSON.parse(response);
+ credentials.access_token = parsed.access_token;
+ credentials.expiry_date = new Date().getTime() + parsed.expires_in * 1000;
+ writeFile(token_path, JSON.stringify(credentials), (err) => {
+ if (err) {
+ console.error(err);
+ reject(err);
+ }
+ console.log('Refreshed token stored to', token_path);
+ oAuth2Client.setCredentials(credentials);
+ resolve({ token: credentials, client: oAuth2Client });
+ });
+ });
+ });
+ };
+
/**
* Get and store new token after prompting for user authorization, and then
* execute the given callback with the authorized OAuth2 client.
@@ -96,7 +146,7 @@ export namespace GoogleApiServerUtils {
* @param {getEventsCallback} callback The callback for the authorized client.
*/
function getNewToken(oAuth2Client: OAuth2Client, token_path: string) {
- return new Promise<OAuth2Client>((resolve, reject) => {
+ return new Promise<TokenResult>((resolve, reject) => {
const authUrl = oAuth2Client.generateAuthUrl({
access_type: 'offline',
scope: SCOPES.map(relative => prefix + relative),
@@ -122,7 +172,7 @@ export namespace GoogleApiServerUtils {
}
console.log('Token stored to', token_path);
});
- resolve(oAuth2Client);
+ resolve({ token, client: oAuth2Client });
});
});
});
diff --git a/src/server/apis/google/GooglePhotosServerUtils.ts b/src/server/apis/google/GooglePhotosServerUtils.ts
new file mode 100644
index 000000000..cb5464abc
--- /dev/null
+++ b/src/server/apis/google/GooglePhotosServerUtils.ts
@@ -0,0 +1,68 @@
+import request = require('request-promise');
+import { Album } from './typings/albums';
+import * as qs from 'query-string';
+
+const apiEndpoint = "https://photoslibrary.googleapis.com/v1/";
+
+export interface Authorization {
+ token: string;
+}
+
+export namespace GooglePhotos {
+
+ export type Query = Album.Query;
+ export type QueryParameters = { query: GooglePhotos.Query };
+ interface DispatchParameters {
+ required: boolean;
+ method: "GET" | "POST";
+ ignore?: boolean;
+ }
+
+ export const ExecuteQuery = async (parameters: Authorization & QueryParameters): Promise<any> => {
+ let action = parameters.query.action;
+ let dispatch = SuffixMap.get(action)!;
+ let suffix = Suffix(parameters, dispatch, action);
+ if (suffix) {
+ let query: any = parameters.query;
+ let options: any = {
+ headers: { 'Content-Type': 'application/json' },
+ auth: { 'bearer': parameters.token },
+ };
+ if (query.body) {
+ options.body = query.body;
+ options.json = true;
+ }
+ let queryParameters = query.parameters;
+ if (queryParameters) {
+ suffix += `?${qs.stringify(queryParameters)}`;
+ }
+ let dispatcher = dispatch.method === "POST" ? request.post : request.get;
+ return dispatcher(apiEndpoint + suffix, options);
+ }
+ };
+
+ const Suffix = (parameters: QueryParameters, dispatch: DispatchParameters, action: Album.Action) => {
+ let query: any = parameters.query;
+ let id = query.albumId;
+ let suffix = 'albums';
+ if (dispatch.required) {
+ if (!id) {
+ return undefined;
+ }
+ suffix += `/${id}${dispatch.ignore ? "" : `:${action}`}`;
+ }
+ return suffix;
+ };
+
+ const SuffixMap = new Map<Album.Action, DispatchParameters>([
+ [Album.Action.AddEnrichment, { required: true, method: "POST" }],
+ [Album.Action.BatchAddMediaItems, { required: true, method: "POST" }],
+ [Album.Action.BatchRemoveMediaItems, { required: true, method: "POST" }],
+ [Album.Action.Create, { required: false, method: "POST" }],
+ [Album.Action.Get, { required: true, ignore: true, method: "GET" }],
+ [Album.Action.List, { required: false, method: "GET" }],
+ [Album.Action.Share, { required: true, method: "POST" }],
+ [Album.Action.Unshare, { required: true, method: "POST" }]
+ ]);
+
+}
diff --git a/src/server/apis/google/GooglePhotosUploadUtils.ts b/src/server/apis/google/GooglePhotosUploadUtils.ts
new file mode 100644
index 000000000..b358f9698
--- /dev/null
+++ b/src/server/apis/google/GooglePhotosUploadUtils.ts
@@ -0,0 +1,28 @@
+import request = require('request-promise');
+import { Authorization } from './GooglePhotosServerUtils';
+
+export namespace GooglePhotosUploadUtils {
+
+ interface UploadInformation {
+ title: string;
+ MEDIA_BINARY_DATA: string;
+ }
+
+ const apiEndpoint = "https://photoslibrary.googleapis.com/v1/uploads";
+
+ export const SubmitUpload = async (parameters: Authorization & UploadInformation) => {
+ let options = {
+ headers: {
+ 'Content-Type': 'application/octet-stream',
+ auth: { 'bearer': parameters.token },
+ 'X-Goog-Upload-File-Name': parameters.title,
+ 'X-Goog-Upload-Protocol': 'raw'
+ },
+ body: { MEDIA_BINARY_DATA: parameters.MEDIA_BINARY_DATA },
+ json: true
+ };
+ const result = await request.post(apiEndpoint, options);
+ return result;
+ };
+
+} \ No newline at end of file
diff --git a/src/server/apis/google/typings/albums.ts b/src/server/apis/google/typings/albums.ts
new file mode 100644
index 000000000..f3025567d
--- /dev/null
+++ b/src/server/apis/google/typings/albums.ts
@@ -0,0 +1,150 @@
+export namespace Album {
+
+ export type Query = (AddEnrichment | BatchAddMediaItems | BatchRemoveMediaItems | Create | Get | List | Share | Unshare);
+
+ export enum Action {
+ AddEnrichment = "addEnrichment",
+ BatchAddMediaItems = "batchAddMediaItems",
+ BatchRemoveMediaItems = "batchRemoveMediaItems",
+ Create = "create",
+ Get = "get",
+ List = "list",
+ Share = "share",
+ Unshare = "unshare"
+ }
+
+ export interface AddEnrichment {
+ action: Action.AddEnrichment;
+ albumId: string;
+ body: {
+ newEnrichmentItem: NewEnrichmentItem;
+ albumPosition: MediaRelativeAlbumPosition;
+ };
+ }
+
+ export interface BatchAddMediaItems {
+ action: Action.BatchAddMediaItems;
+ albumId: string;
+ body: {
+ mediaItemIds: string[];
+ };
+ }
+
+ export interface BatchRemoveMediaItems {
+ action: Action.BatchRemoveMediaItems;
+ albumId: string;
+ body: {
+ mediaItemIds: string[];
+ };
+ }
+
+ export interface Create {
+ action: Action.Create;
+ body: {
+ album: Template;
+ };
+ }
+
+ export interface Get {
+ action: Action.Get;
+ albumId: string;
+ }
+
+ export interface List {
+ action: Action.List;
+ parameters: ListOptions;
+ }
+
+ export interface ListOptions {
+ pageSize: number;
+ pageToken: string;
+ excludeNonAppCreatedData: boolean;
+ }
+
+ export interface Share {
+ action: Action.Share;
+ albumId: string;
+ body: {
+ sharedAlbumOptions: SharedOptions;
+ };
+ }
+
+ export interface Unshare {
+ action: Action.Unshare;
+ albumId: string;
+ }
+
+ export interface Template {
+ title: string;
+ }
+
+ export interface Model {
+ id: string;
+ title: string;
+ productUrl: string;
+ isWriteable: boolean;
+ shareInfo: ShareInfo;
+ mediaItemsCount: string;
+ coverPhotoBaseUrl: string;
+ coverPhotoMediaItemId: string;
+ }
+
+ export interface ShareInfo {
+ sharedAlbumOptions: SharedOptions;
+ shareableUrl: string;
+ shareToken: string;
+ isJoined: boolean;
+ isOwned: boolean;
+ }
+
+ export interface SharedOptions {
+ isCollaborative: boolean;
+ isCommentable: boolean;
+ }
+
+ export enum PositionType {
+ POSITION_TYPE_UNSPECIFIED,
+ FIRST_IN_ALBUM,
+ LAST_IN_ALBUM,
+ AFTER_MEDIA_ITEM,
+ AFTER_ENRICHMENT_ITEM
+ }
+
+ export type Position = GeneralAlbumPosition | MediaRelativeAlbumPosition | EnrichmentRelativeAlbumPosition;
+
+ interface GeneralAlbumPosition {
+ position: PositionType.FIRST_IN_ALBUM | PositionType.LAST_IN_ALBUM | PositionType.POSITION_TYPE_UNSPECIFIED;
+ }
+
+ interface MediaRelativeAlbumPosition {
+ position: PositionType.AFTER_MEDIA_ITEM;
+ relativeMediaItemId: string;
+ }
+
+ interface EnrichmentRelativeAlbumPosition {
+ position: PositionType.AFTER_ENRICHMENT_ITEM;
+ relativeEnrichmentItemId: string;
+ }
+
+ export interface Location {
+ locationName: string;
+ latlng: {
+ latitude: number,
+ longitude: number
+ };
+ }
+
+ export interface NewEnrichmentItem {
+ textEnrichment: {
+ text: string;
+ };
+ locationEnrichment: {
+ location: Location
+ };
+ mapEnrichment: {
+ origin: { location: Location },
+ destination: { location: Location }
+ };
+ }
+
+} \ No newline at end of file
diff --git a/src/server/authentication/config/passport.ts b/src/server/authentication/config/passport.ts
index 10b17de71..8b20d29b1 100644
--- a/src/server/authentication/config/passport.ts
+++ b/src/server/authentication/config/passport.ts
@@ -1,7 +1,6 @@
import * as passport from 'passport';
import * as passportLocal from 'passport-local';
-import * as mongodb from 'mongodb';
-import * as _ from "lodash";
+import _ from "lodash";
import { default as User } from '../models/user_model';
import { Request, Response, NextFunction } from "express";
import { RouteStore } from '../../RouteStore';
diff --git a/src/server/credentials/google_docs_credentials.json b/src/server/credentials/google_docs_credentials.json
index 8d097d363..955c5a3c1 100644
--- a/src/server/credentials/google_docs_credentials.json
+++ b/src/server/credentials/google_docs_credentials.json
@@ -1 +1,11 @@
-{"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
+{
+ "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
index 07c02d56c..e4be9ff60 100644
--- a/src/server/credentials/google_docs_token.json
+++ b/src/server/credentials/google_docs_token.json
@@ -1 +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
+{"access_token":"ya29.Glx4B9gmH9fKJex9Je-k1KBNWEsJBPgQqoEWHhwk0TtTemCxONoyVMO38idAE7Vy9vdjcosjl0H4swnnH1s2QjTwZaspzKPeQr8Oh4sHkCJ-MbNd6naMZBdy1pccjQ","refresh_token":"1/kk_pPY7WBwT34JNPzx_HrSVoZlvfzys4EEVNjp7nqzg6aIbRrEKNMRTb0u0wr9GM","scope":"https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/photoslibrary.sharing https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/photoslibrary","token_type":"Bearer","expiry_date":1567545365417} \ No newline at end of file
diff --git a/src/server/index.ts b/src/server/index.ts
index fca90a585..3940bbd58 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -42,10 +42,8 @@ var AdmZip = require('adm-zip');
import * as YoutubeApi from "./apis/youtube/youtubeApiSample";
import { Response } from 'express-serve-static-core';
import { GoogleApiServerUtils } from "./apis/google/GoogleApiServerUtils";
-import { GaxiosResponse } from 'gaxios';
-import { Opt } from '../new_fields/Doc';
-import { docs_v1 } from 'googleapis';
-import { Endpoint } from 'googleapis-common';
+import { GooglePhotos } from './apis/google/GooglePhotosServerUtils';
+import { GooglePhotosUploadUtils } from './apis/google/GooglePhotosUploadUtils';
const MongoStore = require('connect-mongo')(session);
const mongoose = require('mongoose');
const probe = require("probe-image-size");
@@ -194,7 +192,7 @@ app.get("/version", (req, res) => {
// SEARCH
const solrURL = "http://localhost:8983/solr/#/dash";
-// GETTERS
+// GETTERSå
app.get("/search", async (req, res) => {
const solrQuery: any = {};
@@ -447,7 +445,7 @@ function LoadPage(file: string, pageNumber: number, res: Response) {
console.log(pageNumber);
pdf.getPage(pageNumber).then((page: Pdfjs.PDFPageProxy) => {
console.log("reading " + page);
- let viewport = page.getViewport({ scale: 1 });
+ let viewport = page.getViewport(1);
let canvasAndContext = factory.create(viewport.width, viewport.height);
let renderContext = {
canvasContext: canvasAndContext.context,
@@ -827,6 +825,38 @@ app.post(RouteStore.googleDocs + "/:sector/:action", (req, res) => {
});
});
+app.post(RouteStore.googlePhotosQuery, (req, res) => {
+ GoogleApiServerUtils.RetrieveAccessToken({ credentials, token }).then(
+ token => {
+ GooglePhotos.ExecuteQuery({ token, query: req.body })
+ .then(response => {
+ if (response === undefined) {
+ res.send("Error: unable to build suffix for Google Photos API request");
+ return;
+ }
+ res.send(response);
+ })
+ .catch(error => {
+ res.send(`Error: an exception occurred in the execution of this Google Photos API request\n${error}`);
+ });
+ },
+ error => res.send(error)
+ );
+});
+
+app.post(RouteStore.googlePhotosMediaUpload, (req, res) => {
+ GoogleApiServerUtils.RetrieveAccessToken({ credentials, token }).then(
+ token => {
+ GooglePhotosUploadUtils.SubmitUpload({ token, ...req.body })
+ .then(response => {
+ res.send(response);
+ }).catch(error => {
+ res.send(`Error: an exception occurred in uploading the given media\n${error}`);
+ });
+ },
+ error => res.send(error));
+});
+
const suffixMap: { [type: string]: (string | [string, string | ((json: any) => any)]) } = {
"number": "_n",
"string": "_t",
diff --git a/src/typings/index.d.ts b/src/typings/index.d.ts
index 7939ae8be..36d828fdb 100644
--- a/src/typings/index.d.ts
+++ b/src/typings/index.d.ts
@@ -1,322 +1,324 @@
/// <reference types="node" />
-declare module '@react-pdf/renderer' {
- import * as React from 'react';
-
- namespace ReactPDF {
- interface Style {
- [property: string]: any;
- }
- interface Styles {
- [key: string]: Style;
- }
- type Orientation = 'portrait' | 'landscape';
-
- interface DocumentProps {
- title?: string;
- author?: string;
- subject?: string;
- keywords?: string;
- creator?: string;
- producer?: string;
- onRender?: () => any;
- }
-
- /**
- * This component represent the PDF document itself. It must be the root
- * of your tree element structure, and under no circumstances should it be
- * used as children of another react-pdf component. In addition, it should
- * only have childs of type <Page />.
- */
- class Document extends React.Component<DocumentProps> { }
-
- interface NodeProps {
- style?: Style | Style[];
- /**
- * Render component in all wrapped pages.
- * @see https://react-pdf.org/advanced#fixed-components
- */
- fixed?: boolean;
- /**
- * Force the wrapping algorithm to start a new page when rendering the
- * element.
- * @see https://react-pdf.org/advanced#page-breaks
- */
- break?: boolean;
- }
-
- interface PageProps extends NodeProps {
- /**
- * Enable page wrapping for this page.
- * @see https://react-pdf.org/components#page-wrapping
- */
- wrap?: boolean;
- debug?: boolean;
- size?: string | [number, number] | { width: number; height: number };
- orientation?: Orientation;
- ruler?: boolean;
- rulerSteps?: number;
- verticalRuler?: boolean;
- verticalRulerSteps?: number;
- horizontalRuler?: boolean;
- horizontalRulerSteps?: number;
- ref?: Page;
- }
-
- /**
- * Represents single page inside the PDF document, or a subset of them if
- * using the wrapping feature. A <Document /> can contain as many pages as
- * you want, but ensure not rendering a page inside any component besides
- * Document.
- */
- class Page extends React.Component<PageProps> { }
-
- interface ViewProps extends NodeProps {
- /**
- * Enable/disable page wrapping for element.
- * @see https://react-pdf.org/components#page-wrapping
- */
- wrap?: boolean;
- debug?: boolean;
- render?: (props: { pageNumber: number }) => React.ReactNode;
- children?: React.ReactNode;
- }
-
- /**
- * The most fundamental component for building a UI and is designed to be
- * nested inside other views and can have 0 to many children.
- */
- class View extends React.Component<ViewProps> { }
-
- interface ImageProps extends NodeProps {
- debug?: boolean;
- src: string | { data: Buffer; format: 'png' | 'jpg' };
- cache?: boolean;
- }
-
- /**
- * A React component for displaying network or local (Node only) JPG or
- * PNG images, as well as base64 encoded image strings.
- */
- class Image extends React.Component<ImageProps> { }
-
- interface TextProps extends NodeProps {
- /**
- * Enable/disable page wrapping for element.
- * @see https://react-pdf.org/components#page-wrapping
- */
- wrap?: boolean;
- debug?: boolean;
- render?: (
- props: { pageNumber: number; totalPages: number },
- ) => React.ReactNode;
- children?: React.ReactNode;
- /**
- * How much hyphenated breaks should be avoided.
- */
- hyphenationCallback?: number;
- }
-
- /**
- * A React component for displaying text. Text supports nesting of other
- * Text or Link components to create inline styling.
- */
- class Text extends React.Component<TextProps> { }
-
- interface LinkProps extends NodeProps {
- /**
- * Enable/disable page wrapping for element.
- * @see https://react-pdf.org/components#page-wrapping
- */
- wrap?: boolean;
- debug?: boolean;
- src: string;
- children?: React.ReactNode;
- }
+declare module 'googlephotos';
- /**
- * A React component for displaying an hyperlink. Link’s can be nested
- * inside a Text component, or being inside any other valid primitive.
- */
- class Link extends React.Component<LinkProps> { }
-
- interface NoteProps extends NodeProps {
- children: string;
- }
-
- class Note extends React.Component<NoteProps> { }
-
- interface BlobProviderParams {
- blob: Blob | null;
- url: string | null;
- loading: boolean;
- error: Error | null;
- }
- interface BlobProviderProps {
- document: React.ReactElement<DocumentProps>;
- children: (params: BlobProviderParams) => React.ReactNode;
- }
-
- /**
- * Easy and declarative way of getting document's blob data without
- * showing it on screen.
- * @see https://react-pdf.org/advanced#on-the-fly-rendering
- * @platform web
- */
- class BlobProvider extends React.Component<BlobProviderProps> { }
-
- interface PDFViewerProps {
- width?: number;
- height?: number;
- style?: Style | Style[];
- className?: string;
- children?: React.ReactElement<DocumentProps>;
- }
-
- /**
- * Iframe PDF viewer for client-side generated documents.
- * @platform web
- */
- class PDFViewer extends React.Component<PDFViewerProps> { }
-
- interface PDFDownloadLinkProps {
- document: React.ReactElement<DocumentProps>;
- fileName?: string;
- style?: Style | Style[];
- className?: string;
- children?:
- | React.ReactNode
- | ((params: BlobProviderParams) => React.ReactNode);
+declare module '@react-pdf/renderer' {
+ import * as React from 'react';
+
+ namespace ReactPDF {
+ interface Style {
+ [property: string]: any;
+ }
+ interface Styles {
+ [key: string]: Style;
+ }
+ type Orientation = 'portrait' | 'landscape';
+
+ interface DocumentProps {
+ title?: string;
+ author?: string;
+ subject?: string;
+ keywords?: string;
+ creator?: string;
+ producer?: string;
+ onRender?: () => any;
+ }
+
+ /**
+ * This component represent the PDF document itself. It must be the root
+ * of your tree element structure, and under no circumstances should it be
+ * used as children of another react-pdf component. In addition, it should
+ * only have childs of type <Page />.
+ */
+ class Document extends React.Component<DocumentProps> { }
+
+ interface NodeProps {
+ style?: Style | Style[];
+ /**
+ * Render component in all wrapped pages.
+ * @see https://react-pdf.org/advanced#fixed-components
+ */
+ fixed?: boolean;
+ /**
+ * Force the wrapping algorithm to start a new page when rendering the
+ * element.
+ * @see https://react-pdf.org/advanced#page-breaks
+ */
+ break?: boolean;
+ }
+
+ interface PageProps extends NodeProps {
+ /**
+ * Enable page wrapping for this page.
+ * @see https://react-pdf.org/components#page-wrapping
+ */
+ wrap?: boolean;
+ debug?: boolean;
+ size?: string | [number, number] | { width: number; height: number };
+ orientation?: Orientation;
+ ruler?: boolean;
+ rulerSteps?: number;
+ verticalRuler?: boolean;
+ verticalRulerSteps?: number;
+ horizontalRuler?: boolean;
+ horizontalRulerSteps?: number;
+ ref?: Page;
+ }
+
+ /**
+ * Represents single page inside the PDF document, or a subset of them if
+ * using the wrapping feature. A <Document /> can contain as many pages as
+ * you want, but ensure not rendering a page inside any component besides
+ * Document.
+ */
+ class Page extends React.Component<PageProps> { }
+
+ interface ViewProps extends NodeProps {
+ /**
+ * Enable/disable page wrapping for element.
+ * @see https://react-pdf.org/components#page-wrapping
+ */
+ wrap?: boolean;
+ debug?: boolean;
+ render?: (props: { pageNumber: number }) => React.ReactNode;
+ children?: React.ReactNode;
+ }
+
+ /**
+ * The most fundamental component for building a UI and is designed to be
+ * nested inside other views and can have 0 to many children.
+ */
+ class View extends React.Component<ViewProps> { }
+
+ interface ImageProps extends NodeProps {
+ debug?: boolean;
+ src: string | { data: Buffer; format: 'png' | 'jpg' };
+ cache?: boolean;
+ }
+
+ /**
+ * A React component for displaying network or local (Node only) JPG or
+ * PNG images, as well as base64 encoded image strings.
+ */
+ class Image extends React.Component<ImageProps> { }
+
+ interface TextProps extends NodeProps {
+ /**
+ * Enable/disable page wrapping for element.
+ * @see https://react-pdf.org/components#page-wrapping
+ */
+ wrap?: boolean;
+ debug?: boolean;
+ render?: (
+ props: { pageNumber: number; totalPages: number },
+ ) => React.ReactNode;
+ children?: React.ReactNode;
+ /**
+ * How much hyphenated breaks should be avoided.
+ */
+ hyphenationCallback?: number;
+ }
+
+ /**
+ * A React component for displaying text. Text supports nesting of other
+ * Text or Link components to create inline styling.
+ */
+ class Text extends React.Component<TextProps> { }
+
+ interface LinkProps extends NodeProps {
+ /**
+ * Enable/disable page wrapping for element.
+ * @see https://react-pdf.org/components#page-wrapping
+ */
+ wrap?: boolean;
+ debug?: boolean;
+ src: string;
+ children?: React.ReactNode;
+ }
+
+ /**
+ * A React component for displaying an hyperlink. Link’s can be nested
+ * inside a Text component, or being inside any other valid primitive.
+ */
+ class Link extends React.Component<LinkProps> { }
+
+ interface NoteProps extends NodeProps {
+ children: string;
+ }
+
+ class Note extends React.Component<NoteProps> { }
+
+ interface BlobProviderParams {
+ blob: Blob | null;
+ url: string | null;
+ loading: boolean;
+ error: Error | null;
+ }
+ interface BlobProviderProps {
+ document: React.ReactElement<DocumentProps>;
+ children: (params: BlobProviderParams) => React.ReactNode;
+ }
+
+ /**
+ * Easy and declarative way of getting document's blob data without
+ * showing it on screen.
+ * @see https://react-pdf.org/advanced#on-the-fly-rendering
+ * @platform web
+ */
+ class BlobProvider extends React.Component<BlobProviderProps> { }
+
+ interface PDFViewerProps {
+ width?: number;
+ height?: number;
+ style?: Style | Style[];
+ className?: string;
+ children?: React.ReactElement<DocumentProps>;
+ }
+
+ /**
+ * Iframe PDF viewer for client-side generated documents.
+ * @platform web
+ */
+ class PDFViewer extends React.Component<PDFViewerProps> { }
+
+ interface PDFDownloadLinkProps {
+ document: React.ReactElement<DocumentProps>;
+ fileName?: string;
+ style?: Style | Style[];
+ className?: string;
+ children?:
+ | React.ReactNode
+ | ((params: BlobProviderParams) => React.ReactNode);
+ }
+
+ /**
+ * Anchor tag to enable generate and download PDF documents on the fly.
+ * @see https://react-pdf.org/advanced#on-the-fly-rendering
+ * @platform web
+ */
+ class PDFDownloadLink extends React.Component<PDFDownloadLinkProps> { }
+
+ interface EmojiSource {
+ url: string;
+ format: string;
+ }
+ interface RegisteredFont {
+ src: string;
+ loaded: boolean;
+ loading: boolean;
+ data: any;
+ [key: string]: any;
+ }
+ type HyphenationCallback = (
+ words: string[],
+ glyphString: { [key: string]: any },
+ ) => string[];
+
+ const Font: {
+ register: (
+ src: string,
+ options: { family: string;[key: string]: any },
+ ) => void;
+ getEmojiSource: () => EmojiSource;
+ getRegisteredFonts: () => string[];
+ registerEmojiSource: (emojiSource: EmojiSource) => void;
+ registerHyphenationCallback: (
+ hyphenationCallback: HyphenationCallback,
+ ) => void;
+ getHyphenationCallback: () => HyphenationCallback;
+ getFont: (fontFamily: string) => RegisteredFont | undefined;
+ load: (
+ fontFamily: string,
+ document: React.ReactElement<DocumentProps>,
+ ) => Promise<void>;
+ clear: () => void;
+ reset: () => void;
+ };
+
+ const StyleSheet: {
+ hairlineWidth: number;
+ create: <TStyles>(styles: TStyles) => TStyles;
+ resolve: (
+ style: Style,
+ container: {
+ width: number;
+ height: number;
+ orientation: Orientation;
+ },
+ ) => Style;
+ flatten: (...styles: Style[]) => Style;
+ absoluteFillObject: {
+ position: 'absolute';
+ left: 0;
+ right: 0;
+ top: 0;
+ bottom: 0;
+ };
+ };
+
+ const version: any;
+
+ const PDFRenderer: any;
+
+ const createInstance: (
+ element: {
+ type: string;
+ props: { [key: string]: any };
+ },
+ root?: any,
+ ) => any;
+
+ const pdf: (
+ document: React.ReactElement<DocumentProps>,
+ ) => {
+ isDirty: () => boolean;
+ updateContainer: (document: React.ReactElement<any>) => void;
+ toBuffer: () => NodeJS.ReadableStream;
+ toBlob: () => Blob;
+ toString: () => string;
+ };
+
+ const renderToStream: (
+ document: React.ReactElement<DocumentProps>,
+ ) => NodeJS.ReadableStream;
+
+ const renderToFile: (
+ document: React.ReactElement<DocumentProps>,
+ filePath: string,
+ callback?: (output: NodeJS.ReadableStream, filePath: string) => any,
+ ) => Promise<NodeJS.ReadableStream>;
+
+ const render: typeof renderToFile;
}
- /**
- * Anchor tag to enable generate and download PDF documents on the fly.
- * @see https://react-pdf.org/advanced#on-the-fly-rendering
- * @platform web
- */
- class PDFDownloadLink extends React.Component<PDFDownloadLinkProps> { }
-
- interface EmojiSource {
- url: string;
- format: string;
- }
- interface RegisteredFont {
- src: string;
- loaded: boolean;
- loading: boolean;
- data: any;
- [key: string]: any;
- }
- type HyphenationCallback = (
- words: string[],
- glyphString: { [key: string]: any },
- ) => string[];
-
- const Font: {
- register: (
- src: string,
- options: { family: string;[key: string]: any },
- ) => void;
- getEmojiSource: () => EmojiSource;
- getRegisteredFonts: () => string[];
- registerEmojiSource: (emojiSource: EmojiSource) => void;
- registerHyphenationCallback: (
- hyphenationCallback: HyphenationCallback,
- ) => void;
- getHyphenationCallback: () => HyphenationCallback;
- getFont: (fontFamily: string) => RegisteredFont | undefined;
- load: (
- fontFamily: string,
- document: React.ReactElement<DocumentProps>,
- ) => Promise<void>;
- clear: () => void;
- reset: () => void;
+ const Document: typeof ReactPDF.Document;
+ const Page: typeof ReactPDF.Page;
+ const View: typeof ReactPDF.View;
+ const Image: typeof ReactPDF.Image;
+ const Text: typeof ReactPDF.Text;
+ const Link: typeof ReactPDF.Link;
+ const Note: typeof ReactPDF.Note;
+ const Font: typeof ReactPDF.Font;
+ const StyleSheet: typeof ReactPDF.StyleSheet;
+ const createInstance: typeof ReactPDF.createInstance;
+ const PDFRenderer: typeof ReactPDF.PDFRenderer;
+ const version: typeof ReactPDF.version;
+ const pdf: typeof ReactPDF.pdf;
+
+ export default ReactPDF;
+ export {
+ Document,
+ Page,
+ View,
+ Image,
+ Text,
+ Link,
+ Note,
+ Font,
+ StyleSheet,
+ createInstance,
+ PDFRenderer,
+ version,
+ pdf,
};
-
- const StyleSheet: {
- hairlineWidth: number;
- create: <TStyles>(styles: TStyles) => TStyles;
- resolve: (
- style: Style,
- container: {
- width: number;
- height: number;
- orientation: Orientation;
- },
- ) => Style;
- flatten: (...styles: Style[]) => Style;
- absoluteFillObject: {
- position: 'absolute';
- left: 0;
- right: 0;
- top: 0;
- bottom: 0;
- };
- };
-
- const version: any;
-
- const PDFRenderer: any;
-
- const createInstance: (
- element: {
- type: string;
- props: { [key: string]: any };
- },
- root?: any,
- ) => any;
-
- const pdf: (
- document: React.ReactElement<DocumentProps>,
- ) => {
- isDirty: () => boolean;
- updateContainer: (document: React.ReactElement<any>) => void;
- toBuffer: () => NodeJS.ReadableStream;
- toBlob: () => Blob;
- toString: () => string;
- };
-
- const renderToStream: (
- document: React.ReactElement<DocumentProps>,
- ) => NodeJS.ReadableStream;
-
- const renderToFile: (
- document: React.ReactElement<DocumentProps>,
- filePath: string,
- callback?: (output: NodeJS.ReadableStream, filePath: string) => any,
- ) => Promise<NodeJS.ReadableStream>;
-
- const render: typeof renderToFile;
- }
-
- const Document: typeof ReactPDF.Document;
- const Page: typeof ReactPDF.Page;
- const View: typeof ReactPDF.View;
- const Image: typeof ReactPDF.Image;
- const Text: typeof ReactPDF.Text;
- const Link: typeof ReactPDF.Link;
- const Note: typeof ReactPDF.Note;
- const Font: typeof ReactPDF.Font;
- const StyleSheet: typeof ReactPDF.StyleSheet;
- const createInstance: typeof ReactPDF.createInstance;
- const PDFRenderer: typeof ReactPDF.PDFRenderer;
- const version: typeof ReactPDF.version;
- const pdf: typeof ReactPDF.pdf;
-
- export default ReactPDF;
- export {
- Document,
- Page,
- View,
- Image,
- Text,
- Link,
- Note,
- Font,
- StyleSheet,
- createInstance,
- PDFRenderer,
- version,
- pdf,
- };
} \ No newline at end of file